Files
2021-12-10 10:08:37 -08:00

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();
});