@@ -6,47 +6,40 @@ on('ready', () => {
66 const STORAGE_ATTR = 'gmnotes' ;
77 const DEFAULT_LOC = 'L1' ;
88 const VALID_LAYERS = [ 'objects' , 'map' , 'gmlayer' , 'walls' ] ;
9+ const DEFAULT_RADIUS = 300 ;
910
1011 /*************************
1112 * REGEX
1213 *************************/
13-
14- // Entire hidden storage block
1514 const HOME_BLOCK_REGEX =
1615 / < d i v s t y l e = " d i s p l a y : \s * n o n e " > \s * T O K E N H O M E ( [ \s \S ] * ?) < \/ d i v > / i;
1716
18- // Individual home lines: L1:123,456,objects
1917 const HOME_LINE_REGEX =
2018 / ^ \s * ( L \d + ) \s * : \s * ( - ? \d + (?: \. \d * ) ? ) \s * , \s * ( - ? \d + (?: \. \d * ) ? ) \s * , \s * ( \w + ) \s * $ / gim;
2119
2220 /*************************
2321 * LOW-LEVEL HELPERS
2422 *************************/
25-
26- const extractLocation = ( args ) => {
27- const locArg = args . find ( a => / ^ L \d + $ / i. test ( a ) ) ;
28- return ( locArg || DEFAULT_LOC ) . toUpperCase ( ) ;
29- } ;
30-
31-
3223 const readNotes = ( token ) =>
3324 unescape ( token . get ( STORAGE_ATTR ) || '' ) ;
3425
3526 const writeNotes = ( token , text ) =>
3627 token . set ( STORAGE_ATTR , escape ( text ) ) ;
3728
29+ const distance = ( a , b ) =>
30+ Math . hypot ( a . left - b . left , a . top - b . top ) ;
31+
3832 /*************************
3933 * STORAGE
4034 *************************/
41-
4235 const getHomes = ( token ) => {
4336 const notes = readNotes ( token ) ;
4437 const match = notes . match ( HOME_BLOCK_REGEX ) ;
4538 const homes = { } ;
4639
4740 if ( ! match ) return homes ;
4841
49- HOME_LINE_REGEX . lastIndex = 0 ;
42+ HOME_LINE_REGEX . lastIndex = 0 ;
5043 let m ;
5144 while ( ( m = HOME_LINE_REGEX . exec ( match [ 1 ] ) ) !== null ) {
5245 const [ , loc , left , top , layer ] = m ;
@@ -56,20 +49,14 @@ HOME_LINE_REGEX.lastIndex = 0;
5649 layer : VALID_LAYERS . includes ( layer ) ? layer : 'objects'
5750 } ;
5851 }
59-
6052 return homes ;
6153 } ;
6254
6355 const saveHomes = ( token , homes ) => {
64- let notes = readNotes ( token ) ;
65-
66- // Strip old block entirely
67- notes = notes . replace ( HOME_BLOCK_REGEX , '' ) ;
56+ let notes = readNotes ( token ) . replace ( HOME_BLOCK_REGEX , '' ) ;
6857
6958 const lines = Object . entries ( homes )
70- . map ( ( [ loc , h ] ) =>
71- `${ loc } :${ h . left } ,${ h . top } ,${ h . layer } `
72- )
59+ . map ( ( [ loc , h ] ) => `${ loc } :${ h . left } ,${ h . top } ,${ h . layer } ` )
7360 . join ( '\n' ) ;
7461
7562 if ( ! lines . trim ( ) ) {
@@ -88,103 +75,133 @@ ${lines}
8875
8976 const setHome = ( token , loc ) => {
9077 const homes = getHomes ( token ) ;
91-
9278 homes [ loc ] = {
9379 left : token . get ( 'left' ) ,
9480 top : token . get ( 'top' ) ,
9581 layer : VALID_LAYERS . includes ( token . get ( 'layer' ) )
9682 ? token . get ( 'layer' )
9783 : 'objects'
9884 } ;
99-
10085 saveHomes ( token , homes ) ;
10186 } ;
10287
103- const getHome = ( token , loc ) => {
104- return getHomes ( token ) [ loc ] ;
105- } ;
106-
10788 /*************************
108- * PAGE HELPERS
89+ * ANCHOR + SUMMON
10990 *************************/
110-
111- const getPageForPlayer = ( playerid ) => {
112- if ( playerIsGM ( playerid ) ) {
113- return Campaign ( ) . get ( 'playerpageid' ) ;
91+ const getAnchorFromSelection = ( sel ) => {
92+ if ( ! sel || sel . length !== 1 ) return null ;
93+ const o = sel [ 0 ] ;
94+ const obj = getObj ( o . _type , o . _id ) ;
95+ if ( ! obj ) return null ;
96+
97+ if ( o . _type === 'graphic' || o . _type === 'text' ) {
98+ return { left : obj . get ( 'left' ) , top : obj . get ( 'top' ) } ;
11499 }
100+ if ( o . _type === 'pin' ) {
101+ return { left : obj . get ( 'x' ) , top : obj . get ( 'y' ) } ;
102+ }
103+ return null ;
104+ } ;
115105
116- const psp = Campaign ( ) . get ( 'playerspecificpages' ) ;
117- return psp [ playerid ] || Campaign ( ) . get ( 'playerpageid' ) ;
106+ const findClosestHome = ( homes , anchor , limitLoc ) => {
107+ let best = null ;
108+ Object . entries ( homes ) . forEach ( ( [ loc , h ] ) => {
109+ if ( limitLoc && loc !== limitLoc ) return ;
110+ const d = distance ( h , anchor ) ;
111+ if ( ! best || d < best . dist ) {
112+ best = { home : h , dist : d } ;
113+ }
114+ } ) ;
115+ return best ;
118116 } ;
119117
120118 /*************************
121- * CHAT COMMAND
119+ * PAGE
122120 *************************/
121+ const getPageForPlayer = ( playerid ) =>
122+ Campaign ( ) . get ( 'playerpageid' ) ;
123123
124+ /*************************
125+ * CHAT HANDLER
126+ *************************/
124127 on ( 'chat:message' , ( msg ) => {
125128 if ( msg . type !== 'api' || ! / ^ ! h o m e \b / i. test ( msg . content ) ) return ;
126129 if ( ! playerIsGM ( msg . playerid ) ) return ;
127130
128- const args = msg . content . split ( / \s + - - / ) . slice ( 1 ) ;
129- let sub = ( args [ 0 ] || '' ) . trim ( ) . toLowerCase ( ) ;
130-
131- // If the first argument is a location (L#), it is NOT a subcommand
132- if ( / ^ l \d + $ / i. test ( sub ) ) {
133- sub = '' ;
134- } else {
135- args . shift ( ) ;
136- }
137-
138- const loc = extractLocation ( args . concat ( sub ) ) ;
131+ const rawFlags = msg . content . split ( / \s + - - / ) . slice ( 1 ) . map ( f => f . toLowerCase ( ) ) ;
132+
133+ // Extract location FIRST
134+ let location = null ;
135+ rawFlags . forEach ( f => {
136+ if ( / ^ l \d + $ / . test ( f ) ) location = f . toUpperCase ( ) ;
137+ } ) ;
138+
139+ // Determine mode (location never counts as mode)
140+ let mode = 'recall' ;
141+ if ( rawFlags . includes ( 'set' ) ) mode = 'set' ;
142+ else if ( rawFlags . includes ( 'all' ) ) mode = 'all' ;
143+ else if ( rawFlags . includes ( 'summon' ) ) mode = 'summon' ;
144+
145+ let radius = DEFAULT_RADIUS ;
146+ rawFlags . forEach ( f => {
147+ if ( f . startsWith ( 'radius|' ) ) {
148+ const v = Number ( f . split ( '|' ) [ 1 ] ) ;
149+ if ( ! isNaN ( v ) ) radius = v ;
150+ }
151+ } ) ;
139152
140153 const pageid = getPageForPlayer ( msg . playerid ) ;
141154 const page = getObj ( 'page' , pageid ) ;
155+ if ( ! page ) return ;
142156
143157 const grid = 70 * ( page . get ( 'snapping_increment' ) || 1 ) ;
144158 const half = grid / 2 ;
145159 const maxX = page . get ( 'width' ) * grid ;
146160 const maxY = page . get ( 'height' ) * grid ;
147-
148161 const clamp = ( v , max ) => Math . max ( half , Math . min ( v , max - half ) ) ;
149162
150163 const selected = ( msg . selected || [ ] )
151164 . map ( o => getObj ( 'graphic' , o . _id ) )
152165 . filter ( Boolean ) ;
153166
154- switch ( sub ) {
167+ switch ( mode ) {
155168
156- case 'set' : {
157- selected . forEach ( t => setHome ( t , loc ) ) ;
169+ case 'set' :
170+ selected . forEach ( t => setHome ( t , location || DEFAULT_LOC ) ) ;
158171 break ;
159- }
160172
161- case 'all' : {
162- findObjs ( { type : 'graphic' , pageid } )
163- . forEach ( t => {
164- const h = getHome ( t , loc ) ;
165- if ( ! h ) return ;
173+ case 'all' :
174+ findObjs ( { type : 'graphic' , pageid } ) . forEach ( t => {
175+ const h = getHomes ( t ) [ location || DEFAULT_LOC ] ;
176+ if ( ! h ) return ;
177+ t . set ( { left : clamp ( h . left , maxX ) , top : clamp ( h . top , maxY ) , layer : h . layer } ) ;
178+ } ) ;
179+ break ;
180+
181+ case 'summon' : {
182+ const anchor = getAnchorFromSelection ( msg . selected ) ;
183+ if ( ! anchor ) return ;
166184
185+ findObjs ( { type : 'graphic' , pageid } ) . forEach ( t => {
186+ const homes = getHomes ( t ) ;
187+ const closest = findClosestHome ( homes , anchor , location ) ;
188+ if ( closest && closest . dist <= radius ) {
167189 t . set ( {
168- left : clamp ( h . left , maxX ) ,
169- top : clamp ( h . top , maxY ) ,
170- layer : h . layer
190+ left : clamp ( closest . home . left , maxX ) ,
191+ top : clamp ( closest . home . top , maxY ) ,
192+ layer : closest . home . layer
171193 } ) ;
172- } ) ;
194+ }
195+ } ) ;
173196 break ;
174197 }
175198
176- default : {
199+ default : // recall
177200 selected . forEach ( t => {
178- const h = getHome ( t , loc ) ;
201+ const h = getHomes ( t ) [ location || DEFAULT_LOC ] ;
179202 if ( ! h ) return ;
180-
181- t . set ( {
182- left : clamp ( h . left , maxX ) ,
183- top : clamp ( h . top , maxY ) ,
184- layer : h . layer
185- } ) ;
203+ t . set ( { left : clamp ( h . left , maxX ) , top : clamp ( h . top , maxY ) , layer : h . layer } ) ;
186204 } ) ;
187- }
188205 }
189206 } ) ;
190207} ) ;
0 commit comments