├── .gitignore ├── img ├── showdragdistance.gif └── showdragdistance2.gif ├── module.json ├── lang ├── ko.json ├── en.json └── es.json ├── README.md └── showdragdistance.js /.gitignore: -------------------------------------------------------------------------------- 1 | showdragdistance_bk.js 2 | showdragdistance_new.js 3 | *.zip 4 | -------------------------------------------------------------------------------- /img/showdragdistance.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsaunders1014/ShowDragDistance/HEAD/img/showdragdistance.gif -------------------------------------------------------------------------------- /img/showdragdistance2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wsaunders1014/ShowDragDistance/HEAD/img/showdragdistance2.gif -------------------------------------------------------------------------------- /module.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ShowDragDistance", 3 | "title": "Show Drag Distance", 4 | "description": "Shows the distance you're dragging a token, similar to holding the ctrl key. Because 30 feet isn't as much as it sounds. Also removes the contraint of the normal ruler to hold CTRL and drag from Token. Now you can just press CTRL with token selected and it will show you the range from your token to mouse!", 5 | "authors": [{"name":"Will Saunders","email":"willsaunders1014@gmail.com" }], 6 | "version": "2.2.3", 7 | "minimumCoreVersion": "0.6.6", 8 | "compatibleCoreVersion":"0.7.5", 9 | "esmodules": [ 10 | "showdragdistance.js" 11 | ], 12 | "dependencies":[ 13 | ], 14 | "languages": [ 15 | { 16 | "lang": "en", 17 | "name": "English", 18 | "path": "lang/en.json" 19 | }, 20 | { 21 | "lang": "ko", 22 | "name": "Korean", 23 | "path": "lang/ko.json" 24 | }, 25 | { 26 | "lang": "es", 27 | "name": "Spanish", 28 | "path": "lang/es.json" 29 | } 30 | ], 31 | "changelog":"https://raw.githubusercontent.com/wsaunders1014/ShowDragDistance/master/README.md", 32 | "url":"https://github.com/wsaunders1014/ShowDragDistance", 33 | "manifest":"https://raw.githubusercontent.com/wsaunders1014/ShowDragDistance/master/module.json", 34 | "download":"https://github.com/wsaunders1014/ShowDragDistance/releases/download/2.2.3/ShowDragDistance.zip" 35 | } 36 | -------------------------------------------------------------------------------- /lang/ko.json: -------------------------------------------------------------------------------- 1 | { 2 | "ShowDragDistance.enable-s":"드래그시 거리 표시 활성화", 3 | "ShowDragDistance.enable-l":"드래그시 토큰이 이동하는 거리를 캔버스에서 Ctrl 키를 유지하며 드래그하는 것과 유사하게 표시한다. 마우스 우클릭으로 경유지를 만든다. 경유지를 삭제하려면 X를 누른다.", 4 | "ShowDragDistance.showPath-s":"이동 경로 표시를 기본값으로 설정", 5 | "ShowDragDistance.showPath-l":"드래그를 통한 경로 표시를 기본값으로 표시한다. Ctrl키를 눌러 경로를 숨기며 비활성화될 경우 경로가 숨겨지며 Ctrl 키를 통해 표시된다.", 6 | "ShowDragDistance.rangeFinder-s":"거리 측정기 활성화", 7 | "ShowDragDistance.rangeFinder-l":"활성화할 경우 마우스 이동 시 'Ctrl'키를 유지하면 선택한 토큰의 범위가 표시된다 .", 8 | "ShowDragDistance.maxSpeed-l":"드래그 측정자는 토큰의 최대 속도에 따라 색상이 변경된다. 일부 모듈 또는 시스템과 충돌할 수 있다.", 9 | "ShowDragDistance.dash-s":"질주 색상 변경 활성화", 10 | "ShowDragDistance.dash-l":"드래그 측정자는 질주 속도의 칸에 대해 다른 색사을 표시한다.", 11 | "ShowDragDistance.dashX-s":"질주 승수", 12 | "ShowDragDistance.dashX-l":"질주를 위해 기본 이동 속도를 곱하는 숫자이다. DND5e의 경우 1이다.", 13 | "ShowDragDistance.baseSpeedAttr-s":"기본 속도 속성", 14 | "ShowDragDistance.baseSpeedAttr-l":"토큰 엔티티에서 속도에 대한 객채 속성 경로를 입력한다. DND5e의 경우'actor.data.data.attributes.speed.value'이다.", 15 | "ShowDragDistance.bonusSpeedAttr-s":"보너스 속도 속성", 16 | "ShowDragDistance.bonusSpeedAttr-l":"시스템에 보너스, 또는 특수 속성이 있는 경우 여기 입력하고, 그렇지 않을 경우 비워둔다. 예시(DnD5e):actor.data.data.attributes.speed.special", 17 | "ShowDragDistance.maxSpeedColor-s": "최대 속도 색상", 18 | "ShowDragDistance.maxSpeedColor-l":"이동 속도를 초과한 경우 표시할 색상을 입력한다.", 19 | "ShowDragDistance.dashSpeedColor-s":"질주 속도 색상", 20 | "ShowDragDistance.dashSpeedColor-l":"토큰의 질주 이동을 나타내는 색상이다." 21 | } 22 | -------------------------------------------------------------------------------- /lang/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "ShowDragDistance.enable-s":"Enable Showing Distance on Drag?", 3 | "ShowDragDistance.enable-l":"Shows the distance you're moving your token while dragging, similar to holding the CTRL key and dragging on the canvas. Right click to create waypoints. Press 'X' to delete waypoints.", 4 | "ShowDragDistance.showPath-s":"Show Path by Default?", 5 | "ShowDragDistance.showPath-l":"Shows path on drag as default, press CTRL to hide it. If disabled, path is hidden and CTRL shows it.", 6 | "ShowDragDistance.rangeFinder-s":"Enable Rangefinder?", 7 | "ShowDragDistance.rangeFinder-l":"Enable holding 'ctrl' key to show range from selected token on mouse move.", 8 | "ShowDragDistance.maxSpeed-s":"Enable Max Speed Color Change?", 9 | "ShowDragDistance.maxSpeed-l":"Drag Ruler will change color based on max speed of token. Might conflict with some modules or systems.", 10 | "ShowDragDistance.dash-s":"Enable Dash Color Change?", 11 | "ShowDragDistance.dash-l":"Drag Ruler will show different color for squares within dashing speed.", 12 | "ShowDragDistance.dashX-s":"Dash Multiplier", 13 | "ShowDragDistance.dashX-l":"A number to multiply base movement speed for dashing. For DnD5e, this would be 1.", 14 | "ShowDragDistance.baseSpeedAttr-s":"Base Speed Attribute", 15 | "ShowDragDistance.baseSpeedAttr-l":"Enter the object property path from the Token entity to where the speed is located, ie for DND5e, 'actor.data.data.attributes.speed.value'", 16 | "ShowDragDistance.bonusSpeedAttr-s":"Bonus Speed Attribute", 17 | "ShowDragDistance.bonusSpeedAttr-l":"If system has a bonus or special property, enter it here, otherwise leave it blank. Example(DnD5e):actor.data.data.attributes.speed.special", 18 | "ShowDragDistance.maxSpeedColor-s": "Max Speed Color", 19 | "ShowDragDistance.maxSpeedColor-l":"Enter the color to show when player has exceeded their movement speed.", 20 | "ShowDragDistance.dashSpeedColor-s":"Dash Speed Color", 21 | "ShowDragDistance.dashSpeedColor-l":"The color shown to indicate a token's dash movement." 22 | } -------------------------------------------------------------------------------- /lang/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "ShowDragDistance.enable-s":"¿Activar el Mostrar la Distancia al Arrastrar?", 3 | "ShowDragDistance.enable-l":"Muestra la distancia que se mueve un icono al arrastrarlo, es parecido a pulsar CTRL y arrastrar por el mapa. Usa el clic-derecho para añadir puntos de ruta. Presiona 'X' para borrarlos.", 4 | "ShowDragDistance.showPath-s":"¿Mostrar por defecto la Ruta?", 5 | "ShowDragDistance.showPath-l":"Muestra por defecto la ruta cuando arrastras, pulsa CTRL para ocultarla. Si se deshabilita, la ruta estará oculta y presionando CTRL se mostrará.", 6 | "ShowDragDistance.rangeFinder-s":"¿Activar el Detector de Alcance?", 7 | "ShowDragDistance.rangeFinder-l":"Pulsando CTRL se mostrará el alcance del icono seleccionado a la posición del cursor del ratón.", 8 | "ShowDragDistance.maxSpeed-s":"¿Activar Cambio de Color al alcanzar la Velocidad Máxima?", 9 | "ShowDragDistance.maxSpeed-l":"La regla cambiará de color en base a la velocidad máxima del icono. Puede que entre en conflicto con algunos módulos o sistemas.", 10 | "ShowDragDistance.dash-s":"¿Activar Cambio de Color en Carrera?", 11 | "ShowDragDistance.dash-l":"La regla mostrará un color diferente en las cuadrículas dentro de la velocidad de carrera.", 12 | "ShowDragDistance.dashX-s":"Multiplicador de Carrera", 13 | "ShowDragDistance.dashX-l":"Un número por el que se multiplica la velocidad base para calcular la Carrera. Para DnD5e, debería ser 1.", 14 | "ShowDragDistance.baseSpeedAttr-s":"Atributo de la Velocidad Base", 15 | "ShowDragDistance.baseSpeedAttr-l":"Establezca la ruta a la propiedad del objeto de la entidad icono en la que está almacenada la velocidad. Por ejemplo, en DND5e es 'actor.data.data.attributes.speed.value'", 16 | "ShowDragDistance.bonusSpeedAttr-s":"Atributo de la Velocidad Extra", 17 | "ShowDragDistance.bonusSpeedAttr-l":"Si el sistema tiene una propiedad que define velocidad especial o extra, establézcalo aquí o, en otro caso, déjelo en blanco. Por ejemplo, en DnD5e es 'actor.data.data.attributes.speed.special'", 18 | "ShowDragDistance.maxSpeedColor-s": "Color de Velocidad Máxima", 19 | "ShowDragDistance.maxSpeedColor-l":"Establezca el color que se debe mostrar cuando el jugador excede su velocidad de movimiento.", 20 | "ShowDragDistance.dashSpeedColor-s":"Color de Velocidad de Carrera", 21 | "ShowDragDistance.dashSpeedColor-l":"Establezca el color al mostrar la Velocidad de Carrera del icono." 22 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ShowDragDistance 2 | Shows distance you've dragged the token as if you used the Ruler tool. Right click to set waypoints. Press 'X' to delete waypoints. 3 | 4 | Rangefinder: With a single token selected, hold CTRL on the canvas and it will draw a ruler from token to mouse. Replaces need to CTRL + drag from token(you can still do that) Using the CTRL ruler shortcut unaffected as well. 5 | 6 | 7 | New Settings: 8 | You can now point to where the speed or movement attribute is located on the actor sheet. For instance, for DnD5e it is `actor.data.data.attributes.speed.value`. You can also point to a bonus movement attribute if your system has one. In addition you can use `token.setFlag('ShowDragDistance','speed',{normal:0,dash:0})` to add any one time speed boosts via macro. The DragRuler also calls Hook.call('moveToken', token, dragRuler) when the token moves. Drag colors are configurable in client side settings if they are too close to player's color. 9 | 10 | # Changelog 11 | 2.2.3 - Added PF2 support. Fixed shift dragging not working now. 12 | 13 | 2.2.2 - Rebuilt the line drawing algorithms, Now works on hex grids again. Can now set `token.ignoreTerrain=true` to ignore difficult terrain. 14 | 15 | 2.2.1 - Fixed bug that froze application when dragging token off grid. 16 | 17 | 2.2.0 - Add support for Terrain Layer - Drag Distance now calculates path through difficult terrain. Created a new more efficient method for highlighting squares. Confirm 7.5 support. Removed 'x' key conflict with other modules. 18 | 19 | 2.1.9 - Fix multiple tokens not dragging. Fixed some bugs with gridless maps. Fix math on creating waypoints. Fix some issues with shift dragging off grid. 20 | 21 | 2.1.8 - Fixed shift dropping tokens off grid. GMs can now move tokens through walls again. 22 | 23 | 2.1.7 - Merged Spanish Language pull request. 24 | 25 | 2.1.6 - Token now follows path set with waypoints again. This was broken last release.Tokens with zero movement speed now highlight red as all movement is outside their speed. 26 | 27 | 2.1.5 - Now cancels ruler and movement if you drag token back to starting point. Holding shift will now let you place token off grid, as it should. Pressing escape while dragging will now cancel drag. 28 | 29 | 2.1.4 - Fixed conflict with Pick-Up-Stix. 30 | 31 | 2.1.3 - Fixed user broadcasting. 32 | 33 | 2.1.2 - DragRuler no longer shows for GM movements, unless the GM holds down 'Alt' to broadcast. If your movement speed uses decimals, it will now work properly. You can now also press 'P' to place a waypoint while dragging if you're on a touchpad. 34 | 35 | 36 | 2.1.1 - Fixed broken broadcasting. Fixed bug with Rangefinder firing when trying to use the actual ruler tool. Merged KO pull request. Pressing 'P' now adds a waypoint while dragging as well as right click for users on touch pads. 37 | 38 | 2.1.0 - Added dash measurement, added configurability to work better with other systems. 39 | 40 | 2.0.5 - Fix issue with distance calculation when you set waypoint before exceeding movement speed. Changed broadcast ruler color to be correct user color instead of using player's color. 41 | 42 | 2.0.4 - Added ruler broadcasting to others. 43 | 44 | 2.0.3 - Fixed bug with placeable objects. Pressing X will now cancel movement if there are no more waypoints to remove. 45 | 46 | 2.0.0 - Complete revamp. Seems stable, but might be conflicts with other mods. If so please revert to 1.1.4. 47 | -------------------------------------------------------------------------------- /showdragdistance.js: -------------------------------------------------------------------------------- 1 | // import { PathManager } from "/modules/lib-find-the-path/scripts/pathManager.js"; 2 | // import { MinkowskiParameter,PointFactory,Segment } from "/modules/lib-find-the-path/scripts/point.js"; 3 | // import {FTPUtility} from "/modules/lib-find-the-path/scripts/utility.js"; 4 | let showDragDistance = true; 5 | let handleDragCancel; 6 | let rangeFinder = false; 7 | let ctrlPressed = false; 8 | let altPressed = false; 9 | let dragShift = false; 10 | const TokenSpeedAttributes = {base:"",bonus:""}; 11 | class DragRuler extends Ruler{ 12 | constructor(user, {color=null}={}) { 13 | 14 | super() 15 | this.user = user; 16 | this.dragRuler = this.addChild(new PIXI.Graphics()); 17 | this.ruler = null; 18 | this.tokenSpeed = null; 19 | this.name = `DragRuler.${user._id}`; 20 | this.color = color || colorStringToHex(this.user.data.color) || 0x42F4E2; 21 | this.tokenSpeed = {normal:null,bonus:null} 22 | canvas.grid.addHighlightLayer(this.name); 23 | } 24 | clear() { 25 | this._state = Ruler.STATES.INACTIVE; 26 | this.waypoints = []; 27 | this.dragRuler.clear(); 28 | this.labels.removeChildren().forEach(c => c.destroy()); 29 | canvas.grid.clearHighlightLayer(this.name); 30 | } 31 | _onDragStart(event) { 32 | this.clear(); 33 | this._state = Ruler.STATES.STARTING; 34 | this._addWaypoint(event.data.origin); 35 | this.tokenSpeed = this.getTokenSpeed(this.getToken) 36 | } 37 | getTokenSpeed(token){ 38 | const baseSpeed = parseFloat(getProperty(token,TokenSpeedAttributes.base)); 39 | 40 | const bonusSpeed = (TokenSpeedAttributes.bonus != "" && getProperty(token,TokenSpeedAttributes.bonus) !="") ? parseFloat(getProperty(token,TokenSpeedAttributes.bonus)):0; 41 | const flagBonusSpeed = (typeof token.getFlag('ShowDragDistance','speed') !='undefined') ? token.getFlag('ShowDragDistance','speed').normal:0; 42 | const normalSpeed = baseSpeed + flagBonusSpeed; 43 | const flagDashSpeed = (typeof token.getFlag('ShowDragDistance','speed') !='undefined') ? token.getFlag('ShowDragDistance','speed').dash:0; 44 | const dashSpeed = (normalSpeed + flagDashSpeed) * game.settings.get('ShowDragDistance','dashX'); 45 | 46 | return {normal:normalSpeed,dash:dashSpeed} 47 | } 48 | _onMouseUp(event) { 49 | this._endMeasurement(); 50 | } 51 | _onMouseMove(event) { 52 | 53 | if ( this._state === Ruler.STATES.MOVING ) return; 54 | 55 | 56 | // Extract event data 57 | const mt = event._measureTime || 0; 58 | const ld = event._lastDest || event.data.origin; 59 | const {origin, destination, originalEvent} = event.data; 60 | 61 | // Check measurement distance 62 | let dx = destination.x - origin.x, 63 | dy = destination.y - origin.y; 64 | 65 | let [lastX,lastY] = canvas.grid.grid.getGridPositionFromPixels(ld.x,ld.y); 66 | let [x,y] = canvas.grid.grid.getGridPositionFromPixels(destination.x,destination.y); 67 | 68 | // Hide any existing Token HUD 69 | canvas.hud.token.clear(); 70 | delete event.data.hudState; 71 | 72 | // Draw measurement updates 73 | if(lastX != x || lastY !=y || dragShift){ 74 | 75 | if ( Date.now() - mt > 50) { 76 | 77 | //if(Math.abs(dx) > canvas.dimensions.size/2 || Math.abs(dy) > canvas.dimensions.size/2){ 78 | this.measure(destination, {gridSpaces: !originalEvent.shiftKey}); 79 | event._measureTime = Date.now(); 80 | event._lastDest = destination 81 | this._state = Ruler.STATES.MEASURING; 82 | } 83 | } 84 | } 85 | async measure(destination, {gridSpaces=true}={}) { 86 | 87 | if(!dragShift) 88 | destination = new PIXI.Point(...canvas.grid.getCenter(destination.x, destination.y)); 89 | //else 90 | 91 | const waypoints = this.waypoints.concat([destination]); 92 | const r = this.dragRuler; 93 | this.destination = destination; 94 | 95 | // Iterate over waypoints and construct segment rays 96 | const segments = []; 97 | for ( let [i, dest] of waypoints.slice(1).entries() ) { 98 | const origin = waypoints[i]; 99 | const label = this.labels.children[i]; 100 | const ray = new Ray(origin, dest); 101 | 102 | if ( ray.distance < (0.2 * canvas.grid.size) ) { 103 | if ( label ) label.visible = false; 104 | continue; 105 | } 106 | segments.push({ray, label}); 107 | } 108 | // Clear the grid highlight layer 109 | const hlt = canvas.grid.highlightLayers[this.name]; 110 | hlt.clear(); 111 | 112 | // Draw measured path 113 | r.clear(); 114 | let nDiagonals = 0; 115 | let totalDistance = 0; 116 | const rule = canvas.grid.diagonalRule; 117 | 118 | 119 | for (let i = 0; i 1){ 127 | let grid0 = canvas.grid.grid.getGridPositionFromPixels(s.ray.A.x,s.ray.A.y); 128 | let grid1 = canvas.grid.grid.getGridPositionFromPixels(s.ray.B.x,s.ray.B.y); 129 | 130 | let hex0 = canvas.grid.grid.offsetToCube(grid0[0],grid0[1]); 131 | let hex1 = canvas.grid.grid.offsetToCube(grid1[0],grid1[1]); 132 | 133 | hex0 = new Hex(hex0.q,hex0.r,hex0.s); 134 | 135 | 136 | line = hex0.linedraw(hex1,totalDistance,ignoreTerrain) 137 | 138 | 139 | }else if(canvas.grid.type == 1){ 140 | let p0 = canvas.grid.grid.getGridPositionFromPixels(s.ray.A.x,s.ray.A.y) 141 | let p1 = canvas.grid.grid.getGridPositionFromPixels(s.ray.B.x,s.ray.B.y) 142 | 143 | line = grid_line({row:p0[0],col:p0[1]},{row:p1[0],col:p1[1]},totalDistance,nDiagonals,ignoreTerrain) 144 | } 145 | 146 | nDiagonals = line[line.length-1].nDiagonals; 147 | s.distance = line.reduce((sum,val)=>{return sum + val.distance},0) 148 | totalDistance += s.distance; 149 | s.last = i === (segments.length - 1); 150 | s.text = this._getSegmentLabel(s.distance, totalDistance, s.last); 151 | // Draw line segment 152 | r.lineStyle(6, 0x000000, 0.5).moveTo(s.ray.A.x, s.ray.A.y).lineTo(s.ray.B.x, s.ray.B.y) 153 | .lineStyle(4, this.color, 0.25).moveTo(s.ray.A.x, s.ray.A.y).lineTo(s.ray.B.x, s.ray.B.y); 154 | // Draw the distance label just after the endpoint of the segment 155 | if ( label ) { 156 | label.text = s.text; 157 | label.alpha = s.last ? 1.0 : 0.5; 158 | label.visible = true; 159 | let labelPosition = ray.project((ray.distance + 50) / ray.distance); 160 | label.position.set(labelPosition.x, labelPosition.y); 161 | } 162 | 163 | this._highlightMeasurement(line); 164 | } 165 | for ( let p of waypoints ) { 166 | r.lineStyle(2, 0x000000, 0.5).beginFill(this.color, 0.25).drawCircle(p.x, p.y, 8); 167 | } 168 | return segments; 169 | 170 | } 171 | _getMovementToken() { 172 | 173 | let [x0, y0] = Object.values(this.waypoints[0]); 174 | const tokens = new Set(canvas.tokens.controlled); 175 | 176 | if ( !tokens.size && game.user.character ) { 177 | const charTokens = game.user.character.getActiveTokens(); 178 | if ( charTokens.length ) tokens.add(...charTokens); 179 | } 180 | if ( !tokens.size ) return null; 181 | 182 | let x = Array.from(tokens).find(t => { 183 | let pos = new PIXI.Rectangle(t.x - 1, t.y - 1, t.w + 2, t.h + 2); 184 | return pos.contains(x0, y0); 185 | }) 186 | 187 | return x 188 | } 189 | _highlightMeasurement(line){ 190 | let remainingSpeed = (this.tokenSpeed.normal != null) ? this.tokenSpeed.normal:null; 191 | let dashSpeed = (this.tokenSpeed.dash !== null) ? this.tokenSpeed.dash: null; 192 | let maxSpeed = remainingSpeed; 193 | let color = this.color; 194 | 195 | for(let i=0;i remainingSpeed) { 198 | if(game.settings.get('ShowDragDistance','dash') && line[i].travelled < remainingSpeed + maxSpeed){ 199 | color = colorStringToHex(game.settings.get('ShowDragDistance','dashSpeedColor')) 200 | }else if(game.settings.get('ShowDragDistance','dash') == false || line[i].travelled > remainingSpeed + maxSpeed){ 201 | color = colorStringToHex(game.settings.get('ShowDragDistance','maxSpeedColor')) 202 | } 203 | } 204 | 205 | let [xgh, ygh] = canvas.grid.grid.getPixelsFromGridPosition(line[i].row, line[i].col); 206 | canvas.grid.highlightPosition(this.name, {x: xgh, y: ygh, color: color}); 207 | } 208 | } 209 | 210 | 211 | _addWaypoint(point) { 212 | //const center = canvas.grid.getCenter(point.x, point.y); 213 | this.waypoints.push(new PIXI.Point(point.x, point.y)); 214 | this.labels.addChild(new PIXI.Text("", CONFIG.canvasTextStyle)); 215 | } 216 | async moveToken(dragShift=false) { 217 | 218 | let wasPaused = game.paused; 219 | if ( wasPaused && !game.user.isGM ) { 220 | ui.notifications.warn(game.i18n.localize("GAME.PausedWarning")); 221 | return false; 222 | } 223 | if ( !this.visible || !this.destination ) return false; 224 | 225 | const token = this._getMovementToken(); 226 | 227 | if ( !token ) return; 228 | 229 | // Determine offset relative to the Token top-left. 230 | // This is important so we can position the token relative to the ruler origin for non-1x1 tokens. 231 | let origin; 232 | /*if(!dragShift && canvas.scene.data.gridType !== 0) 233 | origin = canvas.grid.getTopLeft(this.waypoints[0].x, this.waypoints[0].y); 234 | else*/ 235 | 236 | origin = [this.waypoints[0].x , this.waypoints[0].y] 237 | let s2 = canvas.dimensions.size / 2; 238 | 239 | 240 | let dx = Math.round((token.data.x - origin[0]) / s2) * s2; 241 | let dy = Math.round((token.data.y - origin[1]) / s2) * s2; 242 | 243 | if(dragShift == false && canvas.scene.data.gridType !== 0){ 244 | dx = (dx > -70) ? 0:dx - (dx%canvas.dimensions.size); 245 | dy = (dy > -70) ? 0:dy - (dy%canvas.dimensions.size); 246 | } 247 | 248 | // Get the movement rays and check collision along each Ray 249 | // These rays are center-to-center for the purposes of collision checking 250 | const rays = this._getRaysFromWaypoints(this.waypoints, this.destination); 251 | let hasCollision = rays.some(r => canvas.walls.checkCollision(r)); 252 | 253 | if ( hasCollision && !game.user.isGM ) { 254 | this._endMeasurement(); 255 | ui.notifications.error(game.i18n.localize("ERROR.TokenCollide")); 256 | return; 257 | } 258 | 259 | // Execute the movement path. 260 | // Transform each center-to-center ray into a top-left to top-left ray using the prior token offsets. 261 | this._state = Ruler.STATES.MOVING; 262 | token._noAnimate = true; 263 | 264 | for ( let r of rays ) { 265 | if ( !wasPaused && game.paused ) break; 266 | let dest; 267 | if(!dragShift && canvas.scene.data.gridType !== 0) 268 | dest = canvas.grid.getTopLeft(r.B.x , r.B.y ); 269 | else 270 | dest = [r.B.x,r.B.y] 271 | 272 | 273 | const path = new Ray({x: token.x, y: token.y}, {x: dest[0]+dx , y: dest[1]+dy}); 274 | 275 | await token.update(path.B); 276 | await token.animateMovement(path); 277 | 278 | } 279 | Hooks.call('DragRuler.moveToken', token, this) 280 | token._noAnimate = false; 281 | 282 | // Once all animations are complete we can clear the ruler 283 | this._endMeasurement(); 284 | } 285 | toJSON() { 286 | return { 287 | class: "DragRuler", 288 | name: `DragRuler.${game.user._id}`, 289 | waypoints: this.waypoints, 290 | destination: this.destination, 291 | _state: this._state, 292 | speed:this.tokenSpeed 293 | } 294 | } 295 | _endMeasurement() { 296 | 297 | this.clear(); 298 | game.user.broadcastActivity({dragruler: null}); 299 | canvas.mouseInteractionManager.state = MouseInteractionManager.INTERACTION_STATES.HOVER; 300 | } 301 | 302 | /* -------------------------------------------- */ 303 | get getToken(){ 304 | return canvas.tokens.controlled.length > 0 ? canvas.tokens.controlled[0]:null; 305 | } 306 | /** 307 | * Update a Ruler instance using data provided through the cursor activity socket 308 | * @param {Object} data Ruler data with which to update the display 309 | */ 310 | update(data) { 311 | 312 | if ( data.class !== "DragRuler" ) throw new Error("Unable to recreate Ruler instance from provided data"); 313 | 314 | // Populate data 315 | this.waypoints = data.waypoints; 316 | this.destination = data.destination; 317 | this._state = data._state; 318 | this.tokenSpeed = data.speed; 319 | // Ensure labels are created 320 | for ( let i=0; i window.location.reload() 347 | }); 348 | game.settings.register('ShowDragDistance', 'rangeFinder', { 349 | name: "ShowDragDistance.rangeFinder-s", 350 | hint: "ShowDragDistance.rangeFinder-l", 351 | scope: "client", 352 | config: true, 353 | default: true, 354 | type: Boolean 355 | // onChange: x => window.location.reload() 356 | }); 357 | game.settings.register('ShowDragDistance', 'baseSpeedAttr', { 358 | name: "ShowDragDistance.baseSpeedAttr-s", 359 | hint: "ShowDragDistance.baseSpeedAttr-l", 360 | scope: "world", 361 | config: true, 362 | default: "actor.data.data.attributes.speed.value", 363 | type: String, 364 | onChange: x => window.location.reload() 365 | }); 366 | game.settings.register('ShowDragDistance', 'bonusSpeedAttr', { 367 | name: "ShowDragDistance.bonusSpeedAttr-s", 368 | hint: "ShowDragDistance.bonusSpeedAttr-l", 369 | scope: "world", 370 | config: true, 371 | default: "actor.data.data.attributes.speed.special", 372 | type: String, 373 | onChange: x => window.location.reload() 374 | }); 375 | game.settings.register('ShowDragDistance', 'maxSpeed', { 376 | name: "ShowDragDistance.maxSpeed-s", 377 | hint: "ShowDragDistance.maxSpeed-l", 378 | scope: "world", 379 | config: true, 380 | default: true, 381 | type: Boolean 382 | //onChange: x => window.location.reload() 383 | }); 384 | game.settings.register('ShowDragDistance', 'maxSpeedColor', { 385 | name: "ShowDragDistance.maxSpeedColor-s", 386 | hint: "ShowDragDistance.maxSpeedColor-l", 387 | scope: "client", 388 | config: true, 389 | default: '#FF0000', 390 | type: String 391 | //onChange: x => window.location.reload() 392 | }); 393 | game.settings.register('ShowDragDistance', 'dash', { 394 | name: "ShowDragDistance.dash-s", 395 | hint: "ShowDragDistance.dash-l", 396 | scope: "world", 397 | config: true, 398 | default: true, 399 | type: Boolean 400 | //onChange: x => window.location.reload() 401 | }); 402 | game.settings.register('ShowDragDistance', 'dashX', { 403 | name: "ShowDragDistance.dashX-s", 404 | hint: "ShowDragDistance.dashX-l", 405 | scope: "world", 406 | config: true, 407 | default: 1, 408 | type: Number 409 | //onChange: x => window.location.reload() 410 | }); 411 | game.settings.register('ShowDragDistance', 'dashSpeedColor', { 412 | name: "ShowDragDistance.dashSpeedColor-s", 413 | hint: "ShowDragDistance.dashSpeedColor-l", 414 | scope: "client", 415 | config: true, 416 | default: '#00FF00', 417 | type: String 418 | //onChange: x => window.location.reload() 419 | }); 420 | // game.settings.register('ShowDragDistance', 'showPathDefault', { 421 | // name: "ShowDragDistance.showPath-s", 422 | // hint: "ShowDragDistance.showPath-l", 423 | // scope: "client", 424 | // config: true, 425 | // default: true, 426 | // type: Boolean 427 | // // onChange: x => window.location.reload() 428 | // }); 429 | TokenSpeedAttributes.base = game.settings.get('ShowDragDistance','baseSpeedAttr'); 430 | TokenSpeedAttributes.bonus = (game.settings.get('ShowDragDistance','bonusSpeedAttr') !== '') ? game.settings.get('ShowDragDistance','bonusSpeedAttr'):""; 431 | 432 | let _handleUserActivity = Users._handleUserActivity; 433 | Users._handleUserActivity = function(userId, activityData={}){ 434 | 435 | let user2 = game.users.get(userId); 436 | let active2 = "active" in activityData ? activityData.active : true; 437 | // DragRuler measurement 438 | if ( (active2 === false) || (user2.viewedScene !== canvas.scene.id) ) { 439 | canvas.controls.updateDragRuler(user2, null); 440 | } 441 | if ( "dragruler" in activityData ) { 442 | canvas.controls.updateDragRuler(user2, activityData.dragruler); 443 | } 444 | _handleUserActivity(userId,activityData) 445 | } 446 | 447 | 448 | ControlsLayer.prototype.drawDragRulers = function() { 449 | this.dragRulers = this.addChild(new PIXI.Container()); 450 | for (let u of game.users.entities) { 451 | let dragRuler = new DragRuler(u); 452 | this._dragRulers[u._id] = this.dragRulers.addChild(dragRuler); 453 | } 454 | } 455 | ControlsLayer.prototype.getDragRulerForUser = function(userId) { 456 | return this._dragRulers[userId] || null; 457 | } 458 | ControlsLayer.prototype.updateDragRuler = function(user, dragRulerData) { 459 | if ( user === game.user) return; 460 | // Update the Ruler display for the user 461 | let dragRuler = this.getDragRulerForUser(user.id); 462 | if ( !dragRuler ) return; 463 | if ( dragRulerData === null ) dragRuler.clear(); 464 | else dragRuler.update(dragRulerData); 465 | } 466 | 467 | let oldOnDragLeftStart = Token.prototype._onDragLeftStart; 468 | Token.prototype._onDragLeftStart = function(event){ 469 | if(game.settings.get('ShowDragDistance','enabled') === true && typeof this.data.flags['pick-up-stix'] == 'undefined' && canvas.tokens.controlled.length==1){ 470 | event.data.origin = {x:this.x+(canvas.dimensions.size/2),y:this.y+(canvas.dimensions.size/2)}; 471 | //event.data.origin = this.center; 472 | canvas.controls.dragRuler._onDragStart(event) 473 | } 474 | oldOnDragLeftStart.apply(this,[event]) 475 | } 476 | let oldOnDragLeftMove = Token.prototype._onDragLeftMove; 477 | Token.prototype._onDragLeftMove = function(event){ 478 | 479 | if(canvas.controls.dragRuler.active && typeof this.data.flags['pick-up-stix'] == 'undefined'){ 480 | canvas.controls.dragRuler._onMouseMove(event,this) 481 | 482 | if(!this.data.hidden && game.user.isGM && altPressed){ 483 | const dragruler = (canvas.controls.dragRuler._state > 0) ? canvas.controls.dragRuler.toJSON() : null; 484 | game.user.broadcastActivity({dragruler:dragruler}) 485 | }else if(!game.user.isGM) { 486 | const dragruler = (canvas.controls.dragRuler._state > 0) ? canvas.controls.dragRuler.toJSON() : null; 487 | game.user.broadcastActivity({dragruler:dragruler}) 488 | } 489 | 490 | 491 | } 492 | 493 | oldOnDragLeftMove.apply(canvas.tokens.controlled[0],[event]) 494 | 495 | } 496 | let oldOnDragLeftDrop = Token.prototype._onDragLeftDrop; 497 | Token.prototype._onDragLeftDrop = function(event){ 498 | if(game.settings.get('ShowDragDistance','enabled') && canvas.controls.dragRuler.active ){ 499 | 500 | for ( let c of this.layer.preview.children ) { 501 | const o = c._original; 502 | if ( o ) { 503 | o.data.locked = false; 504 | o.alpha = 1.0; 505 | } 506 | } 507 | this.layer.preview.removeChildren(); 508 | 509 | 510 | if(typeof this.data.flags['pick-up-stix'] == 'undefined' ){ 511 | const dragruler = (canvas.controls.dragRuler._state > 0) ? canvas.controls.dragRuler.toJSON() : null; 512 | canvas.controls.dragRuler.moveToken(dragShift) 513 | canvas.controls.dragRuler._onMouseUp(event) 514 | canvas.controls.dragRuler._endMeasurement(); 515 | canvas.controls.dragRuler._state = 0; 516 | //canvas.controls.dragRuler.FTPUtility.traverse(0,0,0); 517 | } 518 | return false; 519 | }else{ 520 | oldOnDragLeftDrop.apply(this,[event]); 521 | } 522 | } 523 | let oldOnDragLeftCancel = Token.prototype._onDragLeftCancel; 524 | Token.prototype._onDragLeftCancel = function(event){ 525 | event.stopPropagation(); 526 | 527 | 528 | oldOnDragLeftCancel.apply(this,[event]) 529 | 530 | } 531 | let handleDragCancel = MouseInteractionManager.prototype._handleDragCancel; 532 | MouseInteractionManager.prototype._handleDragCancel = function(event){ 533 | 534 | if((typeof this.object.data != 'undefined') && typeof this.object.data.flags['pick-up-stix'] == 'undefined'){ 535 | if( canvas.tokens.controlled.length > 0 && canvas.tokens.controlled[0].mouseInteractionManager.state == 3 ){ 536 | switch(event.button){ 537 | case 0: 538 | 539 | handleDragCancel.apply(this,[event]) 540 | break; 541 | case 2: 542 | const point = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(canvas.tokens); 543 | if(!dragShift){ 544 | const center = canvas.grid.grid.getCenter(point.x,point.y) 545 | canvas.controls.dragRuler._addWaypoint(new PIXI.Point(center[0], center[1])); 546 | }else{ 547 | canvas.controls.dragRuler._addWaypoint(new PIXI.Point(point.x,point.y)); 548 | } 549 | 550 | break; 551 | default: 552 | handleDragCancel.apply(this,[event]) 553 | break; 554 | } 555 | }else{ 556 | handleDragCancel.apply(this,[event]) 557 | } 558 | }else{ 559 | handleDragCancel.apply(this,[event]) 560 | } 561 | } 562 | } 563 | } 564 | 565 | Hooks.on('init', DragRuler.init); 566 | Hooks.on('ready',()=>{ 567 | Object.defineProperty(canvas.controls,'dragRuler', { 568 | get() { 569 | return canvas.controls.getDragRulerForUser(game.user._id); 570 | }} 571 | ); 572 | canvas.controls.dragRulers = null; 573 | canvas.controls._dragRulers = {}; 574 | canvas.controls.drawDragRulers(); 575 | $('body').on('keydown',(e)=>{ 576 | 577 | switch(e.which){ 578 | case 17: 579 | ctrlPressed = true; 580 | if(canvas.controls.dragRuler.active == false && e.originalEvent.location == 1 && !rangeFinder && canvas.tokens.controlled.length>0 && game.settings.get('ShowDragDistance','rangeFinder') === true && canvas.mouseInteractionManager.state !=0 && game.activeTool !='ruler'){ 581 | rangeFinder = true; 582 | canvas.controls.ruler._state = Ruler.STATES.MEASURING; 583 | canvas.controls.ruler._addWaypoint(canvas.tokens.controlled[0].center) 584 | canvas.mouseInteractionManager.state = canvas.mouseInteractionManager.states.DRAG 585 | canvas.mouseInteractionManager._activateDragEvents() 586 | e.data = {originalEvent:e.originalEvent,origin:canvas.tokens.controlled[0].center,destination:canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(canvas.tokens)} 587 | canvas.controls.ruler._onMouseMove(e) 588 | canvas.mouseInteractionManager._dragRight = false; 589 | } 590 | break; 591 | case 18: 592 | altPressed = true; 593 | break; 594 | case 88: 595 | if(canvas.controls.dragRuler.waypoints.length>1) 596 | canvas.controls.dragRuler._removeWaypoint(canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(canvas.tokens)) 597 | else if(canvas.controls.dragRuler.waypoints.length==1){ 598 | canvas.controls.dragRuler._removeWaypoint(canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(canvas.tokens)) 599 | for ( let c of canvas.tokens.controlled[0].layer.preview.children ) { 600 | const o = c._original; 601 | if ( o ) { 602 | o.data.locked = false; 603 | o.alpha = 1.0; 604 | } 605 | } 606 | canvas.tokens.controlled[0].layer.preview.removeChildren(); 607 | canvas.controls.dragRuler._onMouseUp(e) 608 | canvas.mouseInteractionManager.state = 1; 609 | canvas.tokens.controlled[0].mouseInteractionManager.state = 0 610 | canvas.tokens.controlled[0]._onDragLeftCancel(e) 611 | //oldOnDragLeftCancel.apply(canvas.tokens.controlled[0],[event]) 612 | } 613 | break; 614 | case 80: 615 | if(canvas.controls.dragRuler.active){ 616 | canvas.controls.dragRuler._addWaypoint(canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(canvas.tokens)) 617 | } 618 | break; 619 | case 27: 620 | if(canvas.tokens.controlled.length > 0) { 621 | for ( let c of canvas.tokens.controlled[0].layer.preview.children ) { 622 | const o = c._original; 623 | if ( o ) { 624 | o.data.locked = false; 625 | o.alpha = 1.0; 626 | } 627 | } 628 | canvas.tokens.controlled[0].layer.preview.removeChildren(); 629 | canvas.controls.dragRuler._onMouseUp(e) 630 | canvas.mouseInteractionManager.state = 1; 631 | canvas.tokens.controlled[0].mouseInteractionManager.state = 0 632 | canvas.tokens.controlled[0]._onDragLeftCancel(e); 633 | canvas.tokens.controlled[0].release() 634 | } 635 | break; 636 | case 16: 637 | dragShift= true; 638 | break; 639 | default: 640 | break; 641 | } 642 | }) 643 | $('body').on('keyup',(e)=>{ 644 | switch(e.which){ 645 | case 17: 646 | ctrlPressed = false; 647 | if(rangeFinder && canvas.tokens.controlled.length>0){ 648 | rangeFinder = false; 649 | canvas.controls.ruler._endMeasurement(); 650 | canvas.mouseInteractionManager._deactivateDragEvents() 651 | canvas.mouseInteractionManager.state = canvas.mouseInteractionManager.states.HOVER 652 | } 653 | break; 654 | case 18: 655 | altPressed = false; 656 | if(canvas.controls.dragRuler.active){ 657 | 658 | game.user.broadcastActivity({dragruler:null}) 659 | } 660 | break; 661 | case 16: 662 | dragShift= false; 663 | break; 664 | default: 665 | break; 666 | } 667 | }) 668 | }) 669 | Hooks.on('canvasReady', ()=>{ 670 | canvas.controls.dragRulers = null; 671 | canvas.controls._dragRulers = {}; 672 | canvas.controls.drawDragRulers(); 673 | }) 674 | Hooks.on('updateUser', (user,data,diff, id)=>{ 675 | canvas.controls.getDragRulerForUser(data._id).color = colorStringToHex(data.color); 676 | }) 677 | /*function getSquaresInLine (start, end) { 678 | 679 | // Translate coordinates 680 | var x1 = start[0] || start.x || 0; 681 | var y1 = start[1] || start.y || 0; 682 | var x2 = end[0] || end.x || 0; 683 | var y2 = end[1] || end.y || 0; 684 | var pointsArray = new Array(); 685 | console.log(x1,y1) 686 | console.log(x2,y2) 687 | // Define differences and error check 688 | var dx = Math.abs(x2 - x1); 689 | var dy = Math.abs(y2 - y1); 690 | console.log(dx,dy) 691 | // var sx,sy; 692 | 693 | // sx = (x1 < x2) ? 1 : -1; 694 | // sy = (y1 < y2) ? 1 : -1; 695 | 696 | // var err = dx - dy; 697 | // let originDist = 0; 698 | // // Main loop 699 | // while (!((x1 == x2) && (y1 == y2))) { 700 | // var e2 = err << 1; 701 | // if (e2 > -dy) { 702 | // err -= dy; 703 | // x1 += sx; 704 | // } 705 | // if (e2 < dx) { 706 | // err += dx; 707 | // y1 += sy; 708 | // } 709 | // originDist+= 1; 710 | // // Set coordinates 711 | // pointsArray.push({x:x1,y: y1,gridDist:originDist}); 712 | // } 713 | let ray = new Ray({x:x1,y:y1},{x:x2,y:y2}) 714 | // Return the result 715 | return pointsArray; 716 | } 717 | function measureDistancesWithDifficultTerrain(segments) { 718 | let size = canvas.dimensions.size; 719 | let distances = segments.map((segment)=>{ 720 | let [startY,startX] = canvas.grid.grid.getGridPositionFromPixels(segment.ray.A.x,segment.ray.A.y) 721 | let [endY,endX] = canvas.grid.grid.getGridPositionFromPixels(segment.ray.B.x,segment.ray.B.y) 722 | 723 | let squares = getSquaresInLine([startX,startY],[endX,endY]) 724 | let totalDistance = 0; 725 | let nDiagonals = 0; 726 | const rule = canvas.grid.diagonalRule; 727 | 728 | for (let i = 0; i < squares.length;i++){ 729 | let {x,y} = squares[i]; 730 | let gridDistance = canvas.scene.data.gridDistance 731 | 732 | let lastX,lastY; 733 | if(i!==0){ 734 | lastX = squares[i-1].x; 735 | lastY = squares[i-1].y; 736 | }else{ 737 | lastY = startY; 738 | lastX = startX; 739 | } 740 | 741 | let dx = Math.abs(lastX - x); 742 | let dy = Math.abs(lastY - y); 743 | let nd = Math.min(dx, dy); 744 | 745 | 746 | if(nd > 0 && canvas.grid.diagonalRule == '5105'){ 747 | nDiagonals++; 748 | if(Math.floor(nDiagonals%2)==0){ 749 | gridDistance = gridDistance * 2; 750 | } 751 | } 752 | 753 | 754 | 755 | if(typeof canvas.terrain?.costGrid[y]?.[x] != 'undefined'){ 756 | let point = canvas.terrain.costGrid[y][x]; 757 | squares[i].dist = (point.multiple * gridDistance); 758 | totalDistance += (point.multiple * gridDistance) 759 | }else{ 760 | squares[i].dist = gridDistance; 761 | totalDistance += gridDistance; 762 | } 763 | 764 | //return totalDistance 765 | 766 | } 767 | return {totalDistance,squares} 768 | 769 | 770 | }) 771 | 772 | return distances; 773 | };*/ 774 | 775 | function grid_line(p0, p1, totalDistance=0, nDiagonals=0,ignoreTerrain=false) { 776 | var dx = p1.col-p0.col, 777 | dy = p1.row-p0.row; 778 | 779 | var nx = Math.abs(dx), ny = Math.abs(dy); 780 | var sign_x = dx > 0? 1 : -1, sign_y = dy > 0? 1 : -1; 781 | let N = Math.max(nx,ny); 782 | let divN = (N==0) ? 0.0 :1.0 / N; 783 | let xStep = dx * divN; 784 | let yStep = dy * divN; 785 | var p = {row:p0.row,col: p0.col,distance:0,nDiagonals:0,travelled:0}; 786 | var points = [p]; 787 | 788 | let col = p0.col; 789 | let row = p0.row; 790 | let travelled = totalDistance; 791 | for(let i = 0;i < N;i++){ 792 | col+=xStep; 793 | row+=yStep; 794 | 795 | let rCol = Math.round(col); 796 | let rRow = Math.round(row); 797 | 798 | let dist = canvas.dimensions.distance; 799 | if((game.system.id == 'dnd5e' && canvas.grid.diagonalRule == '5105') || game.system.id == 'pf2e'){ 800 | 801 | 802 | let dx2 = Math.abs(points[i].col - rCol); 803 | let dy2 = Math.abs(points[i].row - rRow); 804 | let nd = Math.min(dx2, dy2); 805 | 806 | if(nd>0){ 807 | nDiagonals++; 808 | if(nDiagonals%2==0){ 809 | dist = dist * 2; 810 | } 811 | } 812 | } 813 | if(typeof canvas.terrain?.costGrid[rRow]?.[rCol] != 'undefined' && !ignoreTerrain){ 814 | let point = canvas.terrain.costGrid[rRow][rCol]; 815 | 816 | dist = (point.multiple * dist) 817 | } 818 | travelled+=dist; 819 | points.push({row:rRow,col:rCol,distance:dist,nDiagonals:nDiagonals,travelled:travelled}) 820 | } 821 | 822 | return points; 823 | } 824 | class Hex { 825 | constructor(q, r, s) { 826 | this.q = q; 827 | this.r = r; 828 | this.s = s; 829 | if (Math.round(q + r + s) !== 0) 830 | throw "q + r + s must be 0"; 831 | } 832 | add(b) { 833 | return new Hex(this.q + b.q, this.r + b.r, this.s + b.s); 834 | } 835 | subtract(b) { 836 | return new Hex(this.q - b.q, this.r - b.r, this.s - b.s); 837 | } 838 | scale(k) { 839 | return new Hex(this.q * k, this.r * k, this.s * k); 840 | } 841 | rotateLeft() { 842 | return new Hex(-this.s, -this.q, -this.r); 843 | } 844 | rotateRight() { 845 | return new Hex(-this.r, -this.s, -this.q); 846 | } 847 | static direction(direction) { 848 | return Hex.directions[direction]; 849 | } 850 | neighbor(direction) { 851 | return this.add(Hex.direction(direction)); 852 | } 853 | diagonalNeighbor(direction) { 854 | return this.add(Hex.diagonals[direction]); 855 | } 856 | len() { 857 | return (Math.abs(this.q) + Math.abs(this.r) + Math.abs(this.s)) / 2; 858 | } 859 | distance(b) { 860 | return this.subtract(b).len(); 861 | } 862 | round() { 863 | var qi = Math.round(this.q); 864 | var ri = Math.round(this.r); 865 | var si = Math.round(this.s); 866 | var q_diff = Math.abs(qi - this.q); 867 | var r_diff = Math.abs(ri - this.r); 868 | var s_diff = Math.abs(si - this.s); 869 | if (q_diff > r_diff && q_diff > s_diff) { 870 | qi = -ri - si; 871 | } 872 | else if (r_diff > s_diff) { 873 | ri = -qi - si; 874 | } 875 | else { 876 | si = -qi - ri; 877 | } 878 | return new Hex(qi, ri, si); 879 | } 880 | lerp(b, t) { 881 | return new Hex(this.q * (1.0 - t) + b.q * t, this.r * (1.0 - t) + b.r * t, this.s * (1.0 - t) + b.s * t); 882 | } 883 | linedraw(b,totalDistance=0,ignoreTerrain=false) { 884 | var N = this.distance(b); 885 | var a_nudge = new Hex(this.q + 1e-06, this.r + 1e-06, this.s - 2e-06); 886 | var b_nudge = new Hex(b.q + 1e-06, b.r + 1e-06, b.s - 2e-06); 887 | var results = []; 888 | var step = 1.0 / Math.max(N, 1); 889 | let travelled = totalDistance; 890 | for (var i = 0; i <= N; i++) { 891 | let hex = a_nudge.lerp(b_nudge, step * i).round(); 892 | let grid = Coord.rCubeToOffset(hex) 893 | let dist = canvas.dimensions.distance; 894 | if(i == 0) 895 | dist = 0; 896 | else{ 897 | if(typeof canvas.terrain?.costGrid[grid.row]?.[grid.col] != 'undefined' && !ignoreTerrain){ 898 | let point = canvas.terrain.costGrid[grid.row][grid.col]; 899 | dist = (point.multiple * dist) 900 | } 901 | } 902 | travelled += dist; 903 | results.push({row:grid.row,col:grid.col,distance:dist,travelled:travelled}); 904 | } 905 | return results; 906 | } 907 | } 908 | 909 | class Coord { 910 | constructor(row, col) { 911 | this.col = col; 912 | this.row = row; 913 | } 914 | static qoffsetFromCube(offset, h) { 915 | var col = h.q; 916 | var row = h.r + (h.q + offset * (h.q & 1)) / 2; 917 | // if (offset !== OffsetCoord.EVEN && offset !== OffsetCoord.ODD) { 918 | // throw "offset must be EVEN (+1) or ODD (-1)"; 919 | // } 920 | return new OffsetCoord(col, row); 921 | } 922 | static qoffsetToCube(offset, h) { 923 | var q = h.col; 924 | var r = h.row - (h.col + offset * (h.col & 1)) / 2; 925 | var s = -q - r; 926 | // if (offset !== OffsetCoord.EVEN && offset !== OffsetCoord.ODD) { 927 | // throw "offset must be EVEN (+1) or ODD (-1)"; 928 | // } 929 | return new Hex(q, r, s); 930 | } 931 | static rCubeToOffset(cube) { 932 | const offset = (canvas.grid.grid.options.even) ? 1:-1; 933 | let row = cube.s//cube.q + (cube.r + offset * (cube.r % 2)) / 2; // 1 + (-1 + -1 * (-1 % 2)) / 2 = 934 | let col = cube.q + (cube.s + offset * (cube.s % 2)) / 2 935 | // if (offset !== OffsetCoord.EVEN && offset !== OffsetCoord.ODD) { 936 | // throw "offset must be EVEN (+1) or ODD (-1)"; 937 | // } 938 | return {row,col}; 939 | } 940 | static rCoordToCube(row,col) { 941 | const offset = (canvas.grid.grid.options.even) ? 1:-1; 942 | var q = col - (row + offset * (row & 1)) / 2; 943 | var r = row; 944 | var s = -q - r; 945 | // if (offset !== OffsetCoord.EVEN && offset !== OffsetCoord.ODD) { 946 | // throw "offset must be EVEN (+1) or ODD (-1)"; 947 | // } 948 | return new Hex(q, r, s); 949 | } 950 | } 951 | --------------------------------------------------------------------------------