Skip to content

Instantly share code, notes, and snippets.

@shdwjk
Forked from BaldarSilveraxe/SpeechBalloon
Last active March 24, 2023 21:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shdwjk/9cfdaa7efd4bb3dc55474e372f8f87af to your computer and use it in GitHub Desktop.
Save shdwjk/9cfdaa7efd4bb3dc55474e372f8f87af to your computer and use it in GitHub Desktop.
Roll20 SpeechBalloon
const SpeechBalloon = (() => { // eslint-disable-line no-unused-vars
const version = 0.1; // eslint-disable-line no-unused-vars
const schemaVersion = 0.4;
const defaultShowLength = 4; // seconds
const msPerSec = 1000; // for conversions.. no magic numbers!
const checkStepRate = 1000; //ms = 1 second
const checkInstall = () => {
if( ! _.has(state,'SpeechBalloon') || state.SpeechBalloon.version !== schemaVersion) {
log('SpeechBalloon: Resetting state');
/* Default Settings stored in the state. */
state.SpeechBalloon = {
version: schemaVersion,
pageGraphicMap: {},
queue: [],
validUntil: 0,
bubbleShown: false
};
}
if( ! _.has(state,'lastBalloon')) {
log('SpeechBalloon.lastBalloon: Resetting state');
/* Default Settings stored in the state. */
state.lastBalloon = false;
}
setInterval(checkBubbleDisplay,checkStepRate);
};
const bustBalloon = (pageObject) => {
if(state.SpeechBalloon.bubbleShown) {
if (typeof(pageObject) != "undefined") {
const page = pageObject.id;
const bubbleBorder = state.SpeechBalloon.pageGraphicMap[page].BorderId;
const bubbleTail = state.SpeechBalloon.pageGraphicMap[page].TailId;
const bubbleFill = state.SpeechBalloon.pageGraphicMap[page].FillId;
const bubbleText = state.SpeechBalloon.pageGraphicMap[page].TextId;
const hiddenState={
layer: "gmlayer",
width: 35,
height: 35,
left: 35,
top: 35
};
getObj("graphic" , bubbleBorder).set( hiddenState );
getObj("graphic" , bubbleTail).set( hiddenState );
getObj("graphic" , bubbleFill).set( hiddenState );
getObj("text" , bubbleText).set( hiddenState );
state.SpeechBalloon.bubbleShown=false;
}
}
};
const checkBubbleDisplay = () => {
if(state.SpeechBalloon.validUntil < Date.now() ) {
if( state.SpeechBalloon.queue.length == 0 ) {
if ( state.lastBalloon != false ) {
let page = getObj('page',state.lastBalloon.pageid);
if(page){
bustBalloon(page);
}
state.lastBalloon = false;
}
} else {
let nextBubble=state.SpeechBalloon.queue.shift();
let page = getObj('page',nextBubble.pageid);
if ( state.lastBalloon != false ) {
let lastPage = getObj('page',state.lastBalloon.pageid);
if ( lastPage && page.id != lastPage.id ) {
bustBalloon(state.lastBalloon.page);
}
}
if (page && ! page.get("archived") ) {
speechBalloon(nextBubble);
state.SpeechBalloon.validUntil = Date.now()+(nextBubble.duration*msPerSec);
state.lastBalloon = nextBubble;
state.SpeechBalloon.bubbleShown = true;
}
}
}
};
const findOrCreateBubbleParts = (thisMap,thisX,thisY) => {
const creationDefaults = {
_pageid: thisMap.id,
top: thisY,
left: thisX,
width: 70,
height: 70,
layer: "gmlayer"
};
let bubbleBorder;
let bubbleTail;
let bubbleFill;
let bubbleText;
if( _.has(state.SpeechBalloon.pageGraphicMap, thisMap.id) ) {
bubbleBorder = getObj("graphic" , state.SpeechBalloon.pageGraphicMap[thisMap.id].BorderId);
bubbleTail = getObj("graphic" , state.SpeechBalloon.pageGraphicMap[thisMap.id].TailId);
bubbleFill = getObj("graphic" , state.SpeechBalloon.pageGraphicMap[thisMap.id].FillId);
bubbleText = getObj("text" , state.SpeechBalloon.pageGraphicMap[thisMap.id].TextId);
}
if ( ! bubbleBorder) {
bubbleBorder = createObj("graphic", _.defaults({
imgsrc: "https://s3.amazonaws.com/files.d20.io/images/6565520/qJVbhBJQAw7FNDzBubKuNg/thumb.png?1417619659"
},creationDefaults));
toFront(bubbleBorder);
}
if ( ! bubbleTail) {
bubbleTail = createObj("graphic", _.defaults({
width: 140,
height: 140,
imgsrc: "https://s3.amazonaws.com/files.d20.io/images/6565493/BMPVhSPmlFaY_KyB7K8XHQ/thumb.png?1417619533"
},creationDefaults));
toFront(bubbleTail);
}
if ( ! bubbleFill) {
bubbleFill = createObj("graphic", _.defaults({
imgsrc: "https://s3.amazonaws.com/files.d20.io/images/6565524/yTHHF5NwFJcd0ddZ-9nyxg/thumb.png?1417619728"
},creationDefaults));
toFront(bubbleFill);
}
if ( ! bubbleText) {
bubbleText = createObj("text", _.defaults({
text: "DoubleBubbleBumBubblesDouble",
font_size: 16,
color: "rgb(0,0,0)",
font_family: "Courier"
},creationDefaults));
toFront(bubbleText);
}
state.SpeechBalloon.pageGraphicMap[thisMap.id]={
BorderId: bubbleBorder.id,
TailId: bubbleTail.id,
FillId: bubbleFill.id,
TextId: bubbleText.id
};
return {
bubbleTail: bubbleTail,
bubbleFill: bubbleFill,
bubbleBorder: bubbleBorder,
bubbleText: bubbleText
};
};
const speechBalloon = (nextBubble) => {
let token = getObj('graphic',nextBubble.tokenid);
let theseWords = nextBubble.says;
let whoSaid = token.get("name");
const thisMap = getObj('page',nextBubble.pageid);
const thisY = token.get("top");
const thisX = token.get("left");
if (theseWords.indexOf("--show|") != 0) {
sendChat(whoSaid, theseWords);
theseWords = wordwrap(theseWords, 28, "\n");
} else {
theseWords = theseWords.replace("--show|", "");
theseWords = theseWords.replace(/~/g, " ");
theseWords = theseWords.replace(/::/g, "\n");
}
const thisParagraph = theseWords,
lineCount = 1 + (thisParagraph.match(/\n/g)||[]).length,
approximateWidth = 286,
approximateHeight = (lineCount * 25) + 7,
xAdjust = ((thisX-(thisMap.get('width') * 35)) >=0) ? -1 : 1,
yAdjust = ((thisY-(thisMap.get('height') * 35)) >=0) ? -1 : 1,
leftTail = thisX + (105 * xAdjust),
topTail = thisY + (105 * yAdjust),
leftOffsetBubble = thisX + (210 * xAdjust),
topOffsetBubble = thisY + ((Math.floor(approximateHeight/2) + 105 < 159 ? 159 : Math.floor(approximateHeight/2) + 105) * yAdjust),
bubbleParts = findOrCreateBubbleParts(thisMap,thisX,thisY),
tailFlipH = (-1 !== xAdjust),
tailFlipV = (-1 !== yAdjust);
if (bubbleParts.bubbleBorder) {
bubbleParts.bubbleBorder.set({
layer: "map",
width: approximateWidth + 6,
height: approximateHeight + 6,
top: topOffsetBubble,
left: leftOffsetBubble
});
toFront(bubbleParts.bubbleBorder);
}
if (bubbleParts.bubbleTail) {
bubbleParts.bubbleTail.set({
layer: "map",
width: 140,
height: 140,
top: topTail,
left: leftTail,
fliph: tailFlipH,
flipv: tailFlipV
});
toFront(bubbleParts.bubbleTail);
}
if (bubbleParts.bubbleFill) {
bubbleParts.bubbleFill.set({
layer: "map",
width: approximateWidth,
height: approximateHeight,
top: topOffsetBubble,
left: leftOffsetBubble
});
toFront(bubbleParts.bubbleFill);
}
if (bubbleParts.bubbleText) {
bubbleParts.bubbleText.set({
layer: "map",
text: thisParagraph,
top: topOffsetBubble,
left: leftOffsetBubble
});
toFront(bubbleParts.bubbleText);
}
state.SpeechBalloon.bubbleShown=true;
};
const wordwrap = ( str, width, brk, cut ) => {
brk = brk || '\n';
width = width || 75;
cut = cut || false;
if (!str) { return str; }
const regex = '.{1,' +width+ '}(\\s|$)' + (cut ? '|.{' +width+ '}|.+$' : '|\\S+?(\\s|$)');
return str.match( RegExp(regex, 'g') ).join( brk );
};
const checkSelect = (obj,type) => {
if (obj === undefined || obj.length < 1) {
return false;
}
if (obj._type !== type) {
return false;
}
return true;
};
const handleInput = (msg) => {
if ( "api" !== msg.type ) {return; }
const args = msg.content.split(' ');
const obj = _.first(msg.selected);
switch(args.shift()) {
case "!makebubble":
if ( ! checkSelect(obj,"graphic") ) {return; }
state.SpeechBalloon.queue.push({
tokenid: obj._id,
pageid: getObj("graphic", obj._id).get('pageid'),
says: args.join(' '),
duration: defaultShowLength
});
return;
case "!bustBubble":
if ( ! checkSelect(obj,"graphic") ) {return; }
bustBalloon(getObj('page',getObj("graphic", obj._id).get('pageid')));
return;
}
};
const registerEventHandlers = () => {
on('chat:message', handleInput);
};
on("ready",()=>{
checkInstall();
registerEventHandlers();
});
return {
CheckInstall: checkInstall,
RegisterEventHandlers: registerEventHandlers
};
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment