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.
 
 
 

359 lines
12 KiB

import EffectMappingForm from '../applications/effectMappingForm.js';
import { OverlayConfig } from '../applications/overlayConfig.js';
import { TVAOverlay } from './sprite/TVAOverlay.js';
import { evaluateOverlayExpressions } from './token/overlay.js';
export class Reticle {
static app;
static fields;
static reticleOverlay;
static active = false;
static hitTest;
static token = null;
static dialog = null;
// Offset calculation controls
static mode = 'tooltip';
static increment = 1;
static _onReticleMove(event) {
if (this.reticleOverlay.isMouseDown) {
let pos = event.data.getLocalPosition(this.reticleOverlay);
this.config.pOffsetX = 0;
this.config.pOffsetY = 0;
this.config.offsetX = 0;
this.config.offsetY = 0;
if (this.mode === 'token') {
this.config.linkRotation = true;
this.config.linkMirror = true;
}
this.tvaOverlay.refresh(this.config, { preview: true });
const tCoord = { x: this.tvaOverlay.x, y: this.tvaOverlay.y };
if (this.tvaOverlay.overlayConfig.parentID) {
let parent = this.tvaOverlay;
do {
parent = parent.parent;
tCoord.x += parent.x;
tCoord.y += parent.y;
} while (!(parent instanceof TVAOverlay));
}
let dx = pos.x - tCoord.x;
let dy = pos.y - tCoord.y;
let angle = 0;
if (!this.config.animation.relative) {
angle = this.config.angle;
if (this.config.linkRotation) angle += this.tvaOverlay.object.document.rotation;
}
[dx, dy] = rotate(0, 0, dx, dy, angle);
dx = round(dx, this.increment);
dy = round(dy, this.increment);
// let lPos = event.data.getLocalPosition(this.tvaOverlay);
// console.log(lPos);
// // let dx = lPos.x;
// // let dy = lPos.y;
if (this.mode === 'static') {
this.config.pOffsetX = dx;
this.config.pOffsetY = dy;
} else if (this.mode === 'token') {
this.config.offsetX = -dx / this.tvaOverlay.object.w;
this.config.offsetY = -dy / this.tvaOverlay.object.h;
} else {
let token = this.tvaOverlay.object;
let pWidth;
let pHeight;
if (this.tvaOverlay.overlayConfig.parentID) {
pWidth =
(this.tvaOverlay.parent.shapesWidth ?? this.tvaOverlay.parent.width) / this.tvaOverlay.parent.scale.x;
pHeight =
(this.tvaOverlay.parent.shapesHeight ?? this.tvaOverlay.parent.height) / this.tvaOverlay.parent.scale.y;
} else {
pWidth = token.w;
pHeight = token.h;
}
if (this.mode === 'tooltip') {
if (Math.abs(dx) >= pWidth / 2) {
this.config.offsetX = 0.5 * (dx < 0 ? 1 : -1);
dx += (pWidth / 2) * (dx < 0 ? 1 : -1);
} else {
this.config.offsetX = -dx / this.tvaOverlay.object.w;
dx = 0;
}
if (Math.abs(dy) >= pHeight / 2) {
this.config.offsetY = 0.5 * (dy < 0 ? 1 : -1);
dy += (pHeight / 2) * (dy < 0 ? 1 : -1);
} else {
this.config.offsetY = -dy / this.tvaOverlay.object.h;
dy = 0;
}
} else {
if (Math.abs(dx) >= pWidth / 2) {
this.config.offsetX = 0.5 * (dx < 0 ? 1 : -1);
dx += (pWidth / 2) * (dx < 0 ? 1 : -1);
} else if (Math.abs(dy) >= pHeight / 2) {
this.config.offsetY = 0.5 * (dy < 0 ? 1 : -1);
dy += (pHeight / 2) * (dy < 0 ? 1 : -1);
} else {
this.config.offsetX = -dx / this.tvaOverlay.object.w;
dx = 0;
this.config.offsetY = -dy / this.tvaOverlay.object.h;
dy = 0;
}
}
this.config.pOffsetX = dx;
this.config.pOffsetY = dy;
}
this.tvaOverlay.refresh(this.config, { preview: true });
}
}
static minimizeApps() {
Object.values(ui.windows).forEach((app) => {
if (app instanceof OverlayConfig || app instanceof EffectMappingForm) {
app.minimize();
}
});
}
static maximizeApps() {
Object.values(ui.windows).forEach((app) => {
if (app instanceof OverlayConfig || app instanceof EffectMappingForm) {
app.maximize();
}
});
}
static activate({ tvaOverlay = null, config = {} } = {}) {
if (this.deactivate() || !canvas.ready) return false;
if (!tvaOverlay || !config) return false;
if (this.reticleOverlay) {
this.reticleOverlay.destroy(true);
}
const interaction = canvas.app.renderer.plugins.interaction;
if (!interaction.cursorStyles['reticle']) {
interaction.cursorStyles['reticle'] = "url('modules/token-variants/img/reticle.webp'), auto";
}
this.tvaOverlay = tvaOverlay;
this.minimizeApps();
this.config = evaluateOverlayExpressions(deepClone(config), this.tvaOverlay.object, {
overlayConfig: config,
});
// Setup the overlay to be always visible while we're adjusting its position
this.config.alwaysVisible = true;
this.active = true;
// Create the reticle overlay
this.reticleOverlay = new PIXI.Container();
this.reticleOverlay.hitArea = canvas.dimensions.rect;
this.reticleOverlay.cursor = 'reticle';
this.reticleOverlay.interactive = true;
this.reticleOverlay.zIndex = Infinity;
const stopEvent = function (event) {
event.preventDefault();
// event.stopPropagation();
};
this.reticleOverlay.on('mousedown', (event) => {
event.preventDefault();
if (event.data.originalEvent.which != 2 && event.data.originalEvent.nativeEvent.which != 2) {
this.reticleOverlay.isMouseDown = true;
this._onReticleMove(event);
}
});
this.reticleOverlay.on('pointermove', (event) => {
event.preventDefault();
// event.stopPropagation();
this._onReticleMove(event);
});
this.reticleOverlay.on('mouseup', (event) => {
event.preventDefault();
this.reticleOverlay.isMouseDown = false;
});
this.reticleOverlay.on('click', (event) => {
event.preventDefault();
if (event.data.originalEvent.which == 2 || event.data.originalEvent.nativeEvent.which == 2) {
this.deactivate();
}
});
canvas.stage.addChild(this.reticleOverlay);
this.dialog = displayControlDialog();
return true;
}
static deactivate() {
if (this.active) {
if (this.reticleOverlay) this.reticleOverlay.parent?.removeChild(this.reticleOverlay);
this.active = false;
this.tvaOverlay = null;
if (this.dialog && this.dialog._state !== Application.RENDER_STATES.CLOSED) this.dialog.close(true);
this.dialog = null;
this.maximizeApps();
const app = Object.values(ui.windows).find((app) => app instanceof OverlayConfig);
if (!app) {
this.config = null;
return;
}
const form = $(app.form);
['pOffsetX', 'pOffsetY', 'offsetX', 'offsetY'].forEach((field) => {
if (field in this.config) {
form.find(`[name="${field}"]`).val(this.config[field]);
}
});
if (this.mode === 'token') {
['linkRotation', 'linkMirror'].forEach((field) => {
form.find(`[name="${field}"]`).prop('checked', true);
});
['linkDimensionsX', 'linkDimensionsY'].forEach((field) => {
form.find(`[name="${field}"]`).prop('checked', false);
});
} else {
['linkRotation', 'linkMirror'].forEach((field) => {
form.find(`[name="${field}"]`).prop('checked', false);
});
}
if (this.mode === 'hud') {
form.find('[name="ui"]').prop('checked', true).trigger('change');
}
form.find('[name="anchor.x"]').val(this.config.anchor.x);
form.find('[name="anchor.y"]').val(this.config.anchor.y).trigger('change');
this.config = null;
return true;
}
}
}
function displayControlDialog() {
const d = new Dialog({
title: 'Set Overlay Position',
content: `
<style>
.images { display: flex; }
.images a { flex: 20%; width: 50px; margin: 2px; }
.images a.active img { border-color: orange; border-width: 2px; }
.anchorlbl {margin: auto; display: table; }
</style>
<div class="images">
<a data-id="token"><img src="modules/token-variants/img/token_mode.png"></img></a>
<a data-id="tooltip"><img src="modules/token-variants/img/tooltip_mode.png"></img></a>
<a data-id="hud"><img src="modules/token-variants/img/hud_mode.png"></img></a>
<a data-id="static"><img src="modules/token-variants/img/static_mode.png"></img></a>
</div>
<br>
<label class="anchorlbl">Anchor</label>
<div class="tva-anchor">
<input type="radio" class="top left" name="anchor">
<input type="radio" class="top center" name="anchor">
<input type="radio" class="top right" name="anchor">
<input type="radio" class="mid left" name="anchor">
<input type="radio" class="mid center" name="anchor">
<input type="radio" class="mid right" name="anchor">
<input type="radio" class="bot left" name="anchor">
<input type="radio" class="bot center" name="anchor">
<input type="radio" class="bot right" name="anchor">
</div>
<div class="form-group">
<label>Step Size</label>
<div class="form-fields">
<input type="number" name="step" min="0" step="1" value="${Reticle.increment}">
</div>
</div>
<p class="notes"><b>Left-Click</b> to move the overlay</p>
<p class="notes"><b>Middle-Click</b> or <b>Close Dialog</b> to exit overlay positioning</p>
`,
buttons: {},
render: (html) => {
// Mode Images
const images = html.find('.images a');
html.find('.images a').on('click', (event) => {
images.removeClass('active');
const target = $(event.target).closest('a');
target.addClass('active');
Reticle.mode = target.data('id');
});
html.find(`[data-id="${Reticle.mode}"]`).addClass('active');
// Anchor
let anchorX = Reticle.config?.anchor?.x || 0;
let anchorY = Reticle.config?.anchor?.y || 0;
let classes = '';
if (anchorX < 0.5) classes += '.left';
else if (anchorX > 0.5) classes += '.right';
else classes += '.center';
if (anchorY < 0.5) classes += '.top';
else if (anchorY > 0.5) classes += '.bot';
else classes += '.mid';
html.find('.tva-anchor').find(classes).prop('checked', true);
// end - Pre-select anchor
html.find('input[name="anchor"]').on('change', (event) => {
const anchor = $(event.target);
let x;
let y;
if (anchor.hasClass('left')) x = 0;
else if (anchor.hasClass('center')) x = 0.5;
else x = 1;
if (anchor.hasClass('top')) y = 0;
else if (anchor.hasClass('mid')) y = 0.5;
else y = 1;
Reticle.config.anchor.x = x;
Reticle.config.anchor.y = y;
});
html.find('[name="step"]').on('input', (event) => {
Reticle.increment = $(event.target).val() || 1;
});
},
close: () => Reticle.deactivate(),
});
d.render(true);
setTimeout(() => d.setPosition({ left: 200, top: window.innerHeight / 2, height: 'auto' }), 100);
return d;
}
function round(number, increment, offset = 0) {
return Math.ceil((number - offset) / increment) * increment + offset;
}
function rotate(cx, cy, x, y, angle) {
var radians = (Math.PI / 180) * angle,
cos = Math.cos(radians),
sin = Math.sin(radians),
nx = cos * (x - cx) + sin * (y - cy) + cx,
ny = cos * (y - cy) - sin * (x - cx) + cy;
return [nx, ny];
}