I'm not aware of one that exists... ...so I wrote one! !look-at --target @{target|Who to look at|token_id} --looker @{target|Who should look|token_id} It assumes that the up direction for the graphic is where it looks from. If that's not correct, you can specify a rotation to be applied to the watching token whenever it's updated. for example, if your token by default is looking down, you could use this: !look-at --target @{target|Who to look at|token_id} --looker @{target|Who should look|token_id} --rotation 180 Code: on('ready',()=>{
const scriptName = 'LookAt';
const version = '0.1.0';
const schemaVersion = 0.1;
const lastUpdate = 1674229371;
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,
watched: {},
options: {}
};
break;
}
}
};
checkInstall();
const watch = (watchid, watcherid, rotation) => {
const s = state[scriptName];
s.watched[watchid]= s.watched[watchid] || {};
s.watched[watchid][watcherid]={...(s.watched[watchid][watcherid]||{}),rotation};
};
const showHelp = (who) => {
sendChat('',`/w "${who}" <div><code>!look-at --target [token_id] --looker [token_id] --rotation [number]</code></div>`);
};
const GetAngleTo = (target,src) => ( ((180/Math.PI)*Math.atan2(target.y-src.y, target.x-src.x))+90);
const handleChangeGraphic = (obj,prev) => {
const s = state[scriptName];
if(s.watched.hasOwnProperty(obj.id)){
Object.keys(s.watched[obj.id])
.forEach(id=>{
let t = getObj('graphic',id);
if(t){
let rotation = GetAngleTo(
{x: obj.get('left'), y: obj.get('top')},
{x: t.get('left'), y: t.get('top')}
) + s.watched[obj.id][t.id].rotation;
t.set('rotation',rotation);
} else {
delete s.watched[obj.id][id];
}
});
}
};
on('change:graphic',handleChangeGraphic);
class DoubleDashArgs {
#cmd;
#args;
constructor(line){
let p=line.split(/\s+--/);
this.#cmd = p.shift();
this.#args = [...p.map(a=>a.split(/\s+/))];
}
get cmd() {
return this.#cmd;
};
has(arg) {
return !!this.#args.find(a=>arg===a[0]);
}
params(arg) {
return [...(this.#args.find(a=>arg===a[0])||[])];
}
toObject() {
return {cmd:this.#cmd,args:this.#args};
}
}
// !look-at --target @{target|Target|token_id} --looker @{target|Looker|token_id} --rotation 90
on('chat:message',msg=>{
if('api'===msg.type && /^!look-at(\b\s|$)/i.test(msg.content) && playerIsGM(msg.playerid)){
let who = (getObj('player',msg.playerid)||{get:()=>'API'}).get('_displayname');
let DDArgs = new DoubleDashArgs(msg.content);
if(!DDArgs.has('help') && DDArgs.has('target') && DDArgs.has('looker')){
DDArgs.params('looker')
.slice(1)
.forEach( id => {
const t = getObj('graphic', DDArgs.params('target')[1]);
if(t) {
watch(
t.id,
id,
(parseFloat(DDArgs.params('rotation')[1]) || 0)
);
handleChangeGraphic(t,{});
}
});
} else {
showHelp(who);
return;
}
}
});
const cleanupWatchTable = () => {
let keys = Object.keys(state[scriptName].watched);
const burndown = () => {
let k = keys.shift();
if(k){
let o = getObj('graphic',k);
if(!o){
delete state[scriptName].watched[k];
}
setTimeout(burndown,0);
}
};
burndown();
};
cleanupWatchTable();
});