(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'
};
/**
* Gets the total Armor Class for a character.
* @param {Character} character
* @return {number}
*/
function getAC(character) {
return parseInt(getAttrByName(character.get('_id'), 'AC'));
}
/**
* Gets the passive perception for a character. It is assumed that
* Perception is the 12th skill in the character's skill list.
* @param {Character} character
* @return {number}
*/
function getPassivePerception(character) {
var skills = filterObjs(function(o) {
return o.get('type') === 'attribute' &&
o.get('characterid') === character.get('_id') &&
/repeating_skill_(-([0-9a-zA-Z\-_](?!_storage))+?|\$\d+?)_name/.test(o.get('name'));
});
var skill = _.find(skills, function(skill) {
return skill.get('current').toLowerCase().trim() === 'perception';
});
if(skill) {
var attrName = skill.get('name');
var idStart = attrName.indexOf('_', attrName.indexOf('_') + 1) + 1;
var idEnd = attrName.lastIndexOf('_');
var rowId = attrName.substring(idStart, idEnd);
var perception = getAttrByName(character.get('_id'), 'repeating_skill_' + rowId + '_passive');
return parseInt(perception);
}
else {
return undefined;
}
}
/**
* 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.
ItsATrap.announceTrap(data.effect, 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,
effect: effect
};
// Automate trap attack/save mechanics.
if(character) {
// Does the trap make an attack vs AC?
if(effect.attack) {
var ac = getAC(character);
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) {
var saveBonus = getAttrByName(character.get('_id'), saveNames[effect.save]);
saveBonus = parseInt(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);
},
/**
* 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.
var passPerc = getPassivePerception(character);
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);
})();