From edbf5aa55f09acdce4d7d611d793923daf9081c6 Mon Sep 17 00:00:00 2001 From: WBHarry <89362246+WBHarry@users.noreply.github.com> Date: Sat, 2 May 2026 22:34:53 +0200 Subject: [PATCH] [Fix] Player Created Regions (#1855) * Fixed so that creating regions without behaviors work for players. Fixed so that creating regions with behaviors works via GmEmit for players * Updated previous uses of emitAsGM to emitGMUpdate * Fixed linting * Update module/documents/chatMessage.mjs Co-authored-by: Carlos Fernandez --------- Co-authored-by: Carlos Fernandez --- lang/en.json | 3 +- .../applications/dialogs/groupRollDialog.mjs | 4 +- module/applications/dialogs/tagTeamDialog.mjs | 4 +- module/applications/ui/countdownEdit.mjs | 4 +- module/applications/ui/countdowns.mjs | 8 +-- module/applications/ui/fearTracker.mjs | 4 +- module/applications/ui/sceneNavigation.mjs | 4 +- module/canvas/placeables/regionLayer.mjs | 12 ++-- module/data/fields/action/countdownField.mjs | 4 +- module/data/fields/action/effectsField.mjs | 4 +- module/documents/actor.mjs | 6 +- module/documents/chatMessage.mjs | 66 ++++++++++++------- module/systemRegistration/socket.mjs | 54 ++++++++++----- 13 files changed, 108 insertions(+), 69 deletions(-) diff --git a/lang/en.json b/lang/en.json index b4b1410e..817fc355 100755 --- a/lang/en.json +++ b/lang/en.json @@ -3220,7 +3220,8 @@ "domainTouchRequirement": "This domain card requires {nr} {domain} cards in the loadout to be used", "knowTheTide": "Know The Tide gained a token", "lackingItemTransferPermission": "User {user} lacks owner permission needed to transfer items to {target}", - "noTokenTargeted": "No token is targeted" + "noTokenTargeted": "No token is targeted", + "behaviorRegionRequiresGM": "Creating a Region with an attached Behavior requires an online GM" }, "Progress": { "migrationLabel": "Performing system migration. Please wait and do not close Foundry." diff --git a/module/applications/dialogs/groupRollDialog.mjs b/module/applications/dialogs/groupRollDialog.mjs index 48110e4c..bd45fe91 100644 --- a/module/applications/dialogs/groupRollDialog.mjs +++ b/module/applications/dialogs/groupRollDialog.mjs @@ -1,5 +1,5 @@ import { ResourceUpdateMap } from '../../data/action/baseAction.mjs'; -import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; +import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; import Party from '../sheets/actors/party.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; @@ -242,7 +242,7 @@ export default class GroupRollDialog extends HandlebarsApplicationMixin(Applicat }); }; - await emitAsGM( + await emitGMUpdate( GMUpdateEvent.UpdateDocument, gmUpdate, update, diff --git a/module/applications/dialogs/tagTeamDialog.mjs b/module/applications/dialogs/tagTeamDialog.mjs index 325cc445..e06cbe48 100644 --- a/module/applications/dialogs/tagTeamDialog.mjs +++ b/module/applications/dialogs/tagTeamDialog.mjs @@ -1,6 +1,6 @@ import { MemberData } from '../../data/tagTeamData.mjs'; import { getCritDamageBonus } from '../../helpers/utils.mjs'; -import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; +import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; import Party from '../sheets/actors/party.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; @@ -259,7 +259,7 @@ export default class TagTeamDialog extends HandlebarsApplicationMixin(Applicatio }); }; - await emitAsGM( + await emitGMUpdate( GMUpdateEvent.UpdateDocument, gmUpdate, update, diff --git a/module/applications/ui/countdownEdit.mjs b/module/applications/ui/countdownEdit.mjs index 8bb9fc1d..b418107c 100644 --- a/module/applications/ui/countdownEdit.mjs +++ b/module/applications/ui/countdownEdit.mjs @@ -1,6 +1,6 @@ import { DhCountdown } from '../../data/countdowns.mjs'; import { waitForDiceSoNice } from '../../helpers/utils.mjs'; -import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; +import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; @@ -114,7 +114,7 @@ export default class CountdownEdit extends HandlebarsApplicationMixin(Applicatio } await this.data.updateSource(update); - await emitAsGM(GMUpdateEvent.UpdateCountdowns, this.gmSetSetting.bind(this.data), this.data, null, { + await emitGMUpdate(GMUpdateEvent.UpdateCountdowns, this.gmSetSetting.bind(this.data), this.data, null, { refreshType: RefreshType.Countdown }); diff --git a/module/applications/ui/countdowns.mjs b/module/applications/ui/countdowns.mjs index 79a59a07..052564cc 100644 --- a/module/applications/ui/countdowns.mjs +++ b/module/applications/ui/countdowns.mjs @@ -1,5 +1,5 @@ import { waitForDiceSoNice } from '../../helpers/utils.mjs'; -import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; +import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../systemRegistration/socket.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; @@ -204,7 +204,7 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application start: newMax } }); - await emitAsGM(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, { + await emitGMUpdate(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, { refreshType: RefreshType.Countdown }); } @@ -218,7 +218,7 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application ? Math.min(countdown.progress.current + 1, countdown.progress.start) : Math.max(countdown.progress.current - 1, 0); await settings.updateSource({ [`countdowns.${target.id}.progress.current`]: newCurrent }); - await emitAsGM(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, { + await emitGMUpdate(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, { refreshType: RefreshType.Countdown }); } @@ -277,7 +277,7 @@ export default class DhCountdowns extends HandlebarsApplicationMixin(Application return acc; }, {}) }; - await emitAsGM(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, { + await emitGMUpdate(GMUpdateEvent.UpdateCountdowns, DhCountdowns.gmSetSetting.bind(settings), settings, null, { refreshType: RefreshType.Countdown }); } diff --git a/module/applications/ui/fearTracker.mjs b/module/applications/ui/fearTracker.mjs index 4e5e1132..8c247f79 100644 --- a/module/applications/ui/fearTracker.mjs +++ b/module/applications/ui/fearTracker.mjs @@ -1,4 +1,4 @@ -import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs'; +import { emitGMUpdate, GMUpdateEvent } from '../../systemRegistration/socket.mjs'; const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api; @@ -104,7 +104,7 @@ export default class FearTracker extends HandlebarsApplicationMixin(ApplicationV } async updateFear(value) { - return emitAsGM( + return emitGMUpdate( GMUpdateEvent.UpdateFear, game.settings.set.bind(game.settings, CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Resources.Fear), value diff --git a/module/applications/ui/sceneNavigation.mjs b/module/applications/ui/sceneNavigation.mjs index a0005fc7..982063e7 100644 --- a/module/applications/ui/sceneNavigation.mjs +++ b/module/applications/ui/sceneNavigation.mjs @@ -1,4 +1,4 @@ -import { emitAsGM, GMUpdateEvent } from '../../systemRegistration/socket.mjs'; +import { emitGMUpdate, GMUpdateEvent } from '../../systemRegistration/socket.mjs'; export default class DhSceneNavigation extends foundry.applications.ui.SceneNavigation { /** @inheritdoc */ @@ -68,7 +68,7 @@ export default class DhSceneNavigation extends foundry.applications.ui.SceneNavi 1 )[0]; newEnvironments.unshift(newFirst); - emitAsGM( + emitGMUpdate( GMUpdateEvent.UpdateDocument, scene.update.bind(scene), { 'flags.daggerheart.sceneEnvironments': newEnvironments }, diff --git a/module/canvas/placeables/regionLayer.mjs b/module/canvas/placeables/regionLayer.mjs index c53cf782..684fdd5e 100644 --- a/module/canvas/placeables/regionLayer.mjs +++ b/module/canvas/placeables/regionLayer.mjs @@ -57,14 +57,14 @@ export default class DhRegionLayer extends foundry.canvas.layers.RegionLayer { } async placeRegion(data, options = {}) { - const preConfirm = ({ _event, document, _create, _options }) => { - const shape = document.shapes[0]; + const preConfirm = data => { + const shape = data.document.shapes[0]; const isEmanation = shape.type === 'emanation'; if (isEmanation) { const token = this.#findTokenInBounds(shape.base.origin); - if (!token) return options.preConfirm?.() ?? true; + if (!token) return options.preConfirm?.(data) ?? true; const shapeData = shape.toObject(); - document.updateSource({ + data.document.updateSource({ shapes: [ { ...shapeData, @@ -80,10 +80,10 @@ export default class DhRegionLayer extends foundry.canvas.layers.RegionLayer { }); } - return options?.preConfirm?.() ?? true; + return options?.preConfirm?.(data) ?? true; }; - super.placeRegion(data, { ...options, preConfirm }); + return await super.placeRegion(data, { ...options, preConfirm }); } /** Searches for token at origin point, returning null if there are no tokens or multiple overlapping tokens */ diff --git a/module/data/fields/action/countdownField.mjs b/module/data/fields/action/countdownField.mjs index 719ca749..990f8ef1 100644 --- a/module/data/fields/action/countdownField.mjs +++ b/module/data/fields/action/countdownField.mjs @@ -1,4 +1,4 @@ -import { emitAsGM, GMUpdateEvent, RefreshType, socketEvent } from '../../../systemRegistration/socket.mjs'; +import { emitGMUpdate, GMUpdateEvent, RefreshType, socketEvent } from '../../../systemRegistration/socket.mjs'; const fields = foundry.data.fields; @@ -78,7 +78,7 @@ export default class CountdownField extends fields.ArrayField { ); } - await emitAsGM( + await emitGMUpdate( GMUpdateEvent.UpdateCountdowns, async () => { const countdownSetting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns); diff --git a/module/data/fields/action/effectsField.mjs b/module/data/fields/action/effectsField.mjs index 9a4ffc31..1053e51d 100644 --- a/module/data/fields/action/effectsField.mjs +++ b/module/data/fields/action/effectsField.mjs @@ -1,4 +1,4 @@ -import { emitAsGM, GMUpdateEvent } from '../../../systemRegistration/socket.mjs'; +import { emitGMUpdate, GMUpdateEvent } from '../../../systemRegistration/socket.mjs'; const fields = foundry.data.fields; @@ -34,7 +34,7 @@ export default class EffectsField extends fields.ArrayField { } if (EffectsField.getAutomation() || force) { targets ??= (message.system?.targets ?? config.targets).filter(t => !config.hasRoll || t.hit); - await emitAsGM(GMUpdateEvent.UpdateEffect, EffectsField.applyEffects.bind(this), targets, this.uuid); + await emitGMUpdate(GMUpdateEvent.UpdateEffect, EffectsField.applyEffects.bind(this), targets, this.uuid); // EffectsField.applyEffects.call(this, config.targets.filter(t => !config.hasRoll || t.hit)); } } diff --git a/module/documents/actor.mjs b/module/documents/actor.mjs index eb57a186..5df87b6c 100644 --- a/module/documents/actor.mjs +++ b/module/documents/actor.mjs @@ -1,4 +1,4 @@ -import { emitAsGM, GMUpdateEvent } from '../systemRegistration/socket.mjs'; +import { emitGMUpdate, GMUpdateEvent } from '../systemRegistration/socket.mjs'; import { LevelOptionType } from '../data/levelTier.mjs'; import DHFeature from '../data/item/feature.mjs'; import { createScrollText, damageKeyToNumber, getDamageKey, createShallowProxy } from '../helpers/utils.mjs'; @@ -827,7 +827,7 @@ export default class DhpActor extends Actor { const u = updates[key]; if (key === 'items') { Object.values(u).forEach(async item => { - await emitAsGM( + await emitGMUpdate( GMUpdateEvent.UpdateDocument, item.target.update.bind(item.target), item.resources, @@ -836,7 +836,7 @@ export default class DhpActor extends Actor { }); } else { if (Object.keys(u.resources).length > 0) { - await emitAsGM( + await emitGMUpdate( GMUpdateEvent.UpdateDocument, u.target.update.bind(u.target), u.resources, diff --git a/module/documents/chatMessage.mjs b/module/documents/chatMessage.mjs index 2e20fb87..78bab016 100644 --- a/module/documents/chatMessage.mjs +++ b/module/documents/chatMessage.mjs @@ -1,4 +1,4 @@ -import { emitAsGM, GMUpdateEvent } from '../systemRegistration/socket.mjs'; +import { emitGMUpdate, emitGMCreate, GMUpdateEvent } from '../systemRegistration/socket.mjs'; export default class DhpChatMessage extends foundry.documents.ChatMessage { targetHook = null; @@ -214,7 +214,7 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { const action = this.system.action; if (!action || !action?.hasSave) return; game.system.api.fields.ActionFields.SaveField.rollSave.call(action, token.actor, event).then(result => - emitAsGM( + emitGMUpdate( GMUpdateEvent.UpdateSaveMessage, game.system.api.fields.ActionFields.SaveField.updateSaveMessage.bind( action, @@ -259,27 +259,47 @@ export default class DhpChatMessage extends foundry.documents.ChatMessage { const { shape: type, size: range } = selectedArea; const shapeData = CONFIG.Canvas.layers.regions.layerClass.getTemplateShape({ type, range }); - await canvas.regions.placeRegion( - { - name: selectedArea.name, - shapes: [shapeData], - restriction: { enabled: false, type: 'move', priority: 0 }, - behaviors: [ - { - name: game.i18n.localize('TYPES.RegionBehavior.applyActiveEffect'), - type: 'applyActiveEffect', - system: { - effects: effects - } - } - ], - displayMeasurements: true, - locked: false, - ownership: { default: CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE }, - visibility: CONST.REGION_VISIBILITY.ALWAYS - }, - { create: true } - ); + const scene = game.scenes.get(game.user.viewedScene); + const level = scene.levels.find(x => x.isView); + + const regionData = { + name: selectedArea.name, + levels: level ? [level.id] : [], + shapes: [shapeData], + restriction: { enabled: false, type: 'move', priority: 0 }, + behaviors: + effects.length > 0 + ? [ + { + name: game.i18n.localize('TYPES.RegionBehavior.applyActiveEffect'), + type: 'applyActiveEffect', + system: { + effects: effects + } + } + ] + : [], + displayMeasurements: true, + locked: false, + ownership: { default: CONST.DOCUMENT_OWNERSHIP_LEVELS.NONE }, + visibility: CONST.REGION_VISIBILITY.ALWAYS + }; + const placeRegion = data => { + canvas.regions.placeRegion(data, { create: true }); + }; + + // Regions with effects must be placed by the GM + if (effects.length > 0 && !game.user.isGM) { + if (!game.users.activeGM) + return ui.notifications.error( + game.i18n.localize('DAGGERHEART.UI.Notifications.behaviorRegionRequiresGM') + ); + + const region = await canvas.regions.placeRegion(regionData, { create: false }); + emitGMCreate('Region', placeRegion, region, scene.id); + } else { + placeRegion(regionData); + } }; if (this.system.action.areas.length === 1) createArea(this.system.action.areas[0]); diff --git a/module/systemRegistration/socket.mjs b/module/systemRegistration/socket.mjs index 8fed346d..de9bf00c 100644 --- a/module/systemRegistration/socket.mjs +++ b/module/systemRegistration/socket.mjs @@ -6,6 +6,9 @@ export function handleSocketEvent({ action = null, data = {} } = {}) { case socketEvent.GMUpdate: Hooks.callAll(socketEvent.GMUpdate, data); break; + case socketEvent.GMCreate: + Hooks.callAll(socketEvent.GMCreate, data); + break; case socketEvent.DhpFearUpdate: Hooks.callAll(socketEvent.DhpFearUpdate); break; @@ -25,6 +28,7 @@ export function handleSocketEvent({ action = null, data = {} } = {}) { export const socketEvent = { GMUpdate: 'DhGMUpdate', + GMCreate: 'DhGMCreate', Refresh: 'DhRefresh', DhpFearUpdate: 'DhFearUpdate', DowntimeTrigger: 'DowntimeTrigger', @@ -56,14 +60,14 @@ export const registerSocketHooks = () => { const document = data.uuid ? await fromUuid(data.uuid) : null; switch (data.action) { case GMUpdateEvent.UpdateDocument: - if (document && data.update) await document.update(data.update); + if (document && data.data) await document.update(data.data); break; case GMUpdateEvent.UpdateEffect: - if (document && data.update) - await game.system.api.fields.ActionFields.EffectsField.applyEffects.call(document, data.update); + if (document && data.data) + await game.system.api.fields.ActionFields.EffectsField.applyEffects.call(document, data.data); break; case GMUpdateEvent.UpdateSetting: - await game.settings.set(CONFIG.DH.id, data.uuid, data.update); + await game.settings.set(CONFIG.DH.id, data.uuid, data.data); break; case GMUpdateEvent.UpdateFear: await game.settings.set( @@ -73,22 +77,22 @@ export const registerSocketHooks = () => { 0, Math.min( game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Homebrew).maxFear, - data.update + data.data ) ) ); break; case GMUpdateEvent.UpdateCountdowns: - await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data.update); + await game.settings.set(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.Countdowns, data.data); Hooks.callAll(socketEvent.Refresh, { refreshType: RefreshType.Countdown }); break; case GMUpdateEvent.UpdateSaveMessage: - const message = game.messages.get(data.update.message); + const message = game.messages.get(data.data.message); if (!message) return; game.system.api.fields.ActionFields.SaveField.updateSaveMessage( - data.update.result, + data.data.result, message, - data.update.token + data.data.token ); break; } @@ -102,6 +106,17 @@ export const registerSocketHooks = () => { } } }); + + Hooks.on(socketEvent.GMCreate, async ({ data, documentType, scene }) => { + if (!game.user.isGM) return; + + switch (documentType) { + default: + const cls = getDocumentClass(documentType); + cls.create(data, { parent: game.scenes.get(scene) }); + break; + } + }); }; export const registerUserQueries = () => { @@ -109,18 +124,21 @@ export const registerUserQueries = () => { CONFIG.queries.reactionRoll = game.system.api.fields.ActionFields.SaveField.rollSaveQuery; }; -export const emitAsGM = async (eventName, callback, update, uuid = null, refresh = null) => { +export const emitGMUpdate = async (eventName, callback, update, uuid = null, refresh = null) => { + return await emitAsGM(socketEvent.GMUpdate, { action: eventName, callback, data: update, uuid, refresh }); +}; + +export const emitGMCreate = async (documentType, callback, data, scene) => { + return await emitAsGM(socketEvent.GMCreate, { documentType, callback, data, scene }); +}; + +export const emitAsGM = async (event, data = { callback: () => {}, data: {} }) => { if (!game.user.isGM) { return await game.socket.emit(`system.${CONFIG.DH.id}`, { - action: socketEvent.GMUpdate, - data: { - action: eventName, - uuid, - update, - refresh - } + action: event, + data: data }); - } else return callback(update); + } else return data.callback(data.data); }; export const emitAsOwner = (eventName, userId, args) => {