diff --git a/module/actor/sheets/newSheet/character.js b/module/actor/sheets/newSheet/character.js index 9cd2bf03..a688d98a 100644 --- a/module/actor/sheets/newSheet/character.js +++ b/module/actor/sheets/newSheet/character.js @@ -1,644 +1,644 @@ -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)); - - // Death saving throws - html.find('.death-save').click(this._onDeathSave.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 = ` -
-
- -

Known Languages

-
-
${langs}
-
- `; - - // Send to Chat - let rollWhisper = null; - let rollBlind = false; - let rollMode = game.settings.get("core", "rollMode"); - if (["gmroll", "blindroll"].includes(rollMode)) rollWhisper = ChatMessage.getWhisperIDs("GM"); - if (rollMode === "blindroll") rollBlind = true; - ChatMessage.create({ - user: game.user._id, - content: content, - speaker: { - actor: this.actor._id, - token: this.actor.token, - alias: this.actor.name - }, - type: CONST.CHAT_MESSAGE_TYPES.OTHER - }); - }); - - // Item Delete Confirmation - html.find('.item-delete').off("click"); - html.find('.item-delete').click(event => { - let li = $(event.currentTarget).parents('.item'); - let itemId = li.attr("data-item-id"); - let item = this.actor.getOwnedItem(itemId); - new Dialog({ - title: `Deleting ${item.data.name}`, - 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 rolling a death saving throw for the Character - * @param {MouseEvent} event The originating click event - * @private - */ - _onDeathSave(event) { - event.preventDefault(); - return this.actor.rollDeathSave({event: event}); - } - - /* -------------------------------------------- */ - - - /** - * 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(); - } - - /* -------------------------------------------- */ - - /** - * 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) { - - // 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; - - // Add new features for class level - if ( !classWasAlreadyPresent ) { - Actor5e.getClassFeatures(itemData).then(features => { - this.actor.createEmbeddedEntity("OwnedItem", features); - }); - } - - // If the actor already has the class, increment the level instead of creating a new item - // then add new features as long as level increases - if ( classWasAlreadyPresent ) { - const lvl = cls.data.data.levels; - const newLvl = Math.min(lvl + 1, 20 + lvl - this.actor.data.data.details.level); - if ( !(lvl === newLvl) ) { - cls.update({"data.levels": newLvl}); - itemData.data.levels = newLvl; - Actor5e.getClassFeatures(itemData).then(features => { - this.actor.createEmbeddedEntity("OwnedItem", features); - }); - } - return - } - } - - 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)); + + // Death saving throws + html.find('.death-save').click(this._onDeathSave.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 = ` +
+
+ +

Known Languages

+
+
${langs}
+
+ `; + + // Send to Chat + let rollWhisper = null; + let rollBlind = false; + let rollMode = game.settings.get("core", "rollMode"); + if (["gmroll", "blindroll"].includes(rollMode)) rollWhisper = ChatMessage.getWhisperIDs("GM"); + if (rollMode === "blindroll") rollBlind = true; + ChatMessage.create({ + user: game.user._id, + content: content, + speaker: { + actor: this.actor._id, + token: this.actor.token, + alias: this.actor.name + }, + type: CONST.CHAT_MESSAGE_TYPES.OTHER + }); + }); + + // Item Delete Confirmation + html.find('.item-delete').off("click"); + html.find('.item-delete').click(event => { + let li = $(event.currentTarget).parents('.item'); + let itemId = li.attr("data-item-id"); + let item = this.actor.getOwnedItem(itemId); + new Dialog({ + title: `Deleting ${item.data.name}`, + 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 rolling a death saving throw for the Character + * @param {MouseEvent} event The originating click event + * @private + */ + _onDeathSave(event) { + event.preventDefault(); + return this.actor.rollDeathSave({event: event}); + } + + /* -------------------------------------------- */ + + + /** + * 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(); + } + + /* -------------------------------------------- */ + + /** + * 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) { + + // 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; + + // Add new features for class level + if ( !classWasAlreadyPresent ) { + Actor5e.getClassFeatures(itemData).then(features => { + this.actor.createEmbeddedEntity("OwnedItem", features); + }); + } + + // If the actor already has the class, increment the level instead of creating a new item + // then add new features as long as level increases + if ( classWasAlreadyPresent ) { + const lvl = cls.data.data.levels; + const newLvl = Math.min(lvl + 1, 20 + lvl - this.actor.data.data.details.level); + if ( !(lvl === newLvl) ) { + cls.update({"data.levels": newLvl}); + itemData.data.levels = newLvl; + Actor5e.getClassFeatures(itemData).then(features => { + this.actor.createEmbeddedEntity("OwnedItem", features); + }); + } + return + } + } + + 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/packs/packs/lightsaberforms.db b/packs/packs/lightsaberforms.db index 9cc69b22..4d728794 100644 --- a/packs/packs/lightsaberforms.db +++ b/packs/packs/lightsaberforms.db @@ -1,5 +1,6 @@ {"_id":"BQEDghtJMBfhnnr6","name":"Ysannanite Form","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"lightsaberform","data":{"description":{"value":"

As a part of the bonus action to adopt this form, if you took the Attack action, you can engage in Double- or Two-Weapon Fighting.

\n

Additionally, until the end of your next turn, when making a ranged weapon attack while you are within 5 feet of a hostile creature, you do not have disadvantage on the attack roll.

"},"source":{"value":"Expanded Content"}},"flags":{"core":{"sourceId":"Item.QjBBl7KevWVvKDmy"}},"img":"systems/sw5e/packs/Icons/Lightsaber%20Forms/Ysannanite%20Form.webp","effects":[]} {"_id":"CJQwWFwTc98U4BpX","name":"Niman Form","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"lightsaberform","data":{"description":{"value":"

As a part of the bonus action to adopt this form, if you took the Attack action, you can engage in Double- or Two-Weapon Fighting.

Until the start of your next turn, you can use Wisdom or Charisma instead of Strength or Dexterity for the attack and damage rolls of your melee weapon attacks. You must use the same modifier for both rolls.

"},"source":{"value":"PHB"}},"flags":{},"img":"systems/sw5e/packs/Icons/Lightsaber%20Forms/Niman%20Form.webp","effects":[]} +{"_id":"EMTzwdHtEY41M1RI","name":"Vonil/Ishu Form","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"lightsaberform","data":{"description":{"value":"

If you took the Attack action before adopting this form, you can direct one of your companions to strike a creature you hit with an attack.  Choose a friendly creature within 5 feet of the creature you hit with an attack that can see or hear you. That creature can immediately use its reaction to make one weapon attack against the same creature you hit with an attack.

\n

If you did not take the Attack action before adopting this form, and there is a friendly creature within 15 feet of you, you can move up to 10 feet. You must end this movement within 5 feet of the friendly creature.

"},"source":{"value":"Expanded Content"}},"flags":{"core":{"sourceId":"Item.u7vBOskAb0MsFSlZ"}},"img":"systems/sw5e/packs/Icons/Lightsaber%20Forms/Vonil-Ishu%20Form.webp","effects":[]} {"_id":"GBccAWbpxgNPTz0D","name":"Makashi Form","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"lightsaberform","data":{"description":{"value":"

Until the start of your next turn, when a creature makes a melee weapon attack against you and misses, you can use your reaction to make one melee weapon attack against that creature.

"},"source":{"value":"PHB"}},"flags":{},"img":"systems/sw5e/packs/Icons/Lightsaber%20Forms/Makashi%20Form.webp","effects":[]} {"_id":"OmPlZtMXi2b3Vo64","name":"Jar'Kai Form","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"lightsaberform","data":{"description":{"value":"

As a part of the bonus action to adopt this form, if you took the Attack action, you can engage in Double- or Two-Weapon Fighting.

Each time you hit with an attack on this turn, you can move up to 5 feet without provoking opportunity attacks from the creature you hit.

"},"source":{"value":"PHB"}},"flags":{},"img":"systems/sw5e/packs/Icons/Lightsaber%20Forms/Jar%27Kai%20Form.webp","effects":[]} {"_id":"Xg7qB6sQLw3V6V4p","name":"Sokan Form","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"lightsaberform","data":{"description":{"value":"

Until the start of your next turn, you ignore difficult terrain. Additionally, when an opponent makes a melee weapon attack against you, you can use your reaction to move to another space within 5 feet of that opponent without provoking opportunity attacks, imposing disadvantage on the triggering roll.

"},"source":{"value":"PHB"}},"flags":{},"img":"systems/sw5e/packs/Icons/Lightsaber%20Forms/Sokan%20Form.webp","effects":[]}