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.
 
 
 

529 lines
16 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'
/** @typedef {import('@league-of-foundry-developers/foundry-vtt-types/src/foundry/common/data/data.mjs/measuredTemplateData.js').MeasuredTemplateDataProperties} MeasuredTemplateProperties */
/**
* Contains all fields from `MeasuredTemplate#toObject`, plus the following.
*
* @typedef {Object} CrosshairsData
* @borrows MeasuredTemplateProperties
* @prop {boolean} cancelled Workflow cancelled via right click (true)
* @prop {Scene} scene Scene on this crosshairs was last active
* @prop {number} radius Final radius of template, in pixels
* @prop {number} size Final diameter of template, in grid units
*/
/**
* @class
*/
export class Crosshairs extends MeasuredTemplate {
//constructor(gridSize = 1, data = {}){
constructor(config, callbacks = {}) {
const templateData = {
t: "circle",
user: game.user.id,
distance: config.size,
x: config.x,
y: config.y,
fillColor: config.fillColor,
width: 1,
texture: config.texture,
direction: config.direction,
}
const template = new CONFIG.MeasuredTemplate.documentClass(templateData, {parent: canvas.scene});
super(template);
/** @TODO all of these fields should be part of the source data schema for this class **/
/** image path to display in the center (under mouse cursor) */
this.icon = config.icon ?? Crosshairs.ERROR_TEXTURE;
/** text to display below crosshairs' circle */
this.label = config.label;
/** Offsets the default position of the label (in pixels) */
this.labelOffset = config.labelOffset;
/**
* Arbitrary field used to identify this instance
* of a Crosshairs in the canvas.templates.preview
* list
*/
this.tag = config.tag;
/** Should the center icon be shown? */
this.drawIcon = config.drawIcon;
/** Should the outer circle be shown? */
this.drawOutline = config.drawOutline;
/** Opacity of the fill color */
this.fillAlpha = config.fillAlpha;
/** Should the texture (if any) be tiled
* or scaled and offset? */
this.tileTexture = config.tileTexture;
/** locks the size of crosshairs (shift+scroll) */
this.lockSize = config.lockSize;
/** locks the position of crosshairs */
this.lockPosition = config.lockPosition;
/** Number of quantization steps along
* a square's edge (N+1 snap points
* along each edge, conting endpoints)
*/
this.interval = config.interval;
/** Callback functions to execute
* at particular times
*/
this.callbacks = callbacks;
/** Indicates if the user is actively
* placing the crosshairs.
* Setting this to true in the show
* callback will stop execution
* and report the current mouse position
* as the chosen location
*/
this.inFlight = false;
/** indicates if the placement of
* crosshairs was canceled (with
* a right click)
*/
this.cancelled = true;
/**
* Indicators on where cancel was initiated
* for determining if it was a drag or a cancel
*/
this.rightX = 0;
this.rightY = 0;
/** @type {number} */
this.radius = this.document.distance * this.scene.grid.size / 2;
}
/**
* @returns {CrosshairsData} Current Crosshairs class data
*/
toObject() {
/** @type {CrosshairsData} */
const data = foundry.utils.mergeObject(this.document.toObject(), {
cancelled: this.cancelled,
scene: this.scene,
radius: this.radius,
size: this.document.distance,
});
delete data.width;
return data;
}
static ERROR_TEXTURE = 'icons/svg/hazard.svg'
/**
* Will retrieve the active crosshairs instance with the defined tag identifier.
* @param {string} key Crosshairs identifier. Will be compared against the Crosshairs `tag` field for strict equality.
* @returns {PIXI.DisplayObject|undefined}
*/
static getTag(key) {
return canvas.templates.preview.children.find( child => child.tag === key )
}
static getSnappedPosition({x,y}, interval){
const offset = interval < 0 ? canvas.grid.size/2 : 0;
const snapped = canvas.grid.getSnappedPosition(x - offset, y - offset, interval);
return {x: snapped.x + offset, y: snapped.y + offset};
}
/* -----------EXAMPLE CODE FROM MEASUREDTEMPLATE.JS--------- */
/* Portions of the core package (MeasuredTemplate) repackaged
* in accordance with the "Limited License Agreement for Module
* Development, found here: https://foundryvtt.com/article/license/
* Changes noted where possible
*/
/**
* Set the displayed ruler tooltip text and position
* @private
*/
//BEGIN WARPGATE
_setRulerText() {
this.ruler.text = this.label;
/** swap the X and Y to use the default dx/dy of a ray (pointed right)
//to align the text to the bottom of the template */
this.ruler.position.set(-this.ruler.width / 2 + this.labelOffset.x, this.template.height / 2 + 5 + this.labelOffset.y);
//END WARPGATE
}
/** @override */
async draw() {
this.clear();
// Load the texture
const texture = this.document.texture;
if ( texture ) {
this._texture = await loadTexture(texture, {fallback: 'icons/svg/hazard.svg'});
} else {
this._texture = null;
}
// Template shape
this.template = this.addChild(new PIXI.Graphics());
// Rotation handle
//BEGIN WARPGATE
//this.handle = this.addChild(new PIXI.Graphics());
//END WARPGATE
// Draw the control icon
//if(this.drawIcon)
this.controlIcon = this.addChild(this._drawControlIcon());
// Draw the ruler measurement
this.ruler = this.addChild(this._drawRulerText());
// Update the shape and highlight grid squares
this.refresh();
//BEGIN WARPGATE
this._setRulerText();
//this.highlightGrid();
//END WARPGATE
// Enable interactivity, only if the Tile has a true ID
if ( this.id ) this.activateListeners();
return this;
}
/**
* Draw the Text label used for the MeasuredTemplate
* @return {PreciseText}
* @protected
*/
_drawRulerText() {
const style = CONFIG.canvasTextStyle.clone();
style.fontSize = Math.max(Math.round(canvas.dimensions.size * 0.36 * 12) / 12, 36);
const text = new PreciseText(null, style);
//BEGIN WARPGATE
//text.anchor.set(0.5, 0);
text.anchor.set(0, 0);
//END WARPGATE
return text;
}
/**
* Draw the ControlIcon for the MeasuredTemplate
* @return {ControlIcon}
* @protected
*/
_drawControlIcon() {
const size = Math.max(Math.round((canvas.dimensions.size * 0.5) / 20) * 20, 40);
//BEGIN WARPGATE
let icon = new ControlIcon({texture: this.icon, size: size});
icon.visible = this.drawIcon;
//END WARPGATE
icon.pivot.set(size*0.5, size*0.5);
//icon.x -= (size * 0.5);
//icon.y -= (size * 0.5);
icon.angle = this.document.direction;
return icon;
}
/** @override */
refresh() {
if (!this.template) return;
let d = canvas.dimensions;
const document = this.document;
this.position.set(document.x, document.y);
// Extract and prepare data
let {direction, distance} = document;
distance *= (d.size/2);
//BEGIN WARPGATE
//width *= (d.size / d.distance);
//END WARPGATE
direction = Math.toRadians(direction);
// Create ray and bounding rectangle
this.ray = Ray.fromAngle(document.x, document.y, direction, distance);
// Get the Template shape
switch (document.t) {
case "circle":
this.shape = this._getCircleShape(distance);
break;
default: logger.error("Non-circular Crosshairs is unsupported!");
}
// Draw the Template outline
this.template.clear()
.lineStyle(this._borderThickness, this.borderColor, this.drawOutline ? 0.75 : 0)
// Fill Color or Texture
if (this._texture) {
/* assume 0,0 is top left of texture
* and scale/offset this texture (due to origin
* at center of template). tileTexture indicates
* that this texture is tilable and does not
* need to be scaled/offset */
const scale = this.tileTexture ? 1 : distance * 2 / this._texture.width;
const offset = this.tileTexture ? 0 : distance;
this.template.beginTextureFill({
texture: this._texture,
matrix: new PIXI.Matrix().scale(scale, scale).translate(-offset, -offset)
});
} else {
this.template.beginFill(this.fillColor, this.fillAlpha);
}
// Draw the shape
this.template.drawShape(this.shape);
// Draw origin and destination points
//BEGIN WARPGATE
//this.template.lineStyle(this._borderThickness, 0x000000, this.drawOutline ? 0.75 : 0)
// .beginFill(0x000000, 0.5)
//.drawCircle(0, 0, 6)
//.drawCircle(this.ray.dx, this.ray.dy, 6);
//END WARPGATE
// Update visibility
if (this.drawIcon) {
this.controlIcon.visible = true;
this.controlIcon.border.visible = this._hover
this.controlIcon.angle = document.direction;
}
// Draw ruler text
//BEGIN WARPGATE
this._setRulerText()
//END WARPGATE
return this;
}
/* END MEASUREDTEMPLATE.JS USAGE */
/* -----------EXAMPLE CODE FROM ABILITY-TEMPLATE.JS--------- */
/* Foundry VTT 5th Edition
* Copyright (C) 2019 Foundry Network
*
* 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, either version 3 of the License, or
* (at your option) any later version.
*
* 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.
*
* Original License:
* https://gitlab.com/foundrynet/dnd5e/-/blob/master/LICENSE.txt
*/
/**
* Creates a preview of the spell template
*/
async drawPreview() {
// Draw the template and switch to the template layer
this.initialLayer = canvas.activeLayer;
this.layer.activate();
this.draw();
this.layer.preview.addChild(this);
this.layer.interactiveChildren = false;
// Hide the sheet that originated the preview
//BEGIN WARPGATE
this.inFlight = true;
// Activate interactivity
this.activatePreviewListeners();
// Callbacks
this.callbacks?.show?.(this);
/* wait _indefinitely_ for placement to be decided. */
await MODULE.waitFor(() => !this.inFlight, -1)
if (this.activeHandlers) {
this.clearHandlers();
}
//END WARPGATE
return this;
}
/* -------------------------------------------- */
_mouseMoveHandler(event) {
event.stopPropagation();
/* if our position is locked, do not update it */
if (this.lockPosition) return;
// Apply a 20ms throttle
let now = Date.now();
if (now - this.moveTime <= 20) return;
const center = event.data.getLocalPosition(this.layer);
const {x,y} = Crosshairs.getSnappedPosition(center, this.interval);
this.document.updateSource({x, y});
this.refresh();
this.moveTime = now;
canvas._onDragCanvasPan(event.data.originalEvent);
}
_leftClickHandler(event) {
const document = this.document;
const thisSceneSize = this.scene.grid.size;
const destination = Crosshairs.getSnappedPosition(this.document, this.interval);
this.radius = document.distance * thisSceneSize / 2;
this.cancelled = false;
this.document.updateSource({ ...destination });
this.clearHandlers(event);
}
// Rotate the template by 3 degree increments (mouse-wheel)
// none = rotate 5 degrees
// shift = scale size
// ctrl = rotate 30 or 15 degrees (square/hex)
// alt = zoom canvas
_mouseWheelHandler(event) {
if (event.ctrlKey) event.preventDefault(); // Avoid zooming the browser window
if (!event.altKey) event.stopPropagation();
const delta = canvas.grid.type > CONST.GRID_TYPES.SQUARE ? 30 : 15;
const snap = event.ctrlKey ? delta : 5;
//BEGIN WARPGATE
const document = this.document;
const thisSceneSize = this.scene.grid.size;
if (event.shiftKey && !this.lockSize) {
let distance = document.distance + 0.25 * (Math.sign(event.deltaY));
distance = Math.max(distance, 0.25);
this.document.updateSource({ distance });
this.radius = document.distance * thisSceneSize / 2;
} else if (!event.altKey) {
const direction = document.direction + (snap * Math.sign(event.deltaY));
this.document.updateSource({ direction });
}
//END WARPGATE
this.refresh();
}
_rightDownHandler(event) {
if (event.button !== 2) return;
this.rightX = event.screenX;
this.rightY = event.screenY;
}
_rightUpHandler(event) {
if (event.button !== 2) return;
const isWithinThreshold = (current, previous) => Math.abs(current - previous) < 10;
if (isWithinThreshold(this.rightX, event.screenX)
&& isWithinThreshold(this.rightY, event.screenY)
) {
this.cancelled = true;
this.clearHandlers(event);
}
}
_clearHandlers(event) {
//WARPGATE BEGIN
/* remove only ourselves, in case of multiple */
this.layer.preview.removeChild(this);
canvas.stage.off("mousemove", this.activeMoveHandler);
canvas.stage.off("mousedown", this.activeLeftClickHandler);
canvas.app.view.onmousedown = null;
canvas.app.view.onmouseup = null;
canvas.app.view.onwheel = null;
//WARPGATE END
/* re-enable interactivity on this layer */
this.layer.interactiveChildren = true;
/* moving off this layer also deletes ALL active previews?
* unexpected, but manageable
*/
if (this.layer.preview.children.length == 0) {
this.initialLayer.activate();
}
//BEGIN WARPGATE
// Show the sheet that originated the preview
if (this.actorSheet) this.actorSheet.maximize();
this.activeHandlers = false;
this.inFlight = false;
/* mark this pixi element as destroyed */
this._destroyed = true;
//END WARPGATE
}
/**
* Activate listeners for the template preview
*/
activatePreviewListeners() {
this.moveTime = 0;
//BEGIN WARPGATE
this.activeHandlers = true;
/* Activate listeners */
this.activeMoveHandler = this._mouseMoveHandler.bind(this);
this.activeLeftClickHandler = this._leftClickHandler.bind(this);
this.rightDownHandler = this._rightDownHandler.bind(this);
this.rightUpHandler = this._rightUpHandler.bind(this);
this.activeWheelHandler = this._mouseWheelHandler.bind(this);
this.clearHandlers = this._clearHandlers.bind(this);
// Update placement (mouse-move)
canvas.stage.on("mousemove", this.activeMoveHandler);
// Confirm the workflow (left-click)
canvas.stage.on("mousedown", this.activeLeftClickHandler);
// Mouse Wheel rotate
canvas.app.view.onwheel = this.activeWheelHandler;
// Right click cancel
canvas.app.view.onmousedown = this.rightDownHandler;
canvas.app.view.onmouseup = this.rightUpHandler;
// END WARPGATE
}
/** END ABILITY-TEMPLATE.JS USAGE */
}