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