class SmartTarget {
|
|
|
|
static handleTargeting(token,shift) {
|
|
const isTargeted = token.isTargeted;
|
|
const release = shift ? !SmartTarget.settings().release : SmartTarget.settings().release;
|
|
token.setTarget(!isTargeted, { releaseOthers: release });
|
|
}
|
|
|
|
static getBorderColor({hover}={}) {
|
|
const colors = CONFIG.Canvas.dispositionColors;
|
|
if ( this.controlled ) return colors.CONTROLLED;
|
|
else if ( (hover ?? this.hover) || canvas.tokens._highlight ) {
|
|
let d = this.document.disposition;
|
|
if ( !game.user.isGM && this.isOwner ) return colors.CONTROLLED;
|
|
else if ( this.actor?.hasPlayerOwner ) return colors.PARTY;
|
|
else if ( d === CONST.TOKEN_DISPOSITIONS.FRIENDLY ) return colors.FRIENDLY;
|
|
else if ( d === CONST.TOKEN_DISPOSITIONS.NEUTRAL ) return colors.NEUTRAL;
|
|
else if ( d === CONST.TOKEN_DISPOSITIONS.HOSTILE ) return colors.HOSTILE;
|
|
else if ( d === CONST.TOKEN_DISPOSITIONS.SECRET ) return this.isOwner ? colors.SECRET : null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static _tokenOnClickLeft(wrapped, ...args) {
|
|
const mode = SmartTarget.settings().mode;
|
|
switch (mode) {
|
|
case 0:
|
|
return wrapped(...args);
|
|
break;
|
|
case 1:
|
|
if (game.smartTarget.altModifier) {
|
|
SmartTarget.handleTargeting(this,game.keyboard.isModifierActive(KeyboardManager.MODIFIER_KEYS.SHIFT));
|
|
return
|
|
}else{
|
|
return wrapped(...args);
|
|
}
|
|
break;
|
|
case 2:
|
|
if ((!game.user.isGM && !this.isOwner) || (this.isOwner && game.smartTarget.altModifier)) {
|
|
SmartTarget.handleTargeting(this,game.keyboard.isModifierActive(KeyboardManager.MODIFIER_KEYS.SHIFT));
|
|
return
|
|
} else {
|
|
return wrapped(...args);
|
|
}
|
|
break;
|
|
}
|
|
super._onClickLeft(...args);
|
|
}
|
|
|
|
static canvasOnClickLeft(wrapped, ...args) {
|
|
const canvasMousePos = args[0].interactionData.origin
|
|
if (game.smartTarget.altModifier){
|
|
let distance = Infinity
|
|
let closestTemplate = null
|
|
for(let template of canvas.templates.placeables){
|
|
if(!template.owner) continue
|
|
const inTemplate = template.shape.contains(canvasMousePos.x-template.x,canvasMousePos.y-template.y)
|
|
const d = Math.sqrt(Math.pow(template.x-canvasMousePos.x,2)+Math.pow(template.y-canvasMousePos.y,2))
|
|
if(inTemplate && d<distance){
|
|
distance = d
|
|
closestTemplate = template
|
|
}
|
|
}
|
|
if(closestTemplate){
|
|
const release = game.keyboard.isModifierActive(KeyboardManager.MODIFIER_KEYS.SHIFT) ? !SmartTarget.settings().release : SmartTarget.settings().release;
|
|
if (release)canvas.tokens.placeables[0]?.setTarget(false, { releaseOthers: true });
|
|
for(let token of canvas.tokens.placeables){
|
|
if(closestTemplate.shape.contains(token.center.x-closestTemplate.x,token.center.y-closestTemplate.y)){
|
|
token.setTarget(!token.isTargeted, { releaseOthers: false });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return wrapped(...args);
|
|
}
|
|
|
|
static _canControl(wrapped,...args){
|
|
if(!args[1]) return wrapped(...args);
|
|
const mode = SmartTarget.settings().mode;
|
|
if(mode==1 && game.smartTarget.altModifier) return true;
|
|
if(mode==2 && !game.user.isGM && !this.isOwner) return true;
|
|
return wrapped(...args);
|
|
}
|
|
|
|
static getOffset(token, length) {
|
|
const width = token.w;
|
|
const height = token.h;
|
|
const position = game.settings.get(SMARTTARGET_MODULE_NAME, "pipPosition")
|
|
const circleR = game.settings.get(SMARTTARGET_MODULE_NAME, "pipScale") || 12;
|
|
let circleOffsetMult = game.settings.get(SMARTTARGET_MODULE_NAME, "pipOffset") || 16;
|
|
let insidePip = game.settings.get(SMARTTARGET_MODULE_NAME, "insidePips") ? circleR : 0;
|
|
const totalHeight = circleR*2;
|
|
const totalWidth = (circleR*2)*length - circleOffsetMult*(length-1);
|
|
const offset = {
|
|
x: 0,
|
|
y: 0,
|
|
};
|
|
switch (position) {
|
|
case "topleft":
|
|
break;
|
|
case "topright":
|
|
offset.x = width - totalWidth;
|
|
break;
|
|
case "bottomleft":
|
|
offset.y = height - totalHeight;
|
|
break;
|
|
case "bottomright":
|
|
offset.x = width - totalWidth;
|
|
offset.y = height - totalHeight;
|
|
break;
|
|
case "centertop":
|
|
offset.x = (width - totalWidth) / 2;
|
|
break;
|
|
case "centerbottom":
|
|
offset.x = (width - totalWidth) / 2;
|
|
offset.y = height - totalHeight;
|
|
break;
|
|
case "random":
|
|
offset.x = Math.floor(Math.random() * (width - totalWidth));
|
|
offset.y = Math.floor(Math.random() * (height - totalHeight));
|
|
break;
|
|
}
|
|
return offset;
|
|
}
|
|
/**
|
|
* Creates a sprite from the selected avatar and positions around the container
|
|
* @param {User} u -- the user to get
|
|
* @param {int} i -- the current row count
|
|
* @param {token} target -- PIXI.js container for height & width (the token)
|
|
*/
|
|
static buildCharacterPortrait(u, i, target, token, totalOffset) {
|
|
let color = Color.from(u.color);
|
|
let circleR = game.settings.get(SMARTTARGET_MODULE_NAME, "pipScale") || 12;
|
|
let circleOffsetMult =
|
|
game.settings.get(SMARTTARGET_MODULE_NAME, "pipOffset") || 16;
|
|
let scaleMulti =
|
|
game.settings.get(SMARTTARGET_MODULE_NAME, "pipImgScale") || 1;
|
|
let insidePip = game.settings.get(SMARTTARGET_MODULE_NAME, "insidePips")
|
|
? circleR
|
|
: 0;
|
|
let pTex;
|
|
if (!u.isGM) {
|
|
let character = u.character;
|
|
if (!character) {
|
|
character = u.character;
|
|
}
|
|
if (character) {
|
|
pTex = game.settings.get(SMARTTARGET_MODULE_NAME, "useToken")
|
|
? character.prototypeToken.texture.src || character.img
|
|
: character.img || character.prototypeToken.texture.src;
|
|
} else {
|
|
pTex = u.avatar;
|
|
}
|
|
}
|
|
const gmTexSetting = game.settings.get(SMARTTARGET_MODULE_NAME, "useTokenGm")
|
|
let gmTexture = gmTexSetting ? token.document.getFlag(SMARTTARGET_MODULE_NAME,"gmtargetimg") || u.avatar : u.avatar
|
|
function redraw(){
|
|
token._refreshTarget()
|
|
}
|
|
let texture = u.isGM
|
|
? PIXI.Texture.from(gmTexture)
|
|
: PIXI.Texture.from(pTex);
|
|
if (!texture.baseTexture.valid) texture.once("update", redraw);
|
|
let newTexW = scaleMulti * (2 * circleR);
|
|
let newTexH = scaleMulti * (2 * circleR);
|
|
let borderThic = game.settings.get(SMARTTARGET_MODULE_NAME, "borderThicc");
|
|
let portraitCenterOffset =
|
|
scaleMulti >= 1 ? (16 + circleR / 12) * Math.log2(scaleMulti) : 0;
|
|
portraitCenterOffset +=
|
|
game.settings.get(SMARTTARGET_MODULE_NAME, "pipOffsetManualY") || 0;
|
|
let portraitXoffset =
|
|
game.settings.get(SMARTTARGET_MODULE_NAME, "pipOffsetManualX") || 0;
|
|
let matrix = new PIXI.Matrix(
|
|
(scaleMulti * (2 * circleR + 2)) / texture.width,
|
|
0,
|
|
0,
|
|
(scaleMulti * (2 * circleR + 2)) / texture.height,
|
|
newTexW / 2 + 4 + i * circleOffsetMult + portraitXoffset + insidePip + totalOffset.x,
|
|
newTexH / 2 + portraitCenterOffset + insidePip + totalOffset.y
|
|
);
|
|
token.target
|
|
.beginFill(color)
|
|
.drawCircle(2 + i * circleOffsetMult + insidePip + totalOffset.x, 0 + insidePip + totalOffset.y, circleR)
|
|
.beginTextureFill({
|
|
texture: texture,
|
|
alpha: 1,
|
|
matrix: matrix,
|
|
})
|
|
.lineStyle(borderThic, 0x0000000)
|
|
.drawCircle(2 + i * circleOffsetMult + insidePip + totalOffset.x, 0 + insidePip + totalOffset.y, circleR)
|
|
.endFill()
|
|
.lineStyle(borderThic / 2, color)
|
|
.drawCircle(2 + i * circleOffsetMult + insidePip + totalOffset.x, 0 + insidePip + totalOffset.y, circleR)
|
|
}
|
|
|
|
// Draw custom crosshair and pips
|
|
static async _refreshTarget(wrapped, ...args) {
|
|
const usePips = game.settings.get(SMARTTARGET_MODULE_NAME, "portraitPips");
|
|
const selectedIndicator = game.settings.get(SMARTTARGET_MODULE_NAME, "target-indicator");
|
|
|
|
if(!usePips && selectedIndicator == "0") return wrapped(...args);
|
|
|
|
if (!this.target) return;
|
|
let reticule = args[0] ?? {};
|
|
this.target.clear();
|
|
|
|
// We don't show the target arrows for a secret token disposition and non-GM users
|
|
const isSecret = this.document.disposition === CONST.TOKEN_DISPOSITIONS.SECRET && !this.isOwner;
|
|
if (!this.targeted.size || isSecret) return;
|
|
|
|
// Determine whether the current user has target and any other users
|
|
const [others, user] = Array.from(this.targeted).partition(
|
|
(u) => u === game.user
|
|
);
|
|
const userTarget = user.length;
|
|
|
|
// For the current user, draw the target arrows
|
|
if (userTarget) {
|
|
let textColor = game.settings.get(
|
|
SMARTTARGET_MODULE_NAME,
|
|
"crossairColor"
|
|
)
|
|
? game.settings
|
|
.get(SMARTTARGET_MODULE_NAME, "crossairColor")
|
|
.replace("#", "0x")
|
|
: SmartTarget.getBorderColor.bind(this)({hover: true});
|
|
|
|
if (game.settings.get(SMARTTARGET_MODULE_NAME, "use-player-color")) {
|
|
textColor = Color.from(game.user["color"]);
|
|
}
|
|
|
|
let p = 4;
|
|
let aw = 12;
|
|
let h = this.h;
|
|
let hh = h / 2;
|
|
let w = this.w;
|
|
let hw = w / 2;
|
|
let ah = canvas.dimensions.size / 3;
|
|
|
|
switch (selectedIndicator) {
|
|
case "0":
|
|
reticule.color = textColor;
|
|
this._drawTarget(reticule)//{color: textColor})//drawDefault(this, textColor, p, aw, h, hh, w, hw, ah);
|
|
break;
|
|
case "1":
|
|
drawCrossHairs1(this, textColor, p, aw, h, hh, w, hw, ah);
|
|
break;
|
|
case "2":
|
|
drawCrossHairs2(this, textColor, p, aw, h, hh, w, hw, ah);
|
|
break;
|
|
case "3":
|
|
drawBullsEye1(this, textColor, p, aw, h, hh, w, hw, ah);
|
|
break;
|
|
case "4":
|
|
drawBullsEye2(this, textColor, p, aw, h, hh, w, hw, ah);
|
|
break;
|
|
case "5":
|
|
drawBetterTarget(this, textColor, p, aw, h, hh, w, hw, ah);
|
|
break;
|
|
default:
|
|
drawDefault(this, textColor, p, aw, h, hh, w, hw, ah);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// For other users, draw offset pips
|
|
if (usePips) {
|
|
for (let [i, u] of others.entries()) {
|
|
const offset = SmartTarget.getOffset(this, others.length);
|
|
SmartTarget.buildCharacterPortrait(u, i, this.target,this, offset);
|
|
}
|
|
} else {
|
|
const hw2 = this.w / 2 + (others.length % 2 === 0 ? 8 : 0);
|
|
for (let [i, u] of others.entries()) {
|
|
const offset = Math.floor((i + 1) / 2) * 16;
|
|
const sign = i % 2 === 0 ? 1 : -1;
|
|
const x = hw2 + sign * offset;
|
|
this.target.beginFill(Color.from(u.color), 1.0).lineStyle(2, 0x0000000).drawCircle(x, 0, 6);
|
|
}
|
|
}
|
|
}
|
|
|
|
static settings() {
|
|
const settings = {
|
|
mode: game.settings.get(SMARTTARGET_MODULE_NAME, "targetingMode"),
|
|
release: !game.settings.get(SMARTTARGET_MODULE_NAME, "release"),
|
|
};
|
|
return settings;
|
|
}
|
|
}
|
|
|
|
Hooks.on("targetToken", (user,token,targeted) =>{
|
|
const gmTexSetting = game.settings.get(SMARTTARGET_MODULE_NAME, "useTokenGm")
|
|
if(!game.user.isGM || !targeted || !gmTexSetting) return
|
|
|
|
let flag
|
|
if(gmTexSetting == 1) flag = _token?.document.actor?.img || _token?.document.texture.src
|
|
if(gmTexSetting == 2) flag = _token?.document.texture.src || _token?.document.actor?.img
|
|
flag && flag != token.document.getFlag(SMARTTARGET_MODULE_NAME,"gmtargetimg") && token.document.setFlag(SMARTTARGET_MODULE_NAME,"gmtargetimg",flag)
|
|
|
|
})
|
|
|
|
Hooks.on("updateToken", (token, update) => {
|
|
if (update?.flags?.[SMARTTARGET_MODULE_NAME]?.gmtargetimg) {
|
|
token.object._refreshTarget()
|
|
}
|
|
});
|