From a327c915af95b9c5f5d30713c934bae76edbfed3 Mon Sep 17 00:00:00 2001 From: JasonM Date: Sun, 5 Nov 2023 18:24:17 +1100 Subject: [PATCH] Adding hooks to allow token resizing on image change --- ModifyTokenImage/1.0.5/modifytokenimage.js | 395 +++++++++++++++++++++ ModifyTokenImage/script.json | 7 +- 2 files changed, 399 insertions(+), 3 deletions(-) create mode 100644 ModifyTokenImage/1.0.5/modifytokenimage.js diff --git a/ModifyTokenImage/1.0.5/modifytokenimage.js b/ModifyTokenImage/1.0.5/modifytokenimage.js new file mode 100644 index 0000000000..13101d3043 --- /dev/null +++ b/ModifyTokenImage/1.0.5/modifytokenimage.js @@ -0,0 +1,395 @@ +// ModifyTokenImage v1.0.5 +// By: Zachare Sylvestre +// Concept was based on ChangeTokenImage but utilizes the Handout Repository, which allows for a little more robust look for tokens. +// +// Version History: +// 1.0.4: Refactoring the GMNotes so that it works a little better with different JSON Objects that might want to use the same item. +// 1.0.5: Adding optional token size parameters to the handout gmnotes. + +var ModifyTokenImage = ModifyTokenImage || (function() { + 'use strict'; + var version = '1.0.4'; + var isGmRestricted = true; + var outputDebug = false; + + var setIsGmRestricted = function() { + isGmRestricted = !isGmRestricted; + }; + + var showHelp = function(msg) { + logDebugOutput('showHelp'); + whisperTalker(msg, '
' + + '
ModifyTokenImage v' + version + '
' + + + 'This API addon allows you to change the current token image to a specific one.' + + '
' + + 'Setup:' + + '

There must be a folder named "Token Images" located in the Journal. For each token you want to enable this functionality, it must have its own folder named under the same name as the token.

' + + 'Commands' + + '

!ModifyTokenImage --next
Sets the next Token Image for the selected token.

' + + '
'); + }; + + var getTokenImageFolder = function() { + logDebugOutput("getTokenImageFolder"); + var journalFolder = JSON.parse(Campaign().get("_journalfolder")); + var tokenImageFolder = null; + _.each(journalFolder, function(folder) { + if (folder.n == "Token Images") { + tokenImageFolder = folder; + } + }); + return tokenImageFolder; + } + + var getTokenFolder = function(name) { + logDebugOutput("getTokenFolder"); + var tokenImageFolder = getTokenImageFolder(); + if (tokenImageFolder == null) { + return null; + } + var tokenFolder = null; + _.each(tokenImageFolder.i, function (folder) { + if (folder.n == name) { + tokenFolder = folder; + } + }); + return tokenFolder; + }; + + var showCampaignFolder = function(msg) { + logDebugOutput('showCampaignFolder'); + + _.each(getTokenImageFolder, function(folder) { + var folderName = folder.n; + var folderItems = folder.i.length; + whisperTalker(msg, folderName + " Contains " + folderItems + " objects"); + }); + }; + + var currentDateOutput = function() { + var date = new Date(); + var month = date.getMonth() + 1; + var day = date.getDate(); + var hours = date.getHours(); + var minutes = date.getMinutes(); + var seconds = date.getSeconds(); + if (month < 10) { month = "0" + month; } + if (day < 10) { day = "0" + day; } + if (hours < 10) { hours = "0" + hours; } + if (minutes < 10) { minutes = "0" + minutes; } + if (seconds < 10) { seconds = "0" + seconds; } + return date.getFullYear() + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds; + }; + + var setGmNotes = function(token, gmNotesObject) { + token.set({"gmnotes": JSON.stringify(gmNotesObject)}); + }; + + var getGmNotes = function(token) { + var gmNotes = token.get("gmnotes") || "{}"; + try { + gmNotes = decodeURIComponent(gmNotes); + } catch (ex) { + //Ignore. This may fail when the token is empty. + } + if (gmNotes && gmNotes.length) { + gmNotes = gmNotes.trim(); + } + return JSON.parse(gmNotes); + }; + + var whisperTalker = function(msg, contents) { + logDebugOutput('whisperTalker'); + sendChat('ModifyTokenImage', '/w ' + msg.who + ' ' + contents); + }; + + var logDebugOutput = function(item) { + if (!outputDebug) { return; } + log('[ModifyTokenImage:DEBUG:' + currentDateOutput() + '] ' + item); + }; + + var logOutput = function(item) { + log('[ModifyTokenImage:' + currentDateOutput() + '] ' + item); + }; + + var getCleanImagesrc = function (imgsrc) { + var parts = imgsrc.match(/(.*\/images\/.*)(thumb|med|original|max)([^?]*)(\?[^?]+)?$/); + if(parts) { + return parts[1]+'thumb'+parts[3]+(parts[4]?parts[4]:`?${Math.round(Math.random()*9999999)}`); + } + return; + }; + + const getHandoutGmNotes = (handout) => { + return new Promise((resolve) => { + handout.get("gmnotes", (value)=> { + value = decodeURIComponent(value).trim().toString(); + value = value.replace(/(<([^>]+)>)/ig, ';'); + const hashTable = {}; + var valueList = value.split(';'); + for (let i = 0; i < valueList.length; i++) { + var keyval = valueList[i].split(":"); + if (keyval.length != 2) { + continue; + } + var key = keyval[0].trim(); + var val = keyval[1].trim(); + hashTable[key] = val; + } + resolve(hashTable); + }); + }); + }; + + var resizeToken = async function(token, handout) { + let gmnotesObj = await getHandoutGmNotes(handout); + let unit_pixels = 70; + var height = 1; + var width = 1; + + if (gmnotesObj) { + if ("size" in gmnotesObj) { + height = Number(gmnotesObj["size"]); + width = Number(gmnotesObj["size"]); + } else { + if ("width" in gmnotesObj) { + width = Number(gmnotesObj["width"]); + } + if ("height" in gmnotesObj) { + height = Number(gmnotesObj["height"]); + } + } + + } + + let size_params = {"height": (unit_pixels * height), + "width": (unit_pixels * width) }; + token.set(size_params); + }; + + var handleInput = function(msg) { + if (msg.type !== "api") { return; } + if (isGmRestricted && !playerIsGM(msg.playerid)) { return; } + + if (_.has(msg,'inlinerolls')) { + msg.content = _.chain(msg.inlinerolls) + .reduce(function(m,v,k){ + var ti=_.reduce(v.results.rolls,function(m2,v2){ + if(_.has(v2,'table')){ + m2.push(_.reduce(v2.results,function(m3,v3){ + m3.push(v3.tableItem.name); + return m3; + },[]).join(', ')); + } + return m2; + },[]).join(', '); + m['$[['+k+']]']= (ti.length && ti) || v.results.total || 0; + return m; + },{}) + .reduce(function(m,v,k){ + return m.replace(k,v); + },msg.content) + .value(); + } + logDebugOutput(msg.content); + + var args = msg.content + .replace(/\n/g, ' ') + .replace(/(\{\{(.*?)\}\})/g," $2 ") + .split(/\s+--/); + + var isModifyTokenImage = false; + + var first = args.shift(); + var command = null; + var setCommand = null; + + logDebugOutput('first:' + first); + switch (first) { + case '!ModifyTokenImage': + while (args.length) { + var cmds = args.shift().match(/([^\s]+[|#]'[^']+'|[^\s]+[|#]"[^"]+"|[^\s]+)/g); + + switch (cmds.shift()) { + case 'help': + showHelp(msg); + return; + case 'campaignfolder': + showCampaignFolder(msg); + return; + case 'next': + isModifyTokenImage = true; + command = 'next'; + break; + case 'previous': + command = 'previous'; + isModifyTokenImage = true; + break; + case 'set': + if (cmds.length) { + command = 'set'; + setCommand = cmds.shift(); + isModifyTokenImage = true; + } else { + showHelp(msg); + return; + } + break; + case 'gm': + if (!playerIsGM(msg.playerid)) { return; } + setIsGmRestricted(); + var allowed = "allow players to use ModifyTokenImage."; + if (!isGmRestricted) { + allowed = "restrict ModifyTokenImage commands to GMs only." + } + whisperTalker(msg, "Now set to " + allowed); + return; + case 'debug': + if (!playerIsGM(msg.playerid)) { return; } + outputDebug = !outputDebug; + var debug = "Debug output enabled."; + if (!outputDebug) { + debug = "Debug output disabled."; + } + whisperTalker(msg, debug); + return; + } + } + break; + } + if (!isModifyTokenImage) { return; } + _.chain(msg.selected) + .uniq() + .map(function(o) { return getObj('graphic', o._id); }) + .reject(_.isUndefined) + .each(function(token) { + if (!playerIsGM(msg.playerid)) { + var controlledBy = token.get("controlledby").split(","); + if (!controlledBy.includes("All") && !controlledBy.includes(msg.playerid)) { + return; + } + } + var name = token.get("name"); + var tokenFolder = getTokenFolder(name); + if (tokenFolder == null) { + whisperTalker(msg, name + " does not have a Token Folder."); + return; + } + var gmNotes = getGmNotes(token); + var current = gmNotes.ModifyTokenImage || {current:0}; + logDebugOutput("currentToken = " + current.current); + var handoutItemId = null; + switch (command) { + case 'next': + var nextHandout = null; + var loops = 0; + while (!nextHandout) { + current.current = current.current + 1; + if (current.current >= tokenFolder.i.length) { + current.current = 0; + loops = loops + 1; + } + if (loops > 2) { + whisperTalker(msg, name + " does not have any valid icons in its folder!"); + return; + } + handoutItemId = tokenFolder.i[current.current]; + nextHandout = getObj("handout", handoutItemId); + } + break; + case 'previous': + var previousHandout = null; + var loops = 0; + while (!previousHandout) { + current.current = current.current - 1; + if (current.current < 0) { + current.current = tokenFolder.i.length - 1; + loops = loops + 1; + } + if (loops > 2) { + whisperTalker(msg, name + " does not have any valid icons in its folder!"); + return; + } + handoutItemId = tokenFolder.i[current.current]; + previousHandout = getObj("handout", handoutItemId); + } + break; + case 'set': + var active = 0; + _.each(tokenFolder.i, function(itemId) { + logDebugOutput("handout: " + itemId); + var item = getObj("handout", itemId); + if (item == null) { + //Ignore this. If its not a handout, it doesn't count! + return; + } + logDebugOutput(" == " + item.get("name")); + if (item.get("name") == setCommand) { + handoutItemId = itemId; + current.current = active; + } else { + active++; + } + }); + break; + } + if (!handoutItemId) { + whisperTalker(msg, name + " did not find a valid item id to switch to!"); + return; + } + var handout = getObj("handout", handoutItemId); + if (!handout) { + whisperTalker(msg, name + " had some invalid handout error. It failed."); + return; + } + var avatar = handout.get("avatar"); + logDebugOutput("avatar = " + avatar); + if (!avatar) { + whisperTalker(msg, handout.get("name") + " handout didn't have an image set."); + return; + } + var cleanImage = getCleanImagesrc(avatar); + logDebugOutput("cleanImage = " + cleanImage); + + gmNotes.ModifyTokenImage = current; + setGmNotes(token, gmNotes); + token.set({"imgsrc": cleanImage}); + + resizeToken(token, handout); + + }); + }; + + var checkInstall = function() { + logOutput('v' + version + ' Starting Up...'); + }; + + var finishInstall = function() { + if (getTokenImageFolder() == null) { + sendChat("ModifyTokenImage", "WARNING: Startup Failed! You need a 'Token Images' Folder for this addon to work properly."); + logOutput("No 'Token Images' folder detected. Addon functions will fail until this folder is created."); + } else { + logOutput('Started up and running properly.'); + } + + } + + var registerEventHandlers = function() { + on('chat:message', handleInput); + logOutput('Event Handlers registered.'); + }; + + + return { + CheckInstall: checkInstall, + RegisterEventHandlers: registerEventHandlers, + FinishInstall: finishInstall + }; +}()); + +on("ready", function() { + 'use strict'; + ModifyTokenImage.CheckInstall(); + ModifyTokenImage.RegisterEventHandlers(); + ModifyTokenImage.FinishInstall(); +}); \ No newline at end of file diff --git a/ModifyTokenImage/script.json b/ModifyTokenImage/script.json index 3866b700e6..9d9cbd34a2 100644 --- a/ModifyTokenImage/script.json +++ b/ModifyTokenImage/script.json @@ -1,8 +1,8 @@ { "name": "Modify Token Image", "script": "modifytokenimage.js", - "version": "1.0.4", - "previousversions": ["1.0.3"], + "version": "1.0.5", + "previousversions": ["1.0.4"], "description": "Allows you to modify the token images for use in Polymorph or other reasons for changing the Token image without changing the location of the token. \r\rSETUP\rYou need a folder in your Journal named 'Token Images', and each Token that you want enabled to switch token images needs to have its own folder named after the Token. For example, if you have a Token named 'Bugbear', you need a Folder underneath 'Token Images' called 'Bugbear'. In that folder, you need handouts with each of the possible token images that the token can switch to.\r\rUSAGE\rTo utilize any of these utilities, you will use !ModifyTokenImage, with one of the following addons:\r\r!ModifyTokenImage --next\rChanges to the next token image in the list.\r\r!ModifyTokenImage --previous\rChanges to the previous token image in the list.\r\r!ModifyTokenImage --set HandoutName\rSets the token image to the specified handout name's avatar.\r\r!ModifyTokenImage --gm\rToggles if Players can utilize the next, previous and set commands on tokens they control.\r\r!ModifyTokenImage --debug\rThrows Debug information to the API Log.", "authors": "Zachare Sylvestre", "roll20userid": "3482602", @@ -12,6 +12,7 @@ "graphic.imgurl": "read,write", "handout.avatar": "read", "graphic.name": "read", - "handout.name": "read" + "handout.name": "read", + "handout.gmnotes": "read" } }