Skip to content

Commit 39c6aed

Browse files
author
SteveKoontz
authored
Merge pull request Roll20#380 from Cazra/TrapThemeMLP3.1
Trap Theme: Roleplaying Is Magic 3.1
2 parents a1ec023 + 01fbfb8 commit 39c6aed

File tree

3 files changed

+382
-2
lines changed

3 files changed

+382
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,377 @@
1+
(() => {
2+
'use strict';
3+
4+
// A cache mapping character names to the row number of their Perception skill.
5+
// Ideally, the row IDs would be cached instead, since that is impervious to
6+
// changing the order of the rows.
7+
// In order to use row IDs, the 5E Shaped character sheet would need hidden
8+
// attributes in each row to store their IDs.
9+
let perceptionRowNumCache = {};
10+
11+
const CHAT_NAME = 'ItsATrap-RiM4';
12+
13+
const THEME_CSS = {
14+
'bold': {
15+
'font-weight': 'bold'
16+
},
17+
'critFail': {
18+
'border': '2px solid #B31515'
19+
},
20+
'critSuccess': {
21+
'border': '2px solid #3FB315'
22+
},
23+
'hit': {
24+
'color': '#f00',
25+
'font-weight': 'bold'
26+
},
27+
'miss': {
28+
'color': '#620',
29+
'font-weight': 'bold'
30+
},
31+
'paddedRow': {
32+
'padding': '1px 1em'
33+
},
34+
'rollResult': {
35+
'background-color': '#FEF68E',
36+
'cursor': 'help',
37+
'font-size': '1.1em',
38+
'font-weight': 'bold',
39+
'padding': '0 3px'
40+
},
41+
'skillNotes': {
42+
'font-size': '0.8em',
43+
'font-style': 'italic'
44+
},
45+
'trapMessage': {
46+
'background-color': '#ccc',
47+
'font-style': 'italic'
48+
},
49+
'trapTable': {
50+
'background-color': '#fff',
51+
'border': 'solid 1px #000',
52+
'border-collapse': 'separate',
53+
'border-radius': '10px',
54+
'overflow': 'hidden',
55+
'width': '100%'
56+
},
57+
'trapTableHead': {
58+
'background-color': '#000',
59+
'color': '#fff',
60+
'font-weight': 'bold'
61+
}
62+
};
63+
64+
/**
65+
* Gets the passive perception for a character. It is assumed that
66+
* Perception is the 12th skill in the character's skill list.
67+
* @param {Character} character
68+
*/
69+
function getPassivePerception(character) {
70+
let charName = character.get('name');
71+
72+
let mind = parseInt(findObjs({
73+
_type: 'attribute',
74+
_characterid: character.get('_id'),
75+
name: 'mind'
76+
})[0].get('current'));
77+
78+
let skill = RiM4Dice.getSkill(character, 'perception');
79+
let result = 4 + mind;
80+
if(skill) {
81+
if(skill.trained)
82+
result += 3;
83+
if(skill.improved)
84+
result += 2;
85+
if(skill.greater)
86+
result += 1;
87+
result += (skill.misc || 0) + (skill.advDis || 0);
88+
}
89+
return result;
90+
}
91+
92+
/**
93+
* Produces HTML for a faked inline roll result.
94+
* @param {int} result
95+
* @param {string} tooltip
96+
* @return {HtmlBuilder}
97+
*/
98+
function htmlRollResult(result, tooltip) {
99+
return new HtmlBuilder('span.rollResult', result.total, {
100+
title: tooltip
101+
});
102+
}
103+
104+
/**
105+
* Sends an HTML-stylized message about a noticed trap.
106+
* @param {(HtmlBuilder|string)} content
107+
* @param {string} borderColor
108+
*/
109+
function htmlTable(content, borderColor) {
110+
let table = new HtmlBuilder('table.trapTable', '', {
111+
style: { 'border-color': borderColor }
112+
});
113+
table.append('thead.trapTableHead', '', {
114+
style: { 'background-color': borderColor }
115+
}).append('th', 'IT\'S A TRAP!!!');
116+
table.append('tbody').append('tr').append('td', content, {
117+
style: { 'padding': '0' }
118+
});
119+
return table;
120+
}
121+
122+
/**
123+
* Displays the message to notice a trap.
124+
* @param {Character} character
125+
* @param {Graphic} trap
126+
*/
127+
function noticeTrap(character, trap) {
128+
let content = new HtmlBuilder();
129+
content.append('.paddedRow trapMessage', character.get('name') + ' notices a trap:');
130+
content.append('.paddedRow', trap.get('name'));
131+
132+
ItsATrap.noticeTrap(trap, htmlTable(content, '#000').toString(THEME_CSS));
133+
}
134+
135+
/**
136+
* A Promise wrapper for RiM4Dice.rollSkillCheck.
137+
* @param {Character} character
138+
* @param {string} skillName
139+
* @return {Promise<int, str>}
140+
*/
141+
function rollSkillCheck(character, skillName) {
142+
return new Promise((resolve, reject) => {
143+
RiM4Dice.rollSkillCheck(character, skillName, (skillRoll, expr) => {
144+
resolve([skillRoll, expr]);
145+
});
146+
});
147+
}
148+
149+
/**
150+
* Sends an HTML-stylized message about an activated trap.
151+
* @param {TrapEffect} effect
152+
* @param {object} effectResult
153+
*/
154+
function sendHtmlTrapMessage(effect, effectResult) {
155+
let content = new HtmlBuilder('div');
156+
157+
// Add the flavor message.
158+
content.append('.paddedRow trapMessage', effect.message);
159+
160+
// Add message for who triggered it.
161+
if(effectResult.character) {
162+
let row = content.append('.paddedRow');
163+
row.append('span.bold', 'Target: ');
164+
row.append('span', effectResult.character.get('name'));
165+
}
166+
167+
// Add the skill check message.
168+
if(effectResult.character && effectResult.skill) {
169+
let rollHtml = htmlRollResult(effectResult.skill.roll, effectResult.skill.rollExpr);
170+
let row = content.append('.paddedRow');
171+
row.append('span.bold', effectResult.skill.name.toUpperCase() + ' check: ');
172+
row.append('span', rollHtml);
173+
row.append('span', ' vs Dif ' + effectResult.skill.dif);
174+
175+
// Add skill notes.
176+
let skill = RiM4Dice.getSkill(effectResult.character, effectResult.skill.name);
177+
if(skill && skill.notes)
178+
content.append('.paddedRow').append('span.skillNotes', skill.notes);
179+
180+
// Add the hit/miss message.
181+
if(effectResult.trapHit) {
182+
let row = content.append('.paddedRow');
183+
row.append('span.hit', 'HIT! ');
184+
if(effectResult.damage)
185+
row.append('span', 'Damage: [[' + effectResult.damage + ']]');
186+
else
187+
row.append('span', effectResult.character.get('name') + ' falls prey to the trap\'s effects!');
188+
}
189+
else {
190+
let row = content.append('.paddedRow');
191+
row.append('span.miss', 'MISS! ');
192+
if(effectResult.damage && effectResult.missHalf)
193+
row.append('span', 'Half damage: [[floor((' + effectResult.damage + ')/2)]].');
194+
}
195+
}
196+
197+
// Send the HTML message to the chat.
198+
effect.announce(htmlTable(content, '#a22').toString(THEME_CSS));
199+
}
200+
201+
// Register the theme with ItsATrap.
202+
on('ready', () => {
203+
/**
204+
* A theme for the Roleplaying is Magic 4 character sheet.
205+
*/
206+
class TrapThemeMLPRIM4 extends TrapTheme {
207+
get name() {
208+
return 'MLP-RIM-4';
209+
}
210+
211+
/**
212+
* Display the raw message and play the effect's sound.
213+
* @inheritdoc
214+
*/
215+
activateEffect(effect) {
216+
let character = getObj('character', effect.victim.get('represents'));
217+
let effectResult = effect.json;
218+
219+
effectResult.character = character;
220+
221+
// Automate trap attack/save mechanics.
222+
Promise.resolve()
223+
.then(() => {
224+
if(character && effectResult.skill) {
225+
let attr = findObjs({
226+
_type: 'attribute',
227+
_characterid: character.get('_id'),
228+
name: effectResult.skill.attr
229+
})[0].get('current');
230+
231+
return rollSkillCheck(character, effectResult.skill.name)
232+
.then(tuple => {
233+
let skillRoll = tuple[0];
234+
let expr = tuple[1];
235+
236+
if(skillRoll) {
237+
effectResult.skill.roll = skillRoll;
238+
effectResult.skill.rollExpr = expr;
239+
effectResult.trapHit = skillRoll.total < effectResult.skill.dif;
240+
return effectResult;
241+
}
242+
else {
243+
// default to primary attribute.
244+
let rollExpr = '2d6 + ' + attr + ' [' + effectResult.skill.attr + ']';
245+
return TrapTheme.rollAsync('2d6 + ' + attr + ' [' + effectResult.skill.attr + ']')
246+
.then(attrRoll => {
247+
effectResult.skill.roll = attrRoll;
248+
effectResult.skill.rollExpr = rollExpr;
249+
effectResult.trapHit = attrRoll.total < effectResult.skill.dif;
250+
return effectResult;
251+
});
252+
}
253+
});
254+
}
255+
else
256+
return effectResult;
257+
})
258+
.then(effectResult => {
259+
sendHtmlTrapMessage(effect, effectResult);
260+
})
261+
.catch(err => {
262+
sendChat(CHAT_NAME, '/w gm ' + err.message);
263+
});
264+
}
265+
266+
/**
267+
* @inheritdoc
268+
*/
269+
getThemeProperties(trapToken) {
270+
let trapEffect = (new TrapEffect(trapToken)).json;
271+
return [
272+
{
273+
id: 'damage',
274+
name: 'Damage',
275+
desc: `The dice roll expression for the trap's damage.`,
276+
value: trapEffect.damage
277+
},
278+
{
279+
id: 'missHalf',
280+
name: 'Miss - Half Damage',
281+
desc: 'Does the trap deal half damage on a miss?',
282+
value: trapEffect.missHalf ? 'yes' : 'no',
283+
options: ['yes', 'no']
284+
},
285+
{
286+
id: 'skill',
287+
name: 'Skill Check',
288+
desc: `The skill check for avoiding the trap's effects.`,
289+
value: (() => {
290+
let skill = trapEffect.skill;
291+
if(skill) {
292+
return `${skill.name}(${skill.attr}, Dif: ${skill.dif})`;
293+
}
294+
else
295+
return '';
296+
})(),
297+
properties: [
298+
{
299+
id: 'name',
300+
name: 'Skill Name',
301+
desc: 'The name of the skill for the skill check.'
302+
},
303+
{
304+
id: 'attr',
305+
name: 'Primary Attribute',
306+
desc: 'The primary attribute the skill is based on.'
307+
},
308+
{
309+
id: 'dif',
310+
name: 'Difficulty',
311+
desc: 'The difficulty of the skill check.'
312+
}
313+
]
314+
},
315+
{
316+
id: 'spotDif',
317+
name: 'Spot Difficulty',
318+
desc: 'The Perception check difficulty to spot the trap.',
319+
value: trapEffect.spotDif
320+
}
321+
];
322+
}
323+
324+
/**
325+
* @inheritdoc
326+
*/
327+
modifyTrapProperty(trapToken, argv) {
328+
let trapEffect = (new TrapEffect(trapToken)).json;
329+
330+
let prop = argv[0];
331+
let params = argv.slice(1);
332+
333+
if(prop === 'damage')
334+
trapEffect.damage = params[0];
335+
if(prop === 'missHalf')
336+
trapEffect.missHalf = params[0] === 'yes';
337+
if(prop === 'skill') {
338+
trapEffect.skill = {};
339+
trapEffect.skill.name = params[0];
340+
trapEffect.skill.attr = params[1];
341+
trapEffect.skill.dif = params[2];
342+
}
343+
if(prop === 'spotDif')
344+
trapEffect.spotDif = parseInt(params[0]);
345+
346+
trapToken.set('gmnotes', JSON.stringify(trapEffect));
347+
}
348+
349+
/**
350+
* @inheritdoc
351+
*/
352+
passiveSearch(trap, charToken) {
353+
let effect = new TrapEffect(trap, charToken);
354+
let character = getObj('character', charToken.get('represents'));
355+
356+
// Only do passive search for traps that have a spotDC.
357+
if(effect.json.spotDif && character) {
358+
Promise.resolve()
359+
.then(() => {
360+
let passPerception = getPassivePerception(character);
361+
return (passPerception >= effect.json.spotDif);
362+
})
363+
.then(passed => {
364+
if(passed)
365+
noticeTrap(character, trap);
366+
})
367+
.catch(err => {
368+
sendChat(CHAT_NAME, '/w gm ' + err.message);
369+
log(err.stack);
370+
});
371+
}
372+
}
373+
}
374+
375+
ItsATrap.registerTheme(new TrapThemeMLPRIM4());
376+
});
377+
})();

