-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathammoManager.js
357 lines (291 loc) · 14.5 KB
/
ammoManager.js
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
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
/**
* This is a Roll20 script for Fallout2d20, using the official Roll20 sheets.
* There's two parts to it:
* Automatic ammo reduction when firing a shot - as well as checking you have enough ammo to shoot.
* And a validator that should sync the number on all weapons and ammo gear that use the same type of ammo, when any of those values change.
*
* -------------------------------------------------------------------------------
* SPECIAL WEAPONS
* Example, the Syringer which uses different ammo types, or Gatling Laser, which can use Fusion Cells or Cores
* All such cases need to be handled manually (or edit the ammo type to match something in gear).
*
* PERKS
* Unhandled perks because they're conditional: Quick Hands, Gun Fu, Accurate weapon quality.
* Conditional extra damage perks (Black Widow/Lady Killer, Mr Sandman, Nerd Rage) - Should be handled as an attack cannot consume more than 1+fireRate ammo.
* Weapon Type perks (Commando, Rifleman, Gunslinger, Laser Commander, Pyromaniac, Size Matters) - player should manually modify the weapon damage rating in their sheet.
*
* MODS
* Weapon mods are handled, as they modify the weapon's damage rating and fire rate in the sheet.
*
* OTHER NOTES
* Weapon names in the weapons section of the character sheet should be made unique. Otherwise the wrong weapon stats may be used.
* Gatling - when asked to spend additional dice, player should input 2x the number they want to spend in ammo. E.g. Input 10 extra DC to use fire rate 5.
*
* ISSUES
* TODO account for stuff like Laser Muskets, which specially consume 2/3 ammo per shot. (search for "Hungry 2" in description or smth. Manually add.)
* BUG when adding mod (308 -> .50) the ammo count updates to new value correctly, but not viceversa.
*/
function ValidateIsTemplate(msg, templateName) {
return msg.rolltemplate === templateName;
}
// Check that this is a PC sheet, not an NPC.
function ValidateIsPlayerCharacter(character) {
return getAttrByName(character.id, 'sheet_type') === 'pc';
}
// Try to find the cahracter sheet out of the message content.
function GetCharacterSheet(msg) {
const sheetNameMatch = msg.content.match(/{{sheetName=([^}]*)}}/);
if (!sheetNameMatch)
return null;
const sheetName = sheetNameMatch[1];
return findObjs({ type: 'character', name: sheetName })[0];
}
// Params: Character sheet to search, name of item to search, name of repeating section (list), attribute name
function GetItemRowId(character, itemName, repeatingSection, nameAttr, needsExactNameMatch) {
// Loop through all attributes to find the item
const itemAttrs = findObjs({
type: 'attribute',
characterid: character.id
})
.filter(attr => attr.get('name').includes(repeatingSection)
&& attr.get('name').includes(nameAttr));
let searchName = itemName;
if (!needsExactNameMatch) {
// For some reason, many weapons add the ammo as "0.45" or "0.308" in the weapon.ammoType section.
searchName = itemName.startsWith('0') ? itemName.substring(1) : itemName;
// Remove space before "mm", Fallout sheet also lists e.g. 5mm in gear, as 5 mm in weapon.ammoType. Remove space.
searchName = searchName.replace(/(\d)\s+mm/g, '$1mm');
// Ammo in weapon section may also be in plural. Remove trailing "s" if it exists
searchName = searchName.replace(/s$/, '');
}
// Find the requested item
let itemAttr;
for (let attr of itemAttrs) {
if (needsExactNameMatch) {
if (attr.get('current').toLowerCase() === searchName.toLowerCase()) {
itemAttr = attr;
break;
}
}
else if (attr.get('current').toLowerCase().includes(searchName.toLowerCase())) {
itemAttr = attr;
break;
}
}
if (!itemAttr) {
const characterName = character.get('name');
const errorMessage = `ERR: Item ${searchName} not found, make sure names are the same in character sheet, and that the item is actually in the sheet`;
const errorTemplate = `&{template:fallout_injuries} {{playerName=${characterName}}} {{injuryLocation=AMMO API ERROR}} {{injuryEffect=${errorMessage}}}`
sendChat(characterName, errorTemplate);
return null;
}
const rowId = itemAttr.get('name').split('_')[2];
return rowId;
}
function GetAttributeFromRow(character, rowId, repeatingSection, attrName) {
const attr = findObjs({
type: 'attribute',
characterid: character.id,
name: `repeating_${repeatingSection}_${rowId}_${attrName}`
})[0];
return attr;
}
function GetValue(attribute) {
return attribute ? attribute.get('current') : null;
}
function GetValueFromWeapon(character, weaponRowId, attrName) {
const attr = GetAttributeFromRow(character, weaponRowId, 'pc-weapons', attrName);
return GetValue(attr);
}
// Find the weapon used in the roll in the list of weapons
function GetWeaponRowId(msg, character) {
const weaponNameMatch = msg.content.match(/{{weaponName=([^}]*)}}/);
if (!weaponNameMatch)
return null;
return GetItemRowId(character, weaponNameMatch[1], 'repeating_pc-weapons', 'weapon_name', true)
}
function GetAmmoRowId(character, ammoType) {
return GetItemRowId(character, ammoType, 'repeating_gear-ammo', 'ammo_name', false);
}
function GetAmmoSpent(msg, weaponDamage, fireRate, isGatling) {
if (fireRate === 0)
return 1;
const damageRolls = msg.inlinerolls.filter(r => r.expression.indexOf("1d6cs7") !== -1);
let extraShots = damageRolls.length - weaponDamage;
if (isGatling)
extraShots /= 2;
// We use fireRate as a cap, as there could be other effects that increase damage dice past fire rate.
// Perks (Black Widow/Lady Killer, Mr Sandman, Nerd Rage) should be handled by this.
let shotCount = 1 + (Math.min(extraShots, fireRate));
// Subtract the baseline shot if we're just rolling extra dice.
const isAdditionalRoll = !(msg.content.match(/{{showAdditional=([^}]*)}}/));
if (isAdditionalRoll)
shotCount -= 1;
return shotCount;
}
// The fallout sheet doesn't sync as cleanly as we'd wish. Force the syncing of ammo in gear to ammo in weapons.
function UpdateAmmoCountInWeapons(character, ammoType, newAmmoValue) {
if (isNaN(newAmmoValue)) {
log("New ammo value is NaN, change prevented, this should never be called at this stage");
return;
}
// Foreach weapon in pc-weapons, that has a matching ammo type, set the ammo count.
const matchingWeapons = findObjs({
type: 'attribute',
characterid: character.id
})
.filter(attr => attr.get('name').includes('repeating_pc-weapons')
&& attr.get('name').includes('weapon_ammo_type')
&& attr.get('current') === ammoType);
matchingWeapons.forEach(element => {
const rowId = element.get('name').split('_')[2];
// BUG: the filtering doesn't seem to work properly, so we check in here again
// Example case: 3 Hunting rifles, 2 using 0.308 ammo and 1 using a 0.50 cal receiver.
const weaponAmmoType = GetValueFromWeapon(character, rowId, 'weapon_ammo_type');
if (weaponAmmoType != ammoType)
return;
const ammoAttr = GetAttributeFromRow(character, rowId, 'pc-weapons', 'weapon_ammo');
const previousQuantity = GetValue(ammoAttr);
ammoAttr.set('current', newAmmoValue);
log(`${character.get('name')} | SettingA: ${GetValueFromWeapon(character, rowId, 'weapon_name')} was ${previousQuantity}, now ${GetValue(ammoAttr)}`);
});
}
function UpdateAmmoCountInGear(msg, character, ammoRowId, ammoType, ammoSpent) {
if (ammoSpent === 0) {
sendChat(msg.who, "No Ammo Spent");
return;
}
const gearAmmoAttr = GetAttributeFromRow(character, ammoRowId, 'gear-ammo', 'ammo_quantity');
if (!gearAmmoAttr)
return;
const previousQuantity = GetValue(gearAmmoAttr);
gearAmmoAttr.set('current', parseInt(previousQuantity) - ammoSpent);
const newQuantity = GetValue(gearAmmoAttr);
UpdateAmmoCountInWeapons(character, ammoType, newQuantity);
const characterName = character.get('name');
const message = `${ammoType} reduced: ${previousQuantity} -> ${newQuantity}`;
const messageTemplate = `&{template:fallout_gear} {{playerName=${characterName}}} {{gearName=${message}}} {{gearDescription=}}`
sendChat(msg.who, messageTemplate);
}
function HandleDamageRoll(msg) {
const character = GetCharacterSheet(msg);
if (!character || !ValidateIsPlayerCharacter(character))
return;
const weaponRowId = GetWeaponRowId(msg, character);
const ammoType = GetValueFromWeapon(character, weaponRowId, 'weapon_ammo_type');
const weaponDamage = GetValueFromWeapon(character, weaponRowId, 'weapon_damage');
const fireRate = GetValueFromWeapon(character, weaponRowId, 'weapon_fire_rate');
const isGatling = GetValueFromWeapon(character, weaponRowId, 'weapon_qualities').includes('Gatling');
const ammoRowId = GetAmmoRowId(character, ammoType);
const ammoSpent = GetAmmoSpent(msg, weaponDamage, fireRate, isGatling);
UpdateAmmoCountInGear(msg, character, ammoRowId, ammoType, ammoSpent);
}
function HandleAttackRoll(msg) {
const character = GetCharacterSheet(msg);
if (!character || !ValidateIsPlayerCharacter(character))
return;
const weaponRowId = GetWeaponRowId(msg, character);
const ammoType = GetValueFromWeapon(character, weaponRowId, 'weapon_ammo_type');
// Weapon uses no ammo, likely melee or thrown.
if (!ammoType)
return;
const ammoCount = GetValueFromWeapon(character, weaponRowId, 'weapon_ammo');
if (ammoCount <= 0) {
const characterName = character.get('name');
const errorMessage = `Not enough ${ammoType} to fire this weapon.`;
const errorTemplate = `&{template:fallout_injuries} {{playerName=${characterName}}} {{injuryLocation=CLICK!}} {{injuryEffect=${errorMessage}}}`
sendChat(msg.who, errorTemplate);
}
}
function HandleWeaponAmmoChanged(character, weaponRowId, targetAmmoCount) {
const ammoType = GetValueFromWeapon(character, weaponRowId, 'weapon_ammo_type');
const ammoRowId = GetAmmoRowId(character, ammoType);
const gearAmmoAttr = GetAttributeFromRow(character, ammoRowId, 'gear-ammo', 'ammo_quantity');
const gearAmmoCount = GetValue(gearAmmoAttr);
if (gearAmmoCount === targetAmmoCount)
return;
gearAmmoAttr.set('current', targetAmmoCount);
UpdateAmmoCountInWeapons(character, ammoType, targetAmmoCount);
}
function HandleGearAmmoChanged(character, ammoRowId, targetAmmoCount) {
const ammoNameAttr = GetAttributeFromRow(character, ammoRowId, 'gear-ammo', 'ammo_name');
const ammoName = GetValue(ammoNameAttr);
const weaponsWithAmmo = findObjs({
type: 'attribute',
characterid: character.id
})
.filter(attr => attr.get('name').includes('repeating_pc-weapons')
&& attr.get('name').includes('weapon_ammo_type')
&& attr.get('current') != "");
// Loop through all weapons to find ammoType matches.
weaponsWithAmmo.forEach(element => {
const rowId = element.get('name').split('_')[2];
const weaponAmmoName = GetValueFromWeapon(character, rowId, 'weapon_ammo_type');
// For some reason, many weapons add the ammo as "0.45" or "0.308" in the weapon.ammoType section.
let searchName = weaponAmmoName.startsWith('0') ? weaponAmmoName.substring(1) : weaponAmmoName;
// Remove space before "mm", Fallout sheet also lists e.g. 5mm in gear, as 5 mm in weapon.ammoType. Remove space.
searchName = searchName.replace(/(\d)\s+mm/g, '$1mm');
// Ammo in weapon section may also be in plural. Remove trailing "s" if it exists
searchName = searchName.replace(/s$/, '');
if (!ammoName.toLowerCase().includes(searchName.toLowerCase()))
return; // js continue
const ammoAttr = GetAttributeFromRow(character, rowId, 'pc-weapons', 'weapon_ammo');
const previousQuantity = GetValue(ammoAttr);
if (previousQuantity == targetAmmoCount)
return; // js continue
ammoAttr.set('current', parseInt(targetAmmoCount));
log(`${character.get('name')} | SettingB: ${GetValueFromWeapon(character, rowId, 'weapon_name')} was ${previousQuantity}, now ${GetValue(ammoAttr)}`);
});
}
on("ready", function() {
log('=== Initialized Ammo Manager ===');
});
on("chat:message", function(msg) {
try {
if (!msg.inlinerolls)
return;
if (ValidateIsTemplate(msg, 'fallout_attacks'))
HandleAttackRoll(msg);
if (ValidateIsTemplate(msg, 'fallout_damage'))
HandleDamageRoll(msg);
}
catch (err) {
log('AMMO MANAGER ERROR: ' + err.message)
}
});
on("change:attribute", function(obj, prev) {
try {
const character = getObj("character", obj.get('characterid'));
if (!ValidateIsPlayerCharacter(character))
return;
const targetAmmoCount = Number(GetValue(obj));
if (targetAmmoCount === prev.current) {
log("---- Value already the same " + obj.get('name'))
return;
}
const attrName = obj.get('name');
const isWeaponAmmoAttr = attrName.includes('weapon_ammo') && !attrName.includes('_type');
const isGearAmmoAttr = attrName.includes('ammo_quantity');
if (!isWeaponAmmoAttr && !isGearAmmoAttr)
return;
// Sometimes when adding a weapon or changing mods, text can be input into the ammo value.
if (isNaN(targetAmmoCount) || targetAmmoCount === "" || !Number.isInteger(targetAmmoCount)) {
obj.set('current', prev.current);
log("----Caught NaN change attempt " + targetAmmoCount + " Back to " + prev.current);
return;
}
// We changed the ammo count in the weapons section. Update gear and other weapons that use same ammo type.
if (isWeaponAmmoAttr) {
const weaponRowId = attrName.split('_')[2];
HandleWeaponAmmoChanged(character, weaponRowId, targetAmmoCount);
}
// We changed the ammo count in gear section. Update weapons that use the ammo type.
else if (isGearAmmoAttr) {
const ammoRowId = attrName.split('_')[2];
HandleGearAmmoChanged(character, ammoRowId, targetAmmoCount);
}
}
catch (err) {
log('AMMO MANAGER VALIDATOR ERROR: ' + err.message)
}
});