mirror of
https://github.com/Nighthawk42/roll20-character-sheets.git
synced 2026-08-30 03:40:32 +00:00
408 lines
14 KiB
JavaScript
408 lines
14 KiB
JavaScript
var ConanAPI = ConanAPI || (function()
|
|
{
|
|
'use strict';
|
|
|
|
let version = '2.2',
|
|
DeadTokenImgSrc = 'https://s3.amazonaws.com/files.d20.io/images/120063604/QZmTLXMVBSqFoqENP6LTkA/thumb.png?15864718355',
|
|
WoundMarkers = ['Wounds1::1402922', 'Wounds2::1402925', 'Wounds3::1402927','Wounds4::1644476','Wounds5::1644475'],
|
|
TraumaMarkers = ['Trauma1::1644483', 'Trauma2::1644484', 'Trauma3::1644485','Trauma4::1644486','Trauma5::1644482'],
|
|
//BandagedWoundMarkers = ['BNumber1::1644478', 'BNumber2::1644479', 'BNumber3::1644480','BNumber4::1644481','BNumber5::1644482'],
|
|
BandagedWoundMarker = 'BNumber1::1644478',
|
|
combatFlashing = {delay:250, maxCntr:4},
|
|
combatFlashCntr = 0,
|
|
dyingTokenConfig = {delay:25, rotationDirection: -1, rotationValue: 10, beginningRotaton: 0},
|
|
doomTokenName = 'DoomMomentum',
|
|
|
|
getInteger = function(obj)
|
|
{
|
|
if (isInteger(obj))
|
|
return parseInt(obj);
|
|
|
|
return NaN;
|
|
},
|
|
|
|
isInteger = function(obj) {
|
|
if (isNumeric(obj))
|
|
return Number.parseFloat(obj) == Number.parseInt(obj);
|
|
|
|
return false;
|
|
},
|
|
|
|
isNumeric = function(obj) {
|
|
return !isNaN(obj - parseFloat(obj));
|
|
},
|
|
|
|
/**
|
|
* Generic token bar value adjustment
|
|
*/
|
|
updateTokenBar = function(tokenName, barName, barAdjustValue, barMin, barMax)
|
|
{
|
|
var tokens = findObjs({_subtype: 'token'});
|
|
|
|
tokens.forEach(function(token) {
|
|
if (token.get('name') == tokenName) {
|
|
var barValue = getInteger(token.get(barName));
|
|
if ( !isInteger(barValue)) barValue = 0; //forcing bar value to be an integer
|
|
barValue += barAdjustValue;
|
|
if (barValue < barMin) barValue = barMin;
|
|
if (barValue > barMax) barValue = barMax;
|
|
token.set(barName, barValue);
|
|
|
|
//spawnFx(token.get('left'),token.get('top'),'bomb-blood');
|
|
return;
|
|
}
|
|
});
|
|
},
|
|
|
|
|
|
addDegrees = function(currentRotation, degreesAdjustment) {
|
|
var newDegrees = currentRotation += degreesAdjustment;
|
|
|
|
while ((newDegrees < 0) || (newDegrees > 360)) {
|
|
if (newDegrees < 0)
|
|
newDegrees = (360 + newDegrees);
|
|
|
|
if (newDegrees > 360)
|
|
newDegrees = (newDegrees - 360);
|
|
}
|
|
|
|
return newDegrees;
|
|
},
|
|
|
|
distance = function(alpha, beta) {
|
|
var phi = Math.abs(beta - alpha) % 360; // This is either the distance or 360 - distance
|
|
var distance = phi > 180 ? 360 - phi : phi;
|
|
return distance;
|
|
},
|
|
|
|
flashCombatToken = function(token) {
|
|
if (combatFlashCntr < combatFlashing.maxCntr) {
|
|
combatFlashCntr++;
|
|
token.set('fliph', !token.get('fliph'));
|
|
setTimeout(flashCombatToken, combatFlashing.delay, token);
|
|
return;
|
|
}
|
|
combatFlashCntr = 0;
|
|
},
|
|
|
|
|
|
startCombatToken = function(whosToken) {
|
|
var tokens = findObjs({_subtype: 'token'});
|
|
tokens.forEach(function(token) {
|
|
if (token.get('name') == whosToken) {
|
|
setTimeout(flashCombatToken, combatFlashing.delay, token);
|
|
}
|
|
});
|
|
|
|
},
|
|
|
|
|
|
addDeadToken = function(token) {
|
|
toFront(createObj('graphic', {
|
|
_subtype: 'token',
|
|
_pageid: token.get('_pageid'),
|
|
left: token.get('left'),
|
|
top: token.get('top'),
|
|
width: token.get('width'),
|
|
height: token.get('height'),
|
|
layer: 'objects',
|
|
imgsrc: DeadTokenImgSrc,
|
|
name: 'DeadToken'
|
|
}));
|
|
},
|
|
|
|
|
|
killToken = function(token) {
|
|
var rotation = token.get('rotation');
|
|
|
|
rotation = addDegrees(rotation, (dyingTokenConfig.rotationValue * dyingTokenConfig.rotationDirection));
|
|
|
|
if (dyingTokenConfig.rotationDirection < 0) {
|
|
if (distance(dyingTokenConfig.beginningRotaton, rotation) >= 90) {
|
|
rotation = addDegrees(dyingTokenConfig.beginningRotaton, -90);
|
|
dyingTokenConfig.rotationDirection = 1;
|
|
}
|
|
}
|
|
else {
|
|
if (distance(dyingTokenConfig.beginningRotaton, rotation) >= 90) {
|
|
rotation = addDegrees(dyingTokenConfig.beginningRotaton, 90);
|
|
token.set('rotation', rotation);
|
|
dyingTokenConfig.rotationDirection = -1;
|
|
addDeadToken(token);
|
|
return; //dying animation complete
|
|
}
|
|
}
|
|
|
|
token.set('rotation', rotation);
|
|
setTimeout(killToken, dyingTokenConfig.delay, token);
|
|
},
|
|
|
|
startKillToken = function(selectedObect) {
|
|
var token = getObj('graphic', selectedObect._id);
|
|
dyingTokenConfig.beginningRotaton = token.get('rotation') + 0; //force number
|
|
killToken(token);
|
|
},
|
|
|
|
|
|
setTokenStatus = function(token, attrName, attrValue) {
|
|
var statusMarkers = token.get('statusmarkers');
|
|
attrValue--; //adjust for zero offset array; if adjusted to -1, then no marker (zero)
|
|
|
|
if (attrName == 'Wounds') {
|
|
WoundMarkers.forEach(function(item){
|
|
statusMarkers = statusMarkers.replace(new RegExp(item, 'gi'),'');
|
|
});
|
|
if (attrValue > -1 && attrValue <= WoundMarkers.length)
|
|
statusMarkers += ',' + WoundMarkers[attrValue];
|
|
}
|
|
|
|
if (attrName == 'Trauma') {
|
|
TraumaMarkers.forEach(function(item){
|
|
statusMarkers = statusMarkers.replace(new RegExp(item, 'gi'),'');
|
|
});
|
|
if (attrValue > -1 && attrValue <= TraumaMarkers.length)
|
|
statusMarkers += ',' + TraumaMarkers[attrValue];
|
|
}
|
|
|
|
//convert to array, remove empty elements, and set back to status markers on token
|
|
var newMarkers = statusMarkers.split(',')
|
|
.filter(function(marker){ return marker.trim() > ''; })
|
|
.join();
|
|
|
|
token.set('statusmarkers', newMarkers);
|
|
},
|
|
|
|
// on modify character sheet attribute
|
|
// attr_after parm is roll20 attribute object. The attr_before is plain javascript object
|
|
handleChangeCharAttribute = function(attrAfter, attrBefore ) {
|
|
var attrName = attrAfter.get('name');
|
|
var attrValue = attrAfter.get('current');
|
|
var charId = attrAfter.get('_characterid');
|
|
var character = getObj('character', charId);
|
|
var charName = character.get('name');
|
|
|
|
if (attrName.toLowerCase() == 'vigor' ||
|
|
attrName.toLowerCase() == 'wounds' ||
|
|
attrName.toLowerCase() == 'resolve' ||
|
|
attrName.toLowerCase() == 'trauma' ||
|
|
attrName.toLowerCase() == 'fatigue' ||
|
|
attrName.toLowerCase() == 'despair' ||
|
|
attrName.toLowerCase() == 'fortune')
|
|
sendChat(charName,"&{template:conan-default} {{title=" + charName + "'s Attribute Changed}} {{" + attrName+" from " + attrBefore.current + " to " + attrValue + "}}");
|
|
|
|
if (attrName == 'Wounds' || attrName == 'Trauma') {
|
|
//find token and update status markers
|
|
var tokens = findObjs({_subtype: 'token'});
|
|
tokens.forEach(function(token) {
|
|
if (token.get('represents') == charId) {
|
|
setTokenStatus(token, attrName, attrValue);
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
|
|
// on modify graphic
|
|
handleChangeGraphic = function(currentObj, prevObj) {
|
|
//obj is a roll20 object with built in functions
|
|
//prev is plain javascript object
|
|
var currentName = currentObj.get('name');
|
|
|
|
if (currentName == 'DeadToken') {
|
|
var prevName = prevObj.name;
|
|
var currentPoistion = { left: currentObj.get('left'), top: currentObj.get('top') }
|
|
var prevPoistion = { left: prevObj.left, top: prevObj.top }
|
|
|
|
//find token in prev position ... this would be token under the Dead Token icon
|
|
var deadTokens = findObjs({ left: prevObj.left, top: prevObj.top});
|
|
//log('Deadtoken count:' + String(deadTokens.length));
|
|
deadTokens.forEach(function(token) {
|
|
//log('found token:' + token.get('name'));
|
|
token.set('left', currentPoistion.left);
|
|
token.set('top', currentPoistion.top);
|
|
});
|
|
}
|
|
},
|
|
|
|
|
|
/**
|
|
* Get value on right side of '=' in
|
|
* found array element.
|
|
*
|
|
* a: array
|
|
* v: search value
|
|
* d: default retun value when not found
|
|
*/
|
|
findInArray = function(a, v, d)
|
|
{
|
|
let r = a.find(function(e){return e.indexOf(v+'=')>-1 ? true:false;});
|
|
//sendChat("findInArray"," "+r+"\na:"+a+"\nv:"+v+"\nd:"+d);
|
|
if(r==undefined)
|
|
return d;
|
|
|
|
//if value is an inlineroll (starts with $[[) pick out the index value. Ie successes1=$[[0]]; return 0
|
|
|
|
return r.split("=")[1].replace("$[[","").replace("]]","");
|
|
},
|
|
|
|
|
|
/**
|
|
* Handle chat events
|
|
*
|
|
* @param {object} msg
|
|
*/
|
|
handleChatMessage = function(msg)
|
|
{
|
|
let content = msg.content;
|
|
|
|
if(msg.type == "api") {
|
|
|
|
if(content.indexOf("!killtoken") > -1) {
|
|
if(msg.selected && msg.selected.length > 0)
|
|
startKillToken(msg.selected[0]);
|
|
}
|
|
|
|
if(content.indexOf("!bandagetoken") > -1) {
|
|
if(msg.selected && msg.selected.length > 0) {
|
|
var token = getObj('graphic', msg.selected[0]._id);
|
|
var markers = token.get('statusmarkers') + ',' + BandagedWoundMarker;
|
|
token.set('statusmarkers', markers);
|
|
}
|
|
}
|
|
|
|
if (content.indexOf("!dmd+") > -1)
|
|
updateTokenBar(doomTokenName, 'bar3_value', 1, 0, 30);
|
|
|
|
if(content.indexOf("!dmd-") > -1)
|
|
updateTokenBar(doomTokenName, 'bar3_value', -1, 0, 30);
|
|
|
|
if(content.indexOf("!dmm+") > -1)
|
|
updateTokenBar(doomTokenName, 'bar1_value', 1, 0, 6);
|
|
|
|
if(content.indexOf("!dmm-") > -1)
|
|
updateTokenBar(doomTokenName, 'bar1_value', -1, 0, 6);
|
|
|
|
}
|
|
else {
|
|
if(msg.rolltemplate == "skill" && msg.inlinerolls) {
|
|
|
|
/*
|
|
If using the API script, this will capture the skill roll and resend it using the
|
|
skillapi role-template. This will break out the success/complications.
|
|
*/
|
|
|
|
//decode the roll into its actual values; snippet from The Arron
|
|
var cmsg = _.clone(msg);
|
|
if(_.has(cmsg,'inlinerolls')){
|
|
cmsg.content = _.chain(cmsg.inlinerolls)
|
|
.reduce(function(m,v,k){
|
|
m['$[['+k+']]']=v.results.total || 0;
|
|
return m;
|
|
},{})
|
|
.reduce(function(m,v,k){
|
|
return m.replace(k,v);
|
|
},cmsg.content)
|
|
.value();
|
|
}
|
|
|
|
let a = content.replace(/{{/g,"").replace(/}} /g,"}}").split("}}");
|
|
let aclone = cmsg.content.replace(/{{/g,"").replace(/}} /g,"}}").split("}}");
|
|
//sendChat("aclone", "aclone"+aclone);
|
|
|
|
//get these values from content
|
|
let skill_name = findInArray(a,"skill_name","");
|
|
let character_name = findInArray(a,"character_name","");
|
|
let wounds = findInArray(a,"wounds",0);
|
|
let wounds_treated = findInArray(a,"wounds_treated",0);
|
|
let trauma = findInArray(a,"trauma",0);
|
|
let trauma_treated = findInArray(a,"trauma_treated",0);
|
|
let expertise = findInArray(a,"expertise",0);
|
|
let skilled_roll_index = findInArray(a,"successes1",0);
|
|
let unskilled_roll_index = findInArray(a,"successes2",0);
|
|
let show_tn = findInArray(aclone,"show_tn",0);
|
|
|
|
//get these actual roll values from the converted clone
|
|
let expertise_chk = findInArray(aclone,"expertise_chk",0);
|
|
let wounds_chk = findInArray(aclone,"wounds_chk",0);
|
|
let trauma_chk = findInArray(aclone,"trauma_chk",-1); //use this to determine if physical of mental skill
|
|
let encumbered_chk = findInArray(aclone,"encumbered",0);
|
|
let num_dice = findInArray(aclone,"num_dice",0);
|
|
let target_number = findInArray(aclone,"target_number",0);
|
|
let bk_nbr = findInArray(aclone,"bk_nbr",3);
|
|
let api_chk = findInArray(aclone,"api_chk",1);
|
|
|
|
let fail_count = libInline.getDice(msg.inlinerolls[0], 'fumble').length;
|
|
let crit_count = libInline.getDice(msg.inlinerolls[0], 'crit').length;
|
|
let total_successes = getInteger(findInArray(aclone,"successes1",0));
|
|
|
|
if(expertise_chk == 0){
|
|
//use unskilled roll (successes2)
|
|
total_successes = getInteger(findInArray(aclone,"successes2",0));
|
|
fail_count = 0 + libInline.getDice(msg.inlinerolls[1], 'fumble').length;
|
|
crit_count = 0 + libInline.getDice(msg.inlinerolls[1], 'crit').length;
|
|
}
|
|
|
|
total_successes += crit_count;
|
|
|
|
let roll_msg = "&{template:skillapi} {{skill_name="+skill_name+"}} {{character_name="+character_name+"}} {{successes1="+libInline.getRollTip(msg.inlinerolls[skilled_roll_index])+"}}{{successes2="+libInline.getRollTip(msg.inlinerolls[unskilled_roll_index])+"}}{{num_dice="+num_dice+"}} {{target_number="+target_number+"}}{{expertise_chk=[["+expertise_chk+"]]}}{{expertise="+expertise+"}}{{crit_chk=[["+crit_count+"]]}}{{fail_chk=[["+fail_count+"]]}}{{crit_count="+crit_count+"}}{{fail_count="+fail_count+"}}{{total_successes="+total_successes+"}}{{api_chk=[[1]]}}{{bk_nbr=[["+bk_nbr+"]]}}";
|
|
|
|
if (show_tn > 0)
|
|
roll_msg += "{{show_tn="+show_tn+"}}";
|
|
|
|
if (trauma_chk > -1)
|
|
//tramuma based roll
|
|
roll_msg += "{{trauma_chk=[["+trauma_chk+"]]}}{{trauma="+trauma+"}}{{trauma_treated="+trauma_treated+"}}";
|
|
else
|
|
//physcical based roll
|
|
roll_msg += "{{wounds_chk=[["+wounds_chk+"]]}}{{wounds="+wounds+"}}{{wounds_treated="+wounds_treated+"}}{{encumbered=[["+encumbered_chk+"]]}}";
|
|
|
|
if (api_chk > 0) //only sending to skillapi template if chk value greater than zero
|
|
sendChat(msg.who, roll_msg);
|
|
|
|
}
|
|
|
|
if (msg.rolltemplate == "skill" && content.indexOf("skill_name=Melee") > -1) {
|
|
startCombatToken(msg.who);
|
|
}
|
|
|
|
//Checking the skillapi call to see if there were multiple complications.
|
|
//Doing a sendChat based on skillapi and not skill will enforce that the conan double trouble chat display
|
|
//follows the initial roll display.
|
|
if(msg.rolltemplate == "skillapi" && msg.inlinerolls) {
|
|
let a = content.replace(/{{/g,"").replace(/}} /g,"}}").split("}}");
|
|
let fail_count = findInArray(a,"fail_count",0);
|
|
if(fail_count > 1)
|
|
sendChat("Conan","&{template:conan-default} {{title=DOUBLE TROUBLE !!!}} {{subtitle1="+msg.who+" rolled MULTIPLE complications!!!}}");
|
|
}
|
|
|
|
}
|
|
},
|
|
|
|
|
|
init = function()
|
|
{
|
|
log('Starting ConanAPI v' + version);
|
|
sendChat('Conan', 'Starting Conan APIs v' + version);
|
|
|
|
on('change:graphic', handleChangeGraphic);
|
|
on('change:attribute', handleChangeCharAttribute);
|
|
on("chat:message", handleChatMessage);
|
|
|
|
};
|
|
|
|
return {
|
|
init: init
|
|
};
|
|
}());
|
|
|
|
/**
|
|
* Fires when the page has loaded.
|
|
*/
|
|
on("ready", function()
|
|
{
|
|
'use strict';
|
|
|
|
ConanAPI.init();
|
|
});
|
|
|