Skip to content

Commit 2dfc8d7

Browse files
authored
Update TokenHome.js
1 parent 70b1e89 commit 2dfc8d7

1 file changed

Lines changed: 85 additions & 68 deletions

File tree

TokenHome/TokenHome.js

Lines changed: 85 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -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
/<div style="display:\s*none">\s*TOKENHOME([\s\S]*?)<\/div>/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' || !/^!home\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

Comments
 (0)