forked from Roger4325/TaleSpire-VTT
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathPlayerScript.js
More file actions
8367 lines (6520 loc) · 312 KB
/
PlayerScript.js
File metadata and controls
8367 lines (6520 loc) · 312 KB
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
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//Array of level progression and proficiency bonus.
const xpLevels = [
{ xp: 0, level: 1, proficiencyBonus: 2 },
{ xp: 300, level: 2, proficiencyBonus: 2 },
{ xp: 900, level: 3, proficiencyBonus: 2 },
{ xp: 2700, level: 4, proficiencyBonus: 2 },
{ xp: 6500, level: 5, proficiencyBonus: 3 },
{ xp: 14000, level: 6, proficiencyBonus: 3 },
{ xp: 23000, level: 7, proficiencyBonus: 3 },
{ xp: 34000, level: 8, proficiencyBonus: 3 },
{ xp: 48000, level: 9, proficiencyBonus: 4 },
{ xp: 64000, level: 10, proficiencyBonus: 4 },
{ xp: 85000, level: 11, proficiencyBonus: 4 },
{ xp: 100000, level: 12, proficiencyBonus: 4 },
{ xp: 120000, level: 13, proficiencyBonus: 5 },
{ xp: 140000, level: 14, proficiencyBonus: 5 },
{ xp: 165000, level: 15, proficiencyBonus: 5 },
{ xp: 195000, level: 16, proficiencyBonus: 5 },
{ xp: 225000, level: 17, proficiencyBonus: 6 },
{ xp: 265000, level: 18, proficiencyBonus: 6 },
{ xp: 305000, level: 19, proficiencyBonus: 6 },
{ xp: 355000, level: 20, proficiencyBonus: 6 }
];
// checkboxes to determine what type of action the ability is and how to filter it.
let checkboxData = [
{ label: 'Attacks', category: 'attacks' },
{ label: 'Actions', category: 'actions' },
{ label: 'Bonus Actions', category: 'bonus-actions' },
{ label: 'Reactions', category: 'reactions' },
{ label: 'Other', category: 'other' }
];
// Define proficiency levels for skills with 4 options
const skillProficiencyLevels = [
{ class: "notProficient", title: "not proficient", value: "0" },
{ class: "halfProficient", title: "half proficient", value: ".5" },
{ class: "proficient", title: "proficient", value: "1" },
{ class: "expertise", title: "expertise", value: "2" }
];
// Define proficiency levels for saves with 2 options
const savesProficiencyLevels = [
{ class: "notProficient", title: "not proficient", value: "0" },
{ class: "proficient", title: "proficient", value: "1" }
];
let isMe;
//Define all message Types and the functions they should call this should be expanded as I need different types of messages.
const messageHandlers = {
'request-info': handleRequestInfo,
'update-health': handleUpdateHealth,
'roll-dice': handleRollDice,
'target-selection': handleTargetSelection,
'player-init-list': createPlayerInit,
'player-init-turn': handleInitTurn,
'player-init-round': handleInitRound
};
let monsterNames
let monsterData
let baseAC = 10; // Default base AC
let equippedArmor = null; // No armor equipped by default
let equippedShield = null; // No shield equipped by default
let playerWeaponProficiency = [];
let playerArmorProficiency = [];
let playerLanguageProficiency = [];
let playerToolsProficiency = [];
//ConditionsMap is the map of conditions that can be set onto the player. This is used for tracking conditions.
const conditionsMap = new Map();
async function playerSetUP(){
// Adding eventlisteners to the level and xp inputs for character level.
const xpTracker = document.querySelectorAll('.levelStat');
xpTracker.forEach(levelStat => {
levelStat.addEventListener('blur', handleXPChange);
levelStat.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
this.blur();
}
});
});
//adding mini image and linking the id to that character sheet. Allows the user to be targeted via combat tracker.
document.getElementById("get-selection-button").onclick = async () => {
console.log("click")
linkPlayerMini()
}
//Allowing the user to edit their initiative and setting the value of the label from the textContent they update it to.
document.getElementById("initiativeButton").addEventListener('blur', function(){
const labelElement = this.closest('.playerStats').querySelector('.actionButtonLabel');
const inputValue = this.textContent.trim(); // Remove leading and trailing spaces
console.log(inputValue)
let extractedNumber = ""
if (inputValue >= 0){
extractedNumber = parseButtonText(inputValue);
}
else{
extractedNumber = inputValue
}
labelElement.setAttribute('value', extractedNumber);
});
document.getElementById("initiativeButton").addEventListener('keydown', function(event) {
if (event.key === 'Enter') {
this.blur();
}
});
// Event listener for the select box for the character alignment.
const alignmentSelect = document.getElementById('alignment-select');
alignmentSelect.addEventListener('change', function() {
updateContent()
});
// Add event listeners for keyup and blur on editable ability scores
const abilityScores = document.querySelectorAll('.abilityScore');
abilityScores.forEach(score => {
score.addEventListener('blur', handleAbilityScoreChange);
score.addEventListener('keydown', function(e) {
if (e.key === 'Enter'){
this.blur();
}
});
});
// Add event listeners to the "Short Rest" and "Long Rest" buttons
shortRestButton.addEventListener("click", openShortRestModal);
longRestButton.addEventListener("click", openLongRestModal);
hitDiceOpenModalButton.addEventListener("click", openHitDiceModal);
// Add event listeners to the heal and damage buttons
healButton.addEventListener("click", function() {
healCreature(parseInt(healthInput.value));
});
damageButton.addEventListener("click", damageCreature);
// Event listener for checkbox clicks using event delegation
document.body.addEventListener('click', function (event) {
if (event.target.classList.contains('category-checkbox')) {
// Find the closest table row (parent) of the clicked checkbox
const tableRow = event.target.closest('tr');
// Get the current data-category value or set it to an empty string if not present
let currentCategories = tableRow.getAttribute('data-category') || '';
// Toggle the clicked checkbox's category in the currentCategories list
const checkboxCategory = event.target.getAttribute('data-category');
currentCategories = toggleCategory(currentCategories, checkboxCategory);
// Update the data-category attribute of the table row
tableRow.setAttribute('data-category', currentCategories);
// Update displayed rows based on selected categories
updateDisplayedRows();
updateContent();
}
});
document.querySelectorAll('[data-title]').forEach(element => {
const tooltip = document.createElement('div');
tooltip.className = 'tooltip';
tooltip.innerText = element.getAttribute('data-title');
document.body.appendChild(tooltip);
element.addEventListener('mouseenter', () => {
showTimeout = setTimeout(() => {
const rect = element.getBoundingClientRect();
tooltip.style.top = `2%`;
tooltip.style.left = `${rect.left + rect.width - 20}px`;
tooltip.classList.add('show');
// Check bounds to keep tooltip fully visible
const tooltipRect = tooltip.getBoundingClientRect();
if (tooltipRect.left < 0) {
tooltip.style.left = `${tooltipRect.width/2 + 5}px`;
} else if (tooltipRect.right > window.innerWidth) {
tooltip.style.left = `${window.innerWidth - (tooltipRect.width/2+5)}px`;
}
}, 300);
});
element.addEventListener('mouseleave', () => {
clearTimeout(showTimeout);
tooltip.classList.remove('show');
});
});
// Event listener for subtab clicks
document.querySelectorAll('.actionsubtab').forEach(subtab => {
subtab.addEventListener('click', function () {
// Remove 'active' class from all subtabs
document.querySelectorAll('.actionsubtab').forEach(tab => {
tab.classList.remove('active');
});
// Add 'active' class to the clicked subtab
this.classList.add('active');
const selectedCategory = this.getAttribute('data-category');
updateDisplayedRows([selectedCategory]);
});
});
//Setting up event listeners for the Action Catgories.
const dropdownBtn = document.querySelector('.dropbtn');
const dropdownContent = document.querySelector('.dropdown-content');
addToggleDropdownListener(dropdownBtn, dropdownContent,);
const actionTableRows = document.querySelectorAll('.actionTable tbody tr');
actionTableRows.forEach((row) => {
row.addEventListener('blur', updateContent);
});
// attaching eventlisteners for the Actions Table Ability Select Dropdown.
attachAbilityDropdownListeners();
// Call updateAllToHitDice on load
updateAllToHitDice();
addProficiencyButtonListener()
const spellCastingAbility = document.querySelector('.spellcasting-dropdown');
spellCastingAbility.addEventListener('change', function(event) {
console.log(event.target.value); // Logs the selected value
updateSpelltoHitDice(event.target.value);
updateSpellSaveDC(event.target.value)
updateAllSpellDCs()
updateAllSpellDamageDice()
});
//Spell Level Dropdown Listener
document.querySelector('.spell-level-dropdown').addEventListener('change', function() {
const selectedLevel = parseInt(this.value, 10); // Get the selected level as an integer
const spellGroups = document.querySelectorAll('.spell-group'); // Get all spell groups
spellGroups.forEach(spellGroup => {
const spellLevelAttr = spellGroup.getAttribute('spellLevel'); // Get the spellLevel attribute
// Convert spell level attribute to a number for comparison
let spellLevelNumber;
if (spellLevelAttr === 'Cantrip') {
spellLevelNumber = 0; // Treat cantrips as level 0
} else {
spellLevelNumber = parseInt(spellLevelAttr.split('-')[0], 10);
}
// Show or hide the spell group based on the selected level
if (spellLevelNumber <= selectedLevel) {
spellGroup.style.display = 'block';
} else {
spellGroup.style.display = 'none';
}
});
updateContent()
});
document.querySelectorAll('.deathSavesButton').forEach(button => {
button.addEventListener('click', function () {
// Toggle the active class on click
console.log("click")
button.classList.toggle('active');
updateContent()
});
});
// Add event listener to toggle the inspiration button when clicked
document.getElementById("inspirationBox").addEventListener("click", toggleInspiration);
// Select the anchor element
const featuresLink = document.querySelector('a[href="#features"]');
featuresLink.addEventListener('click', function() {
resizeAllTextareas()
});
const initLink = document.querySelector('a[href="#Init"]');
initLink.addEventListener('click', function() {
resizeAllTextareas()
});
//adding event listener to the different proficiency types so that we can select different types of proficiency.
const proficiencySettings = document.querySelectorAll('.proficiency-settings');
proficiencySettings.forEach(function(icon) {
icon.addEventListener('click', function(event) {
console.log("click");
// Find the corresponding dropdown by navigating from the clicked icon
const parentGroup = event.target.closest('.proficiency-group'); // Get the closest proficiency group
const dropdown = parentGroup.querySelector('.proficiency-dropdown'); // Find the dropdown inside the same group
// Close any open dropdowns
const openDropdowns = document.querySelectorAll('.proficiency-dropdown[style="display: block;"]');
openDropdowns.forEach(function(openDropdown) {
// Close the dropdowns except the one we're about to toggle
if (openDropdown !== dropdown) {
openDropdown.style.display = 'none';
}
});
// Toggle visibility of the clicked dropdown
dropdown.style.display = dropdown.style.display === 'block' ? 'none' : 'block';
});
});
// Add an event listener to the document to close the dropdown if clicked outside
document.addEventListener('click', function(event) {
const openDropdown = document.querySelector('.proficiency-dropdown[style="display: block;"]'); // Find the dropdown that's open
// If there's an open dropdown and the click is outside the dropdown or its cog icon, close it
if (openDropdown && !openDropdown.contains(event.target) && !event.target.closest('.proficiency-settings')) {
openDropdown.style.display = 'none';
}
});
// Updated event listener setup with proper translation handling
function setupProficiencyCheckboxes() {
const allCheckboxes = document.querySelectorAll('.proficiency-dropdown input[type="checkbox"]');
allCheckboxes.forEach(function (checkbox) {
checkbox.addEventListener('change', function () {
const parentGroup = checkbox.closest('.proficiency-group');
const categoryTitle = parentGroup.querySelector('h3 span').textContent;
console.warn(categoryTitle , " , ",translations[savedLanguage].proficiencyWeapons )
// Modified detection logic using translation keys
if (categoryTitle.includes(translations[savedLanguage].proficiencyWeapons)) {
updateProficiencyArray(checkbox, playerWeaponProficiency);
} else if (categoryTitle.includes(translations[savedLanguage].proficiencyArmor)) { // "Shield" as anchor
updateProficiencyArray(checkbox, playerArmorProficiency);
} else if (categoryTitle.includes(translations[savedLanguage].proficiencyLanguages)) { // First exotic language
updateProficiencyArray(checkbox, playerLanguageProficiency);
} else if (categoryTitle.includes(translations[savedLanguage].proficiencyTools)) { // First tool category
updateProficiencyArray(checkbox, playerToolsProficiency);
}
updateProficiencyContainers();
updateContent();
});
});
}
// Modified update function to handle both languages
function updateProficiencyArray(checkbox, proficiencyArray) {
const itemKey = checkbox.value;
if (checkbox.checked) {
if (!proficiencyArray.includes(itemKey)) {
proficiencyArray.push(itemKey);
}
} else {
const index = proficiencyArray.indexOf(itemKey);
if (index > -1) {
proficiencyArray.splice(index, 1);
}
}
}
// Updated container update function with translation support
function updateProficiencyContainers() {
const getTranslatedLabels = (items, lang) => items.map(item => {
// Search through all translations to find matching key
for (const category in translations[lang].proficiencies) {
const categoryData = translations[lang].proficiencies[category];
if (Array.isArray(categoryData)) {
const found = categoryData.find(transItem => transItem === item);
if (found) return found;
} else {
for (const subcat in categoryData) {
const found = categoryData[subcat].find(transItem => transItem === item);
if (found) return found;
}
}
}
return item; // Fallback to original if not found
});
// Helper function with language support
function updateContainer(container, array) {
const translatedItems = getTranslatedLabels(array, savedLanguage);
container.textContent = translatedItems.join(', ') || '';
}
updateContainer(document.querySelector('#weaponsContainer'), playerWeaponProficiency);
updateContainer(document.querySelector('#armorContainer'), playerArmorProficiency);
updateContainer(document.querySelector('#languageContainer'), playerLanguageProficiency);
updateContainer(document.querySelector('#toolsContainer'), playerToolsProficiency);
}
function updateProficiencyContainers() {
// Select the containers
const weaponsContainer = document.querySelector('#weaponsContainer');
const armorContainer = document.querySelector('#armorContainer');
const languageContainer = document.querySelector('#languageContainer');
const toolsContainer = document.querySelector('#toolsContainer');
// Helper function to update container content
function updateContainer(container, array) {
if (array.length > 0) {
container.textContent = array.join(', ');
} else {
container.textContent = '';
}
}
// Update each container with its respective proficiency array
updateContainer(weaponsContainer, playerWeaponProficiency);
updateContainer(armorContainer, playerArmorProficiency);
updateContainer(languageContainer, playerLanguageProficiency);
updateContainer(toolsContainer, playerToolsProficiency);
}
function populateProficiencyDropdowns() {
// Data for the checkboxes
const proficiencies = translations[savedLanguage].proficiencies;
// Function to create a list of checkboxes
function createCheckboxList(items) {
const ul = document.createElement('ul');
items.forEach(item => {
const li = document.createElement('li');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = item;
checkbox.id = item
li.appendChild(checkbox);
li.appendChild(document.createTextNode(item));
ul.appendChild(li);
});
return ul;
}
// Populate Weapons Dropdown
const weaponsDropdown = document.getElementById('weapons-dropdown');
const weaponsItems = [
...proficiencies.weapons.categories,
...proficiencies.weapons.simpleMelee,
...proficiencies.weapons.simpleRanged,
...proficiencies.weapons.martialMelee,
...proficiencies.weapons.martialRanged
];
weaponsDropdown.appendChild(createCheckboxList(weaponsItems));
// Populate Armor Dropdown
const armorDropdown = document.getElementById('armor-dropdown');
armorDropdown.appendChild(createCheckboxList(proficiencies.armor));
// Populate Languages Dropdown
const languagesDropdown = document.getElementById('languages-dropdown');
const languagesItems = [
...proficiencies.languages.common,
...proficiencies.languages.exotic
];
languagesDropdown.appendChild(createCheckboxList(languagesItems));
// Populate Tools Dropdown
const toolsDropdown = document.getElementById('tools-dropdown');
const toolsItems = [
...proficiencies.tools.artisan,
...proficiencies.tools.gaming,
...proficiencies.tools.musical,
...proficiencies.tools.other
];
toolsDropdown.appendChild(createCheckboxList(toolsItems));
setupProficiencyCheckboxes()
}
document.getElementById('closeItemCard').addEventListener('click', () => {
hideNotesPanel()
});
const currencyInputs = document.querySelectorAll('.currency-input');
currencyInputs.forEach(input => {
// Ensure the value is formatted on blur
input.addEventListener('blur', () => {
const value = input.value.replace(/,/g, '');
if (!isNaN(value) && value !== '') {
input.value = formatWithCommas(value); // Format and update input
updateContent()
}
});
});
isMe = await TS.players.whoAmI()
const monsterDataObject = AppData.monsterLookupInfo;
monsterNames = monsterDataObject.monsterNames;
monsterData = monsterDataObject.monsterData;
sendDMUpdatedStatsDebounced()
populateMonsterDropdown()
document.querySelector('a[href="#Init"]').addEventListener('click', (event) => {
debounce(requestInitList, 1000)();
});
populateConditionSelect();
populateProficiencyDropdowns();
updateWeight();
}
// Function to format numbers with commas
function formatWithCommas(number) {
return Number(number).toLocaleString('en-US');
}
function toggleInspiration() {
const inspirationButton = document.getElementById("inspirationBox");
const starContainer = inspirationButton.querySelector('.star-container')
// Toggle 'active' and 'inactive' class to show/hide the star icon
if (starContainer.classList.contains("active")) {
starContainer.classList.remove("active");
starContainer.classList.add("inactive");
} else {
starContainer.classList.remove("inactive");
starContainer.classList.add("active");
}
updateContent();
}
function loadInspiration(savedState) {
const inspirationButton = document.getElementById("inspirationBox");
const starContainer = inspirationButton.querySelector('.star-container');
if (savedState === 1) {
// Set to active
starContainer.classList.add("active");
starContainer.classList.remove("inactive");
} else {
// Set to inactive
starContainer.classList.add("inactive");
starContainer.classList.remove("active");
}
}
let initialHitDiceValue = "1d8"; // Default value, adjust as needed
function updateHitDiceLabel() {
// Get the content from the button and split it to separate the dice count and type
const hitDiceLabel = document.getElementById("hitDiceLabel");
const hitDiceText = hitDiceButton.innerText.trim();
// Update regex to match a valid dice pattern with only valid dice types
const dicePattern = /^(\d+)d(4|6|8|10|12|20)$/;
const match = hitDiceText.match(dicePattern);
if (match) {
const diceCount = parseInt(match[1], 10); // Extract the number before "d" and convert to integer
const maxDiceCount = parseInt(hitDiceLabel.getAttribute("max"), 10); // Retrieve the max attribute from the label
const currentHitDice = parseInt(hitDiceLabel.textContent.trim(), 10);
// Check if the dice count exceeds the max allowed
if (diceCount > maxDiceCount) {
showErrorModal(`Invalid input: Dice count cannot exceed ${maxDiceCount} (current level).`);
hitDiceButton.innerText = initialHitDiceValue; // Restore previous value
}
else if (diceCount > currentHitDice) {
showErrorModal(`Invalid input: Dice count cannot exceed ${currentHitDice} (remaining hit dice).`);
hitDiceButton.innerText = initialHitDiceValue; // Restore previous value
}
else {
// Update label with the valid dice count and type
hitDiceLabel.setAttribute("data-dice-type", hitDiceText);
}
} else {
// Show error modal and revert to initial value
showErrorModal("Invalid input: Please enter a valid dice format like '5d6'.");
hitDiceButton.innerText = initialHitDiceValue; // Restore previous value
}
updateHitDiceValue()
updateHitDiceMax()
}
function removeRolledHitDice() {
console.log("remove hit dice")
const hitDiceLabel = document.getElementById("hitDiceLabel");
const hitDiceButton = document.getElementById("hitDiceButton");
// Get the current number of available hit dice from the text content
let currentHitDice = parseInt(hitDiceLabel.textContent.trim(), 10);
const diceType = hitDiceLabel.getAttribute("data-dice-type");
const diceCountMatch = diceType.match(/^(\d+)d/);
const diceCount = parseInt(diceCountMatch[1], 10);
console.log(diceCountMatch)
// Check if currentHitDice is a valid number and greater than 0
if (!isNaN(currentHitDice) && currentHitDice > 0) {
currentHitDice -= diceCount;
// Update the label text content
hitDiceLabel.textContent = currentHitDice > 0 ? currentHitDice : 0; // Avoid negative numbers
const diceSuffix = diceType.match(/d\d+/)[0]; // This matches "d8", "d10", etc.
if (currentHitDice < diceCount) {
console.log("update button text.")
hitDiceButton.textContent = `${currentHitDice}${diceSuffix}`;
}
}
updateContent()
}
function addHalfHitDiceOnRest() {
const hitDiceLabel = document.getElementById("hitDiceLabel");
// Get the current number of available hit dice from the text content
let currentHitDice = parseInt(hitDiceLabel.textContent.trim(), 10);
let maxHitDice = parseInt(hitDiceLabel.getAttribute('max'))
console.log(maxHitDice)
// Check if currentHitDice is a valid number
if (!isNaN(currentHitDice)) {
console.log("here")
// Add back half of the used hit dice (rounded down) but no less than 1
let hitDiceToAdd = Math.max(Math.floor(maxHitDice / 2), 1);
let newHitDice = currentHitDice + hitDiceToAdd
if (newHitDice > maxHitDice) {
hitDiceLabel.textContent = maxHitDice
}
else{
hitDiceLabel.textContent = newHitDice
}
}
updateContent()
}
// Function to update max hit dice based on character level
function updateHitDiceMax() {
console.log("updating hit dice max.")
const characterLevel = document.getElementById("characterLevel");
const hitDiceLabel = document.getElementById("hitDiceLabel");
// Parse the level value from the character level element
const level = parseInt(characterLevel.innerText.trim(), 10);
// Check if level is a valid number greater than zero
if (!isNaN(level) && level > 0) {
// Set the max attribute on hitDiceLabel to the character level
hitDiceLabel.setAttribute("max", level);
} else {
showErrorModal("Invalid level: Please enter a valid level number.");
}
}
// Function to update the hit dice value with the Constitution modifier
function updateHitDiceValue() {
const constitutionScore = document.getElementById("constitutionScore");
const hitDiceLabel = document.getElementById("hitDiceLabel");
// Parse the Constitution score and calculate modifier
const conScore = parseInt(constitutionScore.innerText.trim(), 10);
if (!isNaN(conScore)) {
const conModifier = calculateAbilityModifier(conScore);
// Extract the number of dice (x in xdy) from the data-dice-type attribute
const diceType = hitDiceLabel.getAttribute("data-dice-type");
const diceCountMatch = diceType.match(/^(\d+)d/);
if (diceCountMatch) {
const diceCount = parseInt(diceCountMatch[1], 10);
const totalModifier = conModifier * diceCount;
// Update the value attribute and label text with the calculated modifier
hitDiceLabel.setAttribute("value", totalModifier);
} else {
showErrorModal("Invalid dice format in data-dice-type.");
}
} else {
showErrorModal("Invalid Constitution score: Please enter a valid number.");
}
}
// Function to update AC
function updateAC() {
let finalAC;
const AcDiv = document.getElementById('AC')
const dexLabel = findAbilityScoreLabel("DEX");
const dexScore = parseInt(dexLabel.getAttribute("value"), 10);
let shieldMod = equippedShield ? equippedShield.armor_class.base || 0 : 0;
if (equippedArmor) {
// Use equipped armor's base AC and apply Dex mod up to the limit
let effectiveDexMod = 0;
if (equippedArmor.armor_class.dex_bonus === true){
if (dexScore > equippedArmor.armor_class.max_bonus){
effectiveDexMod = equippedArmor.armor_class.max_bonus || dexScore;
}
else{
effectiveDexMod = dexScore;
}
}
else{
}
finalAC = equippedArmor.armor_class.base + effectiveDexMod + shieldMod;
} else {
// Use base AC and full Dex modifier if no armor is equipped
finalAC = baseAC + dexScore + shieldMod ;
}
let conditionsSet
let HasteBonus = 0
conditionTrackerDiv = document.getElementById('conditionTracker');
const conditionSpans = conditionTrackerDiv.querySelectorAll('.condition-pill span');
conditionsSet = new Set();
// Create a map of condition values
conditionSpans.forEach(span => {
const value = span.getAttribute('value');
if (value) {
conditionsSet.add(value)
}
});
if (conditionsSet) {
console.log(conditionsSet)
if (conditionsSet.has('haste')){
HasteBonus = 2;
}
}
// Add AC bonuses from characterStatBonuses
const acBonuses = characterStatBonuses.combatStats.AC.bonuses || [];
const acBonusTotal = acBonuses.reduce((total, bonus) => total + bonus.value, 0);
finalAC += acBonusTotal + HasteBonus;
// Update the AC display
AcDiv.textContent = finalAC
sendDMUpdatedStatsDebounced()
}
function handleXPChange(event){
const inputElement = event.target;
console.log(inputElement)
if (inputElement === document.getElementById('playerXP')) {
console.log("XP Changed")
const characterXp = document.getElementById('playerXP').textContent
const { level: characterLevel, proficiencyBonus: characterProficiencyBonus } = calculateLevelFromXp(characterXp);
document.getElementById('characterLevel').textContent = characterLevel;
document.getElementById('profBonus').textContent = characterProficiencyBonus;
} else if (inputElement === document.getElementById('characterLevel')) {
console.log("Level Changed")
// Handle changes in level input
const characterNewLevel = parseInt(document.getElementById('characterLevel').textContent);
const newXP = calculateXPFromLevel(characterNewLevel)
const {proficiencyBonus: characterProficiencyBonus } = calculateLevelFromXp(newXP)
document.getElementById('playerXP').textContent = newXP;
document.getElementById('profBonus').textContent = characterProficiencyBonus;
}
updateSkillModifier()
updateSaveModifier()
updateAllToHitDice();
updateAllSpellDCs();
updateAllSpellDamageDice();
updateSpellDCHeader()
updateHitDiceMax()
updateAdjustmentValues()
const spellCastingAbility = document.querySelector('.spellcasting-dropdown').value;
updateSpelltoHitDice(spellCastingAbility)
}
// function to calculate the level Xp from the level they just changed too and set the xp value to the lowest.
function calculateXPFromLevel(level) {
// Find the corresponding XP for the given level in the xpLevels array
for (const levelData of xpLevels) {
if (level === levelData.level) {
return levelData.xp;
}
}
return 0; // Default to 0 XP if the level is not found in the array
}
function calculateLevelFromXp(xp) {
let level = 1;
let proficiencyBonus = 2;
for (const levelData of xpLevels) {
if (xp >= levelData.xp) {
level = levelData.level;
proficiencyBonus = levelData.proficiencyBonus;
} else {
break;
}
}
return { level, proficiencyBonus };
}
async function linkPlayerMini() {
let playerMini = await TS.creatures.getSelectedCreatures();
console.log(playerMini);
let characterPicture = document.getElementById("characterPicture");
let positions = {};
if (playerMini.length > 0) {
characterPicture.replaceChildren();
const firstMini = playerMini[0]; // Get the first selected creature
TS.creatures.getMoreInfo([firstMini]).then((monsterInfos) => {
for (let monsterInfo of monsterInfos) {
console.log(monsterInfo);
positions[monsterInfo.id] = monsterInfo.position;
const playerCharacterId = monsterInfo.id
console.log(playerCharacterId)
TS.contentPacks.findBoardObjectInPacks(monsterInfo.morphs[monsterInfo.activeMorphIndex].boardAssetId, contentPacks).then((foundContent) => {
let boardObject = foundContent.boardObject;
TS.contentPacks.createThumbnailElementForBoardObject(boardObject, 128).then((thumbnail) => {
// Set the display property of the generated span element to "block"
thumbnail.style.display = "block";
// Append the thumbnail directly to the "characterPicture" div
characterPicture.append(thumbnail);
});
});
}
});
}
}
//Function to calculate ability Score based on D&D 5e logic
function calculateAbilityModifier(abilityScore) {
return Math.floor((abilityScore - 10) / 2);
}
function parseButtonText(text) {
// Check if the text starts with a '+' or '-'
if (text.startsWith('+') || text.startsWith('-')) {
// Extract the number after the '+' or '-'
const extractedNumber = parseInt(text.substring(1), 10);
return isNaN(extractedNumber) ? 0 : extractedNumber;
} else {
// If no '+' or '-' sign, parse the entire text
const parsedNumber = parseInt(text, 10);
return isNaN(parsedNumber) ? 0 : parsedNumber;
}
}
//Changes the playerCharacters Ability modifier based on their score.
function handleAbilityScoreChange(event) {
const scoreElement = event.target;
const newValue = parseInt(scoreElement.textContent, 10);
if (!isNaN(newValue)) {
const abilityModifier = calculateAbilityModifier(newValue);
// Find the parent container
const container = scoreElement.parentElement;
// Find the button and label within the container
const button = container.querySelector('.actionButton');
const label = container.querySelector('.actionButtonLabel');
// Update button text content and label value
if (abilityModifier > 0) {
button.textContent = `+${abilityModifier}`;
label.setAttribute('value', abilityModifier);
} else {
button.textContent = `${abilityModifier}`;
label.setAttribute('value', abilityModifier);
}
// Update the ability score if the user entered a valid value
scoreElement.textContent = newValue;
updateAdjustmentValues()
updateSkillModifier();
updateSaveModifier();
updateAllToHitDice();
updateAllSpellDCs();
updateAllSpellDamageDice();
updateSpellDCHeader()
updateHitDiceValue()
updateAC();
const spellCastingAbility = document.querySelector('.spellcasting-dropdown').value;
updateSpelltoHitDice(spellCastingAbility)
}
}
function addBonus(category, key, value) {
console.log(`Adding bonus: ${category} -> ${key}`, value);
if (typeof value.value === "string") {
const abilityScoreLabel = findAbilityScoreLabel(value.value);
const abilityScoreValue = parseInt(abilityScoreLabel.getAttribute('value')) || 0;
value.value = abilityScoreValue;
}
if (characterStatBonuses[category] && characterStatBonuses[category][key]) {
characterStatBonuses[category][key].bonuses.push(value);
updateDerivedStats(category); // Call a category-specific update function
} else {
console.error(`Invalid category or key: ${category} -> ${key}`);
}
}
function removeBonus(category, key, value) {
if (typeof value.value === "string") {
const abilityScoreLabel = findAbilityScoreLabel(value.value);
const abilityScoreValue = parseInt(abilityScoreLabel.getAttribute('value')) || 0;
value.value = abilityScoreValue;
}
if (characterStatBonuses[category] && characterStatBonuses[category][key]) {
const bonuses = characterStatBonuses[category][key].bonuses;
// Find the index of the bonus object by matching its properties
const index = bonuses.findIndex(
(bonus) => bonus.source === value.source && bonus.value === value.value
);
if (index !== -1) {
bonuses.splice(index, 1);
updateDerivedStats(category); // Call a category-specific update function
} else {
console.warn(`Bonus not found for removal:`, value);
}
} else {
console.error(`Invalid category or key: ${category} -> ${key}`);
}
}
// Helper function to update stats for specific categories
function updateDerivedStats(category) {
switch (category) {
case "senses":
updatePassives(); // Recalculate senses-related stats
break;
case "skills":
updateSkillModifier();
break;