import { getIconVisibleActiveEffects } from '../../helpers/utils.mjs'; import DhMeasuredTemplate from './measuredTemplate.mjs'; export default class DhTokenPlaceable extends foundry.canvas.placeables.Token { /** @inheritdoc */ async _draw(options) { await super._draw(options); if (this.document.flags.daggerheart?.createPlacement) this.previewHelp ||= this.addChild(this.#drawPreviewHelp()); } /** @inheritDoc */ async _drawEffects() { this.effects.renderable = false; // Clear Effects Container this.effects.removeChildren().forEach(c => c.destroy()); this.effects.bg = this.effects.addChild(new PIXI.Graphics()); this.effects.bg.zIndex = -1; this.effects.overlay = null; // Categorize effects const activeEffects = getIconVisibleActiveEffects(Array.from(this.actor?.allApplicableEffects() ?? [])); const overlayEffect = activeEffects.findLast(e => e.img && e.getFlag?.('core', 'overlay')); // Draw effects const promises = []; for (const [i, effect] of activeEffects.entries()) { if (!effect.img) continue; const promise = effect === overlayEffect ? this._drawOverlay(effect.img, effect.tint, effect) : this._drawEffect(effect.img, effect.tint, effect); promises.push( promise.then(e => { if (e) e.zIndex = i; }) ); } await Promise.allSettled(promises); this.effects.sortChildren(); this.effects.renderable = true; this.renderFlags.set({ refreshEffects: true }); } /**@inheritdoc */ async _drawEffect(src, tint, effect) { if (!src) return; const tex = await loadTexture(src, { fallback: 'icons/svg/hazard.svg' }); const icon = new PIXI.Sprite(tex); icon.tint = tint ?? 0xffffff; if (effect?.system?.stacking?.enabled && effect.system.stacking.value > 1) { const stackOverlay = new PIXI.Text(effect.system.stacking.value, { fill: '#f3c267', stroke: '#000000', fontSize: 96, strokeThickness: 4 }); const nrDigits = Math.floor(Math.log10(effect.system.stacking.value)) + 1; stackOverlay.y = -8; /* This does not account for 1:s being much less wide than other digits. I don't think it's desired however as it makes it look jumpy */ stackOverlay.x = icon.width - 8 - nrDigits * 56 + (lastDigitIsOne ? 16 : 0); stackOverlay.anchor.set(0, 0); icon.addChild(stackOverlay); } return this.effects.addChild(icon); } async _drawOverlay(src, tint, effect) { const icon = await this._drawEffect(src, tint, effect); if (icon) icon.alpha = 0.8; this.effects.overlay = icon ?? null; return icon; } /** * Returns the distance from this token to another token object. * This value is corrected to handle alternate token sizes and other grid types * according to the diagonal rules. */ distanceTo(target) { if (!canvas.ready) return NaN; if (this === target) return 0; const originPoint = this.center; const targetPoint = target.center; const thisBounds = this.bounds; const targetBounds = target.bounds; const adjacencyBuffer = canvas.grid.distance * 1.75; // handles diagonals with one square elevation difference // Figure out the elevation difference. // This intends to return "grid distance" for adjacent ones, so we add that number if not overlapping. const sizePerUnit = canvas.grid.size / canvas.grid.distance; const thisHeight = Math.max(thisBounds.width, thisBounds.height) / sizePerUnit; const targetHeight = Math.max(targetBounds.width, targetBounds.height) / sizePerUnit; const thisElevation = [this.document.elevation, this.document.elevation + thisHeight]; const targetElevation = [target.document.elevation, target.document.elevation + targetHeight]; const isSameAltitude = thisElevation[0] < targetElevation[1] && // bottom of this must be at or below the top of target thisElevation[1] > targetElevation[0]; // top of this must be at or above the bottom of target const [lower, higher] = [targetElevation, thisElevation].sort((a, b) => a[1] - b[1]); const elevation = isSameAltitude ? 0 : higher[0] - lower[1] + canvas.grid.distance; // Compute for gridless. This version returns circular edge to edge + grid distance, // so that tokens that are touching return 5. if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) { const boundsCorrection = canvas.grid.distance / canvas.grid.size; const originRadius = (thisBounds.width * boundsCorrection) / 2; const targetRadius = (targetBounds.width * boundsCorrection) / 2; const measuredDistance = canvas.grid.measurePath([ { ...originPoint, elevation: 0 }, { ...targetPoint, elevation } ]).distance; const distance = Math.floor(measuredDistance - originRadius - targetRadius + canvas.grid.distance); return Math.min(distance, distance > adjacencyBuffer ? Infinity : canvas.grid.distance); } // Compute what the closest grid space of each token is, then compute that distance const originEdge = this.#getEdgeBoundary(thisBounds, originPoint, targetPoint); const targetEdge = this.#getEdgeBoundary(targetBounds, originPoint, targetPoint); const adjustedOriginPoint = originEdge ? canvas.grid.getTopLeftPoint({ x: originEdge.x + Math.sign(originPoint.x - originEdge.x), y: originEdge.y + Math.sign(originPoint.y - originEdge.y) }) : originPoint; const adjustDestinationPoint = targetEdge ? canvas.grid.getTopLeftPoint({ x: targetEdge.x + Math.sign(targetPoint.x - targetEdge.x), y: targetEdge.y + Math.sign(targetPoint.y - targetEdge.y) }) : targetPoint; const distance = canvas.grid.measurePath([ { ...adjustedOriginPoint, elevation: 0 }, { ...adjustDestinationPoint, elevation } ]).distance; return Math.min(distance, distance > adjacencyBuffer ? Infinity : canvas.grid.distance); } _onHoverIn(event, options) { super._onHoverIn(event, options); // Check if the setting is enabled const setting = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.appearance).showTokenDistance; if (setting === 'never' || (setting === 'encounters' && !game.combat?.started)) return; // Check if this token isn't invisible and is actually being hovered const isTokenValid = this.visible && this.hover && !this.isPreview && !this.document.isSecret && !this.controlled && !this.animation; if (!isTokenValid) return; // Ensure we have a single controlled token const originToken = canvas.tokens.controlled[0]; if (!originToken || canvas.tokens.controlled.length > 1) return; // Determine the actual range const ranges = game.settings.get(CONFIG.DH.id, CONFIG.DH.SETTINGS.gameSettings.variantRules).rangeMeasurement; const distanceResult = DhMeasuredTemplate.getRangeLabels(originToken.distanceTo(this), ranges); const distanceLabel = `${distanceResult.distance} ${distanceResult.units}`.trim(); // Create the element const element = document.createElement('div'); element.id = 'token-hover-distance'; element.classList.add('waypoint-label', 'last'); const ruler = document.createElement('i'); ruler.classList.add('fa-solid', 'fa-ruler'); element.appendChild(ruler); const labelEl = document.createElement('span'); labelEl.classList.add('total-measurement'); labelEl.textContent = distanceLabel; element.appendChild(labelEl); // Position the element and add to the DOM const center = this.getCenterPoint(); element.style.setProperty('--transformY', 'calc(-100% - 10px)'); element.style.setProperty('--position-y', `${this.y}px`); element.style.setProperty('--position-x', `${center.x}px`); element.style.setProperty('--ui-scale', String(canvas.dimensions.uiScale)); document.querySelector('#token-hover-distance')?.remove(); document.querySelector('#measurement').appendChild(element); } _onHoverOut(...args) { super._onHoverOut(...args); document.querySelector('#token-hover-distance')?.remove(); } /** Returns the point at which a line starting at origin and ending at destination intersects the edge of the bounds */ #getEdgeBoundary(bounds, originPoint, destinationPoint) { const points = [ { x: bounds.x, y: bounds.y }, { x: bounds.x + bounds.width, y: bounds.y }, { x: bounds.x + bounds.width, y: bounds.y + bounds.height }, { x: bounds.x, y: bounds.y + bounds.height } ]; const pairsToTest = [ [points[0], points[1]], [points[1], points[2]], [points[2], points[3]], [points[3], points[0]] ]; for (const pair of pairsToTest) { const result = foundry.utils.lineSegmentIntersection(originPoint, destinationPoint, pair[0], pair[1]); if (result) return result; } return null; } /** @inheritDoc */ _drawBar(number, bar, data) { const val = Number(data.value); const pct = Math.clamp(val, 0, data.max) / data.max; // Determine sizing const { width, height } = this.document.getSize(); const s = canvas.dimensions.uiScale; const bw = width; const bh = 8 * (this.document.height >= 2 ? 1.5 : 1) * s; // Determine the color to use const fillColor = number === 0 ? foundry.utils.Color.fromRGB([1, 0, 0]) : foundry.utils.Color.fromString('#0032b1'); // Draw the bar const widthUnit = bw / data.max; bar.clear().lineStyle(s, 0x000000, 1.0); const sections = [...Array(data.max).keys()]; for (let mark of sections) { const x = mark * widthUnit; const marked = mark + 1 <= data.value; const color = marked ? fillColor : foundry.utils.Color.fromRGB([0, 0, 0]); if (mark === 0 || mark === sections.length - 1) { bar.beginFill(color, marked ? 1.0 : 0.5).drawRect(x, 0, widthUnit, bh, 2 * s); // Would like drawRoundedRect, but it's very troublsome with the corners. Leaving for now. } else { bar.beginFill(color, marked ? 1.0 : 0.5).drawRect(x, 0, widthUnit, bh, 2 * s); } } // Set position const posY = number === 0 ? height - bh : 0; bar.position.set(0, posY); return true; } /** * Draw a helptext for previews as a text object * @returns {PreciseText} The Text object for the preview helper */ #drawPreviewHelp() { const { uiScale } = canvas.dimensions; const textStyle = CONFIG.canvasTextStyle.clone(); textStyle.fontSize = 18; textStyle.wordWrapWidth = this.w * 2.5; textStyle.fontStyle = 'italic'; const helpText = new foundry.canvas.containers.PreciseText( `(${game.i18n.localize('DAGGERHEART.UI.Tooltip.previewTokenHelp')})`, textStyle ); helpText.anchor.set(helpText.width / 900, 1); helpText.scale.set(uiScale, uiScale); return helpText; } }