diff --git a/daggerheart.mjs b/daggerheart.mjs index 363430be..23977628 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -196,6 +196,11 @@ Hooks.once('init', () => { makeDefault: true, label: sheetLabel('TYPES.Actor.environment') }); + Actors.registerSheet(SYSTEM.id, applications.sheets.actors.NPC, { + types: ['npc'], + makeDefault: true, + label: sheetLabel('TYPES.Actor.npc') + }); Actors.registerSheet(SYSTEM.id, applications.sheets.actors.Party, { types: ['party'], makeDefault: true, diff --git a/lang/en.json b/lang/en.json index f1841e09..e6486d7b 100755 --- a/lang/en.json +++ b/lang/en.json @@ -23,6 +23,7 @@ "companion": "Companion", "adversary": "Adversary", "environment": "Environment", + "npc": "NPC", "party": "Party" } }, @@ -333,6 +334,13 @@ }, "newAdversary": "New Adversary" }, + "NPC": { + "FIELDS": { + "difficulty": { "label": "Difficulty" }, + "motive": { "label": "Motive" }, + "description": { "label": "Description" } + } + }, "Party": { "Subtitle": { "character": "{community} {ancestry} | {subclass} {class}", diff --git a/module/applications/sheets-configs/_module.mjs b/module/applications/sheets-configs/_module.mjs index 4b83a042..9528a424 100644 --- a/module/applications/sheets-configs/_module.mjs +++ b/module/applications/sheets-configs/_module.mjs @@ -2,6 +2,7 @@ export { default as ActionConfig } from './action-config.mjs'; export { default as ActionSettingsConfig } from './action-settings-config.mjs'; export { default as CharacterSettings } from './character-settings.mjs'; export { default as AdversarySettings } from './adversary-settings.mjs'; +export { default as NPCSettings } from './npc-settings.mjs'; export { default as CompanionSettings } from './companion-settings.mjs'; export { default as SettingFeatureConfig } from './setting-feature-config.mjs'; export { default as EnvironmentSettings } from './environment-settings.mjs'; diff --git a/module/applications/sheets-configs/npc-settings.mjs b/module/applications/sheets-configs/npc-settings.mjs new file mode 100644 index 00000000..c187877c --- /dev/null +++ b/module/applications/sheets-configs/npc-settings.mjs @@ -0,0 +1,85 @@ +import DHBaseActorSettings from '../sheets/api/actor-setting.mjs'; + +/**@typedef {import('@client/applications/_types.mjs').ApplicationClickAction} ApplicationClickAction */ + +export default class DHNPCSettings extends DHBaseActorSettings { + /**@inheritdoc */ + static DEFAULT_OPTIONS = { + classes: ['npc-settings'], + position: { width: 455, height: 'auto' }, + actions: {}, + dragDrop: [ + { dragSelector: null, dropSelector: '.tab.features' }, + { dragSelector: '.feature-item', dropSelector: null } + ] + }; + + /**@override */ + static PARTS = { + header: { + id: 'header', + template: 'systems/daggerheart/templates/sheets-settings/npc-settings/header.hbs' + }, + tabs: { template: 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs' }, + details: { + id: 'details', + template: 'systems/daggerheart/templates/sheets-settings/npc-settings/details.hbs' + }, + features: { + id: 'features', + template: 'systems/daggerheart/templates/sheets-settings/npc-settings/features.hbs' + } + }; + + /** @override */ + static TABS = { + primary: { + tabs: [{ id: 'details' }, { id: 'features' }], + initial: 'details', + labelPrefix: 'DAGGERHEART.GENERAL.Tabs' + } + }; + + async _prepareContext(options) { + const context = await super._prepareContext(options); + + const featureForms = ['passive', 'action', 'reaction']; + context.features = context.document.system.features.sort((a, b) => + a.system.featureForm !== b.system.featureForm + ? featureForms.indexOf(a.system.featureForm) - featureForms.indexOf(b.system.featureForm) + : a.sort - b.sort + ); + + return context; + } + + /* -------------------------------------------- */ + + async _onDragStart(event) { + const featureItem = event.currentTarget.closest('.feature-item'); + + if (featureItem) { + const feature = this.actor.items.get(featureItem.id); + const featureData = { type: 'Item', uuid: feature.uuid, fromInternal: true }; + event.dataTransfer.setData('text/plain', JSON.stringify(featureData)); + event.dataTransfer.setDragImage(featureItem.querySelector('img'), 60, 0); + } + } + + async _onDrop(event) { + event.stopPropagation(); + const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); + + const item = await fromUuid(data.uuid); + if (item?.type === 'feature') { + if (data.fromInternal && item.parent?.uuid === this.actor.uuid) { + return; + } + + const itemData = item.toObject(); + delete itemData._id; + + await this.actor.createEmbeddedDocuments('Item', [itemData]); + } + } +} diff --git a/module/applications/sheets/actors/_module.mjs b/module/applications/sheets/actors/_module.mjs index c4ea2d94..1a2bebfb 100644 --- a/module/applications/sheets/actors/_module.mjs +++ b/module/applications/sheets/actors/_module.mjs @@ -2,4 +2,5 @@ export { default as Adversary } from './adversary.mjs'; export { default as Character } from './character.mjs'; export { default as Companion } from './companion.mjs'; export { default as Environment } from './environment.mjs'; +export { default as NPC } from './npc.mjs'; export { default as Party } from './party.mjs'; diff --git a/module/applications/sheets/actors/npc.mjs b/module/applications/sheets/actors/npc.mjs new file mode 100644 index 00000000..d0f6e459 --- /dev/null +++ b/module/applications/sheets/actors/npc.mjs @@ -0,0 +1,132 @@ +import DHBaseActorSheet from '../api/base-actor.mjs'; + +export default class NPCSheet extends DHBaseActorSheet { + /** @inheritDoc */ + static DEFAULT_OPTIONS = { + classes: ['npc'], + position: { width: 660, height: 600 }, + window: { resizable: true }, + actions: {}, + window: { + resizable: true, + controls: [ + { + icon: 'fa-solid fa-signature', + label: 'DAGGERHEART.UI.Tooltip.configureAttribution', + action: 'editAttribution' + } + ] + }, + dragDrop: [ + { + dragSelector: '[data-item-id][draggable="true"], [data-item-id] [draggable="true"]', + dropSelector: null + } + ] + }; + + static PARTS = { + header: { template: 'systems/daggerheart/templates/sheets/actors/npc/header.hbs' }, + tabs: { template: 'systems/daggerheart/templates/sheets/actors/npc/navigation.hbs' }, + features: { + template: 'systems/daggerheart/templates/sheets/actors/npc/features.hbs', + scrollable: ['.feature-section'] + }, + notes: { + template: 'systems/daggerheart/templates/sheets/actors/npc/notes.hbs' + } + }; + + /** @inheritdoc */ + static TABS = { + primary: { + tabs: [{ id: 'notes' }, { id: 'features' }], + initial: 'notes', + labelPrefix: 'DAGGERHEART.GENERAL.Tabs' + } + }; + + /**@inheritdoc */ + async _prepareContext(options) { + const context = await super._prepareContext(options); + + return context; + } + + /**@inheritdoc */ + async _preparePartContext(partId, context, options) { + context = await super._preparePartContext(partId, context, options); + switch (partId) { + case 'header': + await this._prepareHeaderContext(context, options); + break; + case 'features': + await this._prepareFeaturesContext(context, options); + break; + case 'notes': + await this._prepareNotesContext(context, options); + break; + } + + return context; + } + + /** + * Prepare render context for the Header part. + * @param {ApplicationRenderContext} context + * @param {ApplicationRenderOptions} options + * @returns {Promise} + * @protected + */ + async _prepareHeaderContext(context, _options) { + const { system } = this.document; + const { TextEditor } = foundry.applications.ux; + + context.description = await TextEditor.implementation.enrichHTML(system.description, { + secrets: this.document.isOwner, + relativeTo: this.document + }); + } + + async _prepareFeaturesContext(context, _options) { + const featureForms = ['passive', 'action', 'reaction']; + context.features = this.document.system.features.sort((a, b) => + a.system.featureForm !== b.system.featureForm + ? featureForms.indexOf(a.system.featureForm) - featureForms.indexOf(b.system.featureForm) + : a.sort - b.sort + ); + } + + /** + * Prepare render context for the Biography part. + * @param {ApplicationRenderContext} context + * @param {ApplicationRenderOptions} options + * @returns {Promise} + * @protected + */ + async _prepareNotesContext(context, _options) { + const { system } = this.document; + const { TextEditor } = foundry.applications.ux; + + const paths = { + notes: 'notes' + }; + + for (const [key, path] of Object.entries(paths)) { + const value = foundry.utils.getProperty(system, path); + context[key] = { + field: system.schema.getField(path), + value, + enriched: await TextEditor.implementation.enrichHTML(value, { + secrets: this.document.isOwner, + relativeTo: this.document + }) + }; + } + } + + /**@inheritdoc */ + _attachPartListeners(partId, htmlElement, options) { + super._attachPartListeners(partId, htmlElement, options); + } +} diff --git a/module/data/actor/_module.mjs b/module/data/actor/_module.mjs index 99577620..1fe1ef3f 100644 --- a/module/data/actor/_module.mjs +++ b/module/data/actor/_module.mjs @@ -1,15 +1,17 @@ import DhCharacter from './character.mjs'; import DhCompanion from './companion.mjs'; import DhAdversary from './adversary.mjs'; +import DhNPC from './npc.mjs'; import DhEnvironment from './environment.mjs'; import DhParty from './party.mjs'; -export { DhCharacter, DhCompanion, DhAdversary, DhEnvironment, DhParty }; +export { DhCharacter, DhCompanion, DhAdversary, DhNPC, DhEnvironment, DhParty }; export const config = { character: DhCharacter, companion: DhCompanion, adversary: DhAdversary, + npc: DhNPC, environment: DhEnvironment, party: DhParty }; diff --git a/module/data/actor/npc.mjs b/module/data/actor/npc.mjs new file mode 100644 index 00000000..6fd08f65 --- /dev/null +++ b/module/data/actor/npc.mjs @@ -0,0 +1,38 @@ +import DHNPCSettings from '../../applications/sheets-configs/npc-settings.mjs'; +import BaseDataActor from './base.mjs'; + +export default class DhpNPC extends BaseDataActor { + static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.NPC']; + + static get metadata() { + return foundry.utils.mergeObject(super.metadata, { + label: 'TYPES.Actor.npc', + type: 'npc', + settingSheet: DHNPCSettings, + hasResistances: false, + hasAttribution: true + }); + } + + static defineSchema() { + const fields = foundry.data.fields; + return { + ...super.defineSchema(), + difficulty: new fields.NumberField({ nullable: true, initial: null, integer: true }), + description: new fields.StringField(), + motive: new fields.StringField(), + notes: new fields.HTMLField() + }; + } + + /**@inheritdoc */ + static DEFAULT_ICON = 'systems/daggerheart/assets/icons/documents/actors/dragon-head.svg'; + + get features() { + return this.parent.items.filter(x => x.type === 'feature'); + } + + isItemValid(source) { + return super.isItemValid(source) || source.type === 'feature'; + } +} diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index e4c11a5c..6c462d98 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -109,6 +109,14 @@ export default class DhpActor extends Actor { }); } + if (this.type === 'npc') { + Object.assign(update, { + prototypeToken: { + disposition: CONST.TOKEN_DISPOSITIONS.FRIENDLY + } + }); + } + this.updateSource(update); } diff --git a/styles/less/sheets/actors/npc/header.less b/styles/less/sheets/actors/npc/header.less new file mode 100644 index 00000000..1b787f4f --- /dev/null +++ b/styles/less/sheets/actors/npc/header.less @@ -0,0 +1,54 @@ +.application.sheet.daggerheart.actor.dh-style.npc { + .npc-header-sheet { + width: 100%; + display: flex; + + .portrait { + cursor: pointer; + width: 275px; + + img { + height: 275px; + } + } + + .info-section { + flex: 1; + padding: 0 15px; + padding-top: var(--header-height); + display: flex; + flex-direction: column; + gap: 8px; + + .name-row { + display: flex; + gap: 5px; + align-items: center; + justify-content: space-between; + + h1 { + display: flex; + flex: 1; + padding: 6px 0 0 0; + font-size: var(--font-size-32); + text-align: start; + border: 1px solid transparent; + outline: 2px solid transparent; + transition: all 0.3s ease; + word-break: break-word; + + &:hover { + outline: 2px solid light-dark(@dark, @golden); + } + } + } + + .npc-info { + display: flex; + flex-direction: column; + gap: 12px; + padding: 16px 0; + } + } + } +} \ No newline at end of file diff --git a/styles/less/sheets/actors/npc/sheet.less b/styles/less/sheets/actors/npc/sheet.less new file mode 100644 index 00000000..6e53a143 --- /dev/null +++ b/styles/less/sheets/actors/npc/sheet.less @@ -0,0 +1,13 @@ +.application.sheet.daggerheart.actor.dh-style.npc { + .window-content { + display: grid; + grid-template-rows: auto auto 1fr; + } + + .npc-navigation { + padding: 0 15px; + display: flex; + gap: 8px; + align-items: center; + } +} \ No newline at end of file diff --git a/styles/less/sheets/index.less b/styles/less/sheets/index.less index 7d595614..479982dc 100644 --- a/styles/less/sheets/index.less +++ b/styles/less/sheets/index.less @@ -8,6 +8,9 @@ @import './actors/adversary/sidebar.less'; @import './actors/adversary/effects.less'; +@import './actors/npc/sheet.less'; +@import './actors/npc/header.less'; + @import './actors/character/biography.less'; @import './actors/character/effects.less'; @import './actors/character/features.less'; diff --git a/system.json b/system.json index 2acd7570..8ecea7fc 100644 --- a/system.json +++ b/system.json @@ -244,6 +244,9 @@ "adversary": { "htmlFields": ["notes", "description"] }, + "npc": { + "htmlFields": ["notes"] + }, "environment": { "htmlFields": ["notes", "description"] }, diff --git a/templates/sheets-settings/npc-settings/details.hbs b/templates/sheets-settings/npc-settings/details.hbs new file mode 100644 index 00000000..fd4fb386 --- /dev/null +++ b/templates/sheets-settings/npc-settings/details.hbs @@ -0,0 +1,9 @@ +
+ {{formField systemFields.description value=document._source.system.description}} + {{formField systemFields.motive value=document._source.system.motive}} + {{formGroup systemFields.difficulty value=document._source.system.difficulty localize=true}} +
diff --git a/templates/sheets-settings/npc-settings/features.hbs b/templates/sheets-settings/npc-settings/features.hbs new file mode 100644 index 00000000..2f2f5f47 --- /dev/null +++ b/templates/sheets-settings/npc-settings/features.hbs @@ -0,0 +1,29 @@ +
+ +
+ {{localize tabs.features.label}} +
    + {{#each @root.features as |feature|}} +
  • + +
    + {{feature.name}} +
    +
    + + +
    +
  • + {{/each}} +
+
+ {{localize "DAGGERHEART.GENERAL.dropFeaturesHere"}} +
+
+
\ No newline at end of file diff --git a/templates/sheets-settings/npc-settings/header.hbs b/templates/sheets-settings/npc-settings/header.hbs new file mode 100644 index 00000000..c9cb60fe --- /dev/null +++ b/templates/sheets-settings/npc-settings/header.hbs @@ -0,0 +1,3 @@ +
+

{{document.name}}

+
\ No newline at end of file diff --git a/templates/sheets/actors/npc/features.hbs b/templates/sheets/actors/npc/features.hbs new file mode 100644 index 00000000..3b495e74 --- /dev/null +++ b/templates/sheets/actors/npc/features.hbs @@ -0,0 +1,14 @@ +
+
+ {{> 'daggerheart.inventory-items' + title=tabs.features.label + type='feature' + collection=@root.features + hideContextMenu=true + hideModifyControls=true + canCreate=@root.editable + showActions=@root.editable + }} +
+
\ No newline at end of file diff --git a/templates/sheets/actors/npc/header.hbs b/templates/sheets/actors/npc/header.hbs new file mode 100644 index 00000000..465b5e68 --- /dev/null +++ b/templates/sheets/actors/npc/header.hbs @@ -0,0 +1,30 @@ +
+
+ {{source.name}} +
+
+ + +
+

{{source.name}}

+
+ + + +
+ + {{{description}}} + + + {{localize 'DAGGERHEART.ACTORS.NPC.FIELDS.motive.label'}}: {{{source.system.motive}}} + +
+
+
\ No newline at end of file diff --git a/templates/sheets/actors/npc/navigation.hbs b/templates/sheets/actors/npc/navigation.hbs new file mode 100644 index 00000000..1151280e --- /dev/null +++ b/templates/sheets/actors/npc/navigation.hbs @@ -0,0 +1,6 @@ +
+ {{> 'systems/daggerheart/templates/sheets/global/tabs/tab-navigation.hbs'}} + +
\ No newline at end of file diff --git a/templates/sheets/actors/npc/notes.hbs b/templates/sheets/actors/npc/notes.hbs new file mode 100644 index 00000000..a5c3f706 --- /dev/null +++ b/templates/sheets/actors/npc/notes.hbs @@ -0,0 +1,14 @@ +
+
+ {{localize tabs.notes.label}} + {{formInput notes.field value=notes.value enriched=notes.enriched toggled=true}} +
+ + {{#if (and showAttribution document.system.attribution.artist)}} + + {{/if}} +
\ No newline at end of file diff --git a/templates/ui/sidebar/actor-document-partial.hbs b/templates/ui/sidebar/actor-document-partial.hbs index 2a9f47fa..1bd3ff9a 100644 --- a/templates/ui/sidebar/actor-document-partial.hbs +++ b/templates/ui/sidebar/actor-document-partial.hbs @@ -6,6 +6,8 @@ {{name}} {{#if (or (eq type "adversary") (eq type "environment"))}} {{localize "DAGGERHEART.UI.Sidebar.actorDirectory.tier" tier=system.tier type=(@root.getTypeLabel this)}} + {{else if (eq type "npc")}} + {{localize "TYPES.Actor.npc"}} {{else if (eq type "character")}} {{localize "DAGGERHEART.UI.Sidebar.actorDirectory.character" level=system.levelData.level.current}} {{else if (eq type "companion")}}