diff --git a/daggerheart.mjs b/daggerheart.mjs index 0cd82014..861d16ca 100644 --- a/daggerheart.mjs +++ b/daggerheart.mjs @@ -62,6 +62,7 @@ CONFIG.Token.rulerClass = placeables.DhTokenRuler; CONFIG.Token.hudClass = applications.hud.DHTokenHUD; CONFIG.ui.combat = applications.ui.DhCombatTracker; +CONFIG.ui.nav = applications.ui.DhSceneNavigation; CONFIG.ui.chat = applications.ui.DhChatLog; CONFIG.ui.effectsDisplay = applications.ui.DhEffectsDisplay; CONFIG.ui.hotbar = applications.ui.DhHotbar; diff --git a/lang/en.json b/lang/en.json index 1378daef..c32ffa4e 100755 --- a/lang/en.json +++ b/lang/en.json @@ -2604,7 +2604,9 @@ } }, "disabledText": "Daggerheart Measurements are disabled in System Settings - Variant Rules", - "rangeMeasurement": "Range Measurement" + "rangeMeasurement": "Range Measurement", + "sceneEnvironments": "Scene Environments", + "dragEnvironmentHere": "Drag environments here" } }, "UI": { diff --git a/module/applications/scene/sceneConfigSettings.mjs b/module/applications/scene/sceneConfigSettings.mjs index be8f7b71..1b93aa8c 100644 --- a/module/applications/scene/sceneConfigSettings.mjs +++ b/module/applications/scene/sceneConfigSettings.mjs @@ -1,16 +1,28 @@ +import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; + export default class DhSceneConfigSettings extends foundry.applications.sheets.SceneConfig { - // static DEFAULT_OPTIONS = { - // ...super.DEFAULT_OPTIONS, - // form: { - // handler: this.updateData, - // closeOnSubmit: true - // } - // }; + constructor(options) { + super(options); + + Hooks.on(socketEvent.Refresh, ({ refreshType }) => { + if (refreshType === RefreshType.Scene) { + this.daggerheartFlag = new game.system.api.data.scenes.DHScene(this.document.flags.daggerheart); + this.render(); + } + }); + } + + static DEFAULT_OPTIONS = { + ...super.DEFAULT_OPTIONS, + actions: { + ...super.DEFAULT_OPTIONS.actions, + removeSceneEnvironment: DhSceneConfigSettings.#removeSceneEnvironment + } + }; static buildParts() { const { footer, tabs, ...parts } = super.PARTS; const tmpParts = { - // tabs, tabs: { template: 'systems/daggerheart/templates/scene/tabs.hbs' }, ...parts, dh: { template: 'systems/daggerheart/templates/scene/dh-config.hbs' }, @@ -28,27 +40,45 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S static TABS = DhSceneConfigSettings.buildTabs(); + async _preRender(context, options) { + await super._preFirstRender(context, options); + this.daggerheartFlag = new game.system.api.data.scenes.DHScene(this.document.flags.daggerheart); + } + _attachPartListeners(partId, htmlElement, options) { super._attachPartListeners(partId, htmlElement, options); + switch (partId) { case 'dh': htmlElement.querySelector('#rangeMeasurementSetting')?.addEventListener('change', async event => { - const flagData = foundry.utils.mergeObject(this.document.flags.daggerheart, { - rangeMeasurement: { setting: event.target.value } - }); - this.document.flags.daggerheart = flagData; + this.daggerheartFlag.updateSource({ rangeMeasurement: { setting: event.target.value } }); this.render(); }); + + const dragArea = htmlElement.querySelector('.scene-environments'); + if (dragArea) dragArea.ondrop = this._onDrop.bind(this); + break; } } + async _onDrop(event) { + const data = foundry.applications.ux.TextEditor.implementation.getDragEventData(event); + const item = await foundry.utils.fromUuid(data.uuid); + if (item instanceof game.system.api.documents.DhpActor && item.type === 'environment') { + await this.daggerheartFlag.updateSource({ + sceneEnvironments: [...this.daggerheartFlag.sceneEnvironments, data.uuid] + }); + this.render(); + } + } + /** @inheritDoc */ async _preparePartContext(partId, context, options) { context = await super._preparePartContext(partId, context, options); switch (partId) { case 'dh': - context.data = new game.system.api.data.scenes.DHScene(canvas.scene.flags.daggerheart); + context.data = this.daggerheartFlag; context.variantRules = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules); break; } @@ -56,8 +86,24 @@ export default class DhSceneConfigSettings extends foundry.applications.sheets.S return context; } - // static async updateData(event, _, formData) { - // const data = foundry.utils.expandObject(formData.object); - // this.close(data); - // } + static async #removeSceneEnvironment(_event, button) { + await this.daggerheartFlag.updateSource({ + sceneEnvironments: this.daggerheartFlag.sceneEnvironments.filter( + (_, index) => index !== Number.parseInt(button.dataset.index) + ) + }); + this.render(); + } + + /** @override */ + async _processSubmitData(event, form, submitData, options) { + submitData.flags.daggerheart = this.daggerheartFlag.toObject(); + for (const key of Object.keys(this.document._source.flags.daggerheart?.sceneEnvironments ?? {})) { + if (!submitData.flags.daggerheart.sceneEnvironments[key]) { + submitData.flags.daggerheart.sceneEnvironments[`-=${key}`] = null; + } + } + + super._processSubmitData(event, form, submitData, options); + } } diff --git a/module/applications/ui/_module.mjs b/module/applications/ui/_module.mjs index d5f31906..8c5c020e 100644 --- a/module/applications/ui/_module.mjs +++ b/module/applications/ui/_module.mjs @@ -5,4 +5,5 @@ export { default as DhCombatTracker } from './combatTracker.mjs'; export { default as DhEffectsDisplay } from './effectsDisplay.mjs'; export { default as DhFearTracker } from './fearTracker.mjs'; export { default as DhHotbar } from './hotbar.mjs'; +export { default as DhSceneNavigation } from './sceneNavigation.mjs'; export { ItemBrowser } from './itemBrowser.mjs'; diff --git a/module/applications/ui/sceneNavigation.mjs b/module/applications/ui/sceneNavigation.mjs new file mode 100644 index 00000000..ac16ac99 --- /dev/null +++ b/module/applications/ui/sceneNavigation.mjs @@ -0,0 +1,89 @@ +import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs'; + +export default class DhSceneNavigation extends foundry.applications.ui.SceneNavigation { + /** @inheritdoc */ + static DEFAULT_OPTIONS = { + ...super.DEFAULT_OPTIONS, + classes: ['faded-ui', 'flexcol', 'scene-navigation'], + actions: { + openSceneEnvironment: DhSceneNavigation.#openSceneEnvironment + } + }; + + /** @inheritdoc */ + static PARTS = { + scenes: { + root: true, + template: 'systems/daggerheart/templates/ui/sceneNavigation/scene-navigation.hbs' + } + }; + + /** @inheritdoc */ + async _prepareContext(options) { + const context = await super._prepareContext(options); + + const extendScenes = scenes => + scenes.map(x => { + const scene = game.scenes.get(x.id); + if (!scene.flags.daggerheart) return x; + + const daggerheartInfo = new game.system.api.data.scenes.DHScene(scene.flags.daggerheart); + const environments = daggerheartInfo.sceneEnvironments.filter( + x => x && x.testUserPermission(game.user, 'LIMITED') + ); + const hasEnvironments = environments.length > 0; + return { + ...x, + hasEnvironments, + environmentImage: hasEnvironments ? environments[0].img : null, + environments: environments + }; + }); + context.scenes.active = extendScenes(context.scenes.active); + context.scenes.inactive = extendScenes(context.scenes.inactive); + + return context; + } + + static async #openSceneEnvironment(event, button) { + const scene = game.scenes.get(button.dataset.sceneId); + const sceneEnvironments = new game.system.api.data.scenes.DHScene( + scene.flags.daggerheart + ).sceneEnvironments.filter(x => x.testUserPermission(game.user, 'LIMITED')); + + if (sceneEnvironments.length === 1 || event.shiftKey) { + sceneEnvironments[0].sheet.render(true); + } else { + new foundry.applications.ux.ContextMenu.implementation( + button, + '.scene-environment', + sceneEnvironments.map(environment => ({ + name: environment.name, + callback: () => { + if (scene.flags.daggerheart.sceneEnvironments[0] !== environment.uuid) { + const newEnvironments = scene.flags.daggerheart.sceneEnvironments; + const newFirst = newEnvironments.splice( + newEnvironments.findIndex(x => x === environment.uuid) + )[0]; + newEnvironments.unshift(newFirst); + emitAsGM( + GMUpdateEvent.UpdateDocument, + scene.update.bind(scene), + { 'flags.daggerheart.sceneEnvironments': newEnvironments }, + scene.uuid + ); + } + + environment.sheet.render({ force: true }); + } + })), + { + jQuery: false, + fixed: true + } + ); + + CONFIG.ux.ContextMenu.triggerContextMenu(event, '.scene-environment'); + } + } +} diff --git a/module/applications/ux/contextMenu.mjs b/module/applications/ux/contextMenu.mjs index 09454848..081e6ba0 100644 --- a/module/applications/ux/contextMenu.mjs +++ b/module/applications/ux/contextMenu.mjs @@ -96,11 +96,11 @@ export default class DHContextMenu extends foundry.applications.ux.ContextMenu { * Trigger a context menu event in response to a normal click on a additional options button. * @param {PointerEvent} event */ - static triggerContextMenu(event) { + static triggerContextMenu(event, altSelector) { event.preventDefault(); event.stopPropagation(); const { clientX, clientY } = event; - const selector = '[data-item-uuid]'; + const selector = altSelector ?? '[data-item-uuid]'; const target = event.target.closest(selector) ?? event.currentTarget.closest(selector); target?.dispatchEvent( new PointerEvent('contextmenu', { diff --git a/module/data/actor/environment.mjs b/module/data/actor/environment.mjs index 4ed3819e..0aaf8eb0 100644 --- a/module/data/actor/environment.mjs +++ b/module/data/actor/environment.mjs @@ -1,8 +1,11 @@ import BaseDataActor from './base.mjs'; import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs'; import DHEnvironmentSettings from '../../applications/sheets-configs/environment-settings.mjs'; +import { RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; export default class DhEnvironment extends BaseDataActor { + scenes = new Set(); + /**@override */ static LOCALIZATION_PREFIXES = ['DAGGERHEART.ACTORS.Environment']; @@ -53,6 +56,31 @@ export default class DhEnvironment extends BaseDataActor { } isItemValid(source) { - return source.type === "feature"; + return source.type === 'feature'; + } + + _onUpdate(changes, options, userId) { + super._onUpdate(changes, options, userId); + for (const scene of this.scenes) { + scene.render(); + } + } + + _onDelete(options, userId) { + super._onDelete(options, userId); + for (const scene of this.scenes) { + if (game.user.isActiveGM) { + const newSceneEnvironments = scene.flags.daggerheart.sceneEnvironments.filter( + x => x !== this.parent.uuid + ); + scene.update({ 'flags.daggerheart.sceneEnvironments': newSceneEnvironments }).then(() => { + Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Scene }); + game.socket.emit(`system.${CONFIG.DH.id}`, { + action: socketEvent.Refresh, + data: { refreshType: RefreshType.TagTeamRoll } + }); + }); + } + } } } diff --git a/module/data/scene/scene.mjs b/module/data/scene/scene.mjs index 7cf74ade..f2a24308 100644 --- a/module/data/scene/scene.mjs +++ b/module/data/scene/scene.mjs @@ -1,3 +1,8 @@ +import ForeignDocumentUUIDArrayField from '../fields/foreignDocumentUUIDArrayField.mjs'; + +/* Foundry does not add any system data for subtyped Scenes. The data model is therefore used by instantiating a new instance of it for sceneConfigSettings.mjs. + Needed dataprep and lifetime hooks are handled in documents/scene. +*/ export default class DHScene extends foundry.abstract.DataModel { static defineSchema() { const fields = foundry.data.fields; @@ -13,7 +18,8 @@ export default class DHScene extends foundry.abstract.DataModel { veryClose: new fields.NumberField({ integer: true, label: 'DAGGERHEART.CONFIG.Range.veryClose.name' }), close: new fields.NumberField({ integer: true, label: 'DAGGERHEART.CONFIG.Range.close.name' }), far: new fields.NumberField({ integer: true, label: 'DAGGERHEART.CONFIG.Range.far.name' }) - }) + }), + sceneEnvironments: new ForeignDocumentUUIDArrayField({ type: 'Actor', prune: true }) }; } } diff --git a/module/documents/scene.mjs b/module/documents/scene.mjs index c6cdd2c2..7f880b1d 100644 --- a/module/documents/scene.mjs +++ b/module/documents/scene.mjs @@ -37,4 +37,30 @@ export default class DhScene extends Scene { this.#sizeSyncBatch.clear(); this.updateEmbeddedDocuments('Token', entries, { animation: { movementSpeed: 1.5 } }); }, 0); + + prepareBaseData() { + super.prepareBaseData(); + + if (this instanceof game.system.api.documents.DhScene) { + const system = new game.system.api.data.scenes.DHScene(this.flags.daggerheart); + + // Register this scene to all environements + for (const environment of system.sceneEnvironments) { + environment.system.scenes?.add(this); + } + } + } + + _onDelete(options, userId) { + super._onDelete(options, userId); + + if (this instanceof game.system.api.documents.DhScene) { + const system = new game.system.api.data.scenes.DHScene(this.flags.daggerheart); + + // Clear this scene from all environments that aren't deleted + for (const environment of system.sceneEnvironments) { + environment?.system?.scenes?.delete(this); + } + } + } } diff --git a/module/systemRegistration/socket.mjs b/module/systemRegistration/socket.mjs index 046f1b68..82ca2e1c 100644 --- a/module/systemRegistration/socket.mjs +++ b/module/systemRegistration/socket.mjs @@ -37,7 +37,8 @@ export const GMUpdateEvent = { export const RefreshType = { Countdown: 'DhCoundownRefresh', TagTeamRoll: 'DhTagTeamRollRefresh', - EffectsDisplay: 'DhEffectsDisplayRefresh' + EffectsDisplay: 'DhEffectsDisplayRefresh', + Scene: 'DhSceneRefresh' }; export const registerSocketHooks = () => { @@ -92,6 +93,10 @@ export const registerSocketHooks = () => { } } }); + Hooks.on(socketEvent.RefreshDocument, async data => { + const document = await foundry.utils.fromUuid(data.uuid); + document.sheet.render(); + }); }; export const registerUserQueries = () => { diff --git a/styles/less/ui/index.less b/styles/less/ui/index.less index 7f9ada25..25f51d0f 100644 --- a/styles/less/ui/index.less +++ b/styles/less/ui/index.less @@ -33,3 +33,5 @@ @import './scene-config/scene-config.less'; @import './effects-display/sheet.less'; + +@import './scene-navigation/scene-navigation.less'; diff --git a/styles/less/ui/scene-config/scene-config.less b/styles/less/ui/scene-config/scene-config.less index fb36dd33..664e7526 100644 --- a/styles/less/ui/scene-config/scene-config.less +++ b/styles/less/ui/scene-config/scene-config.less @@ -37,4 +37,63 @@ .helper-text { font-style: italic; } + + .scene-environments { + display: flex; + flex-direction: column; + gap: 8px; + + .scene-environment { + display: flex; + align-items: center; + gap: 8px; + + .scene-environment-inner { + display: flex; + align-items: center; + gap: 16px; + flex: 1; + + img { + height: 36px; + } + + h5 { + margin: 0; + } + + .tags { + display: flex; + gap: 4px; + padding-bottom: 0; + + .tag { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 3px 5px; + font-size: var(--font-size-12); + font: @font-body; + + background: light-dark(@dark-15, @beige-15); + border: 1px solid light-dark(@dark, @beige); + border-radius: 3px; + } + + .label { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + font-size: var(--font-size-12); + } + } + } + + .remove-icon { + font-size: 16px; + } + } + } } diff --git a/styles/less/ui/scene-navigation/scene-navigation.less b/styles/less/ui/scene-navigation/scene-navigation.less new file mode 100644 index 00000000..6b97ddec --- /dev/null +++ b/styles/less/ui/scene-navigation/scene-navigation.less @@ -0,0 +1,36 @@ +#ui-left #ui-left-column-2 { + flex: 0 0 230px; + + .scene-navigation { + .scene-wrapper { + display: flex; + gap: 2px; + height: var(--control-size); + width: 100%; + + .scene-environment { + padding: 0; + + img { + border-radius: 4px; + } + } + } + + .scene { + justify-content: center; + align-content: center; + background: var(--control-bg-color); + border: 1px solid var(--control-border-color); + border-radius: 4px; + color: var(--control-icon-color); + pointer-events: all; + transition: + border 0.25s, + color 0.25s; + text-shadow: none; + width: 200px; + max-width: 200px; + } + } +} diff --git a/templates/scene/dh-config.hbs b/templates/scene/dh-config.hbs index 1f7dcd81..017613ee 100644 --- a/templates/scene/dh-config.hbs +++ b/templates/scene/dh-config.hbs @@ -21,4 +21,39 @@ {{localize "DAGGERHEART.SETTINGS.Scene.disabledText"}} {{/if}} + +
\ No newline at end of file diff --git a/templates/ui/sceneNavigation/scene-navigation.hbs b/templates/ui/sceneNavigation/scene-navigation.hbs new file mode 100644 index 00000000..41e9e3e8 --- /dev/null +++ b/templates/ui/sceneNavigation/scene-navigation.hbs @@ -0,0 +1,36 @@ +