diff --git a/lang/en.json b/lang/en.json index 632846c0..18def838 100755 --- a/lang/en.json +++ b/lang/en.json @@ -236,6 +236,9 @@ } }, "APPLICATIONS": { + "Attribution": { + "title": "Attribution" + }, "CharacterCreation": { "tabs": { "ancestry": "Ancestry", @@ -1905,6 +1908,7 @@ "armorScore": "Armor Score", "activeEffects": "Active Effects", "armorSlots": "Armor Slots", + "artistAttribution": "Artwork By: {artist}", "attack": "Attack", "basics": "Basics", "bonus": "Bonus", @@ -2005,6 +2009,11 @@ }, "ITEMS": { "FIELDS": { + "attribution": { + "source": { "label": "Source" }, + "page": { "label": "Page" }, + "artist": { "label": "Artist" } + }, "resource": { "amount": { "label": "Amount" }, "dieFaces": { "label": "Die Faces" }, @@ -2406,7 +2415,8 @@ "rulesOff": "Rules Off", "remainingUses": "Uses refresh on {type}", "rightClickExtand": "Right-Click to extand", - "companionPartnerLevelBlock": "The companion needs an assigned partner to level up." + "companionPartnerLevelBlock": "The companion needs an assigned partner to level up.", + "configureAttribution": "Configure Attribution" } } } diff --git a/module/applications/dialogs/_module.mjs b/module/applications/dialogs/_module.mjs index 8908ae2b..84ba4037 100644 --- a/module/applications/dialogs/_module.mjs +++ b/module/applications/dialogs/_module.mjs @@ -1,3 +1,4 @@ +export { default as AttributionDialog } from './attributionDialog.mjs'; export { default as BeastformDialog } from './beastformDialog.mjs'; export { default as d20RollDialog } from './d20RollDialog.mjs'; export { default as DamageDialog } from './damageDialog.mjs'; diff --git a/module/applications/dialogs/attributionDialog.mjs b/module/applications/dialogs/attributionDialog.mjs new file mode 100644 index 00000000..a72f6306 --- /dev/null +++ b/module/applications/dialogs/attributionDialog.mjs @@ -0,0 +1,93 @@ +import autocomplete from 'autocompleter'; + +const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; + +export default class AttriubtionDialog extends HandlebarsApplicationMixin(ApplicationV2) { + constructor(item) { + super({}); + + this.item = item; + this.sources = Object.keys(CONFIG.DH.GENERAL.attributionSources).flatMap(groupKey => { + const group = CONFIG.DH.GENERAL.attributionSources[groupKey]; + return group.values.map(x => ({ group: group.label, ...x })); + }); + } + + get title() { + return game.i18n.localize('DAGGERHEART.APPLICATIONS.Attribution.title'); + } + + static DEFAULT_OPTIONS = { + tag: 'form', + classes: ['daggerheart', 'dh-style', 'dialog', 'views', 'attribution'], + position: { width: 'auto', height: 'auto' }, + window: { icon: 'fa-solid fa-signature' }, + form: { handler: this.updateData, submitOnChange: false, closeOnSubmit: true } + }; + + static PARTS = { + main: { template: 'systems/daggerheart/templates/dialogs/attribution.hbs' } + }; + + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + const sources = this.sources; + + htmlElement.querySelectorAll('.attribution-input').forEach(element => { + autocomplete({ + input: element, + fetch: function (text, update) { + if (!text) { + update(sources); + } else { + text = text.toLowerCase(); + var suggestions = sources.filter(n => n.label.toLowerCase().includes(text)); + update(suggestions); + } + }, + render: function (item, search) { + const label = game.i18n.localize(item.label); + const matchIndex = label.toLowerCase().indexOf(search); + + const beforeText = label.slice(0, matchIndex); + const matchText = label.slice(matchIndex, matchIndex + search.length); + const after = label.slice(matchIndex + search.length, label.length); + + const element = document.createElement('li'); + element.innerHTML = `${beforeText}${matchText ? `${matchText}` : ''}${after}`; + if (item.hint) { + element.dataset.tooltip = game.i18n.localize(item.hint); + } + + return element; + }, + renderGroup: function (label) { + const itemElement = document.createElement('div'); + itemElement.textContent = game.i18n.localize(label); + return itemElement; + }, + onSelect: function (item) { + element.value = item.label; + }, + click: e => e.fetch(), + customize: function (_input, _inputRect, container) { + container.style.zIndex = foundry.applications.api.ApplicationV2._maxZ; + }, + minLength: 0 + }); + }); + } + + async _prepareContext(_options) { + const context = await super._prepareContext(_options); + context.item = this.item; + context.data = this.item.system.attribution; + + return context; + } + + static async updateData(_event, _element, formData) { + await this.item.update({ 'system.attribution': formData.object }); + this.item.sheet.refreshFrame(); + } +} diff --git a/module/applications/sheets/actors/adversary.mjs b/module/applications/sheets/actors/adversary.mjs index c128b648..01256bcc 100644 --- a/module/applications/sheets/actors/adversary.mjs +++ b/module/applications/sheets/actors/adversary.mjs @@ -4,6 +4,7 @@ import DHBaseActorSheet from '../api/base-actor.mjs'; /**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ export default class AdversarySheet extends DHBaseActorSheet { + /** @inheritDoc */ static DEFAULT_OPTIONS = { classes: ['adversary'], position: { width: 660, height: 766 }, @@ -12,7 +13,14 @@ export default class AdversarySheet extends DHBaseActorSheet { reactionRoll: AdversarySheet.#reactionRoll }, window: { - resizable: true + resizable: true, + controls: [ + { + icon: 'fa-solid fa-signature', + label: 'DAGGERHEART.UI.Tooltip.configureAttribution', + action: 'editAttribution' + } + ] } }; diff --git a/module/applications/sheets/actors/environment.mjs b/module/applications/sheets/actors/environment.mjs index aa2759a2..11991549 100644 --- a/module/applications/sheets/actors/environment.mjs +++ b/module/applications/sheets/actors/environment.mjs @@ -11,7 +11,14 @@ export default class DhpEnvironment extends DHBaseActorSheet { height: 725 }, window: { - resizable: true + resizable: true, + controls: [ + { + icon: 'fa-solid fa-signature', + label: 'DAGGERHEART.UI.Tooltip.configureAttribution', + action: 'editAttribution' + } + ] }, actions: {}, dragDrop: [{ dragSelector: '.action-section .inventory-item', dropSelector: null }] diff --git a/module/applications/sheets/api/application-mixin.mjs b/module/applications/sheets/api/application-mixin.mjs index 83dc1581..814718c1 100644 --- a/module/applications/sheets/api/application-mixin.mjs +++ b/module/applications/sheets/api/application-mixin.mjs @@ -101,7 +101,8 @@ export default function DHApplicationMixin(Base) { toggleEffect: DHSheetV2.#toggleEffect, toggleExtended: DHSheetV2.#toggleExtended, addNewItem: DHSheetV2.#addNewItem, - browseItem: DHSheetV2.#browseItem + browseItem: DHSheetV2.#browseItem, + editAttribution: DHSheetV2.#editAttribution }, contextMenus: [ { @@ -125,6 +126,33 @@ export default function DHApplicationMixin(Base) { tagifyConfigs: [] }; + /**@inheritdoc */ + async _renderFrame(options) { + const frame = await super._renderFrame(options); + + if (this.document.system.metadata.hasAttribution) { + const { source, page } = this.document.system.attribution; + const attribution = [source, page ? `pg ${page}.` : null].filter(x => x).join('. '); + const element = ``; + this.window.controls.insertAdjacentHTML('beforebegin', element); + } + + return frame; + } + + /** + * Refresh the custom parts of the application frame + */ + refreshFrame() { + if (this.document.system.metadata.hasAttribution) { + const { source, page } = this.document.system.attribution; + const attribution = [source, page ? `pg ${page}.` : null].filter(x => x).join('. '); + + const label = this.window.header.querySelector('.attribution-header-label'); + label.innerHTML = attribution; + } + } + /** * Related documents that should cause a rerender of this application when updated. */ @@ -548,6 +576,14 @@ export default function DHApplicationMixin(Base) { return new ItemBrowser({ presets }).render({ force: true }); } + /** + * Open the attribution dialog + * @type {ApplicationClickAction} + */ + static async #editAttribution() { + new game.system.api.applications.dialogs.AttributionDialog(this.document).render({ force: true }); + } + /** * Create an embedded document. * @type {ApplicationClickAction} diff --git a/module/applications/sheets/api/base-item.mjs b/module/applications/sheets/api/base-item.mjs index a9d3237d..6b548d2a 100644 --- a/module/applications/sheets/api/base-item.mjs +++ b/module/applications/sheets/api/base-item.mjs @@ -13,7 +13,16 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) { static DEFAULT_OPTIONS = { classes: ['item'], position: { width: 600 }, - window: { resizable: true }, + window: { + resizable: true, + controls: [ + { + icon: 'fa-solid fa-signature', + label: 'DAGGERHEART.UI.Tooltip.configureAttribution', + action: 'editAttribution' + } + ] + }, form: { submitOnChange: true }, diff --git a/module/config/generalConfig.mjs b/module/config/generalConfig.mjs index 34ca6009..e99c6ff1 100644 --- a/module/config/generalConfig.mjs +++ b/module/config/generalConfig.mjs @@ -624,6 +624,13 @@ export const rollTypes = { } }; +export const attributionSources = { + daggerheart: { + label: 'Daggerheart', + values: [{ label: 'Daggerheart SRD' }] + } +}; + export const fearDisplay = { token: { value: 'token', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.token' }, bar: { value: 'bar', label: 'DAGGERHEART.SETTINGS.Appearance.fearDisplay.bar' }, diff --git a/module/data/actor/adversary.mjs b/module/data/actor/adversary.mjs index e64c64f3..80bcb43e 100644 --- a/module/data/actor/adversary.mjs +++ b/module/data/actor/adversary.mjs @@ -10,7 +10,8 @@ export default class DhpAdversary extends BaseDataActor { return foundry.utils.mergeObject(super.metadata, { label: 'TYPES.Actor.adversary', type: 'adversary', - settingSheet: DHAdversarySettings + settingSheet: DHAdversarySettings, + hasAttribution: true }); } diff --git a/module/data/actor/base.mjs b/module/data/actor/base.mjs index 5b225228..36573325 100644 --- a/module/data/actor/base.mjs +++ b/module/data/actor/base.mjs @@ -39,7 +39,8 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel { type: 'base', isNPC: true, settingSheet: null, - hasResistances: true + hasResistances: true, + hasAttribution: false }; } @@ -53,6 +54,13 @@ export default class BaseDataActor extends foundry.abstract.TypeDataModel { const fields = foundry.data.fields; const schema = {}; + if (this.metadata.hasAttribution) { + schema.attribution = new fields.SchemaField({ + source: new fields.StringField(), + page: new fields.NumberField(), + artist: new fields.StringField() + }); + } if (this.metadata.isNPC) schema.description = new fields.HTMLField({ required: true, nullable: true }); if (this.metadata.hasResistances) schema.resistance = new fields.SchemaField({ diff --git a/module/data/actor/environment.mjs b/module/data/actor/environment.mjs index adb7dabc..ce1df7cd 100644 --- a/module/data/actor/environment.mjs +++ b/module/data/actor/environment.mjs @@ -12,7 +12,8 @@ export default class DhEnvironment extends BaseDataActor { label: 'TYPES.Actor.environment', type: 'environment', settingSheet: DHEnvironmentSettings, - hasResistances: false + hasResistances: false, + hasAttribution: true }); } diff --git a/module/data/item/base.mjs b/module/data/item/base.mjs index f0b22b44..469b3649 100644 --- a/module/data/item/base.mjs +++ b/module/data/item/base.mjs @@ -26,7 +26,8 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { hasResource: false, isQuantifiable: false, isInventoryItem: false, - hasActions: false + hasActions: false, + hasAttribution: true }; } @@ -37,7 +38,13 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel { /** @inheritDoc */ static defineSchema() { - const schema = {}; + const schema = { + attribution: new fields.SchemaField({ + source: new fields.StringField(), + page: new fields.NumberField(), + artist: new fields.StringField() + }) + }; if (this.metadata.hasDescription) schema.description = new fields.HTMLField({ required: true, nullable: true }); diff --git a/styles/less/dialog/attribution/sheet.less b/styles/less/dialog/attribution/sheet.less new file mode 100644 index 00000000..d20b094d --- /dev/null +++ b/styles/less/dialog/attribution/sheet.less @@ -0,0 +1,23 @@ +.daggerheart.dh-style.dialog.attribution { + .window-content { + padding-top: 0; + } + + .attribution-container { + display: flex; + flex-direction: column; + gap: 8px; + + h4 { + margin-bottom: 0; + } + + footer { + display: flex; + + button { + flex: 1; + } + } + } +} diff --git a/styles/less/dialog/index.less b/styles/less/dialog/index.less index 05593d44..65af4a71 100644 --- a/styles/less/dialog/index.less +++ b/styles/less/dialog/index.less @@ -1,3 +1,4 @@ +@import './attribution/sheet.less'; @import './level-up/navigation-container.less'; @import './level-up/selections-container.less'; @import './level-up/sheet.less'; diff --git a/styles/less/global/elements.less b/styles/less/global/elements.less index 6b5d64c9..b05fd31b 100755 --- a/styles/less/global/elements.less +++ b/styles/less/global/elements.less @@ -528,6 +528,17 @@ } } } + .artist-attribution { + width: 100%; + display: flex; + justify-content: left; + font-style: italic; + font-family: @font-body; + margin-top: 4px; + color: light-dark(#14142599, #efe6d850); + font-size: 12px; + padding-left: 3px; + } } .application.setting.dh-style { diff --git a/styles/less/sheets/actors/actor-sheet-shared.less b/styles/less/sheets/actors/actor-sheet-shared.less index 91a2323f..9fac0e95 100644 --- a/styles/less/sheets/actors/actor-sheet-shared.less +++ b/styles/less/sheets/actors/actor-sheet-shared.less @@ -1,8 +1,14 @@ - .application.sheet.daggerheart.actor.dh-style { - .portrait img, .profile { + .portrait img, + .profile { width: 100%; object-fit: cover; object-position: top center; } -} \ No newline at end of file + + .attribution-header-label { + font-style: italic; + font-family: @font-body; + color: light-dark(@chat-blue-bg, @beige-50); + } +} diff --git a/styles/less/sheets/index.less b/styles/less/sheets/index.less index a8f36a63..ada6188a 100644 --- a/styles/less/sheets/index.less +++ b/styles/less/sheets/index.less @@ -26,3 +26,4 @@ @import './items/class.less'; @import './items/domain-card.less'; @import './items/feature.less'; +@import './items/item-sheet-shared.less'; diff --git a/styles/less/sheets/items/item-sheet-shared.less b/styles/less/sheets/items/item-sheet-shared.less new file mode 100644 index 00000000..4dbb5062 --- /dev/null +++ b/styles/less/sheets/items/item-sheet-shared.less @@ -0,0 +1,7 @@ +.application.sheet.daggerheart.dh-style.item { + .attribution-header-label { + font-style: italic; + font-family: @font-body; + color: light-dark(@chat-blue-bg, @beige-50); + } +} diff --git a/templates/dialogs/attribution.hbs b/templates/dialogs/attribution.hbs new file mode 100644 index 00000000..20f00fb8 --- /dev/null +++ b/templates/dialogs/attribution.hbs @@ -0,0 +1,26 @@ +
+

{{item.name}}

+ +
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + +
\ No newline at end of file diff --git a/templates/sheets/actors/adversary/notes.hbs b/templates/sheets/actors/adversary/notes.hbs index a2378516..ef8ccf9d 100644 --- a/templates/sheets/actors/adversary/notes.hbs +++ b/templates/sheets/actors/adversary/notes.hbs @@ -7,4 +7,8 @@ {{localize tabs.notes.label}} {{formInput notes.field value=notes.value enriched=notes.enriched toggled=true}} + + {{#if document.system.attribution.artist}} + + {{/if}} \ No newline at end of file diff --git a/templates/sheets/actors/environment/notes.hbs b/templates/sheets/actors/environment/notes.hbs index 663a484a..352be465 100644 --- a/templates/sheets/actors/environment/notes.hbs +++ b/templates/sheets/actors/environment/notes.hbs @@ -7,4 +7,8 @@ {{localize tabs.notes.label}} {{formInput notes.field value=notes.value enriched=notes.value toggled=true}} + + {{#if document.system.attribution.artist}} + + {{/if}} \ No newline at end of file diff --git a/templates/sheets/global/tabs/tab-description.hbs b/templates/sheets/global/tabs/tab-description.hbs index 69f62ade..64997bd4 100755 --- a/templates/sheets/global/tabs/tab-description.hbs +++ b/templates/sheets/global/tabs/tab-description.hbs @@ -7,4 +7,8 @@ {{localize "DAGGERHEART.GENERAL.description"}} {{formInput systemFields.description value=document.system.description enriched=enrichedDescription toggled=true}} + + {{#if document.system.attribution.artist}} + + {{/if}} \ No newline at end of file