ItsATrap_theme_RoleplayingIsMagic4/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# It's A Trap! - Roleplaying is Magic 4E theme
22

3+
_3.1 updates:_
4+
* Just bug fixes.
5+
36
_v3.0 updates:_
47
* Updated the trap theme to be compatible with It's A Trap v3.0.
58

ItsATrap_theme_RoleplayingIsMagic4/script.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"name": "It's a Trap! - Roleplaying is Magic 4E theme",
33
"script": "TrapTheme.js",
4-
"version": "3.0",
5-
"previousversions": ["1.0", "1.1", "1.2"],
4+
"version": "3.1",
5+
"previousversions": ["1.0", "1.1", "1.2", "3.0"],
66
"description": "# It's A Trap! - Roleplaying is Magic 4E theme\r\r_v3.0 updates:_\r* Updated the trap theme to be compatible with It's A Trap v3.0.\r\rThis is a My Little Pony: Roleplaying is Magic 4th edition theme for the It's A Trap! script,\rbuilt for use with the Roleplaying is Magic 4E character sheet.\r\rTo use this theme for the It's A Trap! script, enter 'MLP-RIM-4' for the ```theme``` user option.\r\r### Note about passive Perception\r\rThe Roleplaying is Magic 4E system doesn't actually have any defined rolls for\rpassive skill checks. For the purpose of this script, a character's passive Perception is\rcalculated as follows:\r\r```\r4 + (total Advantage/Disadvantage modifier) + Mind + (3 if trained) + (2 if improved) + (1 if greater) + (total Misc modifiers)\r```\r\rSo for example, a character not trained in Perception with a Mind of 2 would\rhave a passive Perception of 6. A character trained in Perception with a Mind\rof 4 would have a passive Perception of 11.\r",
77
"authors": "Stephen Lindberg",
88
"roll20userid": 46544,

0 commit comments

Comments
 (0)