One of my players suggested that "it would be nice, in roll20, to have some way to track that player A would like to say/do something. The digital analogue (pun intended) of raising a hand / flag / signal." We liked the idea, and I threw together the following script. Edit: The leading comment explains the interface. When a player's hand is raised, her avatar color changes from its current value to neon green (or sometimes hunter orange). It's easy to spot people with raised hands by scanning avatars, and enthusiastic people can ping the map. Enjoy! /*
`!raise` to raise or lower your hand, and `!lower` to lower
all hands (GM only).
*/
(() => {
'use strict';
const LastUpdate = 1587557585;
const ScriptName = '!raise';
const ScriptNamespace = 'RaiseMyHand';
const ScriptRE = /^\s*!(?:raise|lower)\s*$/;
const Raise = '!raise';
const debug = false;
const Abort = function(msg){ this.message = msg; };
const abort = msg => {throw new Abort(msg);};
/* Map player IDs to original avatar colors. */
const State = (() => {
// View an object as an imperative finite map from
// strings to values.
const wrapTbl = T => {
const add = (k, v) => {T[k] = v;};
const drop = k => {delete T[k];};
const get = (k, fail) => {
const v = T[k];
return _.isUndefined(v) ? fail() : v;
};
const lookup = k => {return get(k, () => {return null;});};
// Arguments to the apper are `f(k, v)`.
const app = f => {_.each(T, (v, k) => f(k, v));};
return {add, drop, lookup, app};
};
const S = (
state[ScriptNamespace]
|| {version: 1, hands: {}}
);
state[ScriptNamespace] = S;
return {
hand: wrapTbl(S.hands),
pp: () => JSON.stringify(S)
};
})();
const HandColor = '#39FF14'; // neon green
const AltColor = '#FF7900'; // safety orange
const getPlayer = playerid => { return getObj(`player`, playerid); };
const getPlayerColor = player => { return player.get('color').toUpperCase(); };
const setPlayerColor = (player, color) => { return player.set('color', color); };
const raiseHand = playerid => {
if(debug) log(`raiseHand`);
const mcolor = State.hand.lookup(playerid);
const changed = mcolor === null;
if(changed){
const player = getPlayer(playerid);
const oldColor = getPlayerColor(player);
const newColor = (oldColor === HandColor) ? AltColor : HandColor;
State.hand.add(playerid, oldColor);
setPlayerColor(player, newColor);
if(debug)
log(` raised hand: player ${playerid}, oldColor ${oldColor}, newColor ${newColor}`);
}
return changed;
};
const lowerHand = playerid => {
if(debug) log(`lowerHand`);
const mcolor = State.hand.lookup(playerid);
const changed = mcolor !== null;
if(changed){
const player = getPlayer(playerid);
State.hand.drop(playerid);
setPlayerColor(player, mcolor);
if(debug)
log(` lowered hand: player ${playerid}, mcolor ${mcolor}`);
}
return changed;
};
const raiseOrLowerHand = playerid => {
return raiseHand(playerid) || lowerHand(playerid);
};
const lowerAllHands = () => {
if(debug) log(`lowerAllHands`);
const apper = (playerid, color) => { lowerHand(playerid); };
State.hand.app(apper);
};
const exnMessage = e => {
if(e instanceof Abort)
return e.message;
else{
return `<div>Unexpected exception</div>`+
'<div style="font-size: .6em; line-height: 1em; margin:.1em .1em .1em 1em; padding: .1em .3em; color: #666666; border: 1px solid #999999; border-radius: .2em; background-color: white;">'+
JSON.stringify(e.stack)+
'</div>';
}
};
// NB side-effects on `msg` can confuse other scripts: every
// registered chat handler sees the same `msg` object
// ([see the forum](<a href="https://app.roll20.net/forum/permalink/1404015/" rel="nofollow">https://app.roll20.net/forum/permalink/1404015/</a>)).
const onChatMessage = msg => {
if('api' !== msg.type
|| !msg.content.match(ScriptRE))
return;
const playerid = msg.playerid;
try{
if(msg.content.indexOf(Raise) !== -1)
raiseOrLowerHand(playerid);
else if(playerIsGM(playerid))
lowerAllHands();
else
abort('Only a GM can do that.');
}
catch(e){
// What's wrong with just using `msg.who`?
const player = getObj('player', playerid);
const who = player ? player.get('_displayname') : msg.who;
const error = exnMessage(e);
sendChat(ScriptName,
`/w "${who}" <div style="border: 1px solid black; background-color: #ffeeee; padding: .2em; border-radius: .4em;" >${error}</div>`);
}
};
on('ready', () => {
if(debug)
log(`state.${ScriptNamespace} = ${State.pp()}`);
on('chat:message', onChatMessage);
log(`-=> ${ScriptName} <=- [${new Date(LastUpdate*1000)}]`);
});
})();