|
|
- /*
- * 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 <http://www.gnu.org/licenses/>.
- */
-
- 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<CrosshairsData>} 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<String>} [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<String,PlaceableObject>} 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])
- }
-
-
-
- }
|