diff --git a/dnd5e.css b/dnd5e.css index 44512292b6..61f1d50992 100644 --- a/dnd5e.css +++ b/dnd5e.css @@ -22,6 +22,16 @@ /* ----------------------------------------- */ /* Sheet Styles */ /* ----------------------------------------- */ +:root { + /* ----------------------------------------- */ + /* Encumbrance & Capacity Bars */ + /* ----------------------------------------- */ + --dnd5e-capacity-container-background-color: #7a7971; + --dnd5e-capacity-container-border-color: #191813; + --dnd5e-capacity-background-color: #6c8aa5; + --dnd5e-capacity-border-color: #cde4ff; + --dnd5e-capacity-text-color: #eeeeee; +} /* ----------------------------------------- */ /* Flexbox */ /* ----------------------------------------- */ @@ -1083,95 +1093,6 @@ h5 { flex: 0 0 20px; justify-content: flex-end; } -.dnd5e.sheet.actor .inventory-filters .currency { - flex: 0 0 100%; - list-style: none; - margin: 4px 0 8px; - padding: 0; - font-size: 12px; -} -.dnd5e.sheet.actor .inventory-filters .currency label { - flex: 0; - margin-left: 8px; - text-align: right; - line-height: 20px; - color: #7a7971; -} -.dnd5e.sheet.actor .inventory-filters .currency input[type="text"], -.dnd5e.sheet.actor .inventory-filters .currency input[type="number"] { - flex: 0 0 48px; - text-align: center; - margin-left: 8px; - border-bottom: 2px groove #eeede0; -} -.dnd5e.sheet.actor .inventory-list { - padding: 0 5px; -} -.dnd5e.sheet.actor .inventory-list .item .item-name { - cursor: pointer; -} -.dnd5e.sheet.actor .inventory-list .item .item-name.rollable:hover .item-image { - background-image: url("../../icons/svg/d20-grey.svg") !important; -} -.dnd5e.sheet.actor .inventory-list .item .item-name.rollable .item-image:hover { - background-image: url("../../icons/svg/d20-black.svg") !important; -} -.dnd5e.sheet.actor .inventory-list .item .item-name i.attuned { - color: #7a7971; -} -.dnd5e.sheet.actor .inventory-list .item .item-name i.not-attuned { - color: #44191A; -} -.dnd5e.sheet.actor .inventory-list .item .item-uses input { - width: 24px; - text-align: center; -} -.dnd5e.sheet.actor .inventory-list .item .item-properties { - margin-top: 3px; -} -.dnd5e.sheet.actor .inventory-list .item .item-recharge { - flex: 0 0 80px; - text-align: right; - font-size: 11px; - white-space: nowrap; -} -.dnd5e.sheet.actor .inventory-list .inventory-header .item-controls a.item-create { - flex: 0 0 100%; -} -.dnd5e.sheet.actor .inventory-list .item-detail { - flex: 0 0 70px; - font-size: 12px; - text-align: center; - border-right: 1px solid #c9c7b8; - word-break: break-word; - white-space: nowrap; - overflow: hidden; -} -.dnd5e.sheet.actor .inventory-list .item-detail:last-child { - border-right: none; -} -.dnd5e.sheet.actor .inventory-list .item-detail.item-action { - flex: 0 0 100px; -} -.dnd5e.sheet.actor .inventory-list .item-detail.attunement { - flex: 0 0 24px; -} -.dnd5e.sheet.actor .inventory-list .item-weight { - flex: 0 0 60px; - border-left: 1px solid #c9c7b8; - border-right: 1px solid #c9c7b8; -} -.dnd5e.sheet.actor .inventory-list .item-controls { - flex: 0 0 44px; -} -.dnd5e.sheet.actor .inventory-list .item-summary { - flex: 0 0 100%; - font-size: 12px; - line-height: 16px; - padding: 0.25em 0.5em; - color: #191813; - border-top: 1px solid #c9c7b8; -} .dnd5e.sheet.actor .encumbrance { flex: 0 0 12px; background: #7a7971; @@ -1721,6 +1642,127 @@ h5 { .dnd5e.sheet.item .loot-header { margin-bottom: 10px; } +.inventory-list { + padding: 0 5px; +} +.inventory-list .item .item-name { + cursor: pointer; +} +.inventory-list .item .item-name.rollable:hover .item-image { + background-image: url("../../icons/svg/d20-grey.svg") !important; +} +.inventory-list .item .item-name.rollable .item-image:hover { + background-image: url("../../icons/svg/d20-black.svg") !important; +} +.inventory-list .item .item-name i.attuned { + color: #7a7971; +} +.inventory-list .item .item-name i.not-attuned { + color: #44191A; +} +.inventory-list .item .item-uses input { + width: 24px; + text-align: center; +} +.inventory-list .item .item-properties { + margin-top: 3px; +} +.inventory-list .item .item-recharge { + flex: 0 0 80px; + text-align: right; + font-size: 11px; + white-space: nowrap; +} +.inventory-list .inventory-header .item-controls a.item-create { + flex: 0 0 100%; +} +.inventory-list .item-detail { + flex: 0 0 70px; + font-size: 12px; + text-align: center; + border-right: 1px solid #c9c7b8; + word-break: break-word; + white-space: nowrap; + overflow: hidden; +} +.inventory-list .item-detail:last-child { + border-right: none; +} +.inventory-list .item-detail.item-action { + flex: 0 0 100px; +} +.inventory-list .item-detail.attunement { + flex: 0 0 24px; +} +.inventory-list .item-weight { + flex: 0 0 60px; + border-left: 1px solid #c9c7b8; + border-right: 1px solid #c9c7b8; +} +.inventory-list .item-controls { + flex: 0 0 44px; +} +.inventory-list .item-summary { + flex: 0 0 100%; + font-size: 12px; + line-height: 16px; + padding: 0.25em 0.5em; + color: #191813; + border-top: 1px solid #c9c7b8; +} +.capacity { + flex: 0 0 12px; + margin-inline-end: 18px; + contain: content; +} +.capacity meter { + width: 100%; + background: var(--dnd5e-capacity-container-background-color); + border: 1px solid var(--dnd5e-capacity-container-border-color); + border-radius: 3px; +} +.capacity meter::-moz-meter-bar, +.capacity meter::-webkit-meter-bar { + height: calc(100% - 2px); + background: var(--dnd5e-capacity-background-color); + border: 1px solid var(--dnd5e-capacity-border-color); + border-radius: 2px; +} +.capacity label { + position: absolute; + top: 0; + right: 0; + padding-inline: 5px; + text-align: right; + color: var(--dnd5e-capacity-text-color); + text-shadow: 0 0 5px #000; +} +.inventory .inventory-header { + margin: 0 8px; + flex: 0 0 20px; + justify-content: flex-end; +} +.inventory .currency { + flex: 0 0 100%; + justify-content: flex-end; + margin: 4px 0 8px; + padding: 0; + font-size: var(--font-size-12); +} +.inventory .currency label { + flex: 0; + margin-left: 8px; + text-align: right; + line-height: 20px; + color: #7a7971; +} +.inventory .currency input[type="text"], +.inventory .currency input[type="number"] { + flex: 0 0 48px; + text-align: center; + margin-left: 8px; + border-bottom: 2px groove #eeede0; +} /* ----------------------------------------- */ /* SRD Compendium */ /* ----------------------------------------- */ diff --git a/less/actors.less b/less/actors.less index ec1db60b8f..d6ea33746c 100644 --- a/less/actors.less +++ b/less/actors.less @@ -470,107 +470,6 @@ margin: 0 8px; flex: 0 0 20px; justify-content: flex-end; - - .currency { - flex: 0 0 100%; - list-style: none; - margin: 4px 0 8px; - padding: 0; - font-size: 12px; - - label { - flex: 0; - margin-left: 8px; - text-align: right; - line-height: 20px; - color: @colorTan; - } - input[type="text"], input[type="number"] { - flex: 0 0 48px; - text-align: center; - margin-left: 8px; - border-bottom: @borderGroove; - } - } - } - - // Inventory item lists - .inventory-list { - padding: 0 5px; - .item { - .item-name { - cursor: pointer; - &.rollable:hover .item-image { - background-image: url("../../icons/svg/d20-grey.svg") !important; - } - &.rollable .item-image:hover { - background-image: url("../../icons/svg/d20-black.svg") !important; - } - i.attuned { color: @colorTan; } - i.not-attuned { color: @colorCrimson; } - } - - // Item uses - .item-uses input { - width: 24px; - text-align: center; - } - - // Item Dropdown Properties - .item-properties { - margin-top: 3px; - } - - // Charged - .item-recharge { - flex: 0 0 80px; - text-align: right; - font-size: 11px; - white-space: nowrap; - } - } - - // Inventory Header - .inventory-header { - .item-controls a.item-create { - flex: 0 0 100%; - } - } - - // Item Detail Sections - .item-detail { - flex: 0 0 70px; - font-size: 12px; - text-align: center; - border-right: 1px solid @colorFaint; - word-break: break-word; - white-space: nowrap; - overflow: hidden; - &:last-child { border-right: none; } - &.item-action {flex: 0 0 100px} - &.attunement {flex: 0 0 24px} - } - - .item-weight { - flex: 0 0 60px; - border-left: 1px solid @colorFaint; - border-right: 1px solid @colorFaint; - } - - // Item Control Buttons - .item-controls { - flex: 0 0 44px; - } - - // Item Dropdown Summary - .item-summary { - flex: 0 0 100%; - font-size: 12px; - line-height: 16px; - padding: 0.25em 0.5em; - color: @colorDark; - border-top: 1px solid @colorFaint; - } } /* Encumbrance Bar */ diff --git a/less/dnd5e.less b/less/dnd5e.less index 99e3616dd0..870e10c424 100644 --- a/less/dnd5e.less +++ b/less/dnd5e.less @@ -3,6 +3,7 @@ @import "actors.less"; @import "advancement.less"; @import "items.less"; +@import "inventory.less"; @import "journal.less"; @import "chat.less"; @import "character.less"; diff --git a/less/inventory.less b/less/inventory.less new file mode 100644 index 0000000000..01d796663c --- /dev/null +++ b/less/inventory.less @@ -0,0 +1,141 @@ +// Inventory item lists +.inventory-list { + padding: 0 5px; + .item { + .item-name { + cursor: pointer; + &.rollable:hover .item-image { + background-image: url("../../icons/svg/d20-grey.svg") !important; + } + &.rollable .item-image:hover { + background-image: url("../../icons/svg/d20-black.svg") !important; + } + i.attuned { color: @colorTan; } + i.not-attuned { color: @colorCrimson; } + } + + // Item uses + .item-uses input { + width: 24px; + text-align: center; + } + + // Item Dropdown Properties + .item-properties { + margin-top: 3px; + } + + // Charged + .item-recharge { + flex: 0 0 80px; + text-align: right; + font-size: 11px; + white-space: nowrap; + } + } + + // Inventory Header + .inventory-header { + .item-controls a.item-create { + flex: 0 0 100%; + } + } + + // Item Detail Sections + .item-detail { + flex: 0 0 70px; + font-size: 12px; + text-align: center; + border-right: 1px solid @colorFaint; + word-break: break-word; + white-space: nowrap; + overflow: hidden; + &:last-child { border-right: none; } + &.item-action {flex: 0 0 100px} + &.attunement {flex: 0 0 24px} + } + + .item-weight { + flex: 0 0 60px; + border-left: 1px solid @colorFaint; + border-right: 1px solid @colorFaint; + } + + // Item Control Buttons + .item-controls { + flex: 0 0 44px; + } + + // Item Dropdown Summary + .item-summary { + flex: 0 0 100%; + font-size: 12px; + line-height: 16px; + padding: 0.25em 0.5em; + color: @colorDark; + border-top: 1px solid @colorFaint; + } +} + +.capacity { + flex: 0 0 12px; + margin-inline-end: 18px; + contain: content; + + meter { + width: 100%; + + background: var(--dnd5e-capacity-container-background-color); + border: 1px solid var(--dnd5e-capacity-container-border-color); + border-radius: 3px; + + &::-moz-meter-bar, &::-webkit-meter-bar { + height: calc(100% - 2px); + + background: var(--dnd5e-capacity-background-color); + border: 1px solid var(--dnd5e-capacity-border-color); + border-radius: 2px; + } + } + + label { + position: absolute; + top: 0; + right: 0; + padding-inline: 5px; + + text-align: right; + color: var(--dnd5e-capacity-text-color); + text-shadow: 0 0 5px #000; + } +} + +.inventory { + .inventory-header { + margin: 0 8px; + flex: 0 0 20px; + justify-content: flex-end; + } + + .currency { + flex: 0 0 100%; + justify-content: flex-end; + margin: 4px 0 8px; + padding: 0; + font-size: var(--font-size-12); + + label { + flex: 0; + margin-left: 8px; + text-align: right; + line-height: 20px; + color: @colorTan; + } + input[type="text"], input[type="number"] { + flex: 0 0 48px; + text-align: center; + margin-left: 8px; + border-bottom: @borderGroove; + } + } +} diff --git a/less/variables.less b/less/variables.less index 9399ef773b..6e9fefd40f 100644 --- a/less/variables.less +++ b/less/variables.less @@ -40,6 +40,18 @@ @borderGroove: 2px groove #eeede0; @sheetBackground: url("ui/parchment.jpg") repeat; +:root { + /* ----------------------------------------- */ + /* Encumbrance & Capacity Bars */ + /* ----------------------------------------- */ + + --dnd5e-capacity-container-background-color: #7a7971; + --dnd5e-capacity-container-border-color: #191813; + --dnd5e-capacity-background-color: #6c8aa5; + --dnd5e-capacity-border-color: #cde4ff; + --dnd5e-capacity-text-color: #eeeeee; +} + /* ----------------------------------------- */ /* Flexbox */ /* ----------------------------------------- */ diff --git a/module/applications/item/item-sheet.mjs b/module/applications/item/item-sheet.mjs index e5d22b65c6..00da1c47d6 100644 --- a/module/applications/item/item-sheet.mjs +++ b/module/applications/item/item-sheet.mjs @@ -10,13 +10,18 @@ export default class ItemSheet5e extends ItemSheet { constructor(...args) { super(...args); - // Expand the default size of the class sheet - if ( this.object.type === "class" ) { - this.options.width = this.position.width = 600; - this.options.height = this.position.height = 680; - } - else if ( this.object.type === "subclass" ) { - this.options.height = this.position.height = 540; + switch ( this.object.type ) { + case "backpack": + this.options.width = this.position.width = 600; + this.options.height = this.position.height = 540; + break; + case "class": + this.options.width = this.position.width = 600; + this.options.height = this.position.height = 680; + break; + case "subclass": + this.options.height = this.position.height = 540; + break; } } @@ -109,6 +114,38 @@ export default class ItemSheet5e extends ItemSheet { effects: ActiveEffect5e.prepareActiveEffectCategories(item.effects) }); + // Container contents + if ( item.system.contents ) { + const contents = await item.system.containedItems; + context.items = Array.from(contents).map(i => i.toObject(false)); + + context.capacity = { max: item.system.capacity.value }; + if ( item.system.capacity.type === "weight" ) { + context.capacity.value = await item.system.containedItemsWeight; + context.capacity.units = "lbs."; // TODO: Localize & support metric + } else { + context.capacity.value = await item.system.containedItemsCount; + context.capacity.units = "items"; // TODO: Localize + } + + const containedItems = new Set(); + for ( let i of context.items ) { + const item = contents.find(item => i._id === item.id); + if ( item.type === "backpack" ) { + item.system.contents.forEach(id => containedItems.add(id)); + i.totalWeight = await item.system.totalWeight; + } else { + i.totalWeight = (i.system.quantity * i.system.weight).toNearest(0.1); + i.isStack = i.system.quantity > 1; + } + } + + context.items = context.items + .filter(i => !containedItems.has(i._id)) + .sort((a, b) => (a.sort || 0) - (b.sort || 0)); + // TODO: Split contained containers from rest of contents + } + /** @deprecated */ Object.defineProperty(context, "data", { get() { @@ -453,6 +490,10 @@ export default class ItemSheet5e extends ItemSheet { /** @inheritDoc */ activateListeners(html) { super.activateListeners(html); + + html.find(".item .item-name.rollable h4").click(event => this._onItemSummary(event)); + html.find(".item-edit").click(this._onItemEdit.bind(this)); + if ( this.isEditable ) { html.find(".damage-control").click(this._onDamageControl.bind(this)); html.find(".trait-selector").click(this._onConfigureTraits.bind(this)); @@ -464,6 +505,12 @@ export default class ItemSheet5e extends ItemSheet { const t = event.currentTarget; if ( t.dataset.action ) this._onAdvancementAction(t, t.dataset.action); }); + html.find(".item-delete").click(this._onItemDelete.bind(this)); + } + if ( this.item.isEmbedded && this.item.actor.isOwner ) { + html.find(".rollable .item-image").click(event => this._onItemUse(event)); + } else { + html.find(".rollable").each((i, el) => el.classList.remove("rollable")); } // Advancement context menu @@ -675,6 +722,80 @@ export default class ItemSheet5e extends ItemSheet { /* -------------------------------------------- */ + /** + * Handle using an item from the Actor sheet, obtaining the Item instance, and dispatching to its use method. + * @param {Event} event The triggering click event. + * @protected + */ + async _onItemUse(event) { + event.preventDefault(); + const itemId = event.currentTarget.closest(".item").dataset.itemId; + const item = (await this.item.system.containedItems).find(i => i.id === itemId); + // TODO: Add contained items cache to Item5e with better method to find by ID + item?.use({}, {event}); + } + + /* -------------------------------------------- */ + + /** + * Handle toggling and items expanded description. + * @param {Event} event Triggering event. + * @protected + */ + async _onItemSummary(event) { + event.preventDefault(); + const li = event.currentTarget.closest(".item"); + const item = (await this.item.system.containedItems).find(i => i.id === li.dataset.itemId); + const chatData = await item?.getChatData({secrets: this.item.actor?.isOwner ?? false}); + + if ( li.classList.contains("expanded") ) { + let summary = $(li.querySelector(".item-summary")); + summary.slideUp(200, () => summary.remove()); + li.classList.remove("expanded"); + } else { + // TODO: Replace with template to support inline container contents list + const div = $(`