I found the code I was looking for, but I decided to just use it as a reference and write something new. This should handle most of what you want. It defaults to an image I had for the purpose, but you can change it by selecting a graphic and calling: !mark-start --set-marker You can also set the scale factor (defaults to 1.7): !mark-start --set-scale 1.5 And there's help: !mark-start --help MarkTurnStartLocation v0.1.0 Creates a marker to show where a token started on when its turn began. !mark-start [--set-marker] [--set-scale NUMBER] --set-marker Set the marker image to the image source of the single selected token. --set-scale NUMBER Set the scale of the marker image to the supplied NUMBER. Default is 1.7 . 1.0 makes the image the same size as the token. Code: /* global GroupInitiative */
on('ready',()=>{
const scriptName = 'MarkTurnStartLocation';
const version = '0.1.0';
const schemaVersion = 0.1;
const lastUpdate = 1612845685;
const getTurnArray = () => ( '' === Campaign().get('turnorder') ? [] : JSON.parse(Campaign().get('turnorder')));
const checkInstall = () => {
log(`-=> ${scriptName} v${version} <=- [${lastUpdate}]`);
if (
!state.hasOwnProperty(scriptName) ||
state[scriptName].version !== schemaVersion
) {
log(` > Updating Schema to v${schemaVersion} <`);
switch (state[scriptName] && state[scriptName].version) {
case 0.1:
/* break; // intentional dropthrough */ /* falls through */
case "UpdateSchemaVersion":
state[scriptName].version = schemaVersion;
break;
default:
state[scriptName] = {
version: schemaVersion,
options: {
markerImage: `<a href="https://s3.amazonaws.com/files.d20.io/images/4996490/Ii0ukmjd-IFyjKHtfRrG5w/thumb.png?1406962627`" rel="nofollow">https://s3.amazonaws.com/files.d20.io/images/4996490/Ii0ukmjd-IFyjKHtfRrG5w/thumb.png?1406962627`</a>,
scale: 1.7
}
};
break;
}
}
};
const showImg = (img) => `<img src="${img}" style="max-width: 3em;max-height: 3em;border:1px solid #333; background-color: #999; border-radius: .2em;">`;
const getCleanImgsrc = (imgsrc) => {
let 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;
};
on('chat:message',msg=>{
if('api'===msg.type && /^!mark-start(\b\s|$)/i.test(msg.content) && playerIsGM(msg.playerid)){
let who = (getObj('player',msg.playerid)||{get:()=>'API'}).get('_displayname');
let tokens = (msg.selected || [])
.map(o=>getObj('graphic',o._id))
.filter(g=>undefined !== g)
;
let msgs = [];
let args = msg.content.split(/\s+--/).slice(1);
if(0 === args.length){
args.push('help');
}
args.forEach((a)=>{
let parts = a.split(/\s+/);
switch(parts.shift().toLowerCase()){
case 'set-marker': {
if(1 === tokens.length){
let imgsrc = getCleanImgsrc(tokens[0].get('imgsrc'));
if(imgsrc){
state[scriptName].options.markerImage = imgsrc;
msgs.push(`<div><div>${showImg(imgsrc)}</div>Updated start marker.</div>`);
} else {
msgs.push(`<div><div>${showImg(imgsrc)}</div><code>Error: Cannot use Marketplace image.</code></div>`);
}
} else {
msgs.push(`<div><code>Error: Select a single token to collect the image from.</code></div>`);
}
}
break;
case 'set-scale': {
let scale = parseFloat(parts.shift())||0;
if(scale){
if(scale > 5.0){
msgs.push(`<div><code>Warning: Scale seams pretty large.</code></div>`);
} else if( scale < 0.1 ) {
msgs.push(`<div><code>Warning: Scale seams pretty small.</code></div>`);
}
state[scriptName].options.scale = scale;
msgs.push(`<div>Updated start marker scale to <code>${scale}</code>.</div>`);
} else {
msgs.push(`<div><code>Error: Scale should be a decimal number where 1 means the same size as the token, 1.5 means 50% larger, etc.</code></div>`);
}
}
break;
case 'help': {
msgs.push(`<h3>MarkTurnStartLocation v${version}</h3>`);
msgs.push(`<div>Creates a marker to show where a token started on when its turn began.</div>`);
msgs.push(`<div><code>!mark-start [--set-marker] [--set-scale NUMBER]</code></div>`);
msgs.push(`<ul><li><code>--set-marker</code> Set the marker image to the image source of the single selected token.</li><li><code>--set-scale NUMBER</code> Set the scale of the marker image to the supplied NUMBER. Default is <code>1.7</code>. <code>1.0</code> makes the image the same size as the token.</li></ul>`);
}
break;
}
});
sendChat('',`/w "${who}" <div style="border:1px solid #999;background-color: white;padding:.5em;">${msgs.join('')}</div>`);
}
if('api'===msg.type && /^!eot\b/.test(msg.content)){
setTimeout(()=>handleTurnOrderChange(Campaign(),{initiativepage:false,turnorder:JSON.stringify([{id:-1}])}),1000);
}
});
let ClearedIDs = {};
const clearMarkers = ()=>{
findObjs({
type: 'graphic',
controlledby: scriptName
}).forEach(g=>{
ClearedIDs[g.id]=true;
g.remove();
});
};
const addMarker = (tid) => {
let t = getObj('graphic',tid);
if(t){
let layer = (t.get('layer') == 'gmlayer') ? 'gmlayer' : 'map';
let props = [ 'pageid', 'left', 'top', 'width', 'height' ]
.reduce((m,p)=>({...m,[p]:t.get(p)}),{
imgsrc: state[scriptName].options.markerImage,
controlledby: scriptName,
layer
});
props.width *= state[scriptName].options.scale;
props.height *= state[scriptName].options.scale;
let m = createObj('graphic', props);
if('gmlayer' === layer){
toBack(m);
} else {
toFront(m);
}
}
};
const handleTurnOrderChange = (obj,prev) => {
let force = (obj.get('initiativepage') !== prev.initiativepage);
if(obj.get('initiativepage')){
setTimeout(()=>{
let t = getTurnArray()[0];
if(t){
let pto = (''===prev.turnorder ? [] : JSON.parse(prev.turnorder));
if(pto[0].id !== t.id || force){
clearMarkers();
addMarker(t.id);
}
}
},100);
} else {
clearMarkers();
}
};
const handleChangeGraphic = (obj,prev) => {
if(scriptName === prev.controlledby){
obj.set(prev);
}
};
const handleDestroyGraphic = (obj) => {
if(ClearedIDs[obj.id]){
setTimeout(()=>delete ClearedIDs[obj.id],1000);
} else {
let prev = JSON.parse(JSON.stringify(obj));
if(scriptName === prev.controlledby){
prev.pageid=prev._pageid;
let m = createObj('graphic',prev);
if('gmlayer' === prev.layer){
toBack(m);
} else {
toFront(m);
}
}
}
};
const registerEventHandlers = () => {
on('change:campaign:turnorder',handleTurnOrderChange);
on('change:campaign:initiativepage',handleTurnOrderChange);
on('change:graphic',handleChangeGraphic);
on('destroy:graphic',handleDestroyGraphic);
if('undefined' !== typeof GroupInitiative && GroupInitiative.ObserveTurnOrderChange){
GroupInitiative.ObserveTurnOrderChange((o,p)=>handleTurnOrderChange(Campaign(),{initiativepage: false, turnorder:p}));
}
};
checkInstall();
registerEventHandlers();
if(Campaign().get('initiativepage')){
handleTurnOrderChange(Campaign(),{initiativepage:false,turnorder:JSON.stringify([{id:-1}])});
}
});