OK, when I actually got in to look at this, it wasn't the script scoped variable at all. Those are just looking for any value matching a parameter and returning true/false accordingly. That will work fine. The problem was that there was a roll type that was not being accounted for... Type G (Group). The script was testing for C and R types, but not G. Once you roll against the same table twice (ie, 2t[feat]), the roll values get nested pretty deeply. The solution was to obviously add a test for type === 'G' , but also to move the whole testing under its own function. This is because for type G rolls, you have sub-rolls. For each of these you have to call the same procedure recursively to drill down to the type C or type R rolls, and you can't do that with a forEach callback. Important parts: Line 101 (below) kicks over to the new function Line 90 detects the type G roll, and iterates over the sub rolls; there are 2 forEach loops here because the G rolls are an array... of arrays... of roll objects. So we have to get to the rolls by iterating the first array, then the second. You can see this for yourself if you un-remark line 50 and send a basic roll through like /roll {2t[lm-feat]}kh1 . Then we recursively call the same function (line 94), and see if we catch G, R, or C types. This worked in the very small mock up I did of the game/script, but my mock up wasn't complete so there might be edge cases and/or broken functionalities. Post back if you have any trouble. In the meantime, try this: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253 /*
The One Ring Dice Checker for Roll20.
By Michael Heilemann (<a href="mailto:michael.heilemann@me.com" rel="nofollow">michael.heilemann@me.com</a>)
Updated by uhu79
This is an API script for Roll20.net, which checks rolls against the success
criteria of the The One Ring system, and is best used in conjunction with
the custom dice roll tables (<a href="https://wiki.roll20.net/The_One_Ring" rel="nofollow">https://wiki.roll20.net/The_One_Ring</a>).
Basically it will try to output a valuable message about the roll's success,
and works best of supplied with a Target Number, like so:
/roll 1d12 + 3d6 > 14
Or as would be the case if TOR roll tables are set up properly:
/roll 1t[feat] + 3t[normal] > 14
Or for the enemy:
/roll 1t[lm-feat] + 3t[normal] > 14
It know about Gandalf's Rune, and the great lidless eye, wreathed in flame,
and counts them as success and failure respectively (based on whether you
use the feat or lm-feat tables to roll from).
It also checks if your are using 'speaking as' and displays the name accordingly.
Update:
it also checks if you have rolled EDGE on your feat-die with the attack roll
this only works if your macros are following a clear structure and your roll
commands for rolls look like this:
/r 1t[feat] + @{selected|weapon_rating_1}t[@{selected|Weary}] > [[?{modifier|0} +@{selected|stance} + @{target|parry} + @{target|shield}]]
where after ">" you have the target number for this roll
and in case of an attack the roll should look like this:
/r 1t[feat] + @{selected|weapon_rating_1}t[@{selected|Weary}] > [[?{modifier|0} +@{selected|stance} + @{target|parry} + @{target|shield}]] Edge: @{selected|weapon_edge_1}
where at the end you put the edge-attribute of the weapon
Aditionally, this script now also checks if a player rolled an eye on a missed attack.
A chat msg is sent accordingly, stating that the opponent might try a called shot next.
*/
on( 'chat:message' , function (e) {
if (e.type === 'rollresult' ) {
var content = JSON.parse(e.content);
var rolls = content.rolls;
var tn = false ;
var edge = false ;
var automatic = false ;
var eye = false ;
var tengwars = 0 ;
var featResult;
var piercing = "" ;
var eyeOnAttack = "" ;
// log(JSON.stringify(content, undefined,2));
// determine who triggered the roll, player or character
var characters = findObjs({ _type: 'character' });
var speaking;
characters.forEach( function (chr) { if (chr.get( 'name' ) == e.who) speaking = chr; });
const parseRoll = roll => {
// detect Target Number
if (roll.type === 'C' ) {
var text = roll.text.replace( /\s/g , '' ); // remove whitespace
//split the string into an array separated by space
var params = roll.text.splitArgs();
//log(params);
//the target number is found at position 1 of the array (see the macro)
tn = params[ 1 ];
//the edge value is found at position 4 of the array (see the macro)
//a params array for attack-rolls should look like this [">", "(tn)", "Edge:", "(edge)"]
//if this is not an attack-roll then the variable edge will be undefined
edge = params[ 3 ];
}
// loop through dice results
if (roll.type === 'R' ) {
if (roll.sides === 12 ) {
roll.results.forEach(res => {
featResult = typeof featResult === 'undefined' ? res.v : featResult;
automatic = (roll.table === 'lm-feat' && res.tableidx === 10 ? true : automatic); // eye as adversary
automatic = (roll.table === 'feat' && res.tableidx === 11 ? true : automatic); // gandalf as player
eye = (roll.table === 'feat' && res.tableidx === 10 ? true : eye); // eye as player
});
}
if (roll.sides === 6 ) {
// check for tengwars
roll.results.forEach( function (result) {
tengwars = (result.v === 6 ? tengwars + 1 : tengwars);
}, this );
}
}
if (roll.type === 'G' ) {
featResult = typeof featResult === 'undefined' ? roll.results[ 0 ].v : featResult;
roll.rolls.forEach(r => {
r.forEach(r1 => {
parseRoll(r1);
});
});
}
};
rolls.forEach( function (roll) {
parseRoll(roll);
}, this );
//set chat msgs
//if this is an attack, then edge is set (see macro)
if (edge) {
if (featResult >= edge || automatic) {
piercing = " And might inflict a wound!" ;
}
//setting a chat-msg if player has rolled an eye during an attack
if (eye) {
eyeOnAttack = " And provokes the opponent to try a Called Shot next round!" ;
}
}
/*
//old version where the weapon-slot-nr was handed over via macro
//this only worked with players though, so I changed it
if (weapon) {
//looking up the edge-attribute only works if a speaking as character
if (speaking) {
var edge = getAttrByName(speaking.id, 'weapon_edge_'+weapon, 'current');
if (featResult >= edge || automatic) {
piercing = " Und schlägt vielleicht eine Wunde!";
}
}
}
*/
// gandalf rune for feat table, or eye for lm-feat table
if (automatic) {
if (tengwars === 0 ) {
if (speaking) sendChat( 'character|' + speaking.id, '/desc rolls an automatic success!' + piercing);
else sendChat( 'player|' + e.playerid, '/desc rolls an automatic success!' + piercing);
} else if (tengwars === 1 ) {
if (speaking) sendChat( 'character|' + speaking.id, '/desc rolls an automatic great success!' + piercing);
else sendChat( 'player|' + e.playerid, '/desc rolls an automatic great success!' + piercing);
} else if (tengwars > 1 ) {
if (speaking) sendChat( 'character|' + speaking.id, '/desc rolls an automatic extraordinary success!' + piercing);
else sendChat( 'player|' + e.playerid, '/desc rolls an automatic extraordinary success!' + piercing);
}
// a hit
} else if (tn !== false && content.total >= tn) {
if (tengwars === 0 ) {
if (speaking) sendChat( 'character|' + speaking.id, '/desc rolls a success!' + piercing);
else sendChat( 'player|' + e.playerid, '/desc rolls a success!' + piercing);
} else if (tengwars === 1 ) {
if (speaking) sendChat( 'character|' + speaking.id, '/desc rolls a great success!' + piercing);
else sendChat( 'player|' + e.playerid, '/desc rolls a great success!' + piercing);
} else if (tengwars > 1 ) {
if (speaking) sendChat( 'character|' + speaking.id, '/desc rolls an extraordinary success!' + piercing);
else sendChat( 'player|' + e.playerid, '/desc rolls an extraordinary success!' + piercing);
}
// a miss
} else if (tn !== false && content.total < tn) {
if (speaking) sendChat( 'character|' + speaking.id, '/desc misses.' + eyeOnAttack);
else sendChat( 'player|' + e.playerid, '/desc misses.' + eyeOnAttack);
} else {
if (tengwars === 1 ) {
if (speaking) sendChat( 'character|' + speaking.id, '/desc rolls a tengwar.' );
else sendChat( 'player|' + e.playerid, '/desc rolls a tengwar.' );
} else if (tengwars === 2 ) {
if (speaking) sendChat( 'character|' + speaking.id, '/desc rolls two tengwars.' );
else sendChat( 'player|' + e.playerid, '/desc rolls two tengwars.' );
} else if (tengwars > 2 ) {
if (speaking) sendChat( 'character|' + speaking.id, '/desc rolls whole lotta tengwars.' );
else sendChat( 'player|' + e.playerid, '/desc rolls whole lotta tengwars.' );
}
}
}
});
/**
* Splits a string into arguments using some separator. If no separator is
* given, whitespace will be used. Most importantly, quotes in the original
* string will allow you to group delimited tokens. Single and double quotes
* can be nested one level.
*
* As a convenience, this function has been added to the String prototype,
* letting you treat it like a function of the string object.
*
* Example:
on('chat:message', function(msg) {
var command, params;
params = msg.content.splitArgs();
command = params.shift().substring(1);
// msg.content: !command with parameters, "including 'with quotes'"
// command: command
// params: ["with", "parameters,", "including 'with quotes'"]
});
*/
var bshields = bshields || {};
bshields.splitArgs = ( function () {
'use strict' ;
var version = 1.0 ;
function splitArgs(input, separator) {
var singleQuoteOpen = false ,
doubleQuoteOpen = false ,
tokenBuffer = [],
ret = [],
arr = input.split( '' ),
element, i, matches;
separator = separator || /\s/g ;
for (i = 0 ; i < arr.length; i++) {
element = arr[i];
matches = element.match(separator);
if (element === '\'' ) {
if (!doubleQuoteOpen) {
singleQuoteOpen = !singleQuoteOpen;
continue ;
}
} else if (element === '"' ) {
if (!singleQuoteOpen) {
doubleQuoteOpen = !doubleQuoteOpen;
continue ;
}
}
if (!singleQuoteOpen && !doubleQuoteOpen) {
if (matches) {
if (tokenBuffer && tokenBuffer.length > 0 ) {
ret.push(tokenBuffer.join( '' ));
tokenBuffer = [];
}
} else {
tokenBuffer.push(element);
}
} else if (singleQuoteOpen || doubleQuoteOpen) {
tokenBuffer.push(element);
}
}
if (tokenBuffer && tokenBuffer.length > 0 ) {
ret.push(tokenBuffer.join( '' ));
}
return ret;
}
return splitArgs;
}());
String .prototype.splitArgs = String .prototype.splitArgs || function (separator) {
return bshields.splitArgs( this , separator);
};