import SETTINGS from "../../common/Settings" ; function throttle < T > ( fn , threshhold ? ) { threshhold || ( threshhold = 250 ) ; let last ; let hasTimer = false ; let mostRecent ; return < any > function ( this , ... args ) { mostRecent = [ ... args ] ; const now = + new Date ; const context = this ; if ( last && now < last + threshhold ) { if ( hasTimer ) return ; hasTimer = true ; setTimeout ( function ( ) { hasTimer = false ; last = + new Date ; fn . apply ( context , mostRecent ) ; } , threshhold - ( now - last ) ) ; } else { last = now ; fn . apply ( context , mostRecent ) ; } } ; } export default class TemplateTargeting { private static toggleTemplatePatch ( enabled ) { libWrapper . unregister ( SETTINGS . MOD_NAME , 'MeasuredTemplate.prototype.highlightGrid' , false ) ; libWrapper . register ( SETTINGS . MOD_NAME , 'MeasuredTemplate.prototype.highlightGrid' , this . UPDATE_TARGETS , enabled ? 'OVERRIDE' : 'WRAPPER' ) ; } static init ( ) { SETTINGS . register ( 'template-targeting-toggle' , { config : false , scope : 'client' , type : Boolean , default : true , onChange : ( ) => { if ( SETTINGS . get ( 'template-targeting' ) !== 'toggle' ) return ; } } ) ; SETTINGS . register ( 'template-targeting' , { config : true , scope : 'world' , name : game . i18n . localize ( 'DF_QOL.TemplateTargeting.SettingName' ) , hint : 'DF_QOL.TemplateTargeting.SettingHint' , type : String , choices : { never : 'Never' , toggle : 'Toggle (Add toggle button)' , always : 'Always' } , default : 'toggle' , onChange : ( ) => { ui . controls . initialize ( ) ; ui . controls . render ( true ) ; } } ) ; SETTINGS . register ( 'template-preview' , { config : true , scope : 'world' , name : 'DF_QOL.TemplateTargeting.PreviewName' , hint : 'DF_QOL.TemplateTargeting.PreviewHint' , type : Boolean , default : true , onChange : ( newValue ) => TemplateTargeting . toggleTemplatePatch ( newValue || SETTINGS . get ( 'template-targeting-patch5e' ) ) } ) ; SETTINGS . register ( 'template-targeting-patch5e' , { name : 'DF_QOL.TemplateTargeting.Patch5e_Name' , hint : 'DF_QOL.TemplateTargeting.Patch5e_Hint' , config : true , type : Boolean , default : false , scope : 'world' , onChange : ( newValue ) => { TemplateTargeting . toggleTemplatePatch ( newValue || SETTINGS . get ( 'template-preview' ) ) ; canvas . templates ?. placeables . forEach ( ( t ) => t . draw ( ) ) ; } } ) ; SETTINGS . register ( 'template-targeting-patch5e-circle' , { name : 'DF_QOL.TemplateTargeting.Patch5e_Circle_Name' , hint : 'DF_QOL.TemplateTargeting.Patch5e_Circle_Hint' , config : true , type : Boolean , default : false , scope : 'world' , onChange : ( ) => canvas . templates ?. placeables . filter ( ( t ) => t . data . t === "circle" ) . forEach ( ( t ) => t . draw ( ) ) } ) ; libWrapper . register ( SETTINGS . MOD_NAME , 'MeasuredTemplate.prototype.highlightGrid' , this . UPDATE_TARGETS , SETTINGS . get ( 'template-targeting-patch5e' ) || SETTINGS . get ( 'template-preview' ) ? 'OVERRIDE' : 'WRAPPER' ) ; Hooks . on ( 'getSceneControlButtons' , ( controls ) => { if ( SETTINGS . get ( 'template-targeting' ) !== 'toggle' ) return ; const control = controls . find ( x => x . name === 'measure' ) ; control . tools . splice ( 0 , 0 , { icon : 'fas fa-bullseye' , name : 'autoTarget' , title : 'DF_QOL.TemplateTargeting.ToggleTitle' , visible : true , toggle : true , active : SETTINGS . get ( 'template-targeting-toggle' ) , onClick : ( toggled ) => { SETTINGS . set ( 'template-targeting-toggle' , toggled ) ; } } ) ; } ) ; libWrapper . register ( SETTINGS . MOD_NAME , 'PlaceableObject.prototype._createInteractionManager' , function ( this , wrapper ) { if ( ! ( this instanceof MeasuredTemplate ) ) return wrapper ( ) ; const manager = wrapper ( ) ; manager . callbacks . dragLeftCancel = function ( this , event ) { this . refresh ( ) ; PlaceableObject . prototype . _onDragLeftCancel . apply ( this , [ event ] ) ; } ; return manager ; } , 'WRAPPER' ) ; } static ready ( ) { const ThrottledTemplateRefresh = throttle < ( w ? : AnyFunction ) => void > ( function ( this , wrapped ) { TemplateTargeting . UPDATE_TARGETS . apply ( this , [ wrapped ] ) ; } , 1000 / 20 ) ; if ( game . dnd5e ) { libWrapper . register ( SETTINGS . MOD_NAME , 'game.dnd5e.canvas.AbilityTemplate.prototype.refresh' , function ( this , wrapper , ... args ) { ThrottledTemplateRefresh . apply ( this ) ; return wrapper ( ... args ) ; } , 'WRAPPER' ) ; } libWrapper . register ( SETTINGS . MOD_NAME , 'MeasuredTemplate.prototype.refresh' , function ( this , wrapper ) { ThrottledTemplateRefresh . apply ( this , [ null ] ) ; return wrapper ( ) ; } , 'WRAPPER' ) ; const handleTemplateCreation = function ( this , wrapper , ... args ) { canvas . grid . getHighlightLayer ( 'Template.null' ) ?. clear ( ) ; return wrapper ( ... args ) ; } ; libWrapper . register ( SETTINGS . MOD_NAME , 'TemplateLayer.prototype._onDragLeftDrop' , handleTemplateCreation , 'WRAPPER' ) ; libWrapper . register ( SETTINGS . MOD_NAME , 'TemplateLayer.prototype._onDragLeftCancel' , handleTemplateCreation , 'WRAPPER' ) ; } static UPDATE_TARGETS ( this , wrapped ? ) { const mode = SETTINGS . get < string > ( 'template-targeting' ) ; const shouldAutoSelect = mode === 'always' || ( mode === 'toggle' && SETTINGS . get < boolean > ( 'template-targeting-toggle' ) ) ; const isOwner = this . document . author . id === game . userId ; if ( isOwner && shouldAutoSelect && canvas . tokens . objects ) { for ( const t of game . user . targets ) { t . setTarget ( false , { releaseOthers : false , groupSelection : true } ) ; } } if ( ! game . dnd5e || ! SETTINGS . get ( 'template-targeting-patch5e' ) ) { if ( ! SETTINGS . get ( 'template-preview' ) ) { wrapped ?. ( ) ; } else { /************** THIS CODE IS DIRECTLY COPIED FROM 'MeasuredTemplate.prototype.highlightGrid' ****************/ const grid = canvas . grid ; const d = canvas . dimensions ; const border = < number > this . borderColor ; const color = < number > this . fillColor ; /***** START OF CODE EDIT *****/ const id = this . id ?? ( < any > this ) [ '_original' ] ?. id ; if ( ! this . shape ) return ; /****** END OF CODE EDIT ******/ const hl = grid . getHighlightLayer ( `Template.${ id ?? null }` ) ; hl . clear ( ) ; if ( grid . type === CONST . GRID_TYPES . GRIDLESS ) { const shape = this . shape . clone ( ) ; if ( "points" in shape ) { shape . points = shape . points . map ( ( p , i ) => { if ( i % 2 ) return this . y + p ; else return this . x + p ; } ) ; } else { shape . x += this . x ; shape . y += this . y ; } return grid . grid . highlightGridPosition ( hl , { border , color , shape : < any > shape } ) ; } const nr = Math . ceil ( ( ( this . data . distance * 1.5 ) / d . distance ) / ( d . size / grid . h ) ) ; const nc = Math . ceil ( ( ( this . data . distance * 1.5 ) / d . distance ) / ( d . size / grid . w ) ) ; const [ tx , ty ] = canvas . grid . getTopLeft ( this . data . x , this . data . y ) ; const [ row0 , col0 ] = grid . grid . getGridPositionFromPixels ( tx , ty ) ; const hx = canvas . grid . w / 2 ; const hy = canvas . grid . h / 2 ; const isCenter = ( this . data . x - tx === hx ) && ( this . data . y - ty === hy ) ; for ( let r = - nr ; r < nr ; r ++ ) { for ( let c = - nc ; c < nc ; c ++ ) { const [ gx , gy ] = canvas . grid . grid . getPixelsFromGridPosition ( row0 + r , col0 + c ) ; const testX = ( gx + hx ) - this . data . x ; const testY = ( gy + hy ) - this . data . y ; const contains = ( ( r === 0 ) && ( c === 0 ) && isCenter ) || this . shape . contains ( testX , testY ) ; if ( ! contains ) continue ; grid . grid . highlightGridPosition ( hl , { x : gx , y : gy , border , color } ) ; } } } if ( ! isOwner || ! shouldAutoSelect ) return ; const hx = canvas . grid . w / 2 ; const hy = canvas . grid . h / 2 ; for ( const token of canvas . tokens . placeables ) { if ( this . shape . contains ( ( token . x + hx ) - this . data . x , ( token . y + hy ) - this . data . y ) ) { token . setTarget ( true , { user : game . user , releaseOthers : false , groupSelection : true } ) ; } } } else { /************** THIS CODE IS DIRECTLY COPIED FROM 'MeasuredTemplate.prototype.highlightGrid' ****************/ const grid = canvas . grid ; const d = canvas . dimensions ; const border = < number > this . borderColor ; const color = this . fillColor ; const id = this . id ?? ( < any > this ) [ '_original' ] ?. id ; if ( ! this . shape ) return ; const hl = grid . getHighlightLayer ( `Template.${ id ?? null }` ) ; hl . clear ( ) ; if ( grid . type === CONST . GRID_TYPES . GRIDLESS ) { const shape = this . shape . clone ( ) ; if ( "points" in shape ) { shape . points = shape . points . map ( ( p , i ) => { if ( i % 2 ) return this . y + p ; else return this . x + p ; } ) ; } else { shape . x += this . x ; shape . y += this . y ; } return grid . grid . highlightGridPosition ( hl , { border , color : < any > color , shape : < any > shape } ) ; } const nr = Math . ceil ( ( ( this . data . distance * 1.5 ) / d . distance ) / ( d . size / grid . h ) ) ; const nc = Math . ceil ( ( ( this . data . distance * 1.5 ) / d . distance ) / ( d . size / grid . w ) ) ; const [ tx , ty ] = canvas . grid . getTopLeft ( this . data . x , this . data . y ) ; const [ row0 , col0 ] = grid . grid . getGridPositionFromPixels ( tx , ty ) ; const hx = canvas . grid . w / 2 ; const hy = canvas . grid . h / 2 ; const isCenter = ( this . data . x - tx === hx ) && ( this . data . y - ty === hy ) ; /***** START OF CODE EDIT *****/ let { direction , distance , angle , width } = this . data ; distance *= ( d . size / d . distance ) ; width *= ( d . size / d . distance ) ; angle = Math . toRadians ( angle ) ; direction = Math . toRadians ( direction ) ; const isRound = game . settings . get ( "core" , "coneTemplateType" ) === 'round' ; const rayLength = isRound ? distance : ( distance / Math . sin ( ( Math . PI / 2 ) - ( angle / 2 ) ) ) * Math . sin ( Math . PI / 2 ) ; let [ ax1 , ay1 , bx1 , by1 ] = [ 0 , 0 , 0 , 0 ] ; let [ ax2 , ay2 , bx2 , by2 ] = [ 0 , 0 , 0 , 0 ] ; let coneInitialized = false ; const generateConeData = ( ) => { if ( coneInitialized ) return ; coneInitialized = true ; [ ax1 , ay1 , bx1 , by1 ] = [ this . data . x , this . data . y , this . data . x + ( Math . cos ( direction - ( angle / 2 ) ) * rayLength ) , this . data . y + ( Math . sin ( direction - ( angle / 2 ) ) * rayLength ) ] ; [ ax2 , ay2 , bx2 , by2 ] = [ this . data . x , this . data . y , this . data . x + ( Math . cos ( direction + ( angle / 2 ) ) * rayLength ) , this . data . y + ( Math . sin ( direction + ( angle / 2 ) ) * rayLength ) ] ; } ; const generateRayData = ( ) => { if ( coneInitialized ) return ; [ ax1 , ay1 ] = [ this . data . x + ( Math . cos ( direction - ( Math . PI / 2 ) ) * ( width / 2 ) ) , this . data . y + ( Math . sin ( direction - ( Math . PI / 2 ) ) * ( width / 2 ) ) ] ; [ bx1 , by1 ] = [ ax1 + ( Math . cos ( direction ) * distance ) , ay1 + ( Math . sin ( direction ) * distance ) ] ; [ ax2 , ay2 ] = [ this . data . x + ( Math . cos ( direction + ( Math . PI / 2 ) ) * ( width / 2 ) ) , this . data . y + ( Math . sin ( direction + ( Math . PI / 2 ) ) * ( width / 2 ) ) ] ; [ bx2 , by2 ] = [ ax2 + ( Math . cos ( direction ) * distance ) , ay2 + ( Math . sin ( direction ) * distance ) ] ; } ; for ( let r = - nr ; r < nr ; r ++ ) { for ( let c = - nc ; c < nc ; c ++ ) { const [ gx , gy ] = canvas . grid . grid . getPixelsFromGridPosition ( row0 + r , col0 + c ) ; const testX = gx + hx ; const testY = gy + hy ; const testRect = new NormalizedRectangle ( gx , gy , canvas . grid . w , canvas . grid . h ) ; let contains = false ; switch ( this . data . t ) { case "circle" : { const [ rcx , rcy ] = [ testX - this . data . x , testY - this . data . y ] ; contains = ( ( rcx * rcx ) + ( rcy * rcy ) ) <= ( distance * distance ) ; if ( contains || ! SETTINGS . get ( 'template-targeting-patch5e-circle' ) ) break ; const sqrDistance = distance * distance ; let [ vx , vy ] = [ 0 , 0 ] ; const testPoint = ( x , y ) => { [ vx , vy ] = [ x - this . data . x , y - this . data . y ] ; return ( vx * vx + vy * vy ) < sqrDistance ; } ; contains = testPoint ( testRect . left , testRect . top ) || testPoint ( testRect . right , testRect . top ) || testPoint ( testRect . left , testRect . bottom ) || testPoint ( testRect . right , testRect . bottom ) ; break ; } case "rect" : { const rect = this . _getRectShape ( direction , distance ) ; rect . x += this . data . x ; rect . y += this . data . y ; rect . width -= 1 ; rect . height -= 1 ; contains = ! ( rect . left >= testRect . right || rect . right <= testRect . left || rect . top >= testRect . bottom || rect . bottom <= testRect . top ) ; break ; } case "cone" : { contains = ( ( r === 0 ) && ( c === 0 ) && isCenter ) || this . shape . contains ( testX - this . data . x , testY - this . data . y ) ; if ( contains ) break ; const bounds = { xMin : testRect . left , xMax : testRect . right , yMin : testRect . top , yMax : testRect . bottom } ; generateConeData ( ) ; contains = LineToBoxCollision . cohenSutherlandLineClipAndDraw ( ax1 , ay1 , bx1 , by1 , bounds ) ; if ( contains ) break ; contains = LineToBoxCollision . cohenSutherlandLineClipAndDraw ( ax2 , ay2 , bx2 , by2 , bounds ) ; if ( contains ) break ; if ( isRound ) { const sqrDistance = distance * distance ; let [ vx , vy ] = [ 0 , 0 ] ; let mag = 0 ; let vecAngle = 0 ; const testPoint = ( x , y ) => { [ vx , vy ] = [ x - this . data . x , y - this . data . y ] ; return ( vx * vx + vy * vy ) < sqrDistance ; } ; const testAngle = ( ) => { mag = Math . sqrt ( vx * vx + vy * vy ) ; vx /= mag ; vecAngle = Math . acos ( vx ) ; if ( vy < 0 ) vecAngle = ( Math . PI * 2 ) - vecAngle ; const minAngle = direction - ( angle / 2 ) ; const maxAngle = direction + ( angle / 2 ) ; if ( minAngle < 0 ) return vecAngle <= maxAngle || vecAngle >= ( ( Math . PI * 2 ) + minAngle ) ; else if ( maxAngle > Math . PI * 2 ) return vecAngle <= ( maxAngle - ( Math . PI * 2 ) ) || vecAngle >= minAngle ; else return vecAngle <= maxAngle && vecAngle >= minAngle ; } ; if ( testPoint ( testRect . left , testRect . top ) ) { contains = testAngle ( ) ; if ( contains ) break ; } if ( testPoint ( testRect . right , testRect . top ) ) { contains = testAngle ( ) ; if ( contains ) break ; } if ( testPoint ( testRect . left , testRect . bottom ) ) { contains = testAngle ( ) ; if ( contains ) break ; } if ( testPoint ( testRect . right , testRect . bottom ) ) { contains = testAngle ( ) ; } } else contains = LineToBoxCollision . cohenSutherlandLineClipAndDraw ( bx1 , by1 , bx2 , by2 , bounds ) ; break ; } case "ray" : { contains = ( ( r === 0 ) && ( c === 0 ) && isCenter ) || this . shape . contains ( testX - this . data . x , testY - this . data . y ) ; if ( contains ) break ; const bounds = { xMin : testRect . left , xMax : testRect . right , yMin : testRect . top , yMax : testRect . bottom } ; generateRayData ( ) ; contains = LineToBoxCollision . cohenSutherlandLineClipAndDraw ( ax1 , ay1 , bx1 , by1 , bounds ) ; if ( contains ) break ; contains = LineToBoxCollision . cohenSutherlandLineClipAndDraw ( ax2 , ay2 , bx2 , by2 , bounds ) ; if ( contains ) break ; contains = LineToBoxCollision . cohenSutherlandLineClipAndDraw ( ax1 , ay1 , ax2 , ay2 , bounds ) ; if ( contains ) break ; contains = LineToBoxCollision . cohenSutherlandLineClipAndDraw ( bx1 , by1 , bx2 , by2 , bounds ) ; if ( contains ) break ; break ; } } if ( ! contains ) continue ; grid . grid . highlightGridPosition ( hl , { x : gx , y : gy , border , color : < any > color } ) ; if ( ! isOwner || ! shouldAutoSelect ) continue ; for ( const token of canvas . tokens . placeables ) { if ( ! testRect . contains ( token . x + hx , token . y + hy ) ) continue ; token . setTarget ( true , { user : game . user , releaseOthers : false , groupSelection : true } ) ; } } } } } } enum OutCode { INSIDE = 0x0000 , LEFT = 0x0001 , RIGHT = 0x0010 , BOTTOM = 0x0100 , TOP = 0x1000 } /**
 * Uses the highly optimized Cohen–Sutherland algorithm for detecting if a line segment passes through a rectangle on a 2D plane.
 * https://en.wikipedia.org/wiki/Cohen%E2%80%93Sutherland_algorithm
 */ class LineToBoxCollision { private static _computeOutCode ( x , y , bounds ) { let code ; code = OutCode . INSIDE ; if ( x <= bounds . xMin ) code |= OutCode . LEFT ; else if ( x >= bounds . xMax ) code |= OutCode . RIGHT ; if ( y <= bounds . yMin ) code |= OutCode . BOTTOM ; else if ( y >= bounds . yMax ) code |= OutCode . TOP ; return code ; } static cohenSutherlandLineClipAndDraw ( x0 , y0 , x1 , y1 , bounds ) { let outcode0 = this . _computeOutCode ( x0 , y0 , bounds ) ; let outcode1 = this . _computeOutCode ( x1 , y1 , bounds ) ; let accept = false ; while ( true ) { if ( ! ( outcode0 | outcode1 ) ) { accept = true ; break ; } else if ( outcode0 & outcode1 ) { break ; } else { let [ x , y ] = [ 0 , 0 ] ; const outcodeOut = outcode1 > outcode0 ? outcode1 : outcode0 ; if ( outcodeOut & OutCode . TOP ) { x = x0 + ( x1 - x0 ) * ( bounds . yMax - y0 ) / ( y1 - y0 ) ; y = bounds . yMax - 1 ; } else if ( outcodeOut & OutCode . BOTTOM ) { x = x0 + ( x1 - x0 ) * ( bounds . yMin - y0 ) / ( y1 - y0 ) ; y = bounds . yMin + 1 ; } else if ( outcodeOut & OutCode . RIGHT ) { y = y0 + ( y1 - y0 ) * ( bounds . xMax - x0 ) / ( x1 - x0 ) ; x = bounds . xMax - 1 ; } else if ( outcodeOut & OutCode . LEFT ) { y = y0 + ( y1 - y0 ) * ( bounds . xMin - x0 ) / ( x1 - x0 ) ; x = bounds . xMin + 1 ; } if ( outcodeOut == outcode0 ) { x0 = x ; y0 = y ; outcode0 = this . _computeOutCode ( x0 , y0 , bounds ) ; } else { x1 = x ; y1 = y ; outcode1 = this . _computeOutCode ( x1 , y1 , bounds ) ; } } } return accept ; } }