|
| 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