forked from Freyavf/arcdps_top_stats_parser
-
Notifications
You must be signed in to change notification settings - Fork 8
/
TW5_parse_top_stats_tools.py
5952 lines (5188 loc) · 251 KB
/
TW5_parse_top_stats_tools.py
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
#!/usr/bin/env python3
# parse_top_stats_tools.py contains tools for computing top stats in arcdps logs as parsed by Elite Insights.
# Copyright (C) 2021 Freya Fleckenstein
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from cgi import test
from dataclasses import dataclass,field
import os.path
from os import listdir
import sys
from enum import Enum
import importlib
import xlrd
from xlutils.copy import copy
import json
import jsons
import math
import requests
import datetime
import gzip
import csv
from collections import OrderedDict
from GW2_Color_Scheme import ProfessionColor
try:
import Guild_Data
except ImportError:
Guild_Data = None
def set_guild_data(guild_data):
Guild_Data = guild_data
debug = False # enable / disable debug output
class StatType(Enum):
TOTAL = 1
CONSISTENT = 2
AVERAGE = 3
LATE_PERCENTAGE = 4
SWAPPED_PERCENTAGE = 5
PERCENTAGE = 6
class BuffGenerationType(Enum):
SQUAD = 1
GROUP = 2
OFFGROUP = 3
SELF = 4
# This class stores information about a player. Note that a different profession will be treated as a new player / character.
@dataclass
class Player:
account: str # account name
name: str # character name
profession: str # profession name
num_fights_present: int = 0 # the number of fight the player was involved in
num_enemies_present: int = 0 # the number of enemies the player was involved with
num_allies_supported: int = 0 # the number of allies the player supported
num_allies_group_supported: int = 0 # the number of allies in group the player was involved in
attendance_percentage: float = 0. # the percentage of fights the player was involved in out of all fights
duration_fights_present: int = 0 # the total duration of all fights the player was involved in, in s
duration_active: int = 0 # the total duration a player was active (alive or down)
duration_in_combat: int = 0 # the total duration a player was in combat (taking/dealing dmg)
swapped_build: bool = False # a different player character or specialization with this account name was in some of the fights
# fields for all stats defined in config
consistency_stats: dict = field(default_factory=dict) # how many times did this player get into top for each stat?
total_stats: dict = field(default_factory=dict) # what's the total value for this player for each stat?
total_stats_group: dict = field(default_factory=dict) # what's the total value for this player for each stat for their group?
total_stats_self: dict = field(default_factory=dict) # what's the total value for this player for each stat for self?
average_stats: dict = field(default_factory=dict) # what's the average stat per second for this player? (exception: deaths are per minute)
portion_top_stats: dict = field(default_factory=dict) # what percentage of fights did this player get into top for each stat, in relation to the number of fights they were involved in?
stats_per_fight: list = field(default_factory=list) # what's the value of each stat for this player in each fight?
#fields for wt dps per enemy
wt_dps_enemies: list = field(default_factory=list) # list of enemies present by fight
wt_dps_duration: list = field(default_factory=list) # list of enemies present by fight
wt_dps_damage: list = field(default_factory=list) # list of enemies present by fight
def initialize(self, config):
self.total_stats = {key: 0 for key in config.stats_to_compute}
self.total_stats_group = {key: 0 for key in config.stats_to_compute}
self.total_stats_self = {key: 0 for key in config.stats_to_compute}
self.average_stats = {key: 0 for key in config.stats_to_compute}
self.consistency_stats = {key: 0 for key in config.stats_to_compute}
self.portion_top_stats = {key: 0 for key in config.stats_to_compute}
# This class stores information about a fight
@dataclass
class Fight:
skipped: bool = False
duration: int = 0
total_stats: dict = field(default_factory=dict) # what's the over total value for the whole squad for each stat in this fight?
enemies: int = 0
enemies_Red: int = 0
enemies_Blue: int = 0
enemies_Green: int = 0
enemies_Unk: dict = field(default_factory=dict) #enemy unknown teamID and count of enemy
allies: int = 0
squad: int = 0
notSquad: int = 0
kills: int = 0
start_time: str = ""
enemy_squad: dict = field(default_factory=dict) #profession and count of enemies
enemy_Dps: dict = field(default_factory=dict) #enemy name and amount of damage output
squad_Dps: dict = field(default_factory=dict) #squad player name and amount of damage output
enemy_skill_dmg: dict = field(default_factory=dict) #enemy skill_name and amount of damage output
squad_skill_dmg: dict = field(default_factory=dict) #squad skill_name and amount of damage output
squad_spike_dmg: dict = field(default_factory=dict) #squad skill_name and amount of damage output
# This class stores the configuration for running the top stats.
@dataclass
class Config:
num_players_listed: int = 0 # How many players will be listed who achieved top stats most often for each stat?
num_players_considered_top_percentage: float = 0. # % of players considered to be "top" in each fight for each stat?
min_attendance_portion_for_percentage: float = 0. # For what portion of all fights does a player need to be there to be considered for "percentage" awards?
min_attendance_portion_for_late: float = 0. # For what portion of all fights does a player need to be there to be considered for "late but great" awards?
min_attendance_portion_for_buildswap: float = 0. # For what portion of all fights does a player need to be there to be considered for "jack of all trades" awards?
min_attendance_percentage_for_average: float = 0. # For what percentage of all fights does a player need to be there to be considered for "jack of all trades" awards?
portion_of_top_for_total: float = 0. # What portion of the top total player stat does someone need to reach to be considered for total awards?
portion_of_topDamage_for_total: float = 0. # What portion of the top total player stat does someone need to reach to be considered for total awards?
portion_of_top_for_consistent: float = 0. # What portion of the total stat of the top consistent player does someone need to reach to be considered for consistency awards?
portion_of_top_for_percentage: float = 0. # What portion of the consistency stat of the top consistent player does someone need to reach to be considered for percentage awards?
portion_of_top_for_late: float = 0. # What portion of the percentage the top consistent player reached top does someone need to reach to be considered for late but great awards?
portion_of_top_for_buildswap: float = 0. # What portion of the percentage the top consistent player reached top does someone need to reach to be considered for jack of all trades awards?
min_allied_players: int = 0 # minimum number of allied players to consider a fight in the stats
min_fight_duration: int = 0 # minimum duration of a fight to be considered in the stats
min_enemy_players: int = 0 # minimum number of enemies to consider a fight in the stats
summary_title: str = ""
summary_creator: str = ""
charts: bool = False # produce charts for stats_to_compute
include_comp_and_review: bool = True # include completed and reviewed fights in charts
check_for_unknown_team_ids: bool = True # print dict of unknown team ids
damage_overview_only: bool = False # if overview_only = True, do not build individual tables & charts for stats in overview table for Offensive
defensive_overview_only: bool = False # if overview_only = True, do not build individual tables & charts for stats in overview table for Defensive
ignore_role_in_skill_cast: bool = False
use_PlenBot: bool = False
PlenBotPath: str = ""
stat_names: dict = field(default_factory=dict)
profession_abbreviations: dict = field(default_factory=dict)
empty_stats: dict = field(default_factory=dict)
stats_to_compute: list = field(default_factory=list)
aurasIn_to_compute: list = field(default_factory=list)
aurasOut_to_compute: list = field(default_factory=list)
defenses_to_compute: list = field(default_factory=list)
buff_ids: dict = field(default_factory=dict)
buffs_stacking_duration: list = field(default_factory=list)
buffs_stacking_intensity: list = field(default_factory=list)
buff_abbrev: dict = field(default_factory=dict)
condition_ids: dict = field(default_factory=dict)
auras_ids: dict = field(default_factory=dict)
#Stats to include in the general overview summary
general_overview_stats = [
'deaths', 'dmg', 'dmg_taken', 'rips', 'cleanses',
'stability', 'protection', 'aegis', 'might',
'fury', 'resistance', 'resolution', 'quickness',
'swiftness', 'alacrity', 'vigor', 'regeneration',
'heal', 'barrier'
]
#Control Effects Tracking
squad_offensive = {}
squad_Control = {}
squad_Control['appliedCounts'] = {}
squad_Control['totalDuration'] = {}
squad_Control['fightTime'] = {}
enemy_Control = {}
enemy_Control_Player = {}
battle_Standard = {}
#Spike Damage Tracking
squad_damage_output = {}
#Downed Healing from Instant Revive skills
downed_Healing = {}
#Aura Tracking
auras_TableOut = {}
auras_TableIn = {}
#Uptime Tracking
uptime_Table = {}
uptime_Buff_Ids = {1122: 'stability', 717: 'protection', 743: 'aegis', 740: 'might', 725: 'fury', 26980: 'resistance', 873: 'resolution', 1187: 'quickness', 719: 'swiftness', 30328: 'alacrity', 726: 'vigor', 718: 'regeneration'}
#uptime_Buff_Names = { 'stability': 1122, 'protection': 717, 'aegis': 743, 'might': 740, 'fury': 725, 'resistance': 26980, 'resolution': 873, 'quickness': 1187, 'swiftness': 719, 'alacrity': 30328, 'vigor': 726, 'regeneration': 718}
#Composite Uptime Tracking
partyUptimes = {}
squadUptimes = {}
squadUptimes['FightTime'] = 0
squadUptimes['buffs'] = {}
#Stacking Buffs Tracking
stacking_uptime_Table = {}
#Firebrand Pages Tracking
FB_Pages = {}
#Personal Buff Tracking
buffs_personal = {}
#Profession Skills Tracking
prof_role_skills = {}
#Skill Dictionary from all Fights
skill_Dict = {}
#Calculate On Tag Death Variables
On_Tag = 600
Run_Back = 5000
Death_OnTag = {}
#Collect Account Attendance Data
Attendance = {}
#Collect DPS Box Plot Data
DPS_List = {}
DPS_List['acct'] = {}
DPS_List['name'] = {}
DPS_List['prof_name'] = {}
DPS_List['prof'] = {}
#Collect CPS Box Plot Data
CPS_List = {}
CPS_List['acct'] = {}
CPS_List['name'] = {}
CPS_List['prof_name'] = {}
CPS_List['prof'] = {}
#Collect CPS Box Plot Data
SPS_List = {}
SPS_List['acct'] = {}
SPS_List['name'] = {}
SPS_List['prof_name'] = {}
SPS_List['prof'] = {}
#Collect CPS Box Plot Data
HPS_List = {}
HPS_List['acct'] = {}
HPS_List['name'] = {}
HPS_List['prof_name'] = {}
HPS_List['prof'] = {}
#Calculate DPSStats Variables
DPSStats = {}
#Collect MOA Info
MOA_Targets = {}
MOA_Casters = {}
#Collect Commander Tags:
Cmd_Tags = {}
#Collect Player Created Minions
minion_Data = {}
#Collect Total Boon Generation Scores
Total_Boon_Chart = {}
#Collect Outgoing Healing by target
OutgoingHealing = {}
#Collect FightLinks on DPS Reports
Fight_Logs = []
#WvW Specific Buffs
wvwBuffs = {
32699: 'PresenceOfTheKeep',
33794: 'GuildObjectiveAuraI',
32477: 'GuildObjectiveAuraII',
33249: 'GuildObjectiveAuraIII',
33375: 'GuildObjectiveAuraIV',
33791: 'GuildObjectiveAuraV',
32928: 'GuildObjectiveAuraVI',
33010: 'GuildObjectiveAuraVII',
33594: 'GuildObjectiveAuraVIII',
14772: 'MinorBorderlandsBloodlust',
14773: 'MajorBorderlandsBloodlust',
14774: 'SuperiorBorderlandsBloodlust',
20893: 'SpeedOfTheBattlefield'
}
squadWvWBuffs = {}
enemyWvWBuffs = {}
#Collect PlenBot Log Links for summary
Plen_Bot_Logs={}
Session_Fights=[]
#Capture High Scores for stats
HighScores={}
#Total Skill Damage Tracking
total_Squad_Skill_Dmg = {}
total_Enemy_Skill_Dmg = {}
#Player Skill Damage Tracking
Player_Damage_by_Skill = {}
#Damage Modifiers Outgoing and Incoming
squadDamageMods = {}
profModifiers = {}
profModifiers['buffList'] = []
profModifiers['Professions'] = {}
modifierMap={}
modifierMap['Incoming'] = {}
modifierMap['Outgoing'] = {}
modifierMap['Incoming']['Shared'] = {}
modifierMap['Incoming']['Prof'] = {}
modifierMap['Outgoing']['Shared'] = {}
modifierMap['Outgoing']['Prof'] = {}
#Capture Condition Uptime Data for player, party and squad
conditionData = {}
conditionDataGroups = {}
conditionDataSquad = {}
ResistanceData = {}
ResistanceData['Squad'] = {}
ResistanceData['Group'] = {}
#Capture Relic Buff and Relic Skill Data
relic_Stacks = {
"Relic Player Buff (Dragonhunter / Isgarren / Peitha)": 999,
"Relic of the Dragonhunter": 999,
"Relic of the Aristocracy": 5,
"Relic of the Monk": 10,
"Relic of the Thief": 5,
"Relic of the Daredevil": 3,
"Relic of the Herald": 10,
"Relic of the Scourge": 10,
"Relic of Isgarren": 999,
"Mabon's Strength": 10,
"Relic of Peitha": 999,
"Relic of Vass": 3,
"Relic of Nourys": 10,
'Superior Sigil of Bounty': 25,
'Superior Sigil of Corruption': 25,
'Superior Sigil of Cruelty': 25,
'Superior Sigil of Benevolence': 25,
'Superior Sigil of Life': 25,
'Superior Sigil of Bloodlust': 25,
'Superior Sigil of Perception': 25,
'Superior Sigil of Momentum': 25,
'Superior Sigil of the Stars': 25
}
usedRelicBuff = {}
usedRelicSkill = {}
RelicDataBuffs = {}
RelicDataSkills = {}
#fetch Guild Data and Check Guild Status function
#members: Dict[str, Any] = {}
members: dict = field(default_factory=dict)
API_response = ""
#If Guild_Data exists and Guild_ID is a dict, ask which guild to use. Otherwise, use the guild data in Guild_Data variables
if Guild_Data:
if type(Guild_Data.Guild_ID) == dict:
print("Guild Keys Available: ", Guild_Data.Guild_ID.keys())
inputGuild = input('What Guild key for this session?\n')
Guild_ID = Guild_Data.Guild_ID[inputGuild]
API_Key = Guild_Data.API_Key[inputGuild]
print("-----=====:Guild ID:", Guild_ID)
print("-----=====:Guild API:", API_Key)
api_url = "https://api.guildwars2.com/v2/guild/"+Guild_ID+"/members?access_token="+API_Key
else:
Guild_ID = Guild_Data.Guild_ID
API_Key = Guild_Data.API_Key
api_url = "https://api.guildwars2.com/v2/guild/"+Guild_ID+"/members?access_token="+API_Key
#Fetch Guild Data from API
try:
response = requests.get(api_url, timeout=5)
if response.status_code == requests.codes.ok:
response = requests.get(api_url)
members = json.loads(response.text)
print("response code: "+str(response.status_code))
API_response = response.status_code
except (requests.exceptions.HTTPError, requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
print(f"API Failure - Unable to establish connection: {e}.")
members = {}
API_response = " "
except Exception as e:
print(f"Failure - Unknown error occurred: {e}.")
members = {}
API_response = " "
else:
members = {}
API_response = " "
def find_member(guild_data: list, name: str) -> str:
"""
Find the rank of a player in the guild.
Args:
guild_data (list): The list of guild members.
name (str): The player name to find.
Returns:
str: The rank of the player if found, otherwise "--==Non Member==--".
"""
for member in guild_data:
if member["name"] == name:
return member["rank"]
return "--==Non Member==--"
#High Score capture
def update_high_score(stat: str, key: str, value: float) -> None:
"""Update the high scores for a given stat.
Args:
stat (str): The stat to update.
key (str): The key to update or add.
value (float): The value to update or add.
"""
if len(HighScores[stat]) < 5:
HighScores[stat][key] = value
elif value > min(HighScores[stat].values()):
lowest_key = min(HighScores[stat], key=HighScores[stat].get)
del HighScores[stat][lowest_key]
HighScores[stat][key] = value
#define subtype based on consumables
#consumable dictionaries
Heal_Utility = {
53374: "Potent Lucent Oil",
53304: "Enhanced Lucent Oil",
21827: "Toxic Maintenance Oil",
34187: "Peppermint Oil",
38605: "Magnanimous Maintenance Oil",
25879: "Bountiful Maintenance Oil",
9968: "Master Maintenance Oil"
}
Heal_Food = {
57276: "Bowl of Spiced Fruit Salad",
57100: "Bowl of Fruit Salad with Mint Garnish",
69105: "Bowl of Mists-Infused Fruit Salad with Mint Garnish",
26529: "Delicious Rice Ball",
57299: "Plate of Peppercorn-Spiced Poultry Aspic",
57310: "Mint Creme Brulee"
}
Cele_Food = {
57165: "Spherified Peppercorn-Spiced Oyster Soup",
57374: "Spherified Clove-Spiced Oyster Soup",
57037: "Spherified Sesame Oyster Soup",
57201: "Spherified Oyster Soup with Mint Garnish",
69124: "Mists-Infused Spherified Peppercorn-Spiced Oyster Soup",
19451: "Dragon's Revelry Starcake"
}
DPS_Food= {
57051: "Peppercorn-Crusted Sous-Vide Steak",
69141: "Mists-Infused Peppercorn-Crusted Sous-Vide Steak",
57244: "Cilantro Lime Sous-Vide Steak",
57260: "Plate of Peppercorn-Spiced Coq Au Vin",
57129: "Spiced Peppercorn Cheesecake"
}
DPS_Utility = {
9963: "Superior Sharpening Stone",
34657: "Compact Hardened Sharpening Stone",
25882: "Furious Sharpening Stone",
33297: "Writ of Masterful Strength",
34211: "Tin of Fruitcake"
}
def find_sub_type(player: dict) -> str:
"""Determine the subtype of a given player based on their profession, consumables, and stats."""
support_professions = ["Tempest", "Scrapper", "Mechanist", "Druid", "Chronomancer", "Vindicator", "Firebrand", "Spectre", "Spellbreaker", "Willbender", "Guardian", "Berserker", "Scourge"]
if player["profession"] not in support_professions:
# Calculate total damage, power damage, and condi damage
total_damage = 0
power_damage = 0
condi_damage = 0
for target in player["dpsTargets"]:
total_damage += target[0]["damage"]
power_damage += target[0]["powerDamage"]
condi_damage += target[0]["condiDamage"]
# If a player is predominantly condi damage
if condi_damage > power_damage:
return "Condi"
# Assume DPS on a non-support profession
return "Dps"
# Calculate critical hit percentage
critical_count = sum([stats[0]["criticalRate"] for stats in player["statsTargets"]])
critable_count = sum([stats[0]["critableDirectDamageCount"] for stats in player["statsTargets"]])
crit_percentage = critical_count / critable_count if critable_count else 0
# Search consumables for Cele, Heal, or DPS food/utility
if "consumables" in player:
for item in player["consumables"]:
if item["id"] in Cele_Food:
return "Cele"
if item["id"] in Heal_Food or item["id"] in Heal_Utility:
return "Support"
if item["id"] in DPS_Food or item["id"] in DPS_Utility:
return "Dps"
# Only healers should have a crit % lower than 40%
if crit_percentage <= 0.4:
return "Support"
# If all other detection fails, fallback to assuming DPS like before
return "Dps"
#end define subtype based on consumables
# prints output_string to the console and the output_file, with a linebreak at the end
def print_to_file(file, string):
"""Prints the given string to the given file and console."""
print(string)
file.write(string + "\n")
# JEL - format a number with commas every thousand
def my_value(number: int) -> str:
"""Format a number with commas every thousand."""
return "{:,}".format(number)
def get_skill_casts_by_role(player_data, name, player_prof_role, playerRoleActiveTime, skill_map):
if player_prof_role not in prof_role_skills:
prof_role_skills[player_prof_role] = {'castTotals': {}, 'player': {}}
if name not in prof_role_skills[player_prof_role]['player']:
prof_role_skills[player_prof_role]['player'][name] = {
'ActiveTime': playerRoleActiveTime,
'Fights': 1,
'Skills': {}
}
else:
prof_role_skills[player_prof_role]['player'][name]['Fights'] += 1
prof_role_skills[player_prof_role]['player'][name]['ActiveTime'] += playerRoleActiveTime
if 'rotation' in player_data:
for rotation_skill in player_data['rotation']:
skill_id = str(rotation_skill['id'])
skill_name = skill_map['s'+skill_id]['name']
skill_auto = skill_map['s'+skill_id]['autoAttack']
#skip unknown skills:
if skill_name.isnumeric():
continue
# Exclude skills that are not relevant to role performance
excluded_skills = {
# Downed skills
'9149': 'Wrath',
'9096': 'Wave of Light',
'9095': 'Symbol of Judgement',
'28180': 'Essence Sap',
'27063': 'Forceful Displacement',
'27792': 'Vengeful Blast',
'14390': 'Throw Rock',
'14515': 'Hammer Toss',
'14391': 'Vengeance',
'5820': 'Throw Junk',
'5962': 'Grappling Line',
'5963': 'Booby Trap',
'12486': 'Throw Dirt',
'12485': 'Thunderclap',
'12515': 'Lick Wounds',
'13003': 'Trail of Knives',
'13138': 'Venomous Knife',
'13140': 'Shadow Escape',
'13033': 'Smoke Bomb',
# Resurrect skills
'1006': 'Resurrect',
'1066': 'Resurrect',
'1175': 'Bandage',
# Siege golem skills
'14627': 'Punch',
'14709': 'Rocket Punch',
'14713': 'Rocket Punch',
'63185': 'Rocket Punch',
'1656': "Whirling Assualt",
'14639': "Whirling Assualt",
'14642': 'Eject',
# Generic skills to exclude
'14601': 'Turn Left',
'14600': 'Turn Right',
'23284': 'Weapon Draw',
'23285': 'Weapon Stow',
'-2': 'Weapon Swap',
'58083': 'Lance',
'20285': 'Fire Hollowed Boulder',
'9284': 'Flame Blast',
'23275': 'Dodge',
'54877': 'Chain Pull',
'54941': 'Chain Pull',
'54953': 'Chain Pull',
'21615': '((276158))',
'23267': '((290194))',
'18792': '((300969))',
'18793': '((300969))',
'25533': '((300969))',
'27927': '((300969))',
'30765': '((300969))',
'34797': '((300969))',
}
if skill_id in excluded_skills:
continue
#skip auto attack skills
if skill_auto and skill_id != '31796':
continue
#skip node gathering and finishers
if 'Gather' in skill_name or 'Finisher' in skill_name or 'Harvest Plants' in skill_name or 'Unbound Magic' in skill_name:
continue
#skip siege deployment
if 'Deploy' in skill_name and 'Jade Sphere' not in skill_name:
continue
skill_casts = 0
for skill_usage in rotation_skill['skills']:
# When the duration equals the timeLost, the skill was interrupted or cancelled
if skill_usage['duration'] == 0 or skill_usage['duration'] != -skill_usage['timeGained']:
skill_casts += 1
prof_role_skills[player_prof_role]['player'][name]['Skills'][skill_id] = prof_role_skills[player_prof_role]['player'][name]['Skills'].get(skill_id, 0) + skill_casts
prof_role_skills[player_prof_role]['castTotals'][skill_id] = prof_role_skills[player_prof_role]['castTotals'].get(skill_id, 0) + skill_casts
# fills a Config with the given input
def fill_config(config_input):
config = Config()
config.num_players_listed = config_input.num_players_listed
config.num_players_considered_top = config_input.num_players_considered_top_percentage/100
config.player_sorting_stat_type = config_input.player_sorting_stat_type or 'total'
config.min_attendance_portion_for_percentage = config_input.attendance_percentage_for_percentage/100.
config.min_attendance_portion_for_late = config_input.attendance_percentage_for_late/100.
config.min_attendance_portion_for_buildswap = config_input.attendance_percentage_for_buildswap/100.
config.min_attendance_percentage_for_average = config_input.attendance_percentage_for_average
config.min_attendance_percentage_for_top = config_input.attendance_percentage_for_top
config.portion_of_top_for_consistent = config_input.percentage_of_top_for_consistent/100.
config.portion_of_top_for_total = config_input.percentage_of_top_for_total/100.
config.portion_of_topDamage_for_total = config_input.percentage_of_topDamage_for_total/100.
config.portion_of_top_for_average = config_input.percentage_of_top_for_average/100.
config.portion_of_top_for_percentage = config_input.percentage_of_top_for_percentage/100.
config.portion_of_top_for_late = config_input.percentage_of_top_for_late/100.
config.portion_of_top_for_buildswap = config_input.percentage_of_top_for_buildswap/100.
config.min_allied_players = config_input.min_allied_players
config.min_fight_duration = config_input.min_fight_duration
config.min_enemy_players = config_input.min_enemy_players
config.summary_title = config_input.summary_title
config.summary_creator = config_input.summary_creator
config.stat_names = config_input.stat_names
config.profession_abbreviations = config_input.profession_abbreviations
config.stats_to_compute = config_input.stats_to_compute
config.aurasIn_to_compute = config_input.aurasIn_to_compute
config.aurasOut_to_compute = config_input.aurasOut_to_compute
config.defenses_to_compute = config_input.defenses_to_compute
config.empty_stats = {stat: -1 for stat in config.stats_to_compute}
config.empty_stats['time_active'] = -1
config.empty_stats['time_in_combat'] = -1
config.buff_abbrev["Stability"] = 'stability'
config.buff_abbrev["Protection"] = 'protection'
config.buff_abbrev["Aegis"] = 'aegis'
config.buff_abbrev["Might"] = 'might'
config.buff_abbrev["Fury"] = 'fury'
config.buff_abbrev["Superspeed"] = 'superspeed'
config.buff_abbrev["Stealth"] = 'stealth'
config.buff_abbrev["Hide in Shadows"] = 'HiS'
config.buff_abbrev["Regeneration"] = 'regeneration'
config.buff_abbrev["Resistance"] = 'resistance'
config.buff_abbrev["Resolution"] = 'resolution'
config.buff_abbrev["Quickness"] = 'quickness'
config.buff_abbrev["Swiftness"] = 'swiftness'
config.buff_abbrev["Alacrity"] = 'alacrity'
config.buff_abbrev["Vigor"] = 'vigor'
config.buff_abbrev["Illusion of Life"] = 'iol'
config.condition_ids = {720: 'Blinded', 721: 'Crippled', 722: 'Chilled', 727: 'Immobile', 742: 'Weakness', 791: 'Fear', 833: 'Daze', 872: 'Stun', 26766: 'Slow', 27705: 'Taunt', 30778: "Hunter's Mark", 738: 'Vulnerability'}
config.auras_ids = {5677: 'Fire', 5577: 'Shocking', 5579: 'Frost', 5684: 'Magnetic', 25518: 'Light', 39978: 'Dark', 10332: 'Chaos'}
config.charts = config_input.charts
config.check_for_unknown_team_ids = config_input.check_for_unknown_team_ids
config.include_comp_and_review = config_input.include_comp_and_review
config.damage_overview_only = config_input.damage_overview_only
config.defensive_overview_only = config_input.defensive_overview_only
config.use_PlenBot = config_input.use_PlenBot
config.PlenBotPath = config_input.PlenBotPath
config.ignore_role_in_skill_cast = config_input.ignore_role_in_skill_cast
return config
def reset_globals():
#Control Effects Tracking
global squad_offensive, squad_Control, enemy_Control, enemy_Control_Player, battle_Standard
squad_offensive = {}
squad_Control = {}
squad_Control['appliedCounts'] = {}
squad_Control['totalDuration'] = {}
squad_Control['activeSeconds'] = {}
enemy_Control = {}
enemy_Control_Player = {}
battle_Standard = {}
#Spike Damage Tracking
global squad_damage_output
squad_damage_output = {}
#Downed Healing from Instant Revive skills
global downed_Healing
downed_Healing = {}
#Aura Tracking
global auras_TableOut, auras_TableIn
auras_TableOut = {}
auras_TableIn = {}
#Uptime Tracking
global uptime_Table
uptime_Table = {}
#Stacking Buffs Tracking
global stacking_uptime_Table
stacking_uptime_Table = {}
#Personal Buff Tracking
global buffs_personal
buffs_personal = {}
#Profession Skills Tracking
global prof_role_skills
prof_role_skills = {}
#Skill Dictionary from all Fights
global skill_Dict
skill_Dict = {}
#Calculate On Tag Death Variables
global Death_OnTag
Death_OnTag = {}
#Collect Account Attendance Data
global Attendance
Attendance = {}
#Collect DPS Box Plot Data
global DPS_List
DPS_List = {}
DPS_List['acct'] = {}
DPS_List['name'] = {}
DPS_List['prof_name'] = {}
DPS_List['prof'] = {}
#Collect CPS Box Plot Data
global CPS_List
CPS_List = {}
CPS_List['acct'] = {}
CPS_List['name'] = {}
CPS_List['prof_name'] = {}
CPS_List['prof'] = {}
#Collect CPS Box Plot Data
global SPS_List
SPS_List = {}
SPS_List['acct'] = {}
SPS_List['name'] = {}
SPS_List['prof_name'] = {}
SPS_List['prof'] = {}
#Collect CPS Box Plot Data
global HPS_List
HPS_List = {}
HPS_List['acct'] = {}
HPS_List['name'] = {}
HPS_List['prof_name'] = {}
HPS_List['prof'] = {}
#Calculate DPSStats Variables
global DPSStats
DPSStats = {}
#Collect MOA Info
global MOA_Targets, MOA_Casters
MOA_Targets = {}
MOA_Casters = {}
#Collect Commander Tags:
global Cmd_Tags
Cmd_Tags = {}
def increase_top_x_reached(players, sorted_list, config, stat):
"""
For all players considered to be top in stat in this fight, increase
the number of fights they reached top by 1 (i.e. increase
consistency_stats[stat]).
Args:
players (list) = list of all players
sortedList (list)= list of player names+profession, stat_value sorted by stat value in this fight
config = configuration to use
stat (str) = stat that is considered
"""
# initialize variables
valid_values = 0
i = 0
last_value = 0
# special case for distance to tag
if stat == 'dist':
first_valid = True
while i < len(sorted_list) and (valid_values < (len(sorted_list) * config.num_players_considered_top) + 1 or sorted_list[i][1] == last_value):
if sorted_list[i][1] >= 0:
if first_valid:
first_valid = False
else:
players[sorted_list[i][0]].consistency_stats[stat] += 1
valid_values += 1
last_value = sorted_list[i][1]
i += 1
return
# special case for deaths
elif stat == 'deaths':
while i < len(sorted_list) and (valid_values < (len(sorted_list) * config.num_players_considered_top) or sorted_list[i][1] == last_value):
if sorted_list[i][1] < 0:
i += 1
continue
if sorted_list[i][1] == 0:
players[sorted_list[i][0]].consistency_stats[stat] += 1
last_value = sorted_list[i][1]
i += 1
valid_values += 1
return
# increase top stats reached for the first num_players_considered_top players
while i < len(sorted_list) and (valid_values < (len(sorted_list) * config.num_players_considered_top) or sorted_list[i][1] == last_value) and players[sorted_list[i][0]].total_stats[stat] > 0:
if sorted_list[i][1] < 0 or (sorted_list[i][1] == 0 and stat != 'dmg_taken'):
i += 1
continue
players[sorted_list[i][0]].consistency_stats[stat] += 1
last_value = sorted_list[i][1]
i += 1
valid_values += 1
return
def sort_players_by_value_in_fight(players, stat, fight_num):
"""
Sort the list of players by total value in stat for the given fight
Args:
players (list): list of all Players
stat (str): stat that is considered
fight_num: number of the fight that is considered
Returns:
list: List of player index and total stat value, sorted by total stat value
"""
decorated = [(player.stats_per_fight[fight_num][stat], i, player) for i, player in enumerate(players)]
if stat in ('dist', 'dmg_taken', 'deaths'):
decorated.sort()
else:
decorated.sort(reverse=True)
sorted_by_value = [(index, value) for value, index, _ in decorated]
return sorted_by_value
def sort_players_by_total(players, stat):
"""
Sort the list of players by total value in stat.
Args:
players (list): List of all Players.
stat (str): Stat that is considered.
Returns:
list: List of player index and total stat value, sorted by total stat value.
"""
decorated = [(player.total_stats[stat], i, player) for i, player in enumerate(players)]
decorated.sort(key=lambda x: x[0], reverse=stat not in ('dist', 'dmg_taken', 'deaths'))
sorted_by_total = [(i, total) for total, i, _ in decorated]
return sorted_by_total
# sort the list of players by consistency value in stat
def sort_players_by_consistency(players, stat_name):
"""Sort the list of players by consistency value in stat_name.
Args:
players (list[Player]): List of all Players.
stat_name (str): Stat that is considered.
Returns:
list[tuple[int, int]]: List of player index and consistency stat value, sorted by consistency stat value.
"""
decorated = [(player.consistency_stats[stat_name], i) for i, player in enumerate(players)]
decorated.sort(reverse=True)
return [(i, consistency) for consistency, i in decorated]
# sort the list of players by percentage value in stat
def sort_players_by_percentage(players, stat_name):
"""Sort the list of players by percentage value in stat_name.
Args:
players (list[Player]): List of all Players.
stat_name (str): Stat that is considered.
Returns:
list[tuple[int, float]]: List of player index and percentage stat value, sorted by percentage stat value.
"""
decorated = [(player.portion_top_stats[stat_name], i) for i, player in enumerate(players)]
decorated.sort(reverse=True)
return [(i, percentage) for percentage, i in decorated]
# sort the list of players by average value in stat
def sort_players_by_average(players, stat_name):
"""Sort the list of players by average value in stat_name.
Args:
players (list[Player]): List of all Players.
stat_name (str): Stat that is considered.
Returns:
list[tuple[int, float]]: List of player index and average stat value, sorted by average stat value.
"""
decorated = [(player.average_stats[stat_name], i) for i, player in enumerate(players)]
if stat_name in ['dist', 'dmg_taken', 'deaths']:
decorated.sort()
else:
decorated.sort(reverse=True)
return [(i, average) for average, i in decorated]
# list of player indices getting a consistency / total / average award
def get_top_players(players, config, stat, total_or_consistent_or_average):
"""
Get the players that have the top total, consistency, or average values in a given stat.
Args:
players (list[Player]): List of all Players.
config (Config): The configuration being used to determine top players.
stat (str): The stat that is being considered.
total_or_consistent_or_average (StatType): The type of stat that is being considered.
Returns:
list[int]: List of player indices that get a consistency / total / average award.
"""
percentage = 0.
sorted_index = []
percentage_config_name = {
StatType.TOTAL: 'portion_of_top_for_total' if stat != 'dmg' else 'portion_of_topDamage_for_total',
StatType.CONSISTENT: 'portion_of_top_for_consistent',
StatType.AVERAGE: 'portion_of_top_for_average' if stat != 'dmg' else 'portion_of_topDamage_for_total',