All user data for FoundryVTT. Includes worlds, systems, modules, and any asset in the "foundryuserdata" directory. Does NOT include the FoundryVTT installation itself.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

304 lines
10 KiB

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