/*
|
|
* 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])
|
|
}
|
|
|
|
|
|
|
|
}
|