diff --git a/lang/en.json b/lang/en.json index b15c4fb69e..b2a462616b 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1077,6 +1077,7 @@ "DND5E.DefaultAbilityCheck": "Default Ability Check", "DND5E.Description": "Description", "DND5E.DescriptionChat": "Chat Description", +"DND5E.DescriptionEdit": "Edit {description}", "DND5E.DescriptionSummary": "Summary Description", "DND5E.DescriptionUnidentified": "Unidentified Description", "DND5E.Details": "Details", diff --git a/less/dnd5e.less b/less/dnd5e.less index 7cf178e551..e0b220c2f7 100644 --- a/less/dnd5e.less +++ b/less/dnd5e.less @@ -35,6 +35,7 @@ @import "v2/dark/apps.less"; @import "v2/dark/forms.less"; @import "v2/dark/actors.less"; +@import "v2/dark/items.less"; @import "v2/dark/character.less"; @import "v2/dark/npc.less"; @import "v2/dark/inventory.less"; diff --git a/less/v2/dark/items.less b/less/v2/dark/items.less new file mode 100644 index 0000000000..0f53c14713 --- /dev/null +++ b/less/v2/dark/items.less @@ -0,0 +1,9 @@ +.theme-dark .dnd5e2.sheet.item, +.dnd5e2.sheet.item.dnd5e-theme-dark { + .sheet-body .tab.advancement { + .item-name .summary .content-link { + color: var(--color-text-dark-5); + > i { color: var(--color-text-dark-5); } + } + } +} diff --git a/less/v2/items.less b/less/v2/items.less index ab3ecca561..7caa6d35bd 100644 --- a/less/v2/items.less +++ b/less/v2/items.less @@ -198,6 +198,175 @@ form > hr.ampersand { margin-bottom: -4px; } + /* ---------------------------------- */ + /* Sheet Body */ + /* ---------------------------------- */ + + .sheet-body { + .tab { + max-height: calc(70vh - var(--dnd5e-sheet-header-height)); + overflow: auto; + display: flex; + flex-direction: column; + gap: 8px; + margin: -6px -11px -6px -6px; + padding: 6px 2px 6px 6px; + scrollbar-width: thin; + scrollbar-color: var(--dnd5e-color-scrollbar) transparent; + scrollbar-gutter: stable; + + &:not(.active) { display: none; } + &.effects { padding: 12px 2px 58px 6px; } + + > .pills .pill { + font-size: var(--font-size-10); + padding: 3px 6px; + line-height: 1; + } + + &.single-description { + padding-right: 5px; + scrollbar-gutter: auto; + + > .editor { + padding-right: 6px; + scrollbar-width: thin; + scrollbar-color: var(--dnd5e-color-scrollbar) transparent; + } + } + } + + .spell-block { + width: 250px; + margin: 4px auto 4px; + } + + .info-block { + display: flex; + justify-content: space-evenly; + align-items: center; + gap: 16px; + + > div { + display: flex; + flex-direction: column; + align-items: center; + + .label { + font-family: var(--dnd5e-font-roboto-condensed); + font-size: var(--font-size-11); + color: var(--color-text-dark-5); + text-transform: uppercase; + } + + .value { + font-family: var(--dnd5e-font-roboto-slab); + font-weight: bold; + font-size: var(--font-size-24); + } + + .sign { + color: var(--color-text-light-6); + font-size: var(--font-size-18); + } + + &.damage > { + flex: none; + + .value { + --icon-fill: var(--color-text-dark-5); + color: var(--color-text-dark-5); + display: grid; + grid-template-columns: auto auto; + gap: 4px; + font-size: var(--font-size-14); + font-family: var(--dnd5e-font-roboto); + font-weight: normal; + } + } + } + } + + /* Descriptions */ + .item-descriptions { + display: flex; + flex-direction: column; + gap: 8px; + + .description { + padding: 8px; + font-family: var(--dnd5e-font-roboto-condensed); + box-shadow: 0 0 6px var(--dnd5e-shadow-15); + + > .header { + background: none; + border: none; + color: var(--color-text-dark-primary); + display: flex; + justify-content: space-between; + align-items: center; + + > span { + flex: 1; + font-family: var(--dnd5e-font-roboto-slab); + font-weight: bold; + text-align: center; + font-size: var(--font-size-14); + } + + > button { + flex: none; + width: unset; + + &:hover, &:focus-visible { + box-shadow: none; + text-shadow: 0 0 8px var(--color-shadow-primary); + } + } + } + + &:not(.collapsible) > .header { padding-left: 14px; } + + &.collapsible { + > .header { + cursor: pointer; + + &::before { + content: "\f078"; + font-family: var(--font-awesome); + font-weight: bold; + color: var(--color-text-dark-5); + transition: all 250ms ease; + } + } + + &.empty > .header { + cursor: unset; + &::before { visibility: hidden; } + } + + &.collapsed > .header::before { transform: rotate(-90deg); } + + .editor { overflow: hidden; } + } + + .editor { padding: 0 6px; } + } + } + + .editor.prosemirror { + .editor-content { + position: unset; + min-height: 300px; + } + + > textarea { + flex: none; + min-height: 300px; + } + } + } + /* ---------------------------------- */ /* Editable */ /* ---------------------------------- */ diff --git a/module/applications/item/item-sheet.mjs b/module/applications/item/item-sheet.mjs index 32c454d51e..9a3d333bc3 100644 --- a/module/applications/item/item-sheet.mjs +++ b/module/applications/item/item-sheet.mjs @@ -459,6 +459,7 @@ export default class ItemSheet5e extends ItemSheet { }); html.find(".description-edit").click(event => { if ( event.currentTarget.ariaDisabled ) return; + event.stopPropagation(); this.editingDescriptionTarget = event.currentTarget.dataset.target; this.render(); }); diff --git a/module/applications/item/sheet-v2-mixin.mjs b/module/applications/item/sheet-v2-mixin.mjs index 78e11f7078..7d15948e2c 100644 --- a/module/applications/item/sheet-v2-mixin.mjs +++ b/module/applications/item/sheet-v2-mixin.mjs @@ -17,6 +17,13 @@ export default function ItemSheetV2Mixin(Base) { { tab: "advancement", label: "DND5E.AdvancementTitle", condition: this.itemHasAdvancements.bind(this) } ]; + /** + * Store the collapsed state of the description boxes. + * @type {Record} + * @protected + */ + _collapsed = {}; + /* -------------------------------------------- */ /* Rendering */ /* -------------------------------------------- */ @@ -45,6 +52,13 @@ export default function ItemSheetV2Mixin(Base) { const { identified, schema, unidentified } = this.item.system; context.fields = schema.fields; + // Set some default collapsed states on first open. + if ( foundry.utils.isEmpty(this._collapsed) ) Object.assign(this._collapsed, { + "system.description.chat": true, + "system.unidentified.description": game.user.isGM + }); + context.collapsed = this._collapsed; + // Tabs const activeTab = this._tabs?.[0]?.active ?? this.options.tabs[0].initial; context.cssClass += ` tab-${activeTab}`; @@ -71,9 +85,25 @@ export default function ItemSheetV2Mixin(Base) { }; } + // Properties + context.properties = { + active: [], + object: Object.fromEntries((context.source.properties ?? []).map(p => [p, true])), + options: Object.entries(context.properties ?? {}).map(([k, v]) => ({ value: k, ...v })) + }; + if ( game.user.isGM || (identified !== false) ) { + context.properties.active.push( + ...this.item.system.cardProperties ?? [], + ...this.item.system.activatedEffectCardProperties ?? [], + ...this.item.system.equippableItemCardProperties ?? [] + ); + } + // Sub-type context await this.item.system.getSheetData?.(context); + context.properties.active = context.properties.active.filter(_ => _); + return context; } @@ -94,6 +124,7 @@ export default function ItemSheetV2Mixin(Base) { /** @inheritDoc */ activateListeners(html) { super.activateListeners(html); + html.find(".description.collapsible > .header").on("click", this._onToggleOwnDescription.bind(this)); // Play mode only. if ( this._mode === this.constructor.MODES.PLAY ) { @@ -103,6 +134,21 @@ export default function ItemSheetV2Mixin(Base) { /* -------------------------------------------- */ + /** + * Handle toggling one of the item's description categories. + * @param {PointerEvent} event The triggering event. + * @protected + */ + _onToggleOwnDescription(event) { + const description = event.currentTarget.closest("[data-target]"); + if ( !description ) return; + const { target } = description.dataset; + description.classList.toggle("collapsed"); + this._collapsed[target] = description.classList.contains("collapsed"); + } + + /* -------------------------------------------- */ + /** * Handle showing the Item's art. * @protected diff --git a/module/data/item/equipment.mjs b/module/data/item/equipment.mjs index 4825bd69a9..9aff95dc2f 100644 --- a/module/data/item/equipment.mjs +++ b/module/data/item/equipment.mjs @@ -206,6 +206,13 @@ export default class EquipmentData extends ItemDataModel.mixin( { label: this.type.label }, ...this.physicalItemSheetFields ]; + if ( this.armor.value ) { + context.properties.active.shift(); + context.info = [{ + label: "DND5E.ArmorClass", + value: this.type.value === "shield" ? dnd5e.utils.formatModifier(this.armor.value) : this.armor.value + }]; + } } /* -------------------------------------------- */ diff --git a/module/data/item/spell.mjs b/module/data/item/spell.mjs index 59d5b54b25..64a3001d1f 100644 --- a/module/data/item/spell.mjs +++ b/module/data/item/spell.mjs @@ -176,6 +176,7 @@ export default class SpellData extends ItemDataModel.mixin( { label: context.labels.school }, { label: context.itemStatus } ]; + context.properties.active = this.parent.labels?.components?.tags; } /* -------------------------------------------- */ diff --git a/module/data/item/weapon.mjs b/module/data/item/weapon.mjs index 3cdfca4050..49f63b802b 100644 --- a/module/data/item/weapon.mjs +++ b/module/data/item/weapon.mjs @@ -145,6 +145,19 @@ export default class WeaponData extends ItemDataModel.mixin( { label: this.type.label }, ...this.physicalItemSheetFields ]; + context.info = [{ label: "DND5E.ToHit", value: dnd5e.utils.formatModifier(parseInt(this.parent.labels.modifier)) }]; + if ( this.parent.labels.derivedDamage?.length ) { + const config = { ...CONFIG.DND5E.damageTypes, ...CONFIG.DND5E.healingTypes }; + context.info.push({ value: this.parent.labels.derivedDamage.reduce((str, { formula, damageType }) => { + const { label, icon } = config[damageType]; + return `${str} + ${formula} + + + + `; + }, ""), classes: "damage" }); + } } /* -------------------------------------------- */ diff --git a/module/utils.mjs b/module/utils.mjs index c3c3463c7d..2610fdf6ca 100644 --- a/module/utils.mjs +++ b/module/utils.mjs @@ -357,6 +357,7 @@ export async function preloadHandlebarsTemplates() { "systems/dnd5e/templates/items/parts/item-activation.hbs", "systems/dnd5e/templates/items/parts/item-advancement.hbs", "systems/dnd5e/templates/items/parts/item-description.hbs", + "systems/dnd5e/templates/items/parts/item-description2.hbs", "systems/dnd5e/templates/items/parts/item-mountable.hbs", "systems/dnd5e/templates/items/parts/item-spellcasting.hbs", "systems/dnd5e/templates/items/parts/item-source.hbs", diff --git a/templates/items/item-sheet-2.hbs b/templates/items/item-sheet-2.hbs index 4e2722a2d2..85848fa59c 100644 --- a/templates/items/item-sheet-2.hbs +++ b/templates/items/item-sheet-2.hbs @@ -122,6 +122,8 @@ {{!-- Body --}}
+ {{!-- Description Tab --}} + {{> "dnd5e.item-description2" }}
diff --git a/templates/items/parts/item-description2.hbs b/templates/items/parts/item-description2.hbs new file mode 100644 index 0000000000..6e1f85761e --- /dev/null +++ b/templates/items/parts/item-description2.hbs @@ -0,0 +1,95 @@ +{{#*inline "description"}} +
+
+ {{ localize label }} + {{#if @root.owner}} + + {{/if}} +
+
+
+ {{ editor enriched target=target button=false editable=false engine="prosemirror" }} +
+
+
+{{/inline}} + +
+ + {{#if editingDescriptionTarget}} + {{ editor enriched.editing target=editingDescriptionTarget button=false editable=true engine="prosemirror" + collaborate=true }} + {{else}} + + {{#if (eq item.type "spell")}} + {{> "dnd5e.spell-block" activation=system.activation }} + {{/if}} + + {{#if info}} +
+ {{#each info}} +
+ {{#if label}}{{ localize label }}{{/if}} + {{{ value }}} +
+ {{/each}} +
+ {{/if}} + + {{#if singleDescription}} + {{ editor enriched.description target="system.description.value" button=false editable=editable + engine="prosemirror" collaborate=true }} + {{else}} + +
+ + {{!-- When identified, show extra descriptions --}} + {{#if (or isIdentified user.isGM)}} + + {{!-- All users see the main, identified description --}} + {{> description collapsible=owner label="DND5E.Description" target="system.description.value" + enriched=enriched.description }} + + {{!-- Only GM users are able to see and edit the unidentified description --}} + {{#if (and isIdentifiable user.isGM)}} + {{> description collapsible=true label="DND5E.DescriptionUnidentified" + target="system.unidentified.description" enriched=enriched.unidentified }} + {{/if}} + + {{!-- Owners can see and edit the chat description. It is only visible to non-owners if not blank. --}} + {{#if (or owner system.description.chat)}} + {{> description collapsible=owner label="DND5E.DescriptionChat" target="system.description.chat" + enriched=enriched.chat }} + {{/if}} + + {{else}} + + {{!-- When unidentified, non-GM users can only see and edit the unidentified description --}} + {{#if (or owner system.unidentified.description)}} + {{> description collapsible=false label="DND5E.Description" target="system.unidentified.description" + enriched=enriched.unidentified }} + {{/if}} + + {{/if}} + +
+ + {{/if}} + + {{/if}} + + {{#if (or isIdentified user.isGM)}} + + {{/if}} + +