Initial
This commit is contained in:
345
resources/app/client/data/documents/region.js
Normal file
345
resources/app/client/data/documents/region.js
Normal file
@@ -0,0 +1,345 @@
|
||||
/**
|
||||
* @typedef {object} RegionEvent
|
||||
* @property {string} name The name of the event
|
||||
* @property {object} data The data of the event
|
||||
* @property {RegionDocument} region The Region the event was triggered on
|
||||
* @property {User} user The User that triggered the event
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} SocketRegionEvent
|
||||
* @property {string} regionUuid The UUID of the Region the event was triggered on
|
||||
* @property {string} userId The ID of the User that triggered the event
|
||||
* @property {string} eventName The name of the event
|
||||
* @property {object} eventData The data of the event
|
||||
* @property {string[]} eventDataUuids The keys of the event data that are Documents
|
||||
*/
|
||||
|
||||
/**
|
||||
* The client-side Region document which extends the common BaseRegion model.
|
||||
* @extends foundry.documents.BaseRegion
|
||||
* @mixes CanvasDocumentMixin
|
||||
*/
|
||||
class RegionDocument extends CanvasDocumentMixin(foundry.documents.BaseRegion) {
|
||||
|
||||
/**
|
||||
* Activate the Socket event listeners.
|
||||
* @param {Socket} socket The active game socket
|
||||
* @internal
|
||||
*/
|
||||
static _activateSocketListeners(socket) {
|
||||
socket.on("regionEvent", this.#onSocketEvent.bind(this));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle the Region event received via the socket.
|
||||
* @param {SocketRegionEvent} socketEvent The socket Region event
|
||||
*/
|
||||
static async #onSocketEvent(socketEvent) {
|
||||
const {regionUuid, userId, eventName, eventData, eventDataUuids} = socketEvent;
|
||||
const region = await fromUuid(regionUuid);
|
||||
if ( !region ) return;
|
||||
for ( const key of eventDataUuids ) {
|
||||
const uuid = foundry.utils.getProperty(eventData, key);
|
||||
const document = await fromUuid(uuid);
|
||||
foundry.utils.setProperty(eventData, key, document);
|
||||
}
|
||||
const event = {name: eventName, data: eventData, region, user: game.users.get(userId)};
|
||||
await region._handleEvent(event);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Update the tokens of the given regions.
|
||||
* @param {RegionDocument[]} regions The Regions documents, which must be all in the same Scene
|
||||
* @param {object} [options={}] Additional options
|
||||
* @param {boolean} [options.deleted=false] Are the Region documents deleted?
|
||||
* @param {boolean} [options.reset=true] Reset the Token document if animated?
|
||||
* If called during Region/Scene create/update/delete workflows, the Token documents are always reset and
|
||||
* so never in an animated state, which means the reset option may be false. It is important that the
|
||||
* containment test is not done in an animated state.
|
||||
* @internal
|
||||
*/
|
||||
static async _updateTokens(regions, {deleted=false, reset=true}={}) {
|
||||
if ( regions.length === 0 ) return;
|
||||
const updates = [];
|
||||
const scene = regions[0].parent;
|
||||
for ( const region of regions ) {
|
||||
if ( !deleted && !region.object ) continue;
|
||||
for ( const token of scene.tokens ) {
|
||||
if ( !deleted && !token.object ) continue;
|
||||
if ( !deleted && reset && (token.object.animationContexts.size !== 0) ) token.reset();
|
||||
const inside = !deleted && token.object.testInsideRegion(region.object);
|
||||
if ( inside ) {
|
||||
if ( !token._regions.includes(region.id) ) {
|
||||
updates.push({_id: token.id, _regions: [...token._regions, region.id].sort()});
|
||||
}
|
||||
} else {
|
||||
if ( token._regions.includes(region.id) ) {
|
||||
updates.push({_id: token.id, _regions: token._regions.filter(id => id !== region.id)});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await scene.updateEmbeddedDocuments("Token", updates);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static async _onCreateOperation(documents, operation, user) {
|
||||
if ( user.isSelf ) {
|
||||
// noinspection ES6MissingAwait
|
||||
RegionDocument._updateTokens(documents, {reset: false});
|
||||
}
|
||||
for ( const region of documents ) {
|
||||
const status = {active: true};
|
||||
if ( region.parent.isView ) status.viewed = true;
|
||||
// noinspection ES6MissingAwait
|
||||
region._handleEvent({name: CONST.REGION_EVENTS.BEHAVIOR_STATUS, data: status, region, user});
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static async _onUpdateOperation(documents, operation, user) {
|
||||
const changedRegions = [];
|
||||
for ( let i = 0; i < documents.length; i++ ) {
|
||||
const changed = operation.updates[i];
|
||||
if ( ("shapes" in changed) || ("elevation" in changed) ) changedRegions.push(documents[i]);
|
||||
}
|
||||
if ( user.isSelf ) {
|
||||
// noinspection ES6MissingAwait
|
||||
RegionDocument._updateTokens(changedRegions, {reset: false});
|
||||
}
|
||||
for ( const region of changedRegions ) {
|
||||
// noinspection ES6MissingAwait
|
||||
region._handleEvent({
|
||||
name: CONST.REGION_EVENTS.REGION_BOUNDARY,
|
||||
data: {},
|
||||
region,
|
||||
user
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static async _onDeleteOperation(documents, operation, user) {
|
||||
if ( user.isSelf ) {
|
||||
// noinspection ES6MissingAwait
|
||||
RegionDocument._updateTokens(documents, {deleted: true});
|
||||
}
|
||||
const regionEvents = [];
|
||||
for ( const region of documents ) {
|
||||
for ( const token of region.tokens ) {
|
||||
region.tokens.delete(token);
|
||||
regionEvents.push({
|
||||
name: CONST.REGION_EVENTS.TOKEN_EXIT,
|
||||
data: {token},
|
||||
region,
|
||||
user
|
||||
});
|
||||
}
|
||||
region.tokens.clear();
|
||||
}
|
||||
for ( const region of documents ) {
|
||||
const status = {active: false};
|
||||
if ( region.parent.isView ) status.viewed = false;
|
||||
regionEvents.push({name: CONST.REGION_EVENTS.BEHAVIOR_STATUS, data: status, region, user});
|
||||
}
|
||||
for ( const event of regionEvents ) {
|
||||
// noinspection ES6MissingAwait
|
||||
event.region._handleEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The tokens inside this region.
|
||||
* @type {Set<TokenDocument>}
|
||||
*/
|
||||
tokens = new Set();
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Trigger the Region event.
|
||||
* @param {string} eventName The event name
|
||||
* @param {object} eventData The event data
|
||||
* @returns {Promise<void>}
|
||||
* @internal
|
||||
*/
|
||||
async _triggerEvent(eventName, eventData) {
|
||||
|
||||
// Serialize Documents in the event data as UUIDs
|
||||
eventData = foundry.utils.deepClone(eventData);
|
||||
const eventDataUuids = [];
|
||||
const serializeDocuments = (object, key, path=key) => {
|
||||
const value = object[key];
|
||||
if ( (value === null) || (typeof value !== "object") ) return;
|
||||
if ( !value.constructor || (value.constructor === Object) ) {
|
||||
for ( const key in value ) serializeDocuments(value, key, `${path}.${key}`);
|
||||
} else if ( Array.isArray(value) ) {
|
||||
for ( let i = 0; i < value.length; i++ ) serializeDocuments(value, i, `${path}.${i}`);
|
||||
} else if ( value instanceof foundry.abstract.Document ) {
|
||||
object[key] = value.uuid;
|
||||
eventDataUuids.push(path);
|
||||
}
|
||||
};
|
||||
for ( const key in eventData ) serializeDocuments(eventData, key);
|
||||
|
||||
// Emit socket event
|
||||
game.socket.emit("regionEvent", {
|
||||
regionUuid: this.uuid,
|
||||
userId: game.user.id,
|
||||
eventName,
|
||||
eventData,
|
||||
eventDataUuids
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle the Region event.
|
||||
* @param {RegionEvent} event The Region event
|
||||
* @returns {Promise<void>}
|
||||
* @internal
|
||||
*/
|
||||
async _handleEvent(event) {
|
||||
const results = await Promise.allSettled(this.behaviors.filter(b => !b.disabled)
|
||||
.map(b => b._handleRegionEvent(event)));
|
||||
for ( const result of results ) {
|
||||
if ( result.status === "rejected" ) console.error(result.reason);
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Database Event Handlers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* When behaviors are created within the region, dispatch events for Tokens that are already inside the region.
|
||||
* @inheritDoc
|
||||
*/
|
||||
_onCreateDescendantDocuments(parent, collection, documents, data, options, userId) {
|
||||
super._onCreateDescendantDocuments(parent, collection, documents, data, options, userId);
|
||||
if ( collection !== "behaviors" ) return;
|
||||
|
||||
// Trigger events
|
||||
const user = game.users.get(userId);
|
||||
for ( let i = 0; i < documents.length; i++ ) {
|
||||
const behavior = documents[i];
|
||||
if ( behavior.disabled ) continue;
|
||||
|
||||
// Trigger status event
|
||||
const status = {active: true};
|
||||
if ( this.parent.isView ) status.viewed = true;
|
||||
behavior._handleRegionEvent({name: CONST.REGION_EVENTS.BEHAVIOR_STATUS, data: status, region: this, user});
|
||||
|
||||
// Trigger enter events
|
||||
for ( const token of this.tokens ) {
|
||||
const deleted = !this.parent.tokens.has(token.id);
|
||||
if ( deleted ) continue;
|
||||
behavior._handleRegionEvent({
|
||||
name: CONST.REGION_EVENTS.TOKEN_ENTER,
|
||||
data: {token},
|
||||
region: this,
|
||||
user
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* When behaviors are updated within the region, dispatch events for Tokens that are already inside the region.
|
||||
* @inheritDoc
|
||||
*/
|
||||
_onUpdateDescendantDocuments(parent, collection, documents, changes, options, userId) {
|
||||
super._onUpdateDescendantDocuments(parent, collection, documents, changes, options, userId);
|
||||
if ( collection !== "behaviors" ) return;
|
||||
|
||||
// Trigger status events
|
||||
const user = game.users.get(userId);
|
||||
for ( let i = 0; i < documents.length; i++ ) {
|
||||
const disabled = changes[i].disabled;
|
||||
if ( disabled === undefined ) continue;
|
||||
const behavior = documents[i];
|
||||
|
||||
// Trigger exit events
|
||||
if ( disabled ) {
|
||||
for ( const token of this.tokens ) {
|
||||
behavior._handleRegionEvent({
|
||||
name: CONST.REGION_EVENTS.TOKEN_EXIT,
|
||||
data: {token},
|
||||
region: this,
|
||||
user
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Triger status event
|
||||
const status = {active: !disabled};
|
||||
if ( this.parent.isView ) status.viewed = !disabled;
|
||||
behavior._handleRegionEvent({name: CONST.REGION_EVENTS.BEHAVIOR_STATUS, data: status, region: this, user});
|
||||
|
||||
// Trigger enter events
|
||||
if ( !disabled ) {
|
||||
for ( const token of this.tokens ) {
|
||||
const deleted = !this.parent.tokens.has(token.id);
|
||||
if ( deleted ) continue;
|
||||
behavior._handleRegionEvent({
|
||||
name: CONST.REGION_EVENTS.TOKEN_ENTER,
|
||||
data: {token},
|
||||
region: this,
|
||||
user
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* When behaviors are deleted within the region, dispatch events for Tokens that were previously inside the region.
|
||||
* @inheritDoc
|
||||
*/
|
||||
_onDeleteDescendantDocuments(parent, collection, documents, ids, options, userId) {
|
||||
super._onDeleteDescendantDocuments(parent, collection, ids, options, userId);
|
||||
if ( collection !== "behaviors" ) return;
|
||||
|
||||
// Trigger events
|
||||
const user = game.users.get(userId);
|
||||
for ( let i = 0; i < documents.length; i++ ) {
|
||||
const behavior = documents[i];
|
||||
if ( behavior.disabled ) continue;
|
||||
|
||||
// Trigger exit events
|
||||
for ( const token of this.tokens ) {
|
||||
const deleted = !this.parent.tokens.has(token.id);
|
||||
if ( deleted ) continue;
|
||||
behavior._handleRegionEvent({
|
||||
name: CONST.REGION_EVENTS.TOKEN_EXIT,
|
||||
data: {token},
|
||||
region: this,
|
||||
user
|
||||
});
|
||||
}
|
||||
|
||||
// Trigger status event
|
||||
const status = {active: false};
|
||||
if ( this.parent.isView ) status.viewed = false;
|
||||
behavior._handleRegionEvent({name: CONST.REGION_EVENTS.BEHAVIOR_STATUS, data: status, region: this, user});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user