diff --git a/less/original/actors.less b/less/original/actors.less index 5edc2d98..efaf2a7d 100644 --- a/less/original/actors.less +++ b/less/original/actors.less @@ -489,7 +489,6 @@ &.item-action {flex: 0 0 100px} &.attunement {flex: 0 0 24px} } - .item-weight { flex: 0 0 60px; border-left: 1px solid @colorFaint; diff --git a/module/actor/sheets/newSheet/character.js b/module/actor/sheets/newSheet/character.js index 643d3757..5123f957 100644 --- a/module/actor/sheets/newSheet/character.js +++ b/module/actor/sheets/newSheet/character.js @@ -1,619 +1,619 @@ -import ActorSheet5e from "./base.js"; -import Actor5e from "../../entity.js"; - -/** - * An Actor sheet for player character type actors in the SW5E system. - * Extends the base ActorSheet5e class. - * @type {ActorSheet5e} - */ -export default class ActorSheet5eCharacterNew extends ActorSheet5e { - - get template() { - if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html"; - return "systems/sw5e/templates/actors/newActor/character-sheet.html"; - } - /** - * Define default rendering options for the NPC sheet - * @return {Object} - */ - static get defaultOptions() { - - return mergeObject(super.defaultOptions, { - classes: ["swalt", "sw5e", "sheet", "actor", "character"], - blockFavTab: true, - subTabs: null, - width: 800, - tabs: [{ - navSelector: ".root-tabs", - contentSelector: ".sheet-body", - initial: "attributes" - }], - }); - } - - /* -------------------------------------------- */ - - /** - * Add some extra data when rendering the sheet to reduce the amount of logic required within the template. - */ - getData() { - const sheetData = super.getData(); - - // Temporary HP - let hp = sheetData.data.attributes.hp; - if (hp.temp === 0) delete hp.temp; - if (hp.tempmax === 0) delete hp.tempmax; - - // Resources - sheetData["resources"] = ["primary", "secondary", "tertiary"].reduce((arr, r) => { - const res = sheetData.data.resources[r] || {}; - res.name = r; - res.placeholder = game.i18n.localize("SW5E.Resource"+r.titleCase()); - if (res && res.value === 0) delete res.value; - if (res && res.max === 0) delete res.max; - return arr.concat([res]); - }, []); - - // Experience Tracking - sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking"); - sheetData["classLabels"] = this.actor.itemTypes.class.map(c => c.name).join(", "); - - // Return data for rendering - return sheetData; - } - - /* -------------------------------------------- */ - - /** - * Organize and classify Owned Items for Character sheets - * @private - */ - _prepareItems(data) { - - // Categorize items as inventory, powerbook, features, and classes - const inventory = { - weapon: { label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"} }, - equipment: { label: "SW5E.ItemTypeEquipmentPl", items: [], dataset: {type: "equipment"} }, - consumable: { label: "SW5E.ItemTypeConsumablePl", items: [], dataset: {type: "consumable"} }, - tool: { label: "SW5E.ItemTypeToolPl", items: [], dataset: {type: "tool"} }, - backpack: { label: "SW5E.ItemTypeContainerPl", items: [], dataset: {type: "backpack"} }, - loot: { label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"} } - }; - - // Partition items by category - let [items, powers, feats, classes, species, archetypes, classfeatures, backgrounds, fightingstyles, fightingmasteries, lightsaberforms] = data.items.reduce((arr, item) => { - - // Item details - item.img = item.img || DEFAULT_TOKEN; - item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1); - - // Item usage - item.hasUses = item.data.uses && (item.data.uses.max > 0); - item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false); - item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0)); - item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type)); - - // Item toggle state - this._prepareItemToggleState(item); - - // Classify items into types - if ( item.type === "power" ) arr[1].push(item); - else if ( item.type === "feat" ) arr[2].push(item); - else if ( item.type === "class" ) arr[3].push(item); - else if ( item.type === "species" ) arr[4].push(item); - else if ( item.type === "archetype" ) arr[5].push(item); - else if ( item.type === "classfeature" ) arr[6].push(item); - else if ( item.type === "background" ) arr[7].push(item); - else if ( item.type === "fightingstyle" ) arr[8].push(item); - else if ( item.type === "fightingmastery" ) arr[9].push(item); - else if ( item.type === "lightsaberform" ) arr[10].push(item); - else if ( Object.keys(inventory).includes(item.type ) ) arr[0].push(item); - return arr; - }, [[], [], [], [], [], [], [], [], [], [], []]); - - // Apply active item filters - items = this._filterItems(items, this._filters.inventory); - powers = this._filterItems(powers, this._filters.powerbook); - feats = this._filterItems(feats, this._filters.features); - - // Organize items - for ( let i of items ) { - i.data.quantity = i.data.quantity || 0; - i.data.weight = i.data.weight || 0; - i.totalWeight = Math.round(i.data.quantity * i.data.weight * 10) / 10; - inventory[i.type].items.push(i); - } - - // Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...) - const powerbook = this._preparePowerbook(data, powers); - const nPrepared = powers.filter(s => { - return (s.data.level > 0) && (s.data.preparation.mode === "prepared") && s.data.preparation.prepared; - }).length; - - // Organize Features - const features = { - classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true }, - classfeatures: { label: "SW5E.ItemTypeClassFeats", items: [], hasActions: true, dataset: {type: "classfeature"}, isClassfeature: true }, - archetype: { label: "SW5E.ItemTypeArchetype", items: [], hasActions: false, dataset: {type: "archetype"}, isArchetype: true }, - species: { label: "SW5E.ItemTypeSpecies", items: [], hasActions: false, dataset: {type: "species"}, isSpecies: true }, - background: { label: "SW5E.ItemTypeBackground", items: [], hasActions: false, dataset: {type: "background"}, isBackground: true }, - fightingstyles: { label: "SW5E.ItemTypeFightingStylePl", items: [], hasActions: false, dataset: {type: "fightingstyle"}, isFightingstyle: true }, - fightingmasteries: { label: "SW5E.ItemTypeFightingMasteryPl", items: [], hasActions: false, dataset: {type: "fightingmastery"}, isFightingmastery: true }, - lightsaberforms: { label: "SW5E.ItemTypeLightsaberFormPl", items: [], hasActions: false, dataset: {type: "lightsaberform"}, isLightsaberform: true }, - active: { label: "SW5E.FeatureActive", items: [], hasActions: true, dataset: {type: "feat", "activation.type": "action"} }, - passive: { label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"} } - }; - for ( let f of feats ) { - if ( f.data.activation.type ) features.active.items.push(f); - else features.passive.items.push(f); - } - 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; - - // Assign and return - data.inventory = Object.values(inventory); - data.powerbook = powerbook; - data.preparedPowers = nPrepared; - data.features = Object.values(features); - } - - /* -------------------------------------------- */ - - /** - * A helper method to establish the displayed preparation state for an item - * @param {Item} item - * @private - */ - _prepareItemToggleState(item) { - if (item.type === "power") { - const isAlways = getProperty(item.data, "preparation.mode") === "always"; - const isPrepared = getProperty(item.data, "preparation.prepared"); - item.toggleClass = isPrepared ? "active" : ""; - if ( isAlways ) item.toggleClass = "fixed"; - if ( isAlways ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always; - else if ( isPrepared ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared; - else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared"); - } - else { - const isActive = getProperty(item.data, "equipped"); - item.toggleClass = isActive ? "active" : ""; - item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped"); - } - } - - /* -------------------------------------------- */ - /* Event Listeners and Handlers - /* -------------------------------------------- */ - - /** - * Activate event listeners using the prepared sheet HTML - * @param html {HTML} The prepared HTML object ready to be rendered into the DOM - */ - activateListeners(html) { - 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)); - - // Short and Long Rest - html.find('.short-rest').click(this._onShortRest.bind(this)); - html.find('.long-rest').click(this._onLongRest.bind(this)); - - // Rollable sheet actions - html.find(".rollable[data-action]").click(this._onSheetAction.bind(this)); - - // Send Languages to Chat onClick - html.find('[data-options="share-languages"]').click(event => { - event.preventDefault(); - let langs = this.actor.data.data.traits.languages.value.map(l => SW5E.languages[l] || l).join(", "); - let custom = this.actor.data.data.traits.languages.custom; - if (custom) langs += ", " + custom.replace(/;/g, ","); - let content = ` -
Are you sure you want to delete ${item.data.name}?
`, - buttons: { - Yes: { - icon: '', - label: 'Yes', - callback: dlg => { - this.actor.deleteOwnedItem(itemId); - } - }, - cancel: { - icon: '', - label: 'No' - }, - }, - default: 'cancel' - }).render(true); - }); - } - - /* -------------------------------------------- */ - - /** - * Handle mouse click events for character sheet actions - * @param {MouseEvent} event The originating click event - * @private - */ - _onSheetAction(event) { - event.preventDefault(); - const button = event.currentTarget; - switch( button.dataset.action ) { - case "rollDeathSave": - return this.actor.rollDeathSave({event: event}); - case "rollInitiative": - return this.actor.rollInitiative({createCombatants: true}); - } - } - - /* -------------------------------------------- */ - - - /** - * Handle toggling the state of an Owned Item within the Actor - * @param {Event} event The triggering click event - * @private - */ - _onToggleItem(event) { - event.preventDefault(); - const itemId = event.currentTarget.closest(".item").dataset.itemId; - const item = this.actor.getOwnedItem(itemId); - const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped"; - return item.update({[attr]: !getProperty(item.data, attr)}); - } - - /* -------------------------------------------- */ - - /** - * Take a short rest, calling the relevant function on the Actor instance - * @param {Event} event The triggering click event - * @private - */ - async _onShortRest(event) { - event.preventDefault(); - await this._onSubmit(event); - return this.actor.shortRest(); - } - - /* -------------------------------------------- */ - - /** - * Take a long rest, calling the relevant function on the Actor instance - * @param {Event} event The triggering click event - * @private - */ - async _onLongRest(event) { - event.preventDefault(); - await this._onSubmit(event); - return this.actor.longRest(); - } - - /* -------------------------------------------- */ - - /** @override */ - async _onDropItemCreate(itemData) { - - // Increment the number of class levels a character instead of creating a new item - if ( itemData.type === "class" ) { - const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name); - let priorLevel = cls?.data.data.levels ?? 0; - if ( !!cls ) { - const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level); - if ( next > priorLevel ) { - itemData.levels = next; - return cls.update({"data.levels": next}); - } - } - } - - // Default drop handling if levels were not added - super._onDropItemCreate(itemData); - } -} -async function addFavorites(app, html, data) { - // Thisfunction is adapted for the SwaltSheet from the Favorites Item - // Tab Module created for Foundry VTT - by Felix Müller (Felix#6196 on Discord). - // It is licensed under a Creative Commons Attribution 4.0 International License - // and can be found at https://github.com/syl3r86/favtab. - let favItems = []; - let favFeats = []; - let favPowers = { - 0: { - isCantrip: true, - powers: [] - }, - 1: { - powers: [], - value: data.actor.data.powers.power1.value, - max: data.actor.data.powers.power1.max - }, - 2: { - powers: [], - value: data.actor.data.powers.power2.value, - max: data.actor.data.powers.power2.max - }, - 3: { - powers: [], - value: data.actor.data.powers.power3.value, - max: data.actor.data.powers.power3.max - }, - 4: { - powers: [], - value: data.actor.data.powers.power4.value, - max: data.actor.data.powers.power4.max - }, - 5: { - powers: [], - value: data.actor.data.powers.power5.value, - max: data.actor.data.powers.power5.max - }, - 6: { - powers: [], - value: data.actor.data.powers.power6.value, - max: data.actor.data.powers.power6.max - }, - 7: { - powers: [], - value: data.actor.data.powers.power7.value, - max: data.actor.data.powers.power7.max - }, - 8: { - powers: [], - value: data.actor.data.powers.power8.value, - max: data.actor.data.powers.power8.max - }, - 9: { - powers: [], - value: data.actor.data.powers.power9.value, - max: data.actor.data.powers.power9.max - } - } - - let powerCount = 0 - let items = data.actor.items; - for (let item of items) { - if (item.type == "class") continue; - if (item.flags.favtab === undefined || item.flags.favtab.isFavourite === undefined) { - item.flags.favtab = { - isFavourite: false - }; - } - let isFav = item.flags.favtab.isFavourite; - if (app.options.editable) { - let favBtn = $(``); - favBtn.click(ev => { - app.actor.getOwnedItem(item._id).update({ - "flags.favtab.isFavourite": !item.flags.favtab.isFavourite - }); - }); - html.find(`.item[data-item-id="${item._id}"]`).find('.item-controls').prepend(favBtn); - } - - if (isFav) { - item.powerComps = ""; - if (item.data.components) { - let comps = item.data.components; - let v = (comps.vocal) ? "V" : ""; - let s = (comps.somatic) ? "S" : ""; - let m = (comps.material) ? "M" : ""; - let c = (comps.concentration) ? true : false; - let r = (comps.ritual) ? true : false; - item.powerComps = `${v}${s}${m}`; - item.powerCon = c; - item.powerRit = r; - } - - item.editable = app.options.editable; - switch (item.type) { - case 'feat': - if (item.flags.favtab.sort === undefined) { - item.flags.favtab.sort = (favFeats.count + 1) * 100000; // initial sort key if not present - } - favFeats.push(item); - break; - case 'power': - if (item.data.preparation.mode) { - item.powerPrepMode = ` (${CONFIG.SW5E.powerPreparationModes[item.data.preparation.mode]})` - } - if (item.data.level) { - favPowers[item.data.level].powers.push(item); - } else { - favPowers[0].powers.push(item); - } - powerCount++; - break; - default: - if (item.flags.favtab.sort === undefined) { - item.flags.favtab.sort = (favItems.count + 1) * 100000; // initial sort key if not present - } - favItems.push(item); - break; - } - } - } - - // Alter core CSS to fit new button - // if (app.options.editable) { - // html.find('.powerbook .item-controls').css('flex', '0 0 88px'); - // html.find('.inventory .item-controls, .features .item-controls').css('flex', '0 0 90px'); - // html.find('.favourite .item-controls').css('flex', '0 0 22px'); - // } - - let tabContainer = html.find('.favtabtarget'); - data.favItems = favItems.length > 0 ? favItems.sort((a, b) => (a.flags.favtab.sort) - (b.flags.favtab.sort)) : false; - data.favFeats = favFeats.length > 0 ? favFeats.sort((a, b) => (a.flags.favtab.sort) - (b.flags.favtab.sort)) : false; - data.favPowers = powerCount > 0 ? favPowers : false; - data.editable = app.options.editable; - - await loadTemplates(['systems/sw5e/templates/actors/newActor/item.hbs']); - let favtabHtml = $(await renderTemplate('systems/sw5e/templates/actors/newActor/template.hbs', data)); - favtabHtml.find('.item-name h4').click(event => app._onItemSummary(event)); - - if (app.options.editable) { - favtabHtml.find('.item-image').click(ev => app._onItemRoll(ev)); - let handler = ev => app._onDragStart(ev); - favtabHtml.find('.item').each((i, li) => { - if (li.classList.contains("inventory-header")) return; - li.setAttribute("draggable", true); - li.addEventListener("dragstart", handler, false); - }); - //favtabHtml.find('.item-toggle').click(event => app._onToggleItem(event)); - favtabHtml.find('.item-edit').click(ev => { - let itemId = $(ev.target).parents('.item')[0].dataset.itemId; - app.actor.getOwnedItem(itemId).sheet.render(true); - }); - favtabHtml.find('.item-fav').click(ev => { - let itemId = $(ev.target).parents('.item')[0].dataset.itemId; - let val = !app.actor.getOwnedItem(itemId).data.flags.favtab.isFavourite - app.actor.getOwnedItem(itemId).update({ - "flags.favtab.isFavourite": val - }); - }); - - // Sorting - favtabHtml.find('.item').on('drop', ev => { - ev.preventDefault(); - ev.stopPropagation(); - - let dropData = JSON.parse(ev.originalEvent.dataTransfer.getData('text/plain')); - // if (dropData.actorId !== app.actor.id || dropData.data.type === 'power') return; - if (dropData.actorId !== app.actor.id) return; - let list = null; - if (dropData.data.type === 'feat') list = favFeats; - else list = favItems; - let dragSource = list.find(i => i._id === dropData.data._id); - let siblings = list.filter(i => i._id !== dropData.data._id); - let targetId = ev.target.closest('.item').dataset.itemId; - let dragTarget = siblings.find(s => s._id === targetId); - - if (dragTarget === undefined) return; - const sortUpdates = SortingHelpers.performIntegerSort(dragSource, { - target: dragTarget, - siblings: siblings, - sortKey: 'flags.favtab.sort' - }); - const updateData = sortUpdates.map(u => { - const update = u.update; - update._id = u.target._id; - return update; - }); - app.actor.updateEmbeddedEntity("OwnedItem", updateData); - }); - } - tabContainer.append(favtabHtml); - // if(app.options.editable) { - // let handler = ev => app._onDragItemStart(ev); - // tabContainer.find('.item').each((i, li) => { - // if (li.classList.contains("inventory-header")) return; - // li.setAttribute("draggable", true); - // li.addEventListener("dragstart", handler, false); - // }); - //} - // try { - // if (game.modules.get("betterrolls5e") && game.modules.get("betterrolls5e").active) BetterRolls.addItemContent(app.object, favtabHtml, ".item .item-name h4", ".item-properties", ".item > .rollable div"); - // } - // catch (err) { - // // Better Rolls not found! - // } - Hooks.callAll("renderedSwaltSheet", app, html, data); -} -async function addSubTabs(app, html, data) { - if(data.options.subTabs == null) { - //let subTabs = []; //{subgroup: '', target: '', active: false} - data.options.subTabs = {}; - html.find('[data-subgroup-selection] [data-subgroup]').each((idx, el) => { - let subgroup = el.getAttribute('data-subgroup'); - let target = el.getAttribute('data-target'); - let targetObj = {target: target, active: el.classList.contains("active")} - if(data.options.subTabs.hasOwnProperty(subgroup)) { - data.options.subTabs[subgroup].push(targetObj); - } else { - data.options.subTabs[subgroup] = []; - data.options.subTabs[subgroup].push(targetObj); - } - }) - } - - for(const group in data.options.subTabs) { - data.options.subTabs[group].forEach(tab => { - if(tab.active) { - html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).addClass('active'); - } else { - html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).removeClass('active'); - } - }) - } - - html.find('[data-subgroup-selection]').children().on('click', event => { - let subgroup = event.target.closest('[data-subgroup]').getAttribute('data-subgroup'); - let target = event.target.closest('[data-target]').getAttribute('data-target'); - html.find(`[data-subgroup=${subgroup}]`).removeClass('active'); - html.find(`[data-subgroup=${subgroup}][data-target=${target}]`).addClass('active'); - let tabId = data.options.subTabs[subgroup].find(tab => { - return tab.target == target - }); - data.options.subTabs[subgroup].map(el => { - if(el.target == target) { - el.active = true; - } else { - el.active = false; - } - return el; - }) - - }) - - - -} - -Hooks.on("renderActorSheet5eCharacterNew", (app, html, data) => { - addFavorites(app, html, data); - addSubTabs(app, html, data); +import ActorSheet5e from "./base.js"; +import Actor5e from "../../entity.js"; + +/** + * An Actor sheet for player character type actors in the SW5E system. + * Extends the base ActorSheet5e class. + * @type {ActorSheet5e} + */ +export default class ActorSheet5eCharacterNew extends ActorSheet5e { + + get template() { + if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html"; + return "systems/sw5e/templates/actors/newActor/character-sheet.html"; + } + /** + * Define default rendering options for the NPC sheet + * @return {Object} + */ + static get defaultOptions() { + + return mergeObject(super.defaultOptions, { + classes: ["swalt", "sw5e", "sheet", "actor", "character"], + blockFavTab: true, + subTabs: null, + width: 800, + tabs: [{ + navSelector: ".root-tabs", + contentSelector: ".sheet-body", + initial: "attributes" + }], + }); + } + + /* -------------------------------------------- */ + + /** + * Add some extra data when rendering the sheet to reduce the amount of logic required within the template. + */ + getData() { + const sheetData = super.getData(); + + // Temporary HP + let hp = sheetData.data.attributes.hp; + if (hp.temp === 0) delete hp.temp; + if (hp.tempmax === 0) delete hp.tempmax; + + // Resources + sheetData["resources"] = ["primary", "secondary", "tertiary"].reduce((arr, r) => { + const res = sheetData.data.resources[r] || {}; + res.name = r; + res.placeholder = game.i18n.localize("SW5E.Resource"+r.titleCase()); + if (res && res.value === 0) delete res.value; + if (res && res.max === 0) delete res.max; + return arr.concat([res]); + }, []); + + // Experience Tracking + sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking"); + sheetData["classLabels"] = this.actor.itemTypes.class.map(c => c.name).join(", "); + + // Return data for rendering + return sheetData; + } + + /* -------------------------------------------- */ + + /** + * Organize and classify Owned Items for Character sheets + * @private + */ + _prepareItems(data) { + + // Categorize items as inventory, powerbook, features, and classes + const inventory = { + weapon: { label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"} }, + equipment: { label: "SW5E.ItemTypeEquipmentPl", items: [], dataset: {type: "equipment"} }, + consumable: { label: "SW5E.ItemTypeConsumablePl", items: [], dataset: {type: "consumable"} }, + tool: { label: "SW5E.ItemTypeToolPl", items: [], dataset: {type: "tool"} }, + backpack: { label: "SW5E.ItemTypeContainerPl", items: [], dataset: {type: "backpack"} }, + loot: { label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"} } + }; + + // Partition items by category + let [items, powers, feats, classes, species, archetypes, classfeatures, backgrounds, fightingstyles, fightingmasteries, lightsaberforms] = data.items.reduce((arr, item) => { + + // Item details + item.img = item.img || DEFAULT_TOKEN; + item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1); + + // Item usage + item.hasUses = item.data.uses && (item.data.uses.max > 0); + item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false); + item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0)); + item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type)); + + // Item toggle state + this._prepareItemToggleState(item); + + // Classify items into types + if ( item.type === "power" ) arr[1].push(item); + else if ( item.type === "feat" ) arr[2].push(item); + else if ( item.type === "class" ) arr[3].push(item); + else if ( item.type === "species" ) arr[4].push(item); + else if ( item.type === "archetype" ) arr[5].push(item); + else if ( item.type === "classfeature" ) arr[6].push(item); + else if ( item.type === "background" ) arr[7].push(item); + else if ( item.type === "fightingstyle" ) arr[8].push(item); + else if ( item.type === "fightingmastery" ) arr[9].push(item); + else if ( item.type === "lightsaberform" ) arr[10].push(item); + else if ( Object.keys(inventory).includes(item.type ) ) arr[0].push(item); + return arr; + }, [[], [], [], [], [], [], [], [], [], [], []]); + + // Apply active item filters + items = this._filterItems(items, this._filters.inventory); + powers = this._filterItems(powers, this._filters.powerbook); + feats = this._filterItems(feats, this._filters.features); + + // Organize items + for ( let i of items ) { + i.data.quantity = i.data.quantity || 0; + i.data.weight = i.data.weight || 0; + i.totalWeight = Math.round(i.data.quantity * i.data.weight * 10) / 10; + inventory[i.type].items.push(i); + } + + // Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...) + const powerbook = this._preparePowerbook(data, powers); + const nPrepared = powers.filter(s => { + return (s.data.level > 0) && (s.data.preparation.mode === "prepared") && s.data.preparation.prepared; + }).length; + + // Organize Features + const features = { + classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true }, + classfeatures: { label: "SW5E.ItemTypeClassFeats", items: [], hasActions: true, dataset: {type: "classfeature"}, isClassfeature: true }, + archetype: { label: "SW5E.ItemTypeArchetype", items: [], hasActions: false, dataset: {type: "archetype"}, isArchetype: true }, + species: { label: "SW5E.ItemTypeSpecies", items: [], hasActions: false, dataset: {type: "species"}, isSpecies: true }, + background: { label: "SW5E.ItemTypeBackground", items: [], hasActions: false, dataset: {type: "background"}, isBackground: true }, + fightingstyles: { label: "SW5E.ItemTypeFightingStylePl", items: [], hasActions: false, dataset: {type: "fightingstyle"}, isFightingstyle: true }, + fightingmasteries: { label: "SW5E.ItemTypeFightingMasteryPl", items: [], hasActions: false, dataset: {type: "fightingmastery"}, isFightingmastery: true }, + lightsaberforms: { label: "SW5E.ItemTypeLightsaberFormPl", items: [], hasActions: false, dataset: {type: "lightsaberform"}, isLightsaberform: true }, + active: { label: "SW5E.FeatureActive", items: [], hasActions: true, dataset: {type: "feat", "activation.type": "action"} }, + passive: { label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"} } + }; + for ( let f of feats ) { + if ( f.data.activation.type ) features.active.items.push(f); + else features.passive.items.push(f); + } + 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; + + // Assign and return + data.inventory = Object.values(inventory); + data.powerbook = powerbook; + data.preparedPowers = nPrepared; + data.features = Object.values(features); + } + + /* -------------------------------------------- */ + + /** + * A helper method to establish the displayed preparation state for an item + * @param {Item} item + * @private + */ + _prepareItemToggleState(item) { + if (item.type === "power") { + const isAlways = getProperty(item.data, "preparation.mode") === "always"; + const isPrepared = getProperty(item.data, "preparation.prepared"); + item.toggleClass = isPrepared ? "active" : ""; + if ( isAlways ) item.toggleClass = "fixed"; + if ( isAlways ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always; + else if ( isPrepared ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared; + else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared"); + } + else { + const isActive = getProperty(item.data, "equipped"); + item.toggleClass = isActive ? "active" : ""; + item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped"); + } + } + + /* -------------------------------------------- */ + /* Event Listeners and Handlers + /* -------------------------------------------- */ + + /** + * Activate event listeners using the prepared sheet HTML + * @param html {HTML} The prepared HTML object ready to be rendered into the DOM + */ + activateListeners(html) { + 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)); + + // Short and Long Rest + html.find('.short-rest').click(this._onShortRest.bind(this)); + html.find('.long-rest').click(this._onLongRest.bind(this)); + + // Rollable sheet actions + html.find(".rollable[data-action]").click(this._onSheetAction.bind(this)); + + // Send Languages to Chat onClick + html.find('[data-options="share-languages"]').click(event => { + event.preventDefault(); + let langs = this.actor.data.data.traits.languages.value.map(l => SW5E.languages[l] || l).join(", "); + let custom = this.actor.data.data.traits.languages.custom; + if (custom) langs += ", " + custom.replace(/;/g, ","); + let content = ` +Are you sure you want to delete ${item.data.name}?
`, + buttons: { + Yes: { + icon: '', + label: 'Yes', + callback: dlg => { + this.actor.deleteOwnedItem(itemId); + } + }, + cancel: { + icon: '', + label: 'No' + }, + }, + default: 'cancel' + }).render(true); + }); + } + + /* -------------------------------------------- */ + + /** + * Handle mouse click events for character sheet actions + * @param {MouseEvent} event The originating click event + * @private + */ + _onSheetAction(event) { + event.preventDefault(); + const button = event.currentTarget; + switch( button.dataset.action ) { + case "rollDeathSave": + return this.actor.rollDeathSave({event: event}); + case "rollInitiative": + return this.actor.rollInitiative({createCombatants: true}); + } + } + + /* -------------------------------------------- */ + + + /** + * Handle toggling the state of an Owned Item within the Actor + * @param {Event} event The triggering click event + * @private + */ + _onToggleItem(event) { + event.preventDefault(); + const itemId = event.currentTarget.closest(".item").dataset.itemId; + const item = this.actor.getOwnedItem(itemId); + const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped"; + return item.update({[attr]: !getProperty(item.data, attr)}); + } + + /* -------------------------------------------- */ + + /** + * Take a short rest, calling the relevant function on the Actor instance + * @param {Event} event The triggering click event + * @private + */ + async _onShortRest(event) { + event.preventDefault(); + await this._onSubmit(event); + return this.actor.shortRest(); + } + + /* -------------------------------------------- */ + + /** + * Take a long rest, calling the relevant function on the Actor instance + * @param {Event} event The triggering click event + * @private + */ + async _onLongRest(event) { + event.preventDefault(); + await this._onSubmit(event); + return this.actor.longRest(); + } + + /* -------------------------------------------- */ + + /** @override */ + async _onDropItemCreate(itemData) { + + // Increment the number of class levels a character instead of creating a new item + if ( itemData.type === "class" ) { + const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name); + let priorLevel = cls?.data.data.levels ?? 0; + if ( !!cls ) { + const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level); + if ( next > priorLevel ) { + itemData.levels = next; + return cls.update({"data.levels": next}); + } + } + } + + // Default drop handling if levels were not added + super._onDropItemCreate(itemData); + } +} +async function addFavorites(app, html, data) { + // Thisfunction is adapted for the SwaltSheet from the Favorites Item + // Tab Module created for Foundry VTT - by Felix Müller (Felix#6196 on Discord). + // It is licensed under a Creative Commons Attribution 4.0 International License + // and can be found at https://github.com/syl3r86/favtab. + let favItems = []; + let favFeats = []; + let favPowers = { + 0: { + isCantrip: true, + powers: [] + }, + 1: { + powers: [], + value: data.actor.data.powers.power1.value, + max: data.actor.data.powers.power1.max + }, + 2: { + powers: [], + value: data.actor.data.powers.power2.value, + max: data.actor.data.powers.power2.max + }, + 3: { + powers: [], + value: data.actor.data.powers.power3.value, + max: data.actor.data.powers.power3.max + }, + 4: { + powers: [], + value: data.actor.data.powers.power4.value, + max: data.actor.data.powers.power4.max + }, + 5: { + powers: [], + value: data.actor.data.powers.power5.value, + max: data.actor.data.powers.power5.max + }, + 6: { + powers: [], + value: data.actor.data.powers.power6.value, + max: data.actor.data.powers.power6.max + }, + 7: { + powers: [], + value: data.actor.data.powers.power7.value, + max: data.actor.data.powers.power7.max + }, + 8: { + powers: [], + value: data.actor.data.powers.power8.value, + max: data.actor.data.powers.power8.max + }, + 9: { + powers: [], + value: data.actor.data.powers.power9.value, + max: data.actor.data.powers.power9.max + } + } + + let powerCount = 0 + let items = data.actor.items; + for (let item of items) { + if (item.type == "class") continue; + if (item.flags.favtab === undefined || item.flags.favtab.isFavourite === undefined) { + item.flags.favtab = { + isFavourite: false + }; + } + let isFav = item.flags.favtab.isFavourite; + if (app.options.editable) { + let favBtn = $(``); + favBtn.click(ev => { + app.actor.getOwnedItem(item._id).update({ + "flags.favtab.isFavourite": !item.flags.favtab.isFavourite + }); + }); + html.find(`.item[data-item-id="${item._id}"]`).find('.item-controls').prepend(favBtn); + } + + if (isFav) { + item.powerComps = ""; + if (item.data.components) { + let comps = item.data.components; + let v = (comps.vocal) ? "V" : ""; + let s = (comps.somatic) ? "S" : ""; + let m = (comps.material) ? "M" : ""; + let c = (comps.concentration) ? true : false; + let r = (comps.ritual) ? true : false; + item.powerComps = `${v}${s}${m}`; + item.powerCon = c; + item.powerRit = r; + } + + item.editable = app.options.editable; + switch (item.type) { + case 'feat': + if (item.flags.favtab.sort === undefined) { + item.flags.favtab.sort = (favFeats.count + 1) * 100000; // initial sort key if not present + } + favFeats.push(item); + break; + case 'power': + if (item.data.preparation.mode) { + item.powerPrepMode = ` (${CONFIG.SW5E.powerPreparationModes[item.data.preparation.mode]})` + } + if (item.data.level) { + favPowers[item.data.level].powers.push(item); + } else { + favPowers[0].powers.push(item); + } + powerCount++; + break; + default: + if (item.flags.favtab.sort === undefined) { + item.flags.favtab.sort = (favItems.count + 1) * 100000; // initial sort key if not present + } + favItems.push(item); + break; + } + } + } + + // Alter core CSS to fit new button + // if (app.options.editable) { + // html.find('.powerbook .item-controls').css('flex', '0 0 88px'); + // html.find('.inventory .item-controls, .features .item-controls').css('flex', '0 0 90px'); + // html.find('.favourite .item-controls').css('flex', '0 0 22px'); + // } + + let tabContainer = html.find('.favtabtarget'); + data.favItems = favItems.length > 0 ? favItems.sort((a, b) => (a.flags.favtab.sort) - (b.flags.favtab.sort)) : false; + data.favFeats = favFeats.length > 0 ? favFeats.sort((a, b) => (a.flags.favtab.sort) - (b.flags.favtab.sort)) : false; + data.favPowers = powerCount > 0 ? favPowers : false; + data.editable = app.options.editable; + + await loadTemplates(['systems/sw5e/templates/actors/newActor/item.hbs']); + let favtabHtml = $(await renderTemplate('systems/sw5e/templates/actors/newActor/template.hbs', data)); + favtabHtml.find('.item-name h4').click(event => app._onItemSummary(event)); + + if (app.options.editable) { + favtabHtml.find('.item-image').click(ev => app._onItemRoll(ev)); + let handler = ev => app._onDragStart(ev); + favtabHtml.find('.item').each((i, li) => { + if (li.classList.contains("inventory-header")) return; + li.setAttribute("draggable", true); + li.addEventListener("dragstart", handler, false); + }); + //favtabHtml.find('.item-toggle').click(event => app._onToggleItem(event)); + favtabHtml.find('.item-edit').click(ev => { + let itemId = $(ev.target).parents('.item')[0].dataset.itemId; + app.actor.getOwnedItem(itemId).sheet.render(true); + }); + favtabHtml.find('.item-fav').click(ev => { + let itemId = $(ev.target).parents('.item')[0].dataset.itemId; + let val = !app.actor.getOwnedItem(itemId).data.flags.favtab.isFavourite + app.actor.getOwnedItem(itemId).update({ + "flags.favtab.isFavourite": val + }); + }); + + // Sorting + favtabHtml.find('.item').on('drop', ev => { + ev.preventDefault(); + ev.stopPropagation(); + + let dropData = JSON.parse(ev.originalEvent.dataTransfer.getData('text/plain')); + // if (dropData.actorId !== app.actor.id || dropData.data.type === 'power') return; + if (dropData.actorId !== app.actor.id) return; + let list = null; + if (dropData.data.type === 'feat') list = favFeats; + else list = favItems; + let dragSource = list.find(i => i._id === dropData.data._id); + let siblings = list.filter(i => i._id !== dropData.data._id); + let targetId = ev.target.closest('.item').dataset.itemId; + let dragTarget = siblings.find(s => s._id === targetId); + + if (dragTarget === undefined) return; + const sortUpdates = SortingHelpers.performIntegerSort(dragSource, { + target: dragTarget, + siblings: siblings, + sortKey: 'flags.favtab.sort' + }); + const updateData = sortUpdates.map(u => { + const update = u.update; + update._id = u.target._id; + return update; + }); + app.actor.updateEmbeddedEntity("OwnedItem", updateData); + }); + } + tabContainer.append(favtabHtml); + // if(app.options.editable) { + // let handler = ev => app._onDragItemStart(ev); + // tabContainer.find('.item').each((i, li) => { + // if (li.classList.contains("inventory-header")) return; + // li.setAttribute("draggable", true); + // li.addEventListener("dragstart", handler, false); + // }); + //} + // try { + // if (game.modules.get("betterrolls5e") && game.modules.get("betterrolls5e").active) BetterRolls.addItemContent(app.object, favtabHtml, ".item .item-name h4", ".item-properties", ".item > .rollable div"); + // } + // catch (err) { + // // Better Rolls not found! + // } + Hooks.callAll("renderedSwaltSheet", app, html, data); +} +async function addSubTabs(app, html, data) { + if(data.options.subTabs == null) { + //let subTabs = []; //{subgroup: '', target: '', active: false} + data.options.subTabs = {}; + html.find('[data-subgroup-selection] [data-subgroup]').each((idx, el) => { + let subgroup = el.getAttribute('data-subgroup'); + let target = el.getAttribute('data-target'); + let targetObj = {target: target, active: el.classList.contains("active")} + if(data.options.subTabs.hasOwnProperty(subgroup)) { + data.options.subTabs[subgroup].push(targetObj); + } else { + data.options.subTabs[subgroup] = []; + data.options.subTabs[subgroup].push(targetObj); + } + }) + } + + for(const group in data.options.subTabs) { + data.options.subTabs[group].forEach(tab => { + if(tab.active) { + html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).addClass('active'); + } else { + html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).removeClass('active'); + } + }) + } + + html.find('[data-subgroup-selection]').children().on('click', event => { + let subgroup = event.target.closest('[data-subgroup]').getAttribute('data-subgroup'); + let target = event.target.closest('[data-target]').getAttribute('data-target'); + html.find(`[data-subgroup=${subgroup}]`).removeClass('active'); + html.find(`[data-subgroup=${subgroup}][data-target=${target}]`).addClass('active'); + let tabId = data.options.subTabs[subgroup].find(tab => { + return tab.target == target + }); + data.options.subTabs[subgroup].map(el => { + if(el.target == target) { + el.active = true; + } else { + el.active = false; + } + return el; + }) + + }) + + + +} + +Hooks.on("renderActorSheet5eCharacterNew", (app, html, data) => { + addFavorites(app, html, data); + addSubTabs(app, html, data); }); \ No newline at end of file diff --git a/module/actor/sheets/oldSheets/character.js b/module/actor/sheets/oldSheets/character.js index f9d50806..51e181cf 100644 --- a/module/actor/sheets/oldSheets/character.js +++ b/module/actor/sheets/oldSheets/character.js @@ -292,4 +292,4 @@ export default class ActorSheet5eCharacter extends ActorSheet5e { // Default drop handling if levels were not added super._onDropItemCreate(itemData); } -} +} \ No newline at end of file