(function() {
// A mapping of saving throw short names to their attribute names.
var saveNames = {
'str': 'strength_saving_throw_mod_with_sign',
'dex': 'dexterity_saving_throw_mod_with_sign',
'con': 'constitution_saving_throw_mod_with_sign',
'int': 'intelligence_saving_throw_mod_with_sign',
'wis': 'wisdom_saving_throw_mod_with_sign',
'cha': 'charisma_saving_throw_mod_with_sign'
};
// A cache mapping character names to the row number of their Perception skill.
// Ideally, the row IDs would be cached instead, since that is impervious to
// changing the order of the rows.
// In order to use row IDs, the 5E Shaped character sheet would need hidden
// attributes in each row to store their IDs.
var perceptionRowNumCache = {};
/**
* Gets the total Armor Class for a character.
* @param {Character} character
* @param {Function} callback
* The callback takes one parameter - the AC total.
*/
function getAC(character, callback) {
getSheetAttr(character, 'ac_armored_calc', function(armoredBonus) {
if(armoredBonus)
callback(armoredBonus);
else
getSheetAttr(character, ''
});
}
/**
* Gets the passive perception for a character. It is assumed that
* Perception is the 12th skill in the character's skill list.
* @param {[type]} character [description]
* @param {Function} callback [description]
* @return {[type]} [description]
*/
function getPassivePerception(character, callback) {
var charName = character.get('name');
var rowNum = perceptionRowNumCache[charName];
if(rowNum !== undefined) {
getSheetAttr(character, 'repeating_skill_$' + rowNum + '_total_with_sign', function(value) {
callback(10 + value);
});
}
else {
// There's no way at the time of this writing to get the exact number of
// rows in a repeating section. So for now 30 is used as a reasonable upper limit.
var maxSkills = 30;
count = 0;
_.each(_.range(maxSkills), function(row) {
try {
getSheetAttrText(character, 'repeating_skill_$' + row + '_name', function(skillName) {
count++;
if(skillName) {
skillName = skillName.trim().toLowerCase();
if(skillName === 'perception') {
perceptionRowNumCache[charName] = row;
// Now that we know the row number, try again.
getPassivePerception(character, callback);
}
}
else if(count === maxSkills) {
callback(undefined);
}
});
}
catch(err) {
// Do nothing. We've gone beyond the actual number of rows.
}
});
}
}
/**
* Asynchronously gets the value of a character sheet attribute.
* @param {Character} character
* @param {string} attr
* @param {Function} callback
* The callback takes one parameter: the value of the attribute.
*/
function getSheetAttr(character, attr, callback) {
try {
var rollExpr = '@{' + character.get('name') + '|' + attr + '}'
rollAsync(rollExpr, function(roll) {
if(roll)
callback(roll.total);
else {
callback(undefined);
}
});
}
catch(err) {
callback(undefined);
}
}
/**
* Asynchronously gets the value of a text attribute on a character sheet.
* @param {Character} character
* @param {string} attr
* @param {Function} callback
* The callback takes one parameter: the value of the attribute.
*/
function getSheetAttrText(character, attr, callback) {
try {
var rollExpr = '[[0 @{' + character.get('name') + '|' + attr + '}]]';
sendChat('TrapTheme', rollExpr, function(msg) {
try {
var results = msg[0].inlinerolls[0].results;
var text = _.find(results.rolls, function(roll) {
if(roll.type === 'C')
return roll.text;
});
callback(text);
}
catch(err) {
callback(undefined);
}
});
}
catch(err) {
var regex = /"text":"(.*)?"/;
var match = regex.exec(err);
if(match)
callback(match[1]);
else
callback(undefined);
}
}
/**
* Asynchronously gets the value of multiple character sheet attributes in
* parallel.
* @param {Character} character
* @param {string[]} attrList
* @param {Function} callback
* The callback takes one parameter: the value of the attribute.
*/
function getSheetAttrs(character, attrList, callback) {
var count = 0;
var values = {};
_.each(attrList, function(attr) {
getSheetAttr(character, attr, function(value) {
values[attr] = value;
count++;
if(count === attrList.length)
callback(values);
});
});
}
/**
* Produces HTML for a padded table row.
* @param {string} innerHTML
* @param {string} style
* @return {string}
*/
function htmlPaddedRow(innerHTML, style) {
return '
| ' + innerHTML + ' |
';
}
/**
* Produces HTML for a faked inline roll result.
* @param {int} result
* @param {string} expr
* @return {string}
*/
function htmlRollResult(result, expr) {
var d20 = result.rolls[0].results[0].v;
var style = 'background-color: #FEF68E; cursor: help; font-size: 1.1em; font-weight: bold; padding: 0 3px;';
if(d20 === 20)
style += 'border: 2px solid #3FB315;';
if(d20 === 1)
style += 'border: 2px solid #B31515';
return '' + result.total + '';
}
/**
* Asynchronously rolls a dice roll expression and returns the result's total in
* a callback. The result is undefined if an invalid expression is given.
* @param {string} expr
* @return {int}
*/
function rollAsync(expr, callback) {
var rollExpr = '[[' + expr + ']]';
try {
sendChat('TrapTheme', rollExpr, function(msg) {
try {
var results = msg[0].inlinerolls[0].results;
callback(results);
}
catch(err) {
callback(undefined);
}
});
}
catch(err) {
callback(undefined);
}
}
/**
* Sends an HTML-stylized message about an activated trap.
* @param {object} data
*/
function sendHtmlTrapMessage(data) {
var tableStyle = [
'background-color: #fff;',
'border: solid 1px #000;',
'border-collapse: separate;',
'border-radius: 10px;',
'overflow: hidden;',
'width: 100%;'
].join(' ');
var headerStyle = [
'background-color: #000;',
'color: #fff;',
'font-weight: bold;'
].join(' ');
var messageStyle = [
'background-color: #ccc;',
'font-style: italic;'
].join(' ');
// Start message
var msg = '';
msg += "| IT'S A TRAP!!! |
";
msg += '';
// Add the flavor message.
msg += htmlPaddedRow(data.message, messageStyle);
// Add the attack roll message.
if(data.attack) {
var rollHtml = htmlRollResult(data.attack.roll,
'1d20 + ' + data.attack.bonus);
msg += htmlPaddedRow('Attack roll: ' + rollHtml + ' vs AC ' + data.attack.ac);
}
// Add the saving throw message.
if(data.save) {
var rollHtml = htmlRollResult(data.save.roll, '1d20 + ' + data.save.bonus);
var saveMsg = '' + data.save.name.toUpperCase() + ' save: ' + rollHtml
+ ' vs DC ' + data.save.dc;
// If the save result is a secret, whisper it to the GM.
if(data.hideSave)
sendChat('Admiral Ackbar', '/w gm ' + saveMsg);
else
msg += htmlPaddedRow(saveMsg);
}
// Add the hit/miss message.
if(data.trapHit) {
var resultHtml = 'HIT! ';
if(data.damage)
resultHtml += 'Damage: [[' + data.damage + ']]';
else
resultHtml += data.character.get('name') + ' falls prey to the trap\'s effects!';
msg += htmlPaddedRow(resultHtml);
}
else {
var resultHtml = 'MISS! ';
if(data.damage && data.missHalf)
resultHtml += 'Half damage: [[floor((' + data.damage + ')/2)]].';
msg += htmlPaddedRow(resultHtml);
}
// End message.
msg += '
';
// Send the HTML message to the chat.
sendChat('Admiral Ackbar', msg);
}
/**
* A theme for the 5th Edition OGL character sheet.
* @implements ItsATrap#TrapTheme
*/
var theme = {
name: '5E-Shaped',
/**
* Display the raw message and play the effect's sound.
* @inheritdoc
*/
activateEffect: function(effect) {
var charToken = getObj('graphic', effect.victimId);
var character = getObj('character', charToken.get('represents'));
var msgData = {
character: character,
damage: effect.damage,
hideSave: effect.hideSave,
message: effect.message,
missHalf: effect.missHalf
};
// Remind the GM about the trap's effects.
if(effect.notes)
sendChat('Admiral Ackbar', '/w gm Trap Effects:
' + effect.notes);
// Automate trap attack/save mechanics.
if(character) {
// Does the trap make an attack vs AC?
if(effect.attack) {
getAC(character, function(ac) {
rollAsync('1d20 + ' + effect.attack, function(atkRoll) {
msgData.attack = {
ac: ac,
bonus: effect.attack,
roll: atkRoll
};
msgData.trapHit = atkRoll.total >= ac;
sendHtmlTrapMessage(msgData);
});
});
}
// Does the trap require a saving throw?
else if(effect.save && effect.saveDC) {
getSheetAttr(character, saveNames[effect.save], function(saveBonus) {
rollAsync('1d20 + ' + saveBonus, function(saveRoll) {
msgData.save = {
bonus: saveBonus,
dc: effect.saveDC,
name: effect.save,
roll: saveRoll
};
msgData.trapHit = saveRoll.total < effect.saveDC;
sendHtmlTrapMessage(msgData);
});
});
}
// If neither, just send the basic message.
else
sendHtmlTrapMessage(msgData);
}
else
sendHtmlTrapMessage(msgData);
// If the effect has a sound, try to play it.
ItsATrap.playEffectSound(effect);
// If the effect has an api command, execute it.
ItsATrap.executeTrapCommand(effect);
},
/**
* Display a message if the character is within 5 units of the trap.
* @inheritdoc
*/
passiveSearch: function(trap, charToken) {
var effect = ItsATrap.getTrapEffect(charToken, trap);
var character = getObj('character', charToken.get('represents'));
// Only do passive search for traps that have a spotDC.
if(effect.spotDC && character) {
// If the character's passive wisdom beats the spot DC, then
// display a message and mark the trap's trigger area.
getPassivePerception(character, function(passPerc) {
if(passPerc >= effect.spotDC) {
var noticeHtml = "IT'S A TRAP!!!
" +
character.get('name') + ' notices a trap:
' + trap.get('name')
ItsATrap.noticeTrap(trap, noticeHtml);
}
});
}
}
};
// Register the theme with ItsATrap.
var register = setInterval(function() {
if(ItsATrap) {
clearInterval(register);
ItsATrap.registerTheme(theme);
}
}, 1000);
})();