diff --git a/src/lang/en.json b/src/lang/en.json index a0e20e5a..07d30979 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -324,6 +324,7 @@ "DL.ItemShowInfo": "Item Info", "DL.ItemRollText": "Item:", "DL.ItemMaxUsesReached": "Maximum uses of the item reached", + "DL.ContentsTitle": "Contents", "DL.WealthTitle": "Wealth", "DL.WealthLifestyle": "Lifestyle: ", "DL.WealthCoinsGC": "GC: ", diff --git a/src/module/item/item.js b/src/module/item/item.js index 24288fed..15ffed1d 100644 --- a/src/module/item/item.js +++ b/src/module/item/item.js @@ -16,10 +16,10 @@ export class DemonlordItem extends Item { _onUpdate(changed, options, userId) { super._onUpdate(changed, options, userId) // Search for open path/ancestry/role sheets and re-render them. This allows the nested objects to fetch new values - if (!['path', 'ancestry', 'creaturerole'].includes(this.type)) { + if (!['path', 'ancestry', 'creaturerole', 'item'].includes(this.type)) { // eslint-disable-next-line no-prototype-builtins let openSheets = Object.entries(ui.windows).map(i => i[1]).filter(i => Item.prototype.isPrototypeOf(i.object)) - openSheets = openSheets.filter(s => ['path', 'ancestry', 'creaturerole'].includes(s.object.type)) + openSheets = openSheets.filter(s => ['path', 'ancestry', 'creaturerole', 'item'].includes(s.object.type)) openSheets.forEach(s => s.render()) } } @@ -72,4 +72,12 @@ export class DemonlordItem extends Item { hasHealing() { return this.system.healing?.healing ?? false } + + sameItem(item) { + const sources = [this.uuid, this._id] + if (this.flags?.core?.sourceId != undefined) sources.push(this.flags.core.sourceId) + const itemSources = [item.uuid, item._id] + if (item.flags?.core?.sourceId != undefined) itemSources.push(item.flags.core.sourceId) + return (sources.some(r=> itemSources.includes(r))) + } } diff --git a/src/module/item/nested-objects.js b/src/module/item/nested-objects.js index 1dbb347c..922a5167 100644 --- a/src/module/item/nested-objects.js +++ b/src/module/item/nested-objects.js @@ -197,8 +197,14 @@ export async function getNestedItemData(nestedData) { // Remember user selection & enrich description itemData.selected = nestedData.selected + itemData.system.enrichedDescription = await TextEditor.enrichHTML(itemData?.system?.description, { + aync: true}) + itemData.system.enrichedDescriptionUnrolled = await enrichHTMLUnrolled(itemData?.system?.description) + // Keep the quantity previously stored, if any + itemData.system.quantity = nestedData?.system?.quantity ?? itemData.system.quantity + // Return only the data // Warning: here the implicit assertion is that entity is an Item and not an Actor or something else return itemData diff --git a/src/module/item/sheets/base-item-sheet.js b/src/module/item/sheets/base-item-sheet.js index e90a9d10..d212822d 100644 --- a/src/module/item/sheets/base-item-sheet.js +++ b/src/module/item/sheets/base-item-sheet.js @@ -7,6 +7,10 @@ import 'tippy.js/animations/shift-away.css'; import {initDlEditor} from "../../utils/editor"; import {DemonlordItem} from "../item"; import {enrichHTMLUnrolled, i18n} from "../../utils/utils"; +import { + getNestedItemData, + getNestedDocument +} from '../nested-objects'; export default class DLBaseItemSheet extends ItemSheet { /** @override */ @@ -75,6 +79,10 @@ export default class DLBaseItemSheet extends ItemSheet { this.sectionStates = this.sectionStates || new Map() + if (data?.system?.contents != undefined && data.system.contents.length > 0) { + data.system.contents = await Promise.all(data.system.contents.map(getNestedItemData)) + } + return data } @@ -231,6 +239,26 @@ export default class DLBaseItemSheet extends ItemSheet { } else collapsableTitles.removeClass('active') }) + // Contents Quantity Button Info + html.find('.dlToggleInfoBtn').click(ev => { + const root = $(ev.currentTarget).closest('[data-item-id]') + const elem = $(ev.currentTarget) + const selector = '.fa-chevron-down, .fa-chevron-up' + const chevron = elem.is(selector) ? elem : elem.find(selector); + const elements = $(root).find('.dlInfo') + elements.each((_, el) => { + if (el.style.display === 'none') { + $(el).slideDown(100) + chevron?.removeClass('fa-chevron-up') + chevron?.addClass('fa-chevron-down') + } else { + $(el).slideUp(100) + chevron?.removeClass('fa-chevron-down') + chevron?.addClass('fa-chevron-up') + } + }) + }) + // Add drag events. html .find('.drop-area, .dl-drop-zone, .dl-drop-zone *') @@ -238,6 +266,9 @@ export default class DLBaseItemSheet extends ItemSheet { .on('dragleave', await this._onDragLeave.bind(this)) .on('drop', await this._onDrop.bind(this)) + // Create nested items by dropping onto item + this.form.ondrop = ev => this._onDropItem(ev); + // Custom editor initDlEditor(html, this) @@ -245,6 +276,20 @@ export default class DLBaseItemSheet extends ItemSheet { html.find('.create-nested-item').click(async (ev) => await this._onNestedItemCreate(ev)) html.find('.edit-nested-item').click(async (ev) => await this._onNestedItemEdit(ev)) + + // Contents item quantity and delete functions + html.on('mousedown', '.item-uses', async ev => await this._onUpdateContentsItemQuantity(ev)) + html.find('.item-delete').click(async ev => await this._onContentsItemDelete(ev)) + + if (this.object.parent?.isOwner) { + const dragHandler = async ev => await this._onDrag(ev) + html.find('.dl-nested-item').each((i, li) => { + li.setAttribute('draggable', true) + li.addEventListener('dragstart', dragHandler, false) + li.addEventListener('dragend', dragHandler, false) + }) + } + } /* -------------------------------------------- */ @@ -282,6 +327,21 @@ export default class DLBaseItemSheet extends ItemSheet { /* -------------------------------------------- */ + async _onDrag(ev){ + const itemIndex = $(ev.currentTarget).closest('[data-item-index]').data('itemIndex') + const data = await this.getData({}) + if (ev.type == 'dragend') { + if (data.system.contents[itemIndex].system.quantity <= 1) { + await this.deleteContentsItem(itemIndex) + } else { + await this.decreaseContentsItemQuantity(itemIndex) + } + } else if (ev.type == 'dragstart') { + const dragData = { type: 'Item', 'uuid': data.system.contents[itemIndex].uuid } + ev.dataTransfer.setData("text/plain", JSON.stringify(dragData)); + } + } + _onDragOver(ev) { $(ev.originalEvent.target).addClass('drop-hover') } @@ -294,6 +354,52 @@ export default class DLBaseItemSheet extends ItemSheet { $(ev.originalEvent.target).removeClass('drop-hover') } + async _onDropItem(ev) { + if (this.item.system?.contents != undefined){ + try { + const itemData = JSON.parse(ev.dataTransfer.getData('text/plain')) + if (itemData.type === 'Item') { + let actor + const item = await fromUuid(itemData.uuid) + if (!item || !['ammo', 'armor', 'item', 'weapon'].includes(item.type)) return + const itemUpdate = {'_id': item._id} + if (itemData.uuid.startsWith('Actor.')) { + actor = item.parent + /*Since item contents aren't actually embedded we don't want to end up with a reference + to a non-existant item. If our item exists outside of the actor-embedded version, use that as our source. + Otherwise, create it as a world-level item and update the original's core.sourceId flag accordingly.*/ + if (item.flags?.core?.sourceId != undefined) { + game.items.getName(item.name) ? itemData.uuid = game.items.getName(item.name).uuid : itemData.uuid = item.flags.core.sourceId + } else { + const newItem = await this.createNestedItem(duplicate(item), `${actor.name}'s Items`) + itemUpdate['flags.core.sourceId'] = newItem.uuid; + itemData.uuid = newItem.uuid + } + } + if (itemUpdate?.flags?.core?.sourceId == undefined) itemUpdate['flags.core.sourceId'] = itemData.uuid + + //If the item we're adding is the same as the container, bail now + if (this.item.sameItem(item)) { + ui.notifications.warn("Can't put an item inside itself!") + return + } + + //If the item was in an Actor's inventory, update the quantity there + if (actor != undefined) { + if (item.system.quantity > 0) { + itemUpdate['system.quantity'] = item.system.quantity-1; + await actor.updateEmbeddedDocuments('Item', [itemUpdate]) + } + } + + await this.addContentsItem(itemData) + } + } catch (e) { + console.warn(e) + } + } + } + async _onNestedItemCreate(ev) { const type = $(ev.currentTarget).closest('[data-type]').data('type') @@ -305,19 +411,87 @@ export default class DLBaseItemSheet extends ItemSheet { folder = await Folder.create({name:folderName, type: DemonlordItem.documentName}) } - const item = await DemonlordItem.create({ + const item = { name: `New ${type.capitalize()}`, type: type, folder: folder.id, data: {}, - }) + } + await this.createNestedItem(item, folderName) item.sheet.render(true) this.render() return item } // eslint-disable-next-line no-unused-vars - _onNestedItemEdit(ev) { + async _onNestedItemEdit(ev) { + const data = await this.getData({}) + if (!data.item.system.contents) { + return + } + const itemId = $(ev.currentTarget).closest('[data-item-id]').data('itemId') + const nestedData = data.system.contents.find(i => i._id === itemId) + await getNestedDocument(nestedData).then(d => { + if (d.sheet) d.sheet.render(true) + else ui.notifications.warn('The item is not present in the game and cannot be edited.') + }) + } + + async _onUpdateContentsItemQuantity(ev) { + const itemIndex = $(ev.currentTarget).closest('[data-item-index]').data('itemIndex') + + if (ev.button == 0) { + await this.increaseContentsItemQuantity(itemIndex) + } else if (ev.button == 2) { + await this.decreaseContentsItemQuantity(itemIndex) + } + } + + async _onContentsItemDelete(ev) { + const itemIndex = $(ev.currentTarget).closest('[data-item-index]').data('itemIndex') + await this.deleteContentsItem(itemIndex) + + } + + async createNestedItem(itemData, folderName) { + let folder = game.folders.find(f => f.name === folderName) + if (!folder) { + folder = await Folder.create({name:folderName, type: DemonlordItem.documentName}) + } + if (itemData?._id != undefined) delete itemData._id; + itemData.folder = folder._id + + return await DemonlordItem.create(itemData) + } + + async addContentsItem(data) { + const item = await getNestedItemData(data) + const containerData = duplicate(this.item) + containerData.system.contents.push(item) + await this.item.update(containerData, {diff: false}).then(_ => this.render) + } + + async increaseContentsItemQuantity(itemIndex) { + const itemData = duplicate(this.item) + itemData.system.contents[itemIndex].system.quantity++ + await this.item.update(itemData, {diff: false}).then(_ => this.render) + } + + async decreaseContentsItemQuantity(itemIndex) { + const itemData = duplicate(this.item) + if (itemData.system.contents[itemIndex].system.quantity > 0) { + itemData.system.contents[itemIndex].system.quantity-- + await this.item.update(itemData, {diff: false}).then(_ => this.render) + } else { + return + } + } + + async deleteContentsItem(itemIndex) { + const itemData = duplicate(this.item) + + itemData.system.contents.splice(itemIndex, 1) + await this.item.update(itemData) } } diff --git a/src/module/templates.js b/src/module/templates.js index 24826a63..7df38137 100644 --- a/src/module/templates.js +++ b/src/module/templates.js @@ -52,6 +52,7 @@ export const preloadHandlebarsTemplates = async function () { // Item Sheet Partials 'systems/demonlord/templates/item/partial/item-activation.hbs', + 'systems/demonlord/templates/item/partial/item-contents.hbs', 'systems/demonlord/templates/item/partial/item-description.hbs', 'systems/demonlord/templates/item/partial/item-effects.hbs', 'systems/demonlord/templates/item/partial/item-sheet-header.hbs', diff --git a/src/styles/newrules/item-sheet.scss b/src/styles/newrules/item-sheet.scss index b86df847..5b513f01 100644 --- a/src/styles/newrules/item-sheet.scss +++ b/src/styles/newrules/item-sheet.scss @@ -254,6 +254,26 @@ font-size: $f-size-big-slight; } + .dl-item-section-contents { + & > div > b { + display: inline-block; + font-family: $font-primary; + font-weight: bolder; + margin-bottom: 4px; + } + + & > .dl-item-row-header { + display: grid; + grid-template-columns: repeat(12, 1fr); + grid-gap: 16px; + } + + margin: 4px 0; + padding: 0 16px; + font-family: $font-primary; + font-size: $f-size-big-slight; + } + a.create-nested-item { margin-right: 7px; } diff --git a/src/template.json b/src/template.json index 3529de42..e223cc6b 100644 --- a/src/template.json +++ b/src/template.json @@ -238,10 +238,13 @@ "max": 0 } } + }, + "container": { + "contents": [] } }, "item": { - "templates": ["base", "action", "enchantment"], + "templates": ["base", "action", "enchantment", "container"], "quantity": 1, "availability": "", "value": "", diff --git a/src/templates/item/item-item-sheet.hbs b/src/templates/item/item-item-sheet.hbs index 30b43637..8015ad94 100644 --- a/src/templates/item/item-item-sheet.hbs +++ b/src/templates/item/item-item-sheet.hbs @@ -46,6 +46,8 @@
{{/if}} + {{#if system.contents}}{{>"systems/demonlord/templates/item/partial/item-contents.hbs"}}{{/if}} +
{{localize "DL.Availability"}} diff --git a/src/templates/item/partial/item-contents.hbs b/src/templates/item/partial/item-contents.hbs new file mode 100644 index 00000000..93d8db83 --- /dev/null +++ b/src/templates/item/partial/item-contents.hbs @@ -0,0 +1,44 @@ + +
+
{{localize "DL.ContentsTitle"}}
+
+
{{localize "DL.TabsItems"}}
+
{{localize "DL.ItemAmount"}}
+
{{localize "DL.ItemValue"}}
+
+ {{#each item.system.contents as |content index|}} +
+
+
+ + +
+ {{localize "DL.ItemShowInfo"}} +
+
+
+ {{content.system.quantity}} +
+
+ {{defaultValue system.value "―"}} +
+
+ + + +
+
+ {{#if content.system.description}} + + {{/if}} +
+ {{/each}} +
+
+