/* * This file is part of the warpgate module (https://github.com/trioderegion/warpgate) * Copyright (c) 2021 Matthew Haentschke. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import {logger} from './logger.js' import {MODULE} from './module.js' import {Crosshairs} from './crosshairs.js' import { Comms } from './comms.js' import {Propagator} from './propagator.js' const NAME = "Gateway"; /** @typedef {import('./api.js').CrosshairsConfig} CrosshairsConfig */ /** @typedef {import('./crosshairs.js').CrosshairsData} CrosshairsData */ /** * Callback started just prior to the crosshairs template being drawn. Is not awaited. Used for modifying * how the crosshairs is displayed and for responding to its displayed position * * All of the fields in the {@link CrosshairsConfig} object can be modified directly. Any fields owned by * MeasuredTemplate must be changed via `update|updateSource` as other DocumentData|DataModel classes. * Async functions will run in parallel while the user is moving the crosshairs. Serial functions will * block detection of the left and right click operations until return. * * @typedef {function(Crosshairs):any} ParallelShow * @param {Crosshairs} crosshairs The live Crosshairs instance associated with this callback * * @returns {any} */ /** * @class * @private */ export class Gateway { static register() { this.settings(); this.defaults(); } static settings() { const config = true; const settingsData = { openDelete : { scope: "world", config, default: false, type: Boolean, }, updateDelay : { scope: "client", config, default: 0, type: Number } }; MODULE.applySettings(settingsData); } static defaults() { MODULE[NAME] = { /** * type {CrosshairsConfig} * @const */ get crosshairsConfig() { return { size: 1, icon: 'icons/svg/dice-target.svg', label: '', labelOffset: { x: 0, y: 0 }, tag: 'crosshairs', drawIcon: true, drawOutline: true, interval: 2, fillAlpha: 0, tileTexture: false, lockSize: true, lockPosition: false, rememberControlled: false, //Measured template defaults texture: null, //x: 0, //y: 0, direction: 0, fillColor: game.user.color, } } } } /** * dnd5e helper function * @param { Item5e } item * @param {Object} [options={}] * @param {Object} [config={}] V10 Only field * @todo abstract further out of core code */ static async _rollItemGetLevel(item, options = {}, config = {}) { const result = MODULE.isV10 ? await item.use(config, options) : await item.roll(options); // extract the level at which the spell was cast if (!result) return 0; const content = MODULE.isV10 ? result.content : result.data.content; const level = content.charAt(content.indexOf("data-spell-level") + 18); return parseInt(level); } /** * Displays a circular template attached to the mouse cursor that snaps to grid centers * and grid intersections. * * Its size is in grid squares/hexes and can be scaled up and down via shift+mouse scroll. * Resulting data indicates the final position and size of the template. Note: Shift+Scroll * will increase/decrease the size of the crosshairs outline, which increases or decreases * the size of the token spawned, independent of other modifications. * * @param {CrosshairsConfig} [config] Configuration settings for how the crosshairs template should be displayed. * @param {Object} [callbacks] Functions executed at certain stages of the crosshair display process. * @param {ParallelShow} [callbacks.show] * * @returns {Promise} All fields contained by `MeasuredTemplateDocument#toObject`. Notably `x`, `y`, * `width` (in pixels), and the addition of `size` (final size, in grid units, e.g. "2" for a final diameter of 2 squares). * */ static async showCrosshairs(config = {}, callbacks = {}) { /* add in defaults */ mergeObject(config, MODULE[NAME].crosshairsConfig, {overwrite: false}) /* store currently controlled tokens */ let controlled = []; if (config.rememberControlled) { controlled = canvas.tokens.controlled; } /* if a specific initial location is not provided, grab the current mouse location */ if(!config.hasOwnProperty('x') && !config.hasOwnProperty('y')) { let mouseLoc = MODULE.getMouseStagePos(); mouseLoc = Crosshairs.getSnappedPosition(mouseLoc, config.interval); config.x = mouseLoc.x; config.y = mouseLoc.y; } const template = new Crosshairs(config, callbacks); await template.drawPreview(); const dataObj = template.toObject(); /* if we have stored any controlled tokens, * restore that control now */ for( const token of controlled ){ token.control({releaseOthers: false}); } return dataObj; } /* tests if a placeable's center point is within * the radius of the crosshairs */ static _containsCenter(placeable, crosshairsData) { const calcDistance = (A, B) => { return Math.hypot(A.x-B.x, A.y-B.y) }; const distance = calcDistance(placeable.center, crosshairsData); return distance <= crosshairsData.radius; } /** * Returns desired types of placeables whose center point * is within the crosshairs radius. * * @param {Object} crosshairsData Requires at least {x,y,radius,parent} (all in pixels, parent is a Scene) * @param {String|Array} [types='Token'] Collects the desired embedded placeable types. * @param {Function} [containedFilter=Gateway._containsCenter]. Optional function for determining if a placeable * is contained by the crosshairs. Default function tests for centerpoint containment. {@link Gateway._containsCenter} * * @return {Object} List of collected placeables keyed by embeddedName */ static collectPlaceables( crosshairsData, types = 'Token', containedFilter = Gateway._containsCenter ) { const isArray = types instanceof Array; types = isArray ? types : [types]; const result = types.reduce( (acc, embeddedName) => { const collection = crosshairsData.scene.getEmbeddedCollection(embeddedName); let contained = collection.filter( (document) => { return containedFilter(document.object, crosshairsData); }); acc[embeddedName] = contained; return acc; }, {}); /* if we are only collecting one kind of placeable, only return one kind of placeable */ return isArray ? result : result[types[0]]; } /** * Deletes the specified token from the specified scene. This function allows anyone * to delete any specified token unless this functionality is restricted to only * owned tokens in Warp Gate's module settings. This is the same function called * by the "Dismiss" header button on owned actor sheets. * * @param {string} tokenId * @param {string} [sceneId = canvas.scene.id] Needed if the dismissed token does not reside * on the currently viewed scene * @param {string} [onBehalf = game.user.id] Impersonate another user making this request */ static async dismissSpawn(tokenId, sceneId = canvas.scene?.id, onBehalf = game.user.id) { if (!tokenId || !sceneId){ logger.debug("Cannot dismiss null token or from a null scene.", tokenId, sceneId); return; } const tokenData = game.scenes.get(sceneId)?.getEmbeddedDocument("Token",tokenId); if(!tokenData){ logger.debug(`Token [${tokenId}] no longer exists on scene [${sceneId}]`); return; } /* check for permission to delete freely */ if (!MODULE.setting('openDelete')) { /* check permissions on token */ if (!tokenData.isOwner) { logger.error(MODULE.localize('error.unownedDelete')); return; } } logger.debug("Deleting token =>", tokenId, "from scene =>", sceneId); if (!MODULE.firstGM()){ logger.error(MODULE.localize('error.noGm')); return; } /** first gm drives */ if (MODULE.isFirstGM()) { const tokenDocs = await game.scenes.get(sceneId).deleteEmbeddedDocuments("Token",[tokenId]); const actorData = Comms.packToken(tokenDocs[0]); await warpgate.event.notify(warpgate.EVENT.DISMISS, {actorData}, onBehalf); } else { /** otherwise, we need to send a request for deletion */ Comms.requestDismissSpawn(tokenId, sceneId); } return; } /** * returns promise of token creation * @param {PrototypeTokenDocument} protoToken * @param {{ x: number, y: number }} spawnPoint * @param {boolean} collision */ static async _spawnTokenAtLocation(protoToken, spawnPoint, collision) { // Increase this offset for larger summons const gridSize = MODULE.isV10 ? canvas.scene.grid.size : canvas.scene.data.grid; let internalSpawnPoint = {x: spawnPoint.x - (gridSize * (protoToken.width/2)), y:spawnPoint.y - (gridSize * (protoToken.height/2))} /* call ripper's placement algorithm for collision checks * which will try to avoid tokens and walls */ if (collision) { const openPosition = Propagator.getFreePosition(protoToken, internalSpawnPoint); if(!openPosition) { logger.info(MODULE.localize('error.noOpenLocation')); } else { internalSpawnPoint = openPosition } } if ( MODULE.isV10 ) protoToken.updateSource(internalSpawnPoint); else protoToken.update(internalSpawnPoint); return canvas.scene.createEmbeddedDocuments("Token", [protoToken]) } }