Skip to content

Commit 779f906

Browse files
authored
Merge pull request #2175 from keithcurtis1/master
TokenHome
2 parents 4b2c851 + e2a1094 commit 779f906

12 files changed

Lines changed: 3948 additions & 554 deletions

File tree

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
// Script: Fix Turn Order
2+
// By: Keith Curtis
3+
// Contact: https://app.roll20.net/users/162065/keithcurtis
4+
var API_Meta = API_Meta||{}; //eslint-disable-line no-var
5+
API_Meta.fixTurnOrder={offset:Number.MAX_SAFE_INTEGER,lineCount:-1};
6+
{try{throw new Error('');}catch(e){API_Meta.fixTurnOrder.offset=(parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/,'$1'),10)-6);}}
7+
8+
9+
10+
on('ready', () => {
11+
12+
const scriptName = 'Fix Turn Order';
13+
const version = '1.0.0'; //version number set here
14+
log('-=> Fix Turnorder v' + version + ' is loaded. Use !fixturnorder to scan for orphaned turns.');
15+
//1.0.0 Debut
16+
17+
18+
19+
on('chat:message', (msg) => {
20+
if (msg.type !== 'api') return;
21+
if (!playerIsGM(msg.playerid)) return;
22+
23+
24+
25+
26+
27+
/* ---------- helpers ---------- */
28+
29+
const normalizeForChat = (html) =>
30+
html.trim().replace(/\r?\n/g, '');
31+
32+
const Pictos = (char) =>
33+
`<span style="font-family:'Pictos';">${char}</span>`;
34+
35+
const getCSS = () => ({
36+
box: "background:#bababa;border:2px solid #666;border-radius:8px;padding:8px;font-size:14px;color:#222;",
37+
playerBanner: "background:#d6d6d6;border:2px solid #555;border-radius:8px;padding:6px 8px;margin-bottom:6px;line-height:24px;white-space:nowrap;",
38+
playerBannerImage: "height:24px;width:auto;vertical-align:middle;margin-right:6px;",
39+
playerBannerText: "font-size:16px;font-weight:bold;vertical-align:middle;",
40+
header: "font-weight:bold;margin-bottom:6px;",
41+
groupBox: "background:#555;border:1px solid #666;border-radius:8px;padding:6px 8px;margin:8px 0;color:#eee;",
42+
groupHeader: "font-weight:bold;margin:4px 0;color:#eee;",
43+
pageRow: "background:#d0d0d0;border:1px solid #777;border-radius:6px;padding:4px 6px;margin:4px 0;",
44+
tokenRow: "background:#e6e6e6;border:1px solid #999;border-radius:6px;padding:4px 6px;margin:3px 0;",
45+
rowItem: "color:#111;display:inline-block;vertical-align:middle;white-space:nowrap;font-weight:bold",
46+
trashButton: "font-weight:bold;display:inline-block;margin-right:6px;padding:2px 6px;background:#a44;color:#eee;text-decoration:none;border-radius:4px;font-size:14px;",
47+
tokenImage: "display:inline-block;max-height:35px;max-width:35px;border-radius:4px;margin-right:6px;vertical-align:middle;",
48+
tokenName: "font-weight:bold;color:#111;display:inline-block;vertical-align:middle;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;",
49+
footer: "margin-top:10px;text-align:right;",
50+
footerLeft: "float:left;",
51+
confirmButton: "font-weight:bold;padding:3px 8px;background:#156616;color:#eee;text-decoration:none;border-radius:4px;font-size:11px;",
52+
messageContainer: "background:#dcdcdc;border:3px solid #666;border-radius:8px;padding:8px;font-size:14px;color:#222;",
53+
messageTitle: "font-size:16px;font-weight:bold;margin-bottom:4px;",
54+
messageButton: "padding:2px 6px;background:#777;color:#eee;text-decoration:none;border-radius:4px;font-size:14px;"
55+
});
56+
57+
58+
const PLAYER_FLAG_SRC = `data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjkiIGhlaWdodD0iMzUiIHZpZXdCb3g9IjAgMCAyOSAzNSIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTI4IDM0TDE1IDI0LjRMMiAzNFYzLjZDMiAyLjcyIDIuOTc1IDIgNC4xNjY2NyAySDI1LjgzMzNDMjcuMDI1IDIgMjggMi43MiAyOCAzLjZWMzRaIiBzdHJva2U9IiMwQzBDMEMiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPHBhdGggZD0iTTI3IDMzTDE0IDIzLjRMMSAzM1YyLjZDMSAxLjcyIDEuOTc1IDEgMy4xNjY2NyAxSDI0LjgzMzNDMjYuMDI1IDEgMjcgMS43MiAyNyAyLjZWMzNaIiBmaWxsPSIjRkZCQzMzIiBzdHJva2U9IiM5OTY3MDAiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik01LjUgM0M2LjMyODQzIDMgNyAzLjQ0NzcyIDcgNEw3IDIzQzcgMjMuNTUyMyA2LjMyODQzIDI0IDUuNSAyNEM0LjY3MTU3IDI0IDQgMjMuNTUyMyA0IDIzTDQgNEM0IDMuNDQ3NzIgNC42NzE1NyAzIDUuNSAzWiIgZmlsbD0iI0ZGREQ5OSIvPgo8L3N2Zz4K`;
59+
60+
const sendHTML = (html) => {
61+
sendChat(scriptName, normalizeForChat(html), null, { noarchive: true });
62+
};
63+
64+
const sendStyledMessage = (titleOrMessage, messageOrUndefined, isPublic = false) => {
65+
const css = getCSS();
66+
let title, message;
67+
68+
if (messageOrUndefined === undefined) {
69+
title = scriptName;
70+
message = titleOrMessage;
71+
} else {
72+
title = titleOrMessage || scriptName;
73+
message = messageOrUndefined;
74+
}
75+
76+
message = String(message).replace(
77+
/\[([^\]]+)\]\(([^)]+)\)/g,
78+
(_, label, command) =>
79+
`<a href="${command}" style="${css.messageButton}">${label}</a>`
80+
);
81+
82+
const html =
83+
`<div style="${css.messageContainer}">` +
84+
`<div style="${css.messageTitle}">${title}</div>` +
85+
`${message}` +
86+
`</div>`;
87+
88+
sendChat(scriptName, `${isPublic ? '' : '/w gm '}${normalizeForChat(html)}`, null, { noarchive: true });
89+
};
90+
91+
const getPageForPlayer = (playerid) => {
92+
const player = getObj('player', playerid);
93+
if (playerIsGM(playerid)) return player.get('lastpage') || Campaign().get('playerpageid');
94+
const psp = Campaign().get('playerspecificpages');
95+
if (psp && psp[playerid]) return psp[playerid];
96+
return Campaign().get('playerpageid');
97+
};
98+
99+
/* ---------- routing ---------- */
100+
101+
const args = msg.content.trim().split(/\s+/);
102+
if (args[0] !== '!fixturnorder') return;
103+
104+
const playerPageId = Campaign().get('playerpageid');
105+
const gmPageId = getPageForPlayer(msg.playerid);
106+
107+
/* ---------- deletions ---------- */
108+
109+
if (args.length > 1) {
110+
if (gmPageId !== playerPageId) return;
111+
112+
let turnorderRaw = Campaign().get('turnorder');
113+
if (!turnorderRaw || turnorderRaw === "") {
114+
sendStyledMessage('This Turnorder looks correct.');
115+
return;
116+
}
117+
let turnorder = JSON.parse(turnorderRaw);
118+
let modified = false;
119+
120+
if (args[1] === '--clearall') {
121+
let turnorderRaw = Campaign().get('turnorder');
122+
123+
if (!turnorderRaw || turnorderRaw === "") {
124+
sendStyledMessage('Turn order is already empty.');
125+
return;
126+
}
127+
128+
Campaign().set('turnorder', "[]");
129+
sendStyledMessage('The entire Turn Tracker has been cleared.');
130+
return;
131+
}
132+
133+
134+
135+
if (args[1] === '--delete' && args[2]) {
136+
const token = getObj('graphic', args[2]);
137+
const page = token && getObj('page', token.get('pageid'));
138+
const before = turnorder.length;
139+
turnorder = turnorder.filter(e => e.id !== args[2]);
140+
modified = turnorder.length !== before;
141+
142+
if (modified && token) {
143+
sendStyledMessage(`Turn for "${token.get('name') || 'Unnamed Token'}" from page "${page ? page.get('name') : 'Unknown Page'}" was deleted.`);
144+
}
145+
}
146+
147+
if (args[1] === '--deletepage' && args[2]) {
148+
const page = getObj('page', args[2]);
149+
const before = turnorder.length;
150+
151+
turnorder = turnorder.filter(e => {
152+
if (!e.id || e.id === '-1') return true;
153+
const t = getObj('graphic', e.id);
154+
return !t || t.get('pageid') !== args[2];
155+
});
156+
157+
modified = turnorder.length !== before;
158+
159+
if (modified) {
160+
sendStyledMessage(`All turns from page "${page ? page.get('name') : 'Unknown Page'}" were deleted.`);
161+
}
162+
}
163+
164+
if (modified) Campaign().set('turnorder', JSON.stringify(turnorder));
165+
return;
166+
}
167+
168+
/* ---------- page mismatch ---------- */
169+
170+
if (gmPageId !== playerPageId) {
171+
const css = getCSS();
172+
const gmPage = getObj('page', gmPageId);
173+
const playerPage = getObj('page', playerPageId);
174+
175+
176+
sendStyledMessage(
177+
'Page Mismatch',
178+
`You are viewing the page:<br>
179+
<div style="${css.pageRow}font-weight:bold;">
180+
${(gmPage && gmPage.get('name')) || 'Unknown Page'}
181+
</div>
182+
<br>
183+
but the player ribbon is on:<br>
184+
<div style="${css.playerBanner}">
185+
<img src="${PLAYER_FLAG_SRC}" style="${css.playerBannerImage}">
186+
<span style="${css.playerBannerText}">
187+
${(playerPage && playerPage.get('name')) || 'Unknown Page'}
188+
</div>
189+
<br>
190+
Switch pages before running this command.
191+
</div>`
192+
);
193+
return;
194+
}
195+
196+
197+
/* ---------- scan + UI ---------- */
198+
199+
let turnorderRaw = Campaign().get('turnorder');
200+
if (!turnorderRaw) {
201+
sendStyledMessage('This Turnorder looks correct.');
202+
return;
203+
}
204+
205+
const turnorder = JSON.parse(turnorderRaw);
206+
const tokensByPage = {};
207+
const pageNames = {};
208+
const css = getCSS();
209+
210+
turnorder.forEach(e => {
211+
if (!e.id || e.id === '-1') return;
212+
const t = getObj('graphic', e.id);
213+
if (!t || t.get('pageid') === playerPageId) return;
214+
const pid = t.get('pageid');
215+
tokensByPage[pid] = tokensByPage[pid] || [];
216+
tokensByPage[pid].push(t);
217+
if (!pageNames[pid]) {
218+
const p = getObj('page', pid);
219+
pageNames[pid] = p ? p.get('name') : 'Unknown Page';
220+
}
221+
});
222+
223+
const pageIds = Object.keys(tokensByPage);
224+
if (!pageIds.length) {
225+
sendStyledMessage('This Turnorder looks correct.');
226+
return;
227+
}
228+
229+
const playerPage = getObj('page', playerPageId);
230+
const currentPageName = playerPage ? playerPage.get('name') : 'Unknown Page';
231+
232+
233+
234+
let html = `<div style="${css.box}">`;
235+
html += `<div style="${css.header}">This is the active player page; the turn entries below it are from other pages.</div>`;
236+
html += `<div style="${css.playerBanner}"><img src="${PLAYER_FLAG_SRC}" style="${css.playerBannerImage}"><span style="${css.playerBannerText}">${currentPageName}</span></div>`;
237+
238+
pageIds.forEach(pid => {
239+
240+
html += `<div style="${css.groupBox}">`;
241+
242+
html += `<div style="${css.groupHeader}">Delete all turns from this page:</div>`;
243+
html +=
244+
`<div style="${css.pageRow}">` +
245+
`<a href="!fixturnorder --deletepage ${pid}" style="${css.trashButton}">${Pictos('#')}</a>` +
246+
`<span style="${css.rowItem}">${pageNames[pid]}</span>` +
247+
`</div>`;
248+
249+
html += `<div style="${css.groupHeader}">Delete individual off-page turns:</div>`;
250+
251+
tokensByPage[pid].forEach(t => {
252+
html +=
253+
`<div style="${css.tokenRow}">` +
254+
`<a href="!fixturnorder --delete ${t.id}" style="${css.trashButton}">${Pictos('#')}</a>` +
255+
`<img src="${t.get('imgsrc')}" style="${css.tokenImage}">` +
256+
`<span style="${css.tokenName}">${t.get('name') || 'Unnamed Token'}</span>` +
257+
`</div>`;
258+
});
259+
260+
html += `</div>`;
261+
});
262+
263+
html +=
264+
`<div style="${css.footer}">` +
265+
`<span style="${css.footerLeft}">` +
266+
`<a href="!fixturnorder --clearall" style="${css.trashButton}">Clear all turns</a>` +
267+
`</span>` +
268+
`<a href="!fixturnorder" style="${css.confirmButton}">Check again?</a>` +
269+
`</div></div>`;
270+
271+
272+
sendHTML(html);
273+
});
274+
});
275+
276+
{try{throw new Error('');}catch(e){API_Meta.fixTurnOrder.lineCount=(parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/,'$1'),10)-API_Meta.fixTurnOrder.offset);}}

0 commit comments

Comments
 (0)