From f7fe0c615e16a9d7481ff6e622e3ed814fed3aba Mon Sep 17 00:00:00 2001 From: Mel Miller Date: Sun, 17 Jan 2021 12:58:02 -0500 Subject: [PATCH 01/11] Added a bunch of methods to fflogsapi to access v2 of the FFLogs API; these currently include a bunch of work functions related to calculating damage and card information which will need to be moved around for organizational purposes --- cardcalc.py | 2 + damagecalc.py | 0 fflogsapi.py | 572 ++++++++++++++++++++++++++++++++++++++++++++++++++ testing.py | 88 ++++++++ 4 files changed, 662 insertions(+) create mode 100644 damagecalc.py create mode 100644 fflogsapi.py create mode 100644 testing.py diff --git a/cardcalc.py b/cardcalc.py index 7d713a7..6ea2092 100644 --- a/cardcalc.py +++ b/cardcalc.py @@ -101,6 +101,8 @@ def fflogs_api(call, report, options={}): # The Ewer id#1001886 # The Spire id#1001887 """ +def card_to_string(card, start_time): + return '{} played {} on {} at {}'.format(card['source'], card['name'], card['target'], str(timedelta(milliseconds=(card['start']-start_time)))[2:11]) def card_type(guid): return { diff --git a/damagecalc.py b/damagecalc.py new file mode 100644 index 0000000..e69de29 diff --git a/fflogsapi.py b/fflogsapi.py new file mode 100644 index 0000000..a4c5c8e --- /dev/null +++ b/fflogsapi.py @@ -0,0 +1,572 @@ +""" +This contains code for pull requests from v2 of the FFLogs API +as required for damage and card calculations used in cardcalc +and damagecalc +""" + +from datetime import timedelta +import os + +from cardcalc import card_type, card_name, card_bonus + +# Make sure we have the requests library +try: + import requests +except ImportError: + raise ImportError("FFlogs parsing requires the Requests module for python." + "Run the following to install it:\n python -m pip install requests") + +try: + from requests_oauthlib import OAuth2Session +except ImportError: + raise ImportError("This requires the OAuth Lib extension to the Requests module for python.") + +try: + from oauthlib.oauth2 import BackendApplicationClient +except ImportError: + raise ImportError("This requires the OAuth Lib module for python.") + +from python_graphql_client import GraphqlClient + +FFLOGS_CLIENT_ID = os.environ['FFLOGS_CLIENT_ID'] +FFLOGS_CLIENT_SECRET = os.environ['FFLOGS_CLIENT_SECRET'] + +FFLOGS_OAUTH_URL = 'https://www.fflogs.com/oauth/token' +FFLOGS_URL = 'https://www.fflogs.com/api/v2/client' + +client = GraphqlClient(FFLOGS_URL) + +def get_bearer_token(): + client = BackendApplicationClient(client_id=client_id) + oauth = OAuth2Session(client=client) + token = oauth.fetch_token(token_url=FFLOGS_OAUTH_URL, client_id=client_id, client_secret=client_pass) + return token + +def request_fflogs_api(payload, token): + headers = { + 'Content-TYpe': 'application/json', + 'Authorization': 'Bearer {}'.format(token['access_token']), + } + response = requests.request('POST', FFLOGS_URL, data=payload, headers=headers) + + return response + +def call_fflogs_api(query, variables, token): + headers = { + 'Content-TYpe': 'application/json', + 'Authorization': 'Bearer {}'.format(token['access_token']), + } + data = client.execute(query=query, variables=variables, headers=headers) + + return data + +""" + The following are some standard methods for getting fight + data that will be used a lot +""" + +def get_fight_time(report, fight, token): + query = """ +query reportData($code: String!) { + reportData { + report(code: $code) { + fights { + id + startTime + endTime + } + } + } +} +""" + variables = {'code': report} + data = call_fflogs_api(query, variables, token) + fights = data['data']['reportData']['report']['fights'] + + for f in fights: + if f['id'] == fight: + return f + +def get_all_actors(report, start_time, end_time, token): + query = """ +query reportData($code: String!, $startTime: Float!, $endTime: Float) { + reportData { + report(code: $code) { + masterData { + pets: actors(type: "Pet") { + id + name + type + subType + petOwner + } + } + table: table(startTime: $startTime, endTime: $endTime) + } + } +}""" + + variables = { + 'code': report, + 'startTime': start_time, + 'endTime': end_time + } + data = call_fflogs_api(query, variables, token) + master_data = data['data']['reportData']['report']['masterData'] + table = data['data']['reportData']['report']['table'] + + pet_list = master_data['pets'] + composition = table['data']['composition'] + + players = {} + pets = {} + + for p in composition: + players[p['id']] = { + 'name': p['name'], + 'id': p['id'], + 'type': p['type'] + } + + for p in pet_list: + if p['petOwner'] in players: + pets[p['id']] = { + 'name': p['name'], + 'id': p['id'], + 'owner': p['petOwner'], + } + + return (players, pets) + +def get_card_play_events(report, start_time, end_time, token): + query = """ +query reportData($code: String!, $startTime: Float!, $endTime: Float!) { + reportData { + report(code: $code) { + cards: events( + startTime: $startTime, + endTime: $endTime + dataType: Buffs, + filterExpression: "ability.id in (1001877, 1001883, 1001886, 1001887, 1001876, 1001882, 1001884, 1001885)" + ) { + data + } + } + } +} +""" + + variables = { + 'code': report, + 'startTime': start_time, + 'endTime': end_time + } + data = call_fflogs_api(query, variables, token) + card_events = data['data']['reportData']['report']['cards']['data'] + + return card_events + +def get_cards_played(card_events, start_time, end_time): + cards = [] + + # Build list from events + for event in card_events: + # If applying the buff, add an item to the list of + # cards played + if event['type'] == 'applybuff': + cards.append({ + 'source': event['sourceID'], + 'target': event['targetID'], + 'start': event['timestamp'], + 'type': card_type(event['abilityGameID']), + 'name': card_name(event['abilityGameID']), + 'bonus': card_bonus(event['abilityGameID']), + 'id': event['abilityGameID'], + }) + # If removing the buff, add an end timestamp to the matching application + elif event['type'] == 'removebuff': + card_set = [card + for card in cards + if card['target'] == event['targetID'] and card['source'] == event['sourceID'] and card['id'] == event['abilityGameID'] and 'end' not in card] + # add it to the discovered tether + if card_set: + card = card_set[0] + card['end'] = event['timestamp'] + # if there is no start event, add one and set it to 15s prior + else: + cards.append({ + 'source': event['sourceID'], + 'target': event['targetID'], + 'start': max(event['timestamp'] - 15000, start_time), + 'end': event['timestamp'], + 'type': card_type(event['abilityGameID']), + 'name': card_name(event['abilityGameID']), + 'bonus': card_bonus(event['abilityGameID']), + 'id': event['abilityGameID'], + }) + for card in cards: + if 'end' not in card: + card['end'] = min(card['start'] + 15000, end_time) + + return cards + + +def get_card_draw_events(report, start_time, end_time, token): + query = """ +query reportData($code: String!, $startTime: Float!, $endTime: Float!) { + reportData { + report(code: $code) { + draws: events( + startTime: $startTime, + endTime: $endTime, + filterExpression: "ability.id in (3590, 7448, 3593, 1000915, 1000913, 1000914, 1000917, 1000916, 1000918)" + ) { + data + } + } + } +} +""" + variables = { + 'code': report, + 'startTime': start_time, + 'endTime': end_time, + } + + data = call_fflogs_api(query, variables, token) + card_events = data['data']['reportData']['report']['draws']['data'] + + return card_events + +""" +For the initial version of this the following simple rules are use. +Every event starts with one of the following and ends with the same: + (1) Draw + (2) Sleeve Draw + (3) Divination +Redraws and plays are ignored +""" +def get_draw_windows(card_events, start_time, end_time): + + last_time = start_time + last_event = 'Fight Start' + draw_windows = [] + + for events in card_events: + # check if cast and if it's draw/sleeve/div + if event['type'] == 'cast' and event['abilityGameID'] in [3590, 16552, 7448]: + draw_windows.append({ + 'start': last_time, + 'end': event['timestamp'], + 'startEvent': last_event, + 'endEvent': {3590: 'Draw', 16552: 'Divination', 7448: 'Sleeve Draw'}[event['abilityGameID']] + }) + last_time = event['timestamp'] + last_event = {3590: 'Draw', 16552: 'Divination', 7448: 'Sleeve Draw'}[event['abilityGameID']] + + return draw_windows + +def get_damages(report, start_time, end_time, token): + query = """ +query reportData ($code: String!, $startTime: Float!, $endTime: Float!) { + reportData { + report(code: $code) { + table( + startTime: $startTime, + endTime: $endTime, + dataType: DamageDone, + filterExpression: "isTick='false'", + viewBy: Source + ) + } + } +}""" + + variables = { + 'code': report, + 'startTime': start_time, + 'endTime': end_time, + } + + data = call_fflogs_api(query, variables, token) + damage_entries = data['data']['reportData']['report']['table']['data']['entries'] + + damages = {} + + for d in damage_entries: + damages[d['id']] = d['total'] + + return damages + +def get_all_damage_events(report, start_time, end_time, token): + query = """ +query reportData($code: String!, $startTime: Float!, $endTime: Float!) { + reportData { + report(code: $code) { + damage: events( + startTime: $startTime, + endTime: $endTime, + dataType: DamageDone, + filterExpression: "isTick='false' and type!='calculateddamage'" + ) { + data + } + tickDamage: events( + startTime: $startTime, + endTime: $endTime, + dataType: DamageDone, + filterExpression: "isTick='true' and ability.id != 500000" + ) { + data + } + tickEvents: events( + startTime: $startTime, + endTime: $endTime, + dataType: Debuffs, + hostilityType: Enemies, + filterExpression: "ability.id not in (1000493, 1001203, 1001195, 1001221)" + ) { + data + } + groundEvents: events( + startTime: $startTime, + endTime: $endTime, + dataType: Buffs, + filterExpression: "ability.id in (1000749, 1000501, 1001205, 1000312, 1001869)" + ) { + data + } + } + } +} +""" + + variables = { + 'code': report, + 'startTime': start_time, + 'endTime': end_time, + } + data = call_fflogs_api(query, variables, token) + + base_damages = data['data']['reportData']['report']['damage']['data'] + tick_damages = data['data']['reportData']['report']['tickDamage'] ['data'] + tick_events = data['data']['reportData']['report']['tickEvents']['data'] + ground_events = data['data']['reportData']['report']['groundEvents']['data'] + + raw_combined_ticks = tick_damages + tick_events + ground_events + + combined_tick_events = sorted(raw_combined_ticks, key=lambda tick: (tick['timestamp'], event_priority(tick['type']))) + + damage_data = { + 'rawDamage': base_damages, + 'tickDamage': combined_tick_events, + } + return damage_data + +def event_priority(event): + return { + 'applydebuff': 1, + 'applybuff': 1, + 'refreshdebuff': 2, + 'refreshbuff': 2, + 'removedebuff': 4, + 'removebuff': 4, + 'damage': 3, + 'damagesnapshot': 3, + }[event] + +""" +This takes a collection of damage events associated with ticks as well as the + +""" +def sum_tick_damage_snapshots(damage_report): + active_debuffs = {} + summed_tick_damage = [] + + for event in damage_report['tickDamage']: + action = (event['sourceID'], event['targetID'], event['abilityGameID']) + + # these events are either: + # - apply{buff/debuff} + # - reapply{buff,debuff} + # - remove{buff,debuff} (can ignore these) + # - damage + + # damage is summed from the application (apply or reapply) until + # another application event or the end of the data + + # that damage is then reassociated with application event + + if event['type'] in ['applybuff', 'refreshbuff', 'applydebuff', 'refreshdebuff'] and event['timestamp']: + # if it's not an active effect then add it + if action not in active_debuffs: + active_debuffs[action] = { + 'timestamp': event['timestamp'], + 'damage': 0, + } + # if it is an active debuff then add a new damage event associated + # with the sum and restart summing the damage from this event + else: + summed_tick_damage.append({ + 'type': 'damagesnapshot', + 'sourceID': action[0], + 'targetID': action[1], + 'abilityGameID': action[2], + 'amount': active_debuffs[action]['damage'], + 'timestamp': active_debuffs[action]['timestamp'], + }) + active_debuffs[action] = { + 'timestamp': event['timestamp'], + 'damage': 0, + } + elif event['type'] == 'damage': + if action in active_debuffs: + active_debuffs[action]['damage'] += event['amount'] + + # now that we're done we can add the remaining events into the damage array + for action in active_debuffs: + if active_debuffs[action]['damage'] != 0: + summed_tick_damage.append({ + 'type': 'damagesnapshot', + 'sourceID': action[0], + 'targetID': action[1], + 'abilityGameID': action[2], + 'amount': active_debuffs[action]['damage'], + 'timestamp': active_debuffs[action]['timestamp'], + }) + + # finally sort the new array of snapshotdamage events and return it + sorted_tick_damage = sorted(summed_tick_damage, key=lambda tick: tick['timestamp']) + + new_damage_report = { + 'rawDamage': damage_report['rawDamage'], + 'snapshotDamage': sorted_tick_damage, + 'combinedDamage': sorted(sorted_tick_damage + damage_report['rawDamage'], key=lambda tick: tick['timestamp']) + } + + return new_damage_report + +def calculate_total_damage(damage_report, start_time, end_time, players, pets): + total_damage = {} + + # add all raw damage events + for event in damage_report['combinedDamage']: + if event['timestamp'] > start_time and event['timestamp'] < end_time: + if event['sourceID'] in total_damage: + total_damage[event['sourceID']] += event['amount'] + else: + total_damage[event['sourceID']] = event['amount'] + # assume order events and if the current event is after + # the end time then we're done + if event['timestamp'] > end_time: + break + + player_damage = {} + for p in players: + if p in total_damage: + player_damage[p] = total_damage[p] + # else: + # player_damage[p] = 0 + + for p in pets: + if p in total_damage: + if pets[p]['owner'] in player_damage: + player_damage[pets[p]['owner']] += total_damage[p] + else: + player_damage[pets[p]['owner']] = total_damage[p] + + return (total_damage, player_damage) + + +""" +This function is designed to sum the damage done by each actor between the two +timestamp given by start_time and end_time. This involves a simple sum over +the raw damage done and then summing all damage done by tick events applied or +refreshed during the time window + +This should not be used and instead use the calculate_total_damage function +""" +def calculate_total_event_damage(damage_report, start_time, end_time, players, pets): + raw_damage = {} + tick_damage = {} + + # add all raw damage events + for event in damage_report['rawDamage']: + if event['timestamp'] > start_time and event['timestamp'] < end_time: + if event['sourceID'] in raw_damage: + raw_damage[event['sourceID']] += event['amount'] + else: + raw_damage[event['sourceID']] = event['amount'] + # assume order events and if the current event is after + # the end time then we're done + if event['timestamp'] > end_time: + break + + active_debuffs = [] + + for event in damage_report['tickDamage']: + action = (event['sourceID'], event['targetID'], event['abilityGameID']) + + if event['type'] in ['applybuff', 'refreshbuff', 'applydebuff', 'refreshdebuff'] and event['timestamp'] > start_time and event['timestamp'] < end_time: + if action not in active_debuffs: + active_debuffs.append(action) + elif event['type'] in ['applybuff', 'refreshbuff', 'applydebuff', 'refreshdebuff'] and event['timestamp'] > end_time: + if action in active_debuffs: + active_debuffs.remove(action) + # since we removed something we can check if we're done + if len(active_debuffs) == 0 and event['timestamp'] > end_time: + break + elif event['type'] == 'damage': + if action in active_debuffs: + if event['sourceID'] in tick_damage: + tick_damage[event['sourceID']] += event['amount'] + else: + tick_damage[event['sourceID']] = event['amount'] + + combined_damage = {} + for p in players: + combined_damage[p] = 0 + if p in raw_damage: + combined_damage[p] += raw_damage[p] + if p in tick_damage: + combined_damage[p] += tick_damage[p] + + for p in pets: + if p in raw_damage: + combined_damage[pets[p]['owner']] += raw_damage[p] + if p in tick_damage: + combined_damage[pets[p]['owner']] += tick_damage[p] + + return (raw_damage, tick_damage, combined_damage) + +""" +This searches a window of time for the optimal card play + +damage_report: contains all damage instances (both raw and from summing dot snapshots) +start_time: initial value for the search interval to start +end_time: final time that the interval can start +duration: the length of the interval (in milliseconds) +step_size: step_size for the search (in milliseconds) +""" +def search_draw_window(damage_report, start_time, end_time, duration, step_size, players, pets): + + # start searching at the start + interval_start = start_time + interval_end = interval_start + duration + + max_damage = {} + + while interval_start < end_time: + (total_damage, player_damage) = calculate_total_damage(damage_report, interval_start, interval_end, players, pets) + + sorted_damage = sorted(player_damage.items(), key=lambda dmg: dmg[1]) + + max_damage[interval_start] = { + 'id': sorted_damage[0][0], + 'damage': sorted_damage[0][1] + } + + interval_start += step_size + interval_end = interval_start + duration + + return max_damage \ No newline at end of file diff --git a/testing.py b/testing.py new file mode 100644 index 0000000..6fd8407 --- /dev/null +++ b/testing.py @@ -0,0 +1,88 @@ +from datetime import timedelta +import os + +from python_graphql_client import GraphqlClient + +# Make sure we have the requests library +try: + import requests +except ImportError: + raise ImportError("FFlogs parsing requires the Requests module for python." + "Run the following to install it:\n python -m pip install requests") + +from requests_oauthlib import OAuth2Session +from oauthlib.oauth2 import BackendApplicationClient + +from fflogsapi import get_fight_time, get_all_actors, get_card_play_events, get_cards_played, get_damages, get_all_damage_events, calculate_total_damage, sum_tick_damage_snapshots, search_draw_window + +client_id = '9281480c-43fe-4fbd-9cd6-3090bee3dba1' +client_pass = 'KfJPAmBM5a0hUjo5vNFaS4cwLGhdtRRsplEElcyQ' +data = 'grant_type=client_credentials' +oauth_url = 'https://www.fflogs.com/oauth/token' + +# r = requests.get(oauth_url, auth=(client_id, client_pass)) +# print(r) + +client = BackendApplicationClient(client_id=client_id) +oauth = OAuth2Session(client=client) +token = oauth.fetch_token(token_url=oauth_url, client_id=client_id, client_secret=client_pass) + +#https://www.fflogs.com/reports/MQjnkJ7YRwqCaLcN#fight=1 + +# report_id = 'byLqHjz8MnphQP3r' +report_id = 'MQjnkJ7YRwqCaLcN' +fight = 1 + +data = get_fight_time(report_id, fight, token) + +start_time = data['startTime'] +end_time = data['endTime'] + +(players, pets) = get_all_actors(report=report_id, start_time=start_time, end_time=end_time, token=token) + +# card_events = get_card_play_events(report_id, start_time, end_time, token) + +# cards = get_cards_played(card_events, start_time, end_time) + +# player_cards = [] +# for c in cards: +# if c['target'] not in pets: +# player_cards.append(c) + +# cards = player_cards + +custom_start = 100000 +custom_end = 140000 + +damage_data = get_all_damage_events(report_id, start_time, end_time, token) +damage_report = sum_tick_damage_snapshots(damage_data) + +# (total_damage, player_damage) = calculate_total_damage(damage_report, custom_start, custom_end, players, pets) + +max_damage_windows = search_draw_window(damage_report, start_time, end_time, 5000, 1000, players, pets) + +tabular = '{:<11}{:<24}{:>9}' +print(tabular.format('Time', 'Player', 'Damage')) +print('-' * 50) +for m in max_damage_windows: + print(tabular.format(str(timedelta(milliseconds=(m-start_time)))[2:11], players[max_damage_windows[m]['id']]['name'], max_damage_windows[m]['damage'])) + + +# print('Start Time: {}\nEnd Time: {}\n'.format(str(timedelta(milliseconds=(custom_start - start_time)))[2:11], str(timedelta(milliseconds=(custom_end - start_time)))[2:11])) + +# print("Total Damage") +# for p in total_damage: +# if p in players: +# print('{:<24} - {:>9}'.format(players[p]['name'], total_damage[p])) +# elif p in pets: +# print('{:<24} - {:>9}'.format(pets[p]['name'], total_damage[p])) +# else: +# print('{:<24} - {:>9}'.format('id: ' + str(p), total_damage[p])) + +# print() +# print("Player Damage") +# for p in player_damage: +# if p in players: +# print('{:<24} - {:>9}'.format(players[p]['name'], player_damage[p])) +# else: +# print('{:<24} - {:>9}'.format('id: ' + str(p), player_damage[p])) From 722b6a827b4509d7d324c5d8961325ce34ef2e4a Mon Sep 17 00:00:00 2001 From: Mel Miller Date: Sun, 17 Jan 2021 14:11:29 -0500 Subject: [PATCH 02/11] Added some classes for handling fflogs info, damage info, and cards in general --- cardcalc_data.py | 124 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 cardcalc_data.py diff --git a/cardcalc_data.py b/cardcalc_data.py new file mode 100644 index 0000000..bf67e2d --- /dev/null +++ b/cardcalc_data.py @@ -0,0 +1,124 @@ +from datetime import timedelta + +class Player: + def __init__(self, id, name, job, info): + self.id = id + self.name = name + self.job = job + self.type = info + +class Pet: + def __init__(self, id, name, owner): + self.id = id + self.name = name + self.owner = owner + +class CardPlay: + def __init__(self, start, end, source, target, id): + self.start = start + self.end = end + self.source = source + self.target = target + self.id = id + + self.name = CardPlay.Name(id) + self.type = CardPlay.Type(id) + self.bonus = CardPlay.Bonus(id) + + def __str__(self): + return '{} played {} on {} at {}'.format(self.source, self.name, self.target, self.start) + + def String(self, player_list, start_time): + return '{} played {} on {} at {}'.format(player_list[self.source]['name'], self.name, player_list[self.target]['name'], str(timedelta(milliseconds=(self.start-start_time)))[2:11]) + + + @staticmethod + def Name(id): + return { + 1001876: 'Lord of Crowns', + 1001877: 'Lady of Crowns', + 1001882: 'The Balance', + 1001884: 'The Arrow', + 1001885: 'The Spear', + 1001883: 'The Bole', + 1001886: 'The Ewer', + 1001887: 'The Spire', + } [id] + + @staticmethod + def Type(id): + return { + 1001876: 'melee', + 1001877: 'ranged', + 1001882: 'melee', + 1001884: 'melee', + 1001885: 'melee', + 1001883: 'ranged', + 1001886: 'ranged', + 1001887: 'ranged', + } [id] + + @staticmethod + def Bonus(id): + return { + 1001876: 1.08, + 1001877: 1.08, + 1001882: 1.06, + 1001884: 1.06, + 1001885: 1.06, + 1001883: 1.06, + 1001886: 1.06, + 1001887: 1.06, + } [id] + +class BurstWindow: + def __init__(self, start, end): + self.start = start + self.end = end + +class DrawWindow(BurstWindow): + def __init__(self, start, end, startEvent, endEvent): + self.start = start + self.end = end + self.startEvent = startEvent + self.endEvent = endEvent + + def Duration(self): + return(timedelta(self.end-self.start).total_seconds) + + @staticmethod + def Name(id): + return { + 3590: 'Draw', + 16552: 'Divination', + 7448: 'Sleeve Draw', + 3593: 'Redraw', + }[id] + +class FightInfo: + def __init__(self, report_id, fight_number, start_time, end_time): + self.id = report_id + self.index = fight_number + self.start = start_time + self.end = end_time + + def Duration(self): + return(timedelta(self.end-self.start).total_seconds) + +class BurstDamageCollection: + def __init__(self, list): + self.list = list + + def GetMax(self, id=None): + # if an ID is given then return the max damage done + # by that person + if id is not None: + # get the timestamp where they did the most damage + max_item = sorted(self.list.items(), key=lambda dmg: dmg[1][id], reverse=True)[0] + # otherwise return the max damage done by anyone + else: + # get the timestamp where the most damage was done by anyone + max_item = sorted(self.list.items(), key=lambda dmg: sorted(dmg[1].items(), key=lambda ids: ids[1], reverse=True)[0][1], reverse=True)[0] + # get the id of the person who did the most damage at that time + id = sorted(max_item[1].items(), key=lambda item: item[1], reverse=True)[0][0] + return (max_item[0], id, max_item[1][id]) From 2d0914303afd1002895d039cf05f028d7e8b8b25 Mon Sep 17 00:00:00 2001 From: Mel Miller Date: Sun, 17 Jan 2021 17:52:35 -0500 Subject: [PATCH 03/11] Tested working version on TEA fight, expanded event max limit, split up functions into damagecalc and cardcalc, cleaned up some old code --- Pipfile | 8 +- Pipfile.lock | 510 +++++++++++++++++++++++++++++++++++++++-------- cardcalc.py | 472 +++---------------------------------------- cardcalc_data.py | 95 +++++++-- damagecalc.py | 139 +++++++++++++ fflogsapi.py | 434 +++++++--------------------------------- localtesting.py | 58 ------ plotting.py | 2 + testing.py | 109 ++++------ 9 files changed, 801 insertions(+), 1026 deletions(-) delete mode 100644 localtesting.py create mode 100644 plotting.py diff --git a/Pipfile b/Pipfile index 2fc1c68..700a91c 100644 --- a/Pipfile +++ b/Pipfile @@ -17,8 +17,12 @@ flask-migrate = "*" flask-sqlalchemy = "*" gunicorn = "*" "psycopg2-binary" = "*" - +"requests_oauthlib" = "*" +"oauthlib" = "*" +"python_graphql_client" = "*" +"plotly" = "*" +"pandas" = "*" [requires] -python_version = "3.6" +python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock index c70a46a..936f4ca 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "edf1bcf93c642f3e5d1b27e0a88b3615a4e5b50c57e8d9cec52b1b31943d8399" + "sha256": "adacce8982b630d22b2d75c5abfc0b206f911a5711aac13e0f99c8455b19814f" }, "pipfile-spec": 6, "requires": { - "python_version": "3.6" + "python_version": "3.9" }, "sources": [ { @@ -16,18 +16,79 @@ ] }, "default": { + "aiohttp": { + "hashes": [ + "sha256:0b795072bb1bf87b8620120a6373a3c61bfcb8da7e5c2377f4bb23ff4f0b62c9", + "sha256:0d438c8ca703b1b714e82ed5b7a4412c82577040dadff479c08405e2a715564f", + "sha256:16a3cb5df5c56f696234ea9e65e227d1ebe9c18aa774d36ff42f532139066a5f", + "sha256:1edfd82a98c5161497bbb111b2b70c0813102ad7e0aa81cbeb34e64c93863005", + "sha256:2406dc1dda01c7f6060ab586e4601f18affb7a6b965c50a8c90ff07569cf782a", + "sha256:2858b2504c8697beb9357be01dc47ef86438cc1cb36ecb6991796d19475faa3e", + "sha256:2a7b7640167ab536c3cb90cfc3977c7094f1c5890d7eeede8b273c175c3910fd", + "sha256:3228b7a51e3ed533f5472f54f70fd0b0a64c48dc1649a0f0e809bec312934d7a", + "sha256:328b552513d4f95b0a2eea4c8573e112866107227661834652a8984766aa7656", + "sha256:39f4b0a6ae22a1c567cb0630c30dd082481f95c13ca528dc501a7766b9c718c0", + "sha256:3b0036c978cbcc4a4512278e98e3e6d9e6b834dc973206162eddf98b586ef1c6", + "sha256:3ea8c252d8df5e9166bcf3d9edced2af132f4ead8ac422eac723c5781063709a", + "sha256:41608c0acbe0899c852281978492f9ce2c6fbfaf60aff0cefc54a7c4516b822c", + "sha256:59d11674964b74a81b149d4ceaff2b674b3b0e4d0f10f0be1533e49c4a28408b", + "sha256:5e479df4b2d0f8f02133b7e4430098699450e1b2a826438af6bec9a400530957", + "sha256:684850fb1e3e55c9220aad007f8386d8e3e477c4ec9211ae54d968ecdca8c6f9", + "sha256:6ccc43d68b81c424e46192a778f97da94ee0630337c9bbe5b2ecc9b0c1c59001", + "sha256:6d42debaf55450643146fabe4b6817bb2a55b23698b0434107e892a43117285e", + "sha256:710376bf67d8ff4500a31d0c207b8941ff4fba5de6890a701d71680474fe2a60", + "sha256:756ae7efddd68d4ea7d89c636b703e14a0c686688d42f588b90778a3c2fc0564", + "sha256:77149002d9386fae303a4a162e6bce75cc2161347ad2ba06c2f0182561875d45", + "sha256:78e2f18a82b88cbc37d22365cf8d2b879a492faedb3f2975adb4ed8dfe994d3a", + "sha256:7d9b42127a6c0bdcc25c3dcf252bb3ddc70454fac593b1b6933ae091396deb13", + "sha256:8389d6044ee4e2037dca83e3f6994738550f6ee8cfb746762283fad9b932868f", + "sha256:9c1a81af067e72261c9cbe33ea792893e83bc6aa987bfbd6fdc1e5e7b22777c4", + "sha256:c1e0920909d916d3375c7a1fdb0b1c78e46170e8bb42792312b6eb6676b2f87f", + "sha256:c68fdf21c6f3573ae19c7ee65f9ff185649a060c9a06535e9c3a0ee0bbac9235", + "sha256:c733ef3bdcfe52a1a75564389bad4064352274036e7e234730526d155f04d914", + "sha256:c9c58b0b84055d8bc27b7df5a9d141df4ee6ff59821f922dd73155861282f6a3", + "sha256:d03abec50df423b026a5aa09656bd9d37f1e6a49271f123f31f9b8aed5dc3ea3", + "sha256:d2cfac21e31e841d60dc28c0ec7d4ec47a35c608cb8906435d47ef83ffb22150", + "sha256:dcc119db14757b0c7bce64042158307b9b1c76471e655751a61b57f5a0e4d78e", + "sha256:df3a7b258cc230a65245167a202dd07320a5af05f3d41da1488ba0fa05bc9347", + "sha256:df48a623c58180874d7407b4d9ec06a19b84ed47f60a3884345b1a5099c1818b", + "sha256:e1b95972a0ae3f248a899cdbac92ba2e01d731225f566569311043ce2226f5e7", + "sha256:f326b3c1bbfda5b9308252ee0dcb30b612ee92b0e105d4abec70335fab5b1245", + "sha256:f411cb22115cb15452d099fec0ee636b06cf81bfb40ed9c02d30c8dc2bc2e3d1" + ], + "markers": "python_version >= '3.6'", + "version": "==3.7.3" + }, "alembic": { "hashes": [ - "sha256:cdb7d98bd5cbf65acd38d70b1c05573c432e6473a82f955cdea541b5c153b0cc" + "sha256:4e02ed2aa796bd179965041afa092c55b51fb077de19d61835673cc80672c01c", + "sha256:5334f32314fb2a56d86b4c4dd1ae34b08c03cae4cb888bc699942104d66bc245" ], - "version": "==1.0.11" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.4.3" + }, + "async-timeout": { + "hashes": [ + "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", + "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" + ], + "markers": "python_full_version >= '3.5.3'", + "version": "==3.0.1" + }, + "attrs": { + "hashes": [ + "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", + "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==20.3.0" }, "certifi": { "hashes": [ - "sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939", - "sha256:945e3ba63a0b9f577b1395204e13c3a231f9bc0223888be653286534e5873695" + "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", + "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" ], - "version": "==2019.6.16" + "version": "==2020.12.5" }, "chardet": { "hashes": [ @@ -38,69 +99,74 @@ }, "click": { "hashes": [ - "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", - "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", + "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" ], - "version": "==7.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==7.1.2" }, "flask": { "hashes": [ - "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52", - "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6" + "sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060", + "sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557" ], "index": "pypi", - "version": "==1.1.1" + "version": "==1.1.2" }, "flask-migrate": { "hashes": [ - "sha256:6fb038be63d4c60727d5dfa5f581a6189af5b4e2925bc378697b4f0a40cfb4e1", - "sha256:a96ff1875a49a40bd3e8ac04fce73fdb0870b9211e6168608cbafa4eb839d502" + "sha256:4dc4a5cce8cbbb06b8dc963fd86cf8136bd7d875aabe2d840302ea739b243732", + "sha256:a69d508c2e09d289f6e55a417b3b8c7bfe70e640f53d2d9deb0d056a384f37ee" ], "index": "pypi", - "version": "==2.5.2" + "version": "==2.5.3" }, "flask-sqlalchemy": { "hashes": [ - "sha256:0c9609b0d72871c540a7945ea559c8fdf5455192d2db67219509aed680a3d45a", - "sha256:8631bbea987bc3eb0f72b1f691d47bd37ceb795e73b59ab48586d76d75a7c605" + "sha256:05b31d2034dd3f2a685cbbae4cfc4ed906b2a733cff7964ada450fd5e462b84e", + "sha256:bfc7150eaf809b1c283879302f04c42791136060c6eeb12c0c6674fb1291fae5" ], "index": "pypi", - "version": "==2.4.0" + "version": "==2.4.4" }, "gunicorn": { "hashes": [ - "sha256:aa8e0b40b4157b36a5df5e599f45c9c76d6af43845ba3b3b0efe2c70473c2471", - "sha256:fa2662097c66f920f53f70621c6c58ca4a3c4d3434205e608e121b5b3b71f4f3" + "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626", + "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c" ], "index": "pypi", - "version": "==19.9.0" + "version": "==20.0.4" }, "idna": { "hashes": [ - "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", - "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "version": "==2.8" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" }, "itsdangerous": { "hashes": [ "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.1.0" }, "jinja2": { "hashes": [ - "sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013", - "sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b" + "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0", + "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035" ], - "version": "==2.10.1" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==2.11.2" }, "mako": { "hashes": [ - "sha256:f5a642d8c5699269ab62a68b296ff990767eb120f51e2e8f3d6afb16bdb57f4b" + "sha256:17831f0b7087c313c0ffae2bcbbd3c1d5ba9eeac9c38f2eb7b50e8c99fe9d5ab" ], - "version": "==1.0.14" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.1.4" }, "markupsafe": { "hashes": [ @@ -108,13 +174,16 @@ "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", + "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42", "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", + "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b", "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", + "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15", "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", @@ -131,93 +200,378 @@ "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", - "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" + "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2", + "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7", + "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.1.1" }, + "multidict": { + "hashes": [ + "sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a", + "sha256:051012ccee979b2b06be928a6150d237aec75dd6bf2d1eeeb190baf2b05abc93", + "sha256:05c20b68e512166fddba59a918773ba002fdd77800cad9f55b59790030bab632", + "sha256:07b42215124aedecc6083f1ce6b7e5ec5b50047afa701f3442054373a6deb656", + "sha256:0e3c84e6c67eba89c2dbcee08504ba8644ab4284863452450520dad8f1e89b79", + "sha256:0e929169f9c090dae0646a011c8b058e5e5fb391466016b39d21745b48817fd7", + "sha256:1ab820665e67373de5802acae069a6a05567ae234ddb129f31d290fc3d1aa56d", + "sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5", + "sha256:2e68965192c4ea61fff1b81c14ff712fc7dc15d2bd120602e4a3494ea6584224", + "sha256:2f1a132f1c88724674271d636e6b7351477c27722f2ed789f719f9e3545a3d26", + "sha256:37e5438e1c78931df5d3c0c78ae049092877e5e9c02dd1ff5abb9cf27a5914ea", + "sha256:3a041b76d13706b7fff23b9fc83117c7b8fe8d5fe9e6be45eee72b9baa75f348", + "sha256:3a4f32116f8f72ecf2a29dabfb27b23ab7cdc0ba807e8459e59a93a9be9506f6", + "sha256:46c73e09ad374a6d876c599f2328161bcd95e280f84d2060cf57991dec5cfe76", + "sha256:46dd362c2f045095c920162e9307de5ffd0a1bfbba0a6e990b344366f55a30c1", + "sha256:4b186eb7d6ae7c06eb4392411189469e6a820da81447f46c0072a41c748ab73f", + "sha256:54fd1e83a184e19c598d5e70ba508196fd0bbdd676ce159feb412a4a6664f952", + "sha256:585fd452dd7782130d112f7ddf3473ffdd521414674c33876187e101b588738a", + "sha256:5cf3443199b83ed9e955f511b5b241fd3ae004e3cb81c58ec10f4fe47c7dce37", + "sha256:6a4d5ce640e37b0efcc8441caeea8f43a06addace2335bd11151bc02d2ee31f9", + "sha256:7df80d07818b385f3129180369079bd6934cf70469f99daaebfac89dca288359", + "sha256:806068d4f86cb06af37cd65821554f98240a19ce646d3cd24e1c33587f313eb8", + "sha256:830f57206cc96ed0ccf68304141fec9481a096c4d2e2831f311bde1c404401da", + "sha256:929006d3c2d923788ba153ad0de8ed2e5ed39fdbe8e7be21e2f22ed06c6783d3", + "sha256:9436dc58c123f07b230383083855593550c4d301d2532045a17ccf6eca505f6d", + "sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf", + "sha256:ace010325c787c378afd7f7c1ac66b26313b3344628652eacd149bdd23c68841", + "sha256:b47a43177a5e65b771b80db71e7be76c0ba23cc8aa73eeeb089ed5219cdbe27d", + "sha256:b797515be8743b771aa868f83563f789bbd4b236659ba52243b735d80b29ed93", + "sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f", + "sha256:d5c65bdf4484872c4af3150aeebe101ba560dcfb34488d9a8ff8dbcd21079647", + "sha256:d81eddcb12d608cc08081fa88d046c78afb1bf8107e6feab5d43503fea74a635", + "sha256:dc862056f76443a0db4509116c5cd480fe1b6a2d45512a653f9a855cc0517456", + "sha256:ecc771ab628ea281517e24fd2c52e8f31c41e66652d07599ad8818abaad38cda", + "sha256:f200755768dc19c6f4e2b672421e0ebb3dd54c38d5a4f262b872d8cfcc9e93b5", + "sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281", + "sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80" + ], + "markers": "python_version >= '3.6'", + "version": "==5.1.0" + }, + "numpy": { + "hashes": [ + "sha256:012426a41bc9ab63bb158635aecccc7610e3eff5d31d1eb43bc099debc979d94", + "sha256:06fab248a088e439402141ea04f0fffb203723148f6ee791e9c75b3e9e82f080", + "sha256:0eef32ca3132a48e43f6a0f5a82cb508f22ce5a3d6f67a8329c81c8e226d3f6e", + "sha256:1ded4fce9cfaaf24e7a0ab51b7a87be9038ea1ace7f34b841fe3b6894c721d1c", + "sha256:2e55195bc1c6b705bfd8ad6f288b38b11b1af32f3c8289d6c50d47f950c12e76", + "sha256:2ea52bd92ab9f768cc64a4c3ef8f4b2580a17af0a5436f6126b08efbd1838371", + "sha256:36674959eed6957e61f11c912f71e78857a8d0604171dfd9ce9ad5cbf41c511c", + "sha256:384ec0463d1c2671170901994aeb6dce126de0a95ccc3976c43b0038a37329c2", + "sha256:39b70c19ec771805081578cc936bbe95336798b7edf4732ed102e7a43ec5c07a", + "sha256:400580cbd3cff6ffa6293df2278c75aef2d58d8d93d3c5614cd67981dae68ceb", + "sha256:43d4c81d5ffdff6bae58d66a3cd7f54a7acd9a0e7b18d97abb255defc09e3140", + "sha256:50a4a0ad0111cc1b71fa32dedd05fa239f7fb5a43a40663269bb5dc7877cfd28", + "sha256:603aa0706be710eea8884af807b1b3bc9fb2e49b9f4da439e76000f3b3c6ff0f", + "sha256:6149a185cece5ee78d1d196938b2a8f9d09f5a5ebfbba66969302a778d5ddd1d", + "sha256:759e4095edc3c1b3ac031f34d9459fa781777a93ccc633a472a5468587a190ff", + "sha256:7fb43004bce0ca31d8f13a6eb5e943fa73371381e53f7074ed21a4cb786c32f8", + "sha256:811daee36a58dc79cf3d8bdd4a490e4277d0e4b7d103a001a4e73ddb48e7e6aa", + "sha256:8b5e972b43c8fc27d56550b4120fe6257fdc15f9301914380b27f74856299fea", + "sha256:99abf4f353c3d1a0c7a5f27699482c987cf663b1eac20db59b8c7b061eabd7fc", + "sha256:a0d53e51a6cb6f0d9082decb7a4cb6dfb33055308c4c44f53103c073f649af73", + "sha256:a12ff4c8ddfee61f90a1633a4c4afd3f7bcb32b11c52026c92a12e1325922d0d", + "sha256:a4646724fba402aa7504cd48b4b50e783296b5e10a524c7a6da62e4a8ac9698d", + "sha256:a76f502430dd98d7546e1ea2250a7360c065a5fdea52b2dffe8ae7180909b6f4", + "sha256:a9d17f2be3b427fbb2bce61e596cf555d6f8a56c222bd2ca148baeeb5e5c783c", + "sha256:ab83f24d5c52d60dbc8cd0528759532736b56db58adaa7b5f1f76ad551416a1e", + "sha256:aeb9ed923be74e659984e321f609b9ba54a48354bfd168d21a2b072ed1e833ea", + "sha256:c843b3f50d1ab7361ca4f0b3639bf691569493a56808a0b0c54a051d260b7dbd", + "sha256:cae865b1cae1ec2663d8ea56ef6ff185bad091a5e33ebbadd98de2cfa3fa668f", + "sha256:cc6bd4fd593cb261332568485e20a0712883cf631f6f5e8e86a52caa8b2b50ff", + "sha256:cf2402002d3d9f91c8b01e66fbb436a4ed01c6498fffed0e4c7566da1d40ee1e", + "sha256:d051ec1c64b85ecc69531e1137bb9751c6830772ee5c1c426dbcfe98ef5788d7", + "sha256:d6631f2e867676b13026e2846180e2c13c1e11289d67da08d71cacb2cd93d4aa", + "sha256:dbd18bcf4889b720ba13a27ec2f2aac1981bd41203b3a3b27ba7a33f88ae4827", + "sha256:df609c82f18c5b9f6cb97271f03315ff0dbe481a2a02e56aeb1b1a985ce38e60" + ], + "markers": "python_version >= '3.6'", + "version": "==1.19.5" + }, + "oauthlib": { + "hashes": [ + "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889", + "sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea" + ], + "index": "pypi", + "version": "==3.1.0" + }, + "pandas": { + "hashes": [ + "sha256:0be6102dd99910513e75ed6536284743ead810349c51bdeadd2a5b6649f30abb", + "sha256:272675a98fa4954b9fc0933df775596fc942e50015d7e75d8f19548808a2bfdf", + "sha256:2d8b4f532db37418121831a461fd107d826c240b098f52e7a1b4ab3d5aaa4fb2", + "sha256:33318fa24b192b1a4684347ff76679a7267fd4e547da9f71556a5914f0dc10e7", + "sha256:3bc6d2be03cb75981d8cbeda09503cd9d6d699fc0dc28a65e197165ad527b7b8", + "sha256:43482789c55cbabeed9482263cfc98a11e8fcae900cb63ef038948acb4a72570", + "sha256:616478c1bd8fe1e600f521ae2da434e021c11e7a4e5da3451d02906143d3629a", + "sha256:6c1a57e4d0d6f9633a07817c44e6b36d81c265fe4c52d0c0505513a2d0f7953c", + "sha256:7904ee438549b5223ce8dc008772458dd7c5cf0ccc64cf903e81202400702235", + "sha256:7b54c14130a3448d81eed1348f52429c23e27188d9db6e6d4afeae792bc49c11", + "sha256:8f92b07cdbfa3704d85b4264e52c216cafe6c0059b0d07cdad8cb29e0b90f2b8", + "sha256:91fd0b94e7b98528177a05e6f65efea79d7ef9dec15ee48c7c69fc39fdd87235", + "sha256:9c6692cea6d56da8650847172bdb148622f545e7782d17995822434c79d7a211", + "sha256:9e18631d996fe131de6cb31a8bdae18965cc8f39eb23fdfbbf42808ecc63dabf", + "sha256:cba93d4fd3b0a42858b2b599495aff793fb5d94587979f45a14177d1217ba446", + "sha256:e03386615b970b8b41da6a68afe717626741bb2431cec993640685614c0680e4", + "sha256:f8b87d2f541cd9bc4ecfe85a561abac85c33fe4de4ce70cca36b2768af2611f5" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "plotly": { + "hashes": [ + "sha256:7d8aaeed392e82fb8e0e48899f2d3d957b12327f9d38cdd5802bc574a8a39d91", + "sha256:d68fc15fcb49f88db27ab3e0c87110943e65fee02a47f33a8590f541b3042461" + ], + "index": "pypi", + "version": "==4.14.3" + }, "psycopg2-binary": { "hashes": [ - "sha256:080c72714784989474f97be9ab0ddf7b2ad2984527e77f2909fcd04d4df53809", - "sha256:110457be80b63ff4915febb06faa7be002b93a76e5ba19bf3f27636a2ef58598", - "sha256:171352a03b22fc099f15103959b52ee77d9a27e028895d7e5fde127aa8e3bac5", - "sha256:19d013e7b0817087517a4b3cab39c084d78898369e5c46258aab7be4f233d6a1", - "sha256:249b6b21ae4eb0f7b8423b330aa80fab5f821b9ffc3f7561a5e2fd6bb142cf5d", - "sha256:2ac0731d2d84b05c7bb39e85b7e123c3a0acd4cda631d8d542802c88deb9e87e", - "sha256:2b6d561193f0dc3f50acfb22dd52ea8c8dfbc64bcafe3938b5f209cc17cb6f00", - "sha256:2bd23e242e954214944481124755cbefe7c2cf563b1a54cd8d196d502f2578bf", - "sha256:3e1239242ca60b3725e65ab2f13765fc199b03af9eaf1b5572f0e97bdcee5b43", - "sha256:3eb70bb697abbe86b1d2b1316370c02ba320bfd1e9e35cf3b9566a855ea8e4e5", - "sha256:51a2fc7e94b98bd1bb5d4570936f24fc2b0541b63eccadf8fdea266db8ad2f70", - "sha256:52f1bdafdc764b7447e393ed39bb263eccb12bfda25a4ac06d82e3a9056251f6", - "sha256:5b3581319a3951f1e866f4f6c5e42023db0fae0284273b82e97dfd32c51985cd", - "sha256:63c1b66e3b2a3a336288e4bcec499e0dc310cd1dceaed1c46fa7419764c68877", - "sha256:8123a99f24ecee469e5c1339427bcdb2a33920a18bb5c0d58b7c13f3b0298ba3", - "sha256:85e699fcabe7f817c0f0a412d4e7c6627e00c412b418da7666ff353f38e30f67", - "sha256:8dbff4557bbef963697583366400822387cccf794ccb001f1f2307ed21854c68", - "sha256:908d21d08d6b81f1b7e056bbf40b2f77f8c499ab29e64ec5113052819ef1c89b", - "sha256:af39d0237b17d0a5a5f638e9dffb34013ce2b1d41441fd30283e42b22d16858a", - "sha256:af51bb9f055a3f4af0187149a8f60c9d516cf7d5565b3dac53358796a8fb2a5b", - "sha256:b2ecac57eb49e461e86c092761e6b8e1fd9654dbaaddf71a076dcc869f7014e2", - "sha256:cd37cc170678a4609becb26b53a2bc1edea65177be70c48dd7b39a1149cabd6e", - "sha256:d17e3054b17e1a6cb8c1140f76310f6ede811e75b7a9d461922d2c72973f583e", - "sha256:d305313c5a9695f40c46294d4315ed3a07c7d2b55e48a9010dad7db7a66c8b7f", - "sha256:dd0ef0eb1f7dd18a3f4187226e226a7284bda6af5671937a221766e6ef1ee88f", - "sha256:e1adff53b56db9905db48a972fb89370ad5736e0450b96f91bcf99cadd96cfd7", - "sha256:f0d43828003c82dbc9269de87aa449e9896077a71954fbbb10a614c017e65737", - "sha256:f78e8b487de4d92640105c1389e5b90be3496b1d75c90a666edd8737cc2dbab7" + "sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c", + "sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67", + "sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0", + "sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6", + "sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db", + "sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94", + "sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52", + "sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056", + "sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b", + "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd", + "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550", + "sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679", + "sha256:89705f45ce07b2dfa806ee84439ec67c5d9a0ef20154e0e475e2b2ed392a5b83", + "sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77", + "sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2", + "sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77", + "sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2", + "sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd", + "sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859", + "sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1", + "sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25", + "sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152", + "sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf", + "sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f", + "sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729", + "sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71", + "sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66", + "sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4", + "sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449", + "sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da", + "sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a", + "sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c", + "sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb", + "sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4", + "sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5" ], "index": "pypi", - "version": "==2.8.3" + "version": "==2.8.6" }, "python-dateutil": { "hashes": [ - "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", - "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" + "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", + "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" ], - "version": "==2.8.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.8.1" }, "python-editor": { "hashes": [ "sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d", "sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b", - "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8" + "sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8", + "sha256:c3da2053dbab6b29c94e43c486ff67206eafbe7eb52dbec7390b5e2fb05aac77", + "sha256:ea87e17f6ec459e780e4221f295411462e0d0810858e055fc514684350a2f522" ], "version": "==1.0.4" }, + "python-graphql-client": { + "hashes": [ + "sha256:951adb2be22ebe4483f87d96009009c22b3f687f4cb109869e0fbfb5aff23f63", + "sha256:a4727d8661c5d5ebe3011bb6160e7b8c2c87299b9cd9d925656d038914469d3f" + ], + "index": "pypi", + "version": "==0.4.2" + }, + "pytz": { + "hashes": [ + "sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4", + "sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5" + ], + "version": "==2020.5" + }, "requests": { "hashes": [ - "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", - "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" + "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", + "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" + ], + "index": "pypi", + "version": "==2.25.1" + }, + "requests-oauthlib": { + "hashes": [ + "sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d", + "sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a", + "sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc" ], "index": "pypi", - "version": "==2.22.0" + "version": "==1.3.0" + }, + "retrying": { + "hashes": [ + "sha256:08c039560a6da2fe4f2c426d0766e284d3b736e355f8dd24b37367b0bb41973b" + ], + "version": "==1.3.3" }, "six": { "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", + "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "version": "==1.12.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.15.0" }, "sqlalchemy": { "hashes": [ - "sha256:c30925d60af95443458ebd7525daf791f55762b106049ae71e18f8dd58084c2f" + "sha256:04f995fcbf54e46cddeb4f75ce9dfc17075d6ae04ac23b2bacb44b3bc6f6bf11", + "sha256:0c6406a78a714a540d980a680b86654feadb81c8d0eecb59f3d6c554a4c69f19", + "sha256:0c72b90988be749e04eff0342dcc98c18a14461eb4b2ad59d611b57b31120f90", + "sha256:108580808803c7732f34798eb4a329d45b04c562ed83ee90f09f6a184a42b766", + "sha256:1418f5e71d6081aa1095a1d6b567a562d2761996710bdce9b6e6ba20a03d0864", + "sha256:17610d573e698bf395afbbff946544fbce7c5f4ee77b5bcb1f821b36345fae7a", + "sha256:216ba5b4299c95ed179b58f298bda885a476b16288ab7243e89f29f6aeced7e0", + "sha256:2ff132a379838b1abf83c065be54cef32b47c987aedd06b82fc76476c85225eb", + "sha256:314f5042c0b047438e19401d5f29757a511cfc2f0c40d28047ca0e4c95eabb5b", + "sha256:318b5b727e00662e5fc4b4cd2bf58a5116d7c1b4dd56ffaa7d68f43458a8d1ed", + "sha256:3ab5b44a07b8c562c6dcb7433c6a6c6e03266d19d64f87b3333eda34e3b9936b", + "sha256:426ece890153ccc52cc5151a1a0ed540a5a7825414139bb4c95a868d8da54a52", + "sha256:491fe48adc07d13e020a8b07ef82eefc227003a046809c121bea81d3dbf1832d", + "sha256:4a84c7c7658dd22a33dab2e2aa2d17c18cb004a42388246f2e87cb4085ef2811", + "sha256:54da615e5b92c339e339fe8536cce99fe823b6ed505d4ea344852aefa1c205fb", + "sha256:5a7f224cdb7233182cec2a45d4c633951268d6a9bcedac37abbf79dd07012aea", + "sha256:61628715931f4962e0cdb2a7c87ff39eea320d2aa96bd471a3c293d146f90394", + "sha256:62285607a5264d1f91590abd874d6a498e229d5840669bd7d9f654cfaa599bd0", + "sha256:62fb881ba51dbacba9af9b779211cf9acff3442d4f2993142015b22b3cd1f92a", + "sha256:68428818cf80c60dc04aa0f38da20ad39b28aba4d4d199f949e7d6e04444ea86", + "sha256:6aaa13ee40c4552d5f3a59f543f0db6e31712cc4009ec7385407be4627259d41", + "sha256:70121f0ae48b25ef3e56e477b88cd0b0af0e1f3a53b5554071aa6a93ef378a03", + "sha256:715b34578cc740b743361f7c3e5f584b04b0f1344f45afc4e87fbac4802eb0a0", + "sha256:758fc8c4d6c0336e617f9f6919f9daea3ab6bb9b07005eda9a1a682e24a6cacc", + "sha256:7d4b8de6bb0bc736161cb0bbd95366b11b3eb24dd6b814a143d8375e75af9990", + "sha256:81d8d099a49f83111cce55ec03cc87eef45eec0d90f9842b4fc674f860b857b0", + "sha256:888d5b4b5aeed0d3449de93ea80173653e939e916cc95fe8527079e50235c1d2", + "sha256:95bde07d19c146d608bccb9b16e144ec8f139bcfe7fd72331858698a71c9b4f5", + "sha256:9bf572e4f5aa23f88dd902f10bb103cb5979022a38eec684bfa6d61851173fec", + "sha256:bab5a1e15b9466a25c96cda19139f3beb3e669794373b9ce28c4cf158c6e841d", + "sha256:bd4b1af45fd322dcd1fb2a9195b4f93f570d1a5902a842e3e6051385fac88f9c", + "sha256:bde677047305fe76c7ee3e4492b545e0018918e44141cc154fe39e124e433991", + "sha256:c389d7cc2b821853fb018c85457da3e7941db64f4387720a329bc7ff06a27963", + "sha256:d055ff750fcab69ca4e57b656d9c6ad33682e9b8d564f2fbe667ab95c63591b0", + "sha256:d53f59744b01f1440a1b0973ed2c3a7de204135c593299ee997828aad5191693", + "sha256:f115150cc4361dd46153302a640c7fa1804ac207f9cc356228248e351a8b4676", + "sha256:f1e88b30da8163215eab643962ae9d9252e47b4ea53404f2c4f10f24e70ddc62", + "sha256:f8191fef303025879e6c3548ecd8a95aafc0728c764ab72ec51a0bdf0c91a341" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.3.22" + }, + "typing-extensions": { + "hashes": [ + "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", + "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", + "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" ], - "version": "==1.3.5" + "version": "==3.7.4.3" }, "urllib3": { "hashes": [ - "sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1", - "sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232" + "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08", + "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473" ], - "version": "==1.25.3" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.26.2" + }, + "websockets": { + "hashes": [ + "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5", + "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5", + "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308", + "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb", + "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a", + "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c", + "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170", + "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422", + "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8", + "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485", + "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f", + "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8", + "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc", + "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779", + "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989", + "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1", + "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092", + "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824", + "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d", + "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55", + "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36", + "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b" + ], + "markers": "python_full_version >= '3.6.1'", + "version": "==8.1" }, "werkzeug": { "hashes": [ - "sha256:87ae4e5b5366da2347eb3116c0e6c681a0e939a33b2805e2c0cbd282664932c4", - "sha256:a13b74dd3c45f758d4ebdb224be8f1ab8ef58b3c0ffc1783a8c7d9f4f50227e6" + "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43", + "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==1.0.1" + }, + "yarl": { + "hashes": [ + "sha256:00d7ad91b6583602eb9c1d085a2cf281ada267e9a197e8b7cae487dadbfa293e", + "sha256:0355a701b3998dcd832d0dc47cc5dedf3874f966ac7f870e0f3a6788d802d434", + "sha256:15263c3b0b47968c1d90daa89f21fcc889bb4b1aac5555580d74565de6836366", + "sha256:2ce4c621d21326a4a5500c25031e102af589edb50c09b321049e388b3934eec3", + "sha256:31ede6e8c4329fb81c86706ba8f6bf661a924b53ba191b27aa5fcee5714d18ec", + "sha256:324ba3d3c6fee56e2e0b0d09bf5c73824b9f08234339d2b788af65e60040c959", + "sha256:329412812ecfc94a57cd37c9d547579510a9e83c516bc069470db5f75684629e", + "sha256:4736eaee5626db8d9cda9eb5282028cc834e2aeb194e0d8b50217d707e98bb5c", + "sha256:4953fb0b4fdb7e08b2f3b3be80a00d28c5c8a2056bb066169de00e6501b986b6", + "sha256:4c5bcfc3ed226bf6419f7a33982fb4b8ec2e45785a0561eb99274ebbf09fdd6a", + "sha256:547f7665ad50fa8563150ed079f8e805e63dd85def6674c97efd78eed6c224a6", + "sha256:5b883e458058f8d6099e4420f0cc2567989032b5f34b271c0827de9f1079a424", + "sha256:63f90b20ca654b3ecc7a8d62c03ffa46999595f0167d6450fa8383bab252987e", + "sha256:68dc568889b1c13f1e4745c96b931cc94fdd0defe92a72c2b8ce01091b22e35f", + "sha256:69ee97c71fee1f63d04c945f56d5d726483c4762845400a6795a3b75d56b6c50", + "sha256:6d6283d8e0631b617edf0fd726353cb76630b83a089a40933043894e7f6721e2", + "sha256:72a660bdd24497e3e84f5519e57a9ee9220b6f3ac4d45056961bf22838ce20cc", + "sha256:73494d5b71099ae8cb8754f1df131c11d433b387efab7b51849e7e1e851f07a4", + "sha256:7356644cbed76119d0b6bd32ffba704d30d747e0c217109d7979a7bc36c4d970", + "sha256:8a9066529240171b68893d60dca86a763eae2139dd42f42106b03cf4b426bf10", + "sha256:8aa3decd5e0e852dc68335abf5478a518b41bf2ab2f330fe44916399efedfae0", + "sha256:97b5bdc450d63c3ba30a127d018b866ea94e65655efaf889ebeabc20f7d12406", + "sha256:9ede61b0854e267fd565e7527e2f2eb3ef8858b301319be0604177690e1a3896", + "sha256:b2e9a456c121e26d13c29251f8267541bd75e6a1ccf9e859179701c36a078643", + "sha256:b5dfc9a40c198334f4f3f55880ecf910adebdcb2a0b9a9c23c9345faa9185721", + "sha256:bafb450deef6861815ed579c7a6113a879a6ef58aed4c3a4be54400ae8871478", + "sha256:c49ff66d479d38ab863c50f7bb27dee97c6627c5fe60697de15529da9c3de724", + "sha256:ce3beb46a72d9f2190f9e1027886bfc513702d748047b548b05dab7dfb584d2e", + "sha256:d26608cf178efb8faa5ff0f2d2e77c208f471c5a3709e577a7b3fd0445703ac8", + "sha256:d597767fcd2c3dc49d6eea360c458b65643d1e4dbed91361cf5e36e53c1f8c96", + "sha256:d5c32c82990e4ac4d8150fd7652b972216b204de4e83a122546dce571c1bdf25", + "sha256:d8d07d102f17b68966e2de0e07bfd6e139c7c02ef06d3a0f8d2f0f055e13bb76", + "sha256:e46fba844f4895b36f4c398c5af062a9808d1f26b2999c58909517384d5deda2", + "sha256:e6b5460dc5ad42ad2b36cca524491dfcaffbfd9c8df50508bddc354e787b8dc2", + "sha256:f040bcc6725c821a4c0665f3aa96a4d0805a7aaf2caf266d256b8ed71b9f041c", + "sha256:f0b059678fd549c66b89bed03efcabb009075bd131c248ecdf087bdb6faba24a", + "sha256:fcbb48a93e8699eae920f8d92f7160c03567b421bc17362a9ffbbd706a816f71" ], - "version": "==0.15.5" + "markers": "python_version >= '3.6'", + "version": "==1.6.3" } }, "develop": {} diff --git a/cardcalc.py b/cardcalc.py index 6ea2092..9adf3a3 100644 --- a/cardcalc.py +++ b/cardcalc.py @@ -15,474 +15,66 @@ from datetime import timedelta import os -# Make sure we have the requests library -try: - import requests -except ImportError: - raise ImportError("FFlogs parsing requires the Requests module for python." - "Run the following to install it:\n python -m pip install requests") +from cardcalc_data import Player, Pet, CardPlay, BurstWindow, DrawWindow, FightInfo, BurstDamageCollection + +from fflogsapi import class CardCalcException(Exception): pass -def fflogs_fetch(api_url, options): - """ - Gets a url and handles any API errors - """ - # for now hard card the api key - options['api_key'] = os.environ['FFLOGS_API_KEY'] - options['translate'] = True - - response = requests.get(api_url, params=options) - - # Handle non-JSON response - try: - response_dict = response.json() - except: - raise CardCalcException('Could not parse response: ' + response.text) - - # Handle bad request - if response.status_code != 200: - if 'error' in response_dict: - raise CardCalcException('FFLogs error: ' + response_dict['error']) - else: - raise CardCalcException('Unexpected FFLogs response code: ' + response.status_code) - - return response_dict - -def fflogs_api(call, report, options={}): - """ - Makes a call to the FFLogs API and returns a dictionary - """ - if call not in ['fights', 'events/summary', 'tables/damage-done']: - return {} - - api_url = 'https://www.fflogs.com:443/v1/report/{}/{}'.format(call, report) - - data = fflogs_fetch(api_url, options) - - # If this is a fight list, we're done already - if call in ['fights', 'tables/damage-done']: - return data - - # If this is events, there might be more. Fetch until we have all of it - while 'nextPageTimestamp' in data: - # Set the new start time - options['start'] = data['nextPageTimestamp'] - - # Get the extra data - more_data = fflogs_fetch(api_url, options) - - # Add the new events to the existing data - data['events'].extend(more_data['events']) - - # Continue the loop if there's more - if 'nextPageTimestamp' in more_data: - data['nextPageTimestamp'] = more_data['nextPageTimestamp'] - else: - del data['nextPageTimestamp'] - break - - # Return the event data - return data - """ -# Cards: -# -# Melee: -# Lord of Crowns id#1001876 -# The Balance id#1001882 -# The Arrow id#1001884 -# The Spear id#1001885 -# -# Ranged: -# Lady of Crowns id#1001877 -# The Bole id#1001883 -# The Ewer id#1001886 -# The Spire id#1001887 +For the initial version of this the following simple rules are use. +Every event starts with one of the following and ends with the same: + (1) Draw + (2) Sleeve Draw + (3) Divination +Redraws and plays are ignored """ -def card_to_string(card, start_time): - return '{} played {} on {} at {}'.format(card['source'], card['name'], card['target'], str(timedelta(milliseconds=(card['start']-start_time)))[2:11]) - -def card_type(guid): - return { - 1001876: 'melee', - 1001877: 'ranged', - 1001882: 'melee', - 1001884: 'melee', - 1001885: 'melee', - 1001883: 'ranged', - 1001886: 'ranged', - 1001887: 'ranged', - } [guid] - -def card_name(guid): - return { - 1001876: 'Lord of Crowns', - 1001877: 'Lady of Crowns', - 1001882: 'The Balance', - 1001884: 'The Arrow', - 1001885: 'The Spear', - 1001883: 'The Bole', - 1001886: 'The Ewer', - 1001887: 'The Spire', - } [guid] - -def card_bonus(guid): - return { - 1001876: 1.08, - 1001877: 1.08, - 1001882: 1.06, - 1001884: 1.06, - 1001885: 1.06, - 1001883: 1.06, - 1001886: 1.06, - 1001887: 1.06, - } [guid] - -def get_draws_divinations(report, start, end): - """ - Gets a list of the card draw events - """ - # x card drawn buffs - #'filter': 'ability.id in (1000915, 1000913, 1000914, 1000917, 1000916, 1000918)', - options = { - 'start': start, - 'end': end, - 'filter': 'ability.id in (3590, 7448, 3593, 16552, 1000915, 1000913, 1000914, 1000917, 1000916, 1000918)', - } +def get_draw_windows(card_events, start_time, end_time): - event_data = fflogs_api('events/summary', report, options) + last_time = start_time + last_event = DrawWindow.Name(0) + draw_windows = [] - draws = [] - - for event in event_data['events']: - # if applybuff then create/modify event with the - # card drawn - if event['type'] == 'applybuff': - draw_set = [draw - for draw in draws - if draw['source'] == event['sourceID'] - and draw['time'] == event['timestamp'] - and 'card' not in draw] - if draw_set: - draw = draw_set[0] - draw['card'] = event['ability']['name'] - draw['id'] = event['ability']['guid'] - else: - draws.append({ - 'source': event['sourceID'], - 'time': event['timestamp'], - 'card': event['ability']['name'], - 'id': event['ability']['guid'], - }) - # if cast then create/modify even with the draw type - # from (draw, redraw, sleevedraw) - elif event['type'] == 'cast' and event['ability']['name'] != 'Divination': - draw_set = [draw - for draw in draws - if draw['source'] == event['sourceID'] - and draw['time'] == event['timestamp'] - and 'type' not in draw] - if draw_set: - draw = draw_set[0] - draw['type'] = event['ability']['name'] - else: - draws.append({ - 'source': event['sourceID'], - 'time': event['timestamp'], - 'type': event['ability']['name'], - }) - - divinations = [] - for event in event_data: - if event['ability']['name'] == 'Divination': - divinations.append({ - 'source': event['sourceID'], - 'time': event['timestamp'], - 'type': event['ability']['name'], - }) - - return (draws, divinations) - -def get_cards_played(report, start, end): - """ - Gets a list of cards played - """ - options = { - 'start': start, - 'end': end, - 'filter': 'ability.id in (1001877, 1001883, 1001886, 1001887, 1001876, 1001882, 1001884, 1001885)' - } + for events in card_events: + # check if cast and if it's draw/sleeve/div + if event['type'] == 'cast' and event['abilityGameID'] in [3590, 16552, 7448]: + draw_windows.append(DrawWindow(last_time, event['timestamp'], last_event, DrawWindow.Name(event['abilityGameID']))) - # print('API Call: https://www.fflogs.com:443/v1/report/{}/{}'.format('events/summary',report)) - # print('Start: {}'.format(options['start'])) - # print('End: {}'.format(options['end'])) - # print('Filter: {}'.format(options['filter'])) + last_time = event['timestamp'] + last_event = DrawWindow.Name(event['abilityGameID']) - event_data = fflogs_api('events/summary', report, options) + draw_windows.append(DrawWindow(last_time, end_time, last_event, DrawWindow.Name(-1))) + + return draw_windows +def get_cards_played(card_events, start_time, end_time): cards = [] # Build list from events - for event in event_data['events']: - # If applying the buff, add an item to the tethers + for event in card_events: + # If applying the buff, add an item to the list of + # cards played if event['type'] == 'applybuff': - cards.append({ - 'source': event['sourceID'], - 'target': event['targetID'], - 'start': event['timestamp'], - 'type': card_type(event['ability']['guid']), - 'name': card_name(event['ability']['guid']), - 'bonus': card_bonus(event['ability']['guid']), - 'id': event['ability']['guid'], - }) + cards.append(CardPlay(event['timestamp'], None, event['sourceID'], event['targetID'], event['abilityGameID'])) # If removing the buff, add an end timestamp to the matching application elif event['type'] == 'removebuff': card_set = [card for card in cards - if card['target'] == event['targetID'] and card['source'] == event['sourceID'] and card['id'] == event['ability']['guid'] and 'end' not in card] + if card.target == event['targetID'] and card.source == event['sourceID'] and card.id == event['abilityGameID'] and card.end is None] # add it to the discovered tether if card_set: card = card_set[0] - card['end'] = event['timestamp'] + card.end = event['timestamp'] # if there is no start event, add one and set it to 15s prior else: - cards.append({ - 'source': event['sourceID'], - 'target': event['targetID'], - 'start': max(event['timestamp'] - 15000, start), - 'end': event['timestamp'], - 'type': card_type(event['ability']['guid']), - 'name': card_name(event['ability']['guid']), - 'bonus': card_bonus(event['ability']['guid']), - 'id': event['ability']['guid'], - }) + cards.append(CardPlay(max(event['timestamp'] - 15000, start_time), event['timestamp'], event['sourceID'], event['targetID'], event['abilityGameID'])) for card in cards: - if 'end' not in card: - # print('Card is missing end') - card['end'] = min(card['start'] + 15000, end) + if card.end is None: + card.end = min(card['start'] + 15000, end_time) return cards -def get_damages(report, start, end): - """ - Gets non-tick, non-pet damage caused between start and end - """ - options = { - 'start': start, - 'end': end, - 'filter': 'isTick="false"' - } - - damage_data = fflogs_api('tables/damage-done', report, options) - - damages = {} - - for damage in damage_data['entries']: - damages[damage['id']] = damage['total'] - - return damages - -def get_tick_damages(report, version, start, end): - """ - Gets the damage each player caused between start and - end from tick damage that was snapshotted in the - start-end window - """ - # Set up initial options to count ticks - options = { - 'start': start, - 'end': end + 60000, # 60s is the longest dot - 'filter': """ - ability.id not in (1000493, 1000819, 1000820, 1001203, 1000821, 1000140, 1001195, 1001291, 1001221) - and ( - ( - type="applydebuff" or type="refreshdebuff" or type="removedebuff" - ) or ( - isTick="true" and - type="damage" and - target.disposition="enemy" and - ability.name!="Combined DoTs" - ) or ( - ( - type="applybuff" or type="refreshbuff" or type="removebuff" - ) and ( - ability.id=1000190 or ability.id=1000749 or ability.id=1000501 or ability.id=1001205 - ) - ) or ( - type="damage" and ability.id=799 - ) - ) - """ - # Filter explanation: - # 1. exclude non-dot debuff events like foe req that spam event log to minimize requests - # 2. include debuff events - # 3. include individual dot ticks on enemy - # 4. include only buffs corresponding to ground effect dots - # 5. include radiant shield damage - } - - tick_data = fflogs_api('events/summary', report, options) - - # Active debuff window. These will be the debuffs whose damage will count, because they - # were applied within the tether window. List of tuples (sourceID, abilityID) - active_debuffs = [] - - # These will be how much tick damage was applied by a source, only counting - # debuffs applied during the window - tick_damage = {} - - # Wildfire instances. These get special handling afterwards, for stormblood logs - wildfires = {} - - for event in tick_data['events']: - # Fix rare issue where full source is reported instead of just sourceID - if 'sourceID' not in event and 'source' in event and 'id' in event['source']: - event['sourceID'] = event['source']['id'] - - action = (event['sourceID'], event['ability']['guid']) - - # Record wildfires but skip processing for now. Only for stormblood logs - if event['ability']['guid'] == 1000861 and version < 20: - if event['sourceID'] in wildfires: - wildfire = wildfires[event['sourceID']] - else: - wildfire = {} - - if event['type'] == 'applydebuff': - if 'start' not in wildfire: - wildfire['start'] = event['timestamp'] - elif event['type'] == 'removedebuff': - if 'end' not in wildfire: - # Effective WF duration is 9.25 - wildfire['end'] = event['timestamp'] - 750 - elif event['type'] == 'damage': - if 'damage' not in wildfire: - wildfire['damage'] = event['amount'] - - wildfire['target'] = event['targetID'] - - wildfires[event['sourceID']] = wildfire - continue - - # Debuff applications inside window - if event['type'] in ['applydebuff', 'refreshdebuff', 'applybuff', 'refreshbuff'] and event['timestamp'] < end: - # Add to active if not present - if action not in active_debuffs: - active_debuffs.append(action) - - # Debuff applications outside window - elif event['type'] in ['applydebuff', 'refreshdebuff', 'applybuff', 'refreshbuff'] and event['timestamp'] > end: - # Remove from active if present - if action in active_debuffs: - active_debuffs.remove(action) - - # Debuff fades don't have to be removed. Wildfire (ShB) will - # occasionally log its tick damage after the fade event, so faded - # debuffs that deal damage should still be included as implicitly - # belonging to the last application - - # Damage tick - elif event['type'] == 'damage': - # If this is radiant shield, add to the supportID - if action[1] == 799 and event['timestamp'] < end: - if event['supportID'] in tick_damage: - tick_damage[event['supportID']] += event['amount'] - else: - tick_damage[event['supportID']] = event['amount'] - - # Add damage only if it's from a snapshotted debuff - elif action in active_debuffs: - if event['sourceID'] in tick_damage: - tick_damage[event['sourceID']] += event['amount'] - else: - tick_damage[event['sourceID']] = event['amount'] - - # Wildfire handling. This part is hard - # There will be no wildfires for shadowbringers logs, since they are handled - # as a normal DoT tick. - for source, wildfire in wildfires.items(): - # If wildfire never went off, set to 0 damage - if 'damage' not in wildfire: - wildfire['damage'] = 0 - - # If entirely within the window, just add the real value - if ('start' in wildfire and - 'end' in wildfire and - wildfire['start'] > start and - wildfire['end'] < end): - if source in tick_damage: - tick_damage[source] += wildfire['damage'] - else: - tick_damage[source] = wildfire['damage'] - - # If it started after the window, ignore it - elif 'start' in wildfire and wildfire['start'] > end: - pass - - # If it's only partially in the window, calculate how much damage tether would've affected - # Shoutout to [Odin] Lynn Nuvestrahl for explaining wildfire mechanics to me - elif 'end' in wildfire: - # If wildfire started before dragon sight, the start will be tether start - if 'start' not in wildfire: - wildfire['start'] = start - # If wildfire ended after dragon sight, the end will be tether end - if wildfire['end'] > end: - wildfire['end'] = end - - # Set up query for applicable mch damage - options['start'] = wildfire['start'] - options['end'] = wildfire['end'] - - # Only damage on the WF target by the player, not the turret - options['filter'] = 'source.type!="pet"' - options['filter'] += ' and source.id=' + str(source) - options['filter'] += ' and target.id=' + str(wildfire['target']) - - wildfire_data = fflogs_api('tables/damage-done', report, options) - - # If there's 0 damage there won't be any entries - if not len(wildfire_data['entries']): - pass - - # Filter is strict enough that we can just use the number directly - elif source in tick_damage: - tick_damage[source] += int(0.25 * wildfire_data['entries'][0]['total']) - else: - tick_damage[source] = int(0.25 * wildfire_data['entries'][0]['total']) - - return tick_damage - -def get_real_damages(damages, tick_damages, pets): - """ - Combines the two arguments, since cards work with pet damage - this also needs to add in the tick damage from pets - """ - real_damages = {} - for source in damages.keys(): - if source in tick_damages: - real_damages[source] = damages[source] + tick_damages[source] - else: - real_damages[source] = damages[source] - - # search through pets for those owned by anyone in the damage - # sources (this isn't elegant but it works for now) - for pet in pets: - if pets[pet]['petOwner'] in damages.keys() and pet in tick_damages: - real_damages[pets[pet]['petOwner']] += tick_damages[pet] - - return real_damages - -def get_blocked_damage_totals(report, start, end, interval=1, duration=15): - """ - Okay, here's the really complicated and slow process - - I want to go from the start of the fight to the end of the fight in some interval size (default: 1) and check how much damage would be snapshot for a buff of a given duration (default: 15) if played at the start of that interval - - Then combine all of these values for each actor so this information can be parsed/plotted/etc (I don't know, this is gonna be a massive amount of data parsing and ultimately I can't afford to actually make this many API requests so I'm gonna need to grab the whole fight at once and slowly parse it?????) - """ - def print_results(results, friends, encounter_info): """ Prints the results of the tether calculations diff --git a/cardcalc_data.py b/cardcalc_data.py index bf67e2d..b3bde2f 100644 --- a/cardcalc_data.py +++ b/cardcalc_data.py @@ -1,11 +1,10 @@ from datetime import timedelta class Player: - def __init__(self, id, name, job, info): + def __init__(self, id, name, job): self.id = id self.name = name self.job = job - self.type = info class Pet: def __init__(self, id, name, owner): @@ -13,8 +12,34 @@ def __init__(self, id, name, owner): self.name = name self.owner = owner +class ActorList: + def __init__(self, players: dict, pets: dict): + self.players = players + self.pets = pets + + def PrintAll(self): + tabular = '{:<24}{:>4} {}' + print('Players') + print(tabular.format('Name','ID','Job')) + print('-'*40) + for _, p in self.players.items(): + print(tabular.format(p.name, p.id, p.job)) + + print('\n') + print('Pets') + print(tabular.format('Name','ID','Owner')) + print('-'*40) + for _, p in self.pets.items(): + print(tabular.format(p.name, p.id, self.players[p.owner].name)) + + def GetPlayerID(self, name): + for i, p in self.players.items(): + if p.name == name: + return i + return -1 + class CardPlay: - def __init__(self, start, end, source, target, id): + def __init__(self, start: int, end: int, source: int, target: int, id: int): self.start = start self.end = end self.source = source @@ -71,6 +96,13 @@ def Bonus(id): 1001887: 1.06, } [id] +class SearchWindow: + def __init__(self, start, end, duration, step): + self.start = start + self.end = end + self.duration = duration + self.step = step + class BurstWindow: def __init__(self, start, end): self.start = start @@ -89,6 +121,8 @@ def Duration(self): @staticmethod def Name(id): return { + -1: 'Fight End', + 0: 'Fight Start', 3590: 'Draw', 16552: 'Divination', 7448: 'Sleeve Draw', @@ -102,23 +136,50 @@ def __init__(self, report_id, fight_number, start_time, end_time): self.start = start_time self.end = end_time - def Duration(self): - return(timedelta(self.end-self.start).total_seconds) + def Duration(self, time = None): + if time is not None: + return timedelta(milliseconds=(time-self.start)).total_seconds() + else: + return timedelta(milliseconds=(self.end-self.start)).total_seconds() + + def TimeElapsed(self, time = None): + if time is not None: + return str(timedelta(milliseconds=(time-self.start)))[2:11] + else: + return str(timedelta(milliseconds=(self.end-self.start)))[2:11] class BurstDamageCollection: - def __init__(self, list): + def __init__(self, list, duration): self.list = list + self.duration = duration - def GetMax(self, id=None): + # this returns a tuple with the (timestamp, id, damage) set which is the + # max + def GetMax(self, id=None, time=None): # if an ID is given then return the max damage done - # by that person - if id is not None: - # get the timestamp where they did the most damage - max_item = sorted(self.list.items(), key=lambda dmg: dmg[1][id], reverse=True)[0] - # otherwise return the max damage done by anyone + # by that person over the duration defined by the collection + + if time is not None: + if time in self.list: + if id is not None: + # print('Getting max value for {} at {}'.format(id,time)) + return (time, id, self.list[time][id]) + else: + # print('Getting max value for any actor at {}'.format(time)) + max_item = sorted(self.list[time].items(), key=lambda dmg: dmg[1], reverse=True )[0] + return(time, max_item[0], max_item[1]) + else: + return (time, 0, 0) else: - # get the timestamp where the most damage was done by anyone - max_item = sorted(self.list.items(), key=lambda dmg: sorted(dmg[1].items(), key=lambda ids: ids[1], reverse=True)[0][1], reverse=True)[0] - # get the id of the person who did the most damage at that time - id = sorted(max_item[1].items(), key=lambda item: item[1], reverse=True)[0][0] - return (max_item[0], id, max_item[1][id]) + if id is not None: + # get the timestamp where they did the most damage + # print('Getting max value for {}'.format(id)) + max_item = sorted(self.list.items(), key=lambda dmg: dmg[1][id], reverse=True)[0] + # otherwise return the max damage done by anyone + else: + # print('Getting max value for any actor') + # get the timestamp where the most damage was done by anyone + max_item = sorted(self.list.items(), key=lambda dmg: sorted(dmg[1].items(), key=lambda ids: ids[1], reverse=True)[0][1], reverse=True)[0] + # get the id of the person who did the most damage at that time + id = sorted(max_item[1].items(), key=lambda item: item[1], reverse=True)[0][0] + return (max_item[0], id, max_item[1][id]) diff --git a/damagecalc.py b/damagecalc.py index e69de29..089c6a4 100644 --- a/damagecalc.py +++ b/damagecalc.py @@ -0,0 +1,139 @@ +from cardcalc_data import Player, Pet, SearchWindow, FightInfo, BurstDamageCollection, ActorList + +""" +This takes a collection of damage events associated with ticks as well as the +""" +def calculate_tick_snapshot_damage(damage_events): + active_debuffs = {} + summed_tick_damage = [] + + for event in damage_events['tickDamage']: + action = (event['sourceID'], event['targetID'], event['abilityGameID']) + + # these events are either: + # - apply{buff/debuff} + # - reapply{buff,debuff} + # - remove{buff,debuff} (can ignore these) + # - damage + + # damage is summed from the application (apply or reapply) until + # another application event or the end of the data + + # that damage is then reassociated with application event + + if event['type'] in ['applybuff', 'refreshbuff', 'applydebuff', 'refreshdebuff'] and event['timestamp']: + # if it's not an active effect then add it + if action not in active_debuffs: + active_debuffs[action] = { + 'timestamp': event['timestamp'], + 'damage': 0, + } + # if it is an active debuff then add a new damage event associated + # with the sum and restart summing the damage from this event + else: + summed_tick_damage.append({ + 'type': 'damagesnapshot', + 'sourceID': action[0], + 'targetID': action[1], + 'abilityGameID': action[2], + 'amount': active_debuffs[action]['damage'], + 'timestamp': active_debuffs[action]['timestamp'], + }) + active_debuffs[action] = { + 'timestamp': event['timestamp'], + 'damage': 0, + } + elif event['type'] == 'damage': + if action in active_debuffs: + active_debuffs[action]['damage'] += event['amount'] + + # now that we're done we can add the remaining events into the damage array + for action in active_debuffs: + if active_debuffs[action]['damage'] != 0: + summed_tick_damage.append({ + 'type': 'damagesnapshot', + 'sourceID': action[0], + 'targetID': action[1], + 'abilityGameID': action[2], + 'amount': active_debuffs[action]['damage'], + 'timestamp': active_debuffs[action]['timestamp'], + }) + + # finally sort the new array of snapshotdamage events and return it + sorted_tick_damage = sorted(summed_tick_damage, key=lambda tick: tick['timestamp']) + + damage_report = { + 'rawDamage': damage_events['rawDamage'], + 'snapshotDamage': sorted_tick_damage, + 'combinedDamage': sorted(sorted_tick_damage + damage_events['rawDamage'], key=lambda tick: tick['timestamp']) + } + + return damage_report + +def calculate_total_damage(damage_report, start_time, end_time, actors: ActorList): + combined_damage = {} + + # add all raw damage events + for event in damage_report['combinedDamage']: + if event['timestamp'] > start_time and event['timestamp'] < end_time: + if event['sourceID'] in combined_damage: + combined_damage[event['sourceID']] += event['amount'] + else: + combined_damage[event['sourceID']] = event['amount'] + # assume order events and if the current event is after + # the end time then we're done + if event['timestamp'] > end_time: + break + + player_damage = {} + for p in actors.players: + if p in combined_damage: + player_damage[p] = combined_damage[p] + else: + player_damage[p] = 0 + combined_damage[p] = 0 + + pet_damage = {} + for p in actors.pets: + if p in combined_damage: + pet_damage[p] = combined_damage[p] + if actors.pets[p].owner in player_damage: + player_damage[actors.pets[p].owner] += combined_damage[p] + else: + player_damage[actors.pets[p].owner] = combined_damage[p] + else: + pet_damage[p] = 0 + combined_damage[p] = 0 + + return (combined_damage, player_damage, pet_damage) + +""" +This searches a window of time for the optimal card play + +damage_report: contains all damage instances (both raw and from summing dot snapshots) +start_time: initial value for the search interval to start +end_time: final time that the interval can start +duration: the length of the interval (in milliseconds) +step_size: step_size for the search (in milliseconds) +""" +def search_burst_window(damage_report, search_window: SearchWindow, actors: ActorList): + # start searching at the start + interval_start = search_window.start + interval_end = interval_start + search_window.duration + + damage_collection = {} + + while interval_start < search_window.end: + (total_damage, _, _) = calculate_total_damage(damage_report, interval_start, interval_end, actors) + + # sorted_damage = sorted(total_damage.items(), key=lambda dmg: dmg[1]) + + # add all values to the collection at this timestamp + damage_collection[interval_start] = {} + for ind in total_damage: + damage_collection[interval_start][ind] = total_damage[ind] + + interval_start += search_window.step + interval_end = interval_start + search_window.duration + + return BurstDamageCollection(damage_collection, search_window.duration) \ No newline at end of file diff --git a/fflogsapi.py b/fflogsapi.py index a4c5c8e..05af731 100644 --- a/fflogsapi.py +++ b/fflogsapi.py @@ -7,26 +7,14 @@ from datetime import timedelta import os -from cardcalc import card_type, card_name, card_bonus +# local imports +from cardcalc_data import Player, Pet, FightInfo -# Make sure we have the requests library -try: - import requests -except ImportError: - raise ImportError("FFlogs parsing requires the Requests module for python." - "Run the following to install it:\n python -m pip install requests") - -try: - from requests_oauthlib import OAuth2Session -except ImportError: - raise ImportError("This requires the OAuth Lib extension to the Requests module for python.") - -try: - from oauthlib.oauth2 import BackendApplicationClient -except ImportError: - raise ImportError("This requires the OAuth Lib module for python.") - -from python_graphql_client import GraphqlClient +# Imports related to making API requests +import requests +from requests_oauthlib import OAuth2Session +from oauthlib.oauth2 import BackendApplicationClient +from python_graphql_client import GraphqlClient FFLOGS_CLIENT_ID = os.environ['FFLOGS_CLIENT_ID'] FFLOGS_CLIENT_SECRET = os.environ['FFLOGS_CLIENT_SECRET'] @@ -36,13 +24,26 @@ client = GraphqlClient(FFLOGS_URL) +# this is used to handle sorting events +def event_priority(event): + return { + 'applydebuff': 1, + 'applybuff': 1, + 'refreshdebuff': 2, + 'refreshbuff': 2, + 'removedebuff': 4, + 'removebuff': 4, + 'damage': 3, + 'damagesnapshot': 3, + }[event] + +# used to obtain a bearer token from the fflogs api def get_bearer_token(): - client = BackendApplicationClient(client_id=client_id) - oauth = OAuth2Session(client=client) - token = oauth.fetch_token(token_url=FFLOGS_OAUTH_URL, client_id=client_id, client_secret=client_pass) + token_client = BackendApplicationClient(client_id=FFLOGS_CLIENT_ID) + oauth = OAuth2Session(client=token_client) + token = oauth.fetch_token(token_url=FFLOGS_OAUTH_URL, client_id=FFLOGS_CLIENT_ID, client_secret=FFLOGS_CLIENT_SECRET) return token -def request_fflogs_api(payload, token): headers = { 'Content-TYpe': 'application/json', 'Authorization': 'Bearer {}'.format(token['access_token']), @@ -51,6 +52,8 @@ def request_fflogs_api(payload, token): return response +# make a request for the data defined in query given a set of +# variables def call_fflogs_api(query, variables, token): headers = { 'Content-TYpe': 'application/json', @@ -60,12 +63,10 @@ def call_fflogs_api(query, variables, token): return data -""" - The following are some standard methods for getting fight - data that will be used a lot -""" - -def get_fight_time(report, fight, token): +def get_fight_info(report, fight, token): + variables = { + 'code': report + } query = """ query reportData($code: String!) { reportData { @@ -79,15 +80,20 @@ def get_fight_time(report, fight, token): } } """ - variables = {'code': report} data = call_fflogs_api(query, variables, token) fights = data['data']['reportData']['report']['fights'] - + for f in fights: if f['id'] == fight: - return f + return FightInfo(report_id=report, fight_number=fight, start_time=f['startTime'], end_time=f['endTime']) -def get_all_actors(report, start_time, end_time, token): +def get_actor_lists(fight_info: FightInfo, token): + variables = { + 'code': fight_info.id, + 'startTime': fight_info.start, + 'endTime': fight_info.end, + } + query = """ query reportData($code: String!, $startTime: Float!, $endTime: Float) { reportData { @@ -106,11 +112,6 @@ def get_all_actors(report, start_time, end_time, token): } }""" - variables = { - 'code': report, - 'startTime': start_time, - 'endTime': end_time - } data = call_fflogs_api(query, variables, token) master_data = data['data']['reportData']['report']['masterData'] table = data['data']['reportData']['report']['table'] @@ -122,23 +123,20 @@ def get_all_actors(report, start_time, end_time, token): pets = {} for p in composition: - players[p['id']] = { - 'name': p['name'], - 'id': p['id'], - 'type': p['type'] - } + players[p['id']] = Player(p['id'], p['name'], p['type']) for p in pet_list: if p['petOwner'] in players: - pets[p['id']] = { - 'name': p['name'], - 'id': p['id'], - 'owner': p['petOwner'], - } + pets[p['id']] = Pet(p['id'], p['name'], p['petOwner']) return (players, pets) -def get_card_play_events(report, start_time, end_time, token): +def get_card_play_events(fight_info: FightInfo, token): + variables = { + 'code': fight_info.id, + 'startTime': fight_info.start, + 'endTime': fight_info.end, + } query = """ query reportData($code: String!, $startTime: Float!, $endTime: Float!) { reportData { @@ -150,68 +148,23 @@ def get_card_play_events(report, start_time, end_time, token): filterExpression: "ability.id in (1001877, 1001883, 1001886, 1001887, 1001876, 1001882, 1001884, 1001885)" ) { data - } + } } } } """ - variables = { - 'code': report, - 'startTime': start_time, - 'endTime': end_time - } data = call_fflogs_api(query, variables, token) card_events = data['data']['reportData']['report']['cards']['data'] return card_events -def get_cards_played(card_events, start_time, end_time): - cards = [] - - # Build list from events - for event in card_events: - # If applying the buff, add an item to the list of - # cards played - if event['type'] == 'applybuff': - cards.append({ - 'source': event['sourceID'], - 'target': event['targetID'], - 'start': event['timestamp'], - 'type': card_type(event['abilityGameID']), - 'name': card_name(event['abilityGameID']), - 'bonus': card_bonus(event['abilityGameID']), - 'id': event['abilityGameID'], - }) - # If removing the buff, add an end timestamp to the matching application - elif event['type'] == 'removebuff': - card_set = [card - for card in cards - if card['target'] == event['targetID'] and card['source'] == event['sourceID'] and card['id'] == event['abilityGameID'] and 'end' not in card] - # add it to the discovered tether - if card_set: - card = card_set[0] - card['end'] = event['timestamp'] - # if there is no start event, add one and set it to 15s prior - else: - cards.append({ - 'source': event['sourceID'], - 'target': event['targetID'], - 'start': max(event['timestamp'] - 15000, start_time), - 'end': event['timestamp'], - 'type': card_type(event['abilityGameID']), - 'name': card_name(event['abilityGameID']), - 'bonus': card_bonus(event['abilityGameID']), - 'id': event['abilityGameID'], - }) - for card in cards: - if 'end' not in card: - card['end'] = min(card['start'] + 15000, end_time) - - return cards - - -def get_card_draw_events(report, start_time, end_time, token): +def get_card_draw_events(fight_info: FightInfo, token): + variables = { + 'code': fight_info.id, + 'startTime': fight_info.start, + 'endTime': fight_info.end, + } query = """ query reportData($code: String!, $startTime: Float!, $endTime: Float!) { reportData { @@ -227,46 +180,19 @@ def get_card_draw_events(report, start_time, end_time, token): } } """ - variables = { - 'code': report, - 'startTime': start_time, - 'endTime': end_time, - } data = call_fflogs_api(query, variables, token) card_events = data['data']['reportData']['report']['draws']['data'] return card_events -""" -For the initial version of this the following simple rules are use. -Every event starts with one of the following and ends with the same: - (1) Draw - (2) Sleeve Draw - (3) Divination -Redraws and plays are ignored -""" -def get_draw_windows(card_events, start_time, end_time): - - last_time = start_time - last_event = 'Fight Start' - draw_windows = [] - - for events in card_events: - # check if cast and if it's draw/sleeve/div - if event['type'] == 'cast' and event['abilityGameID'] in [3590, 16552, 7448]: - draw_windows.append({ - 'start': last_time, - 'end': event['timestamp'], - 'startEvent': last_event, - 'endEvent': {3590: 'Draw', 16552: 'Divination', 7448: 'Sleeve Draw'}[event['abilityGameID']] - }) - last_time = event['timestamp'] - last_event = {3590: 'Draw', 16552: 'Divination', 7448: 'Sleeve Draw'}[event['abilityGameID']] - - return draw_windows - -def get_damages(report, start_time, end_time, token): +# this shouldn't be used much but can be useful so I'm leaving it in +def get_damages(fight_info: FightInfo, token): + variables = { + 'code': fight_info.id, + 'startTime': fight_info.start, + 'endTime': fight_info.end, + } query = """ query reportData ($code: String!, $startTime: Float!, $endTime: Float!) { reportData { @@ -281,12 +207,6 @@ def get_damages(report, start_time, end_time, token): } } }""" - - variables = { - 'code': report, - 'startTime': start_time, - 'endTime': end_time, - } data = call_fflogs_api(query, variables, token) damage_entries = data['data']['reportData']['report']['table']['data']['entries'] @@ -298,7 +218,12 @@ def get_damages(report, start_time, end_time, token): return damages -def get_all_damage_events(report, start_time, end_time, token): +def get_damage_events(fight_info: FightInfo, token): + variables = { + 'code': fight_info.id, + 'startTime': fight_info.start, + 'endTime': fight_info.end, + } query = """ query reportData($code: String!, $startTime: Float!, $endTime: Float!) { reportData { @@ -307,6 +232,7 @@ def get_all_damage_events(report, start_time, end_time, token): startTime: $startTime, endTime: $endTime, dataType: DamageDone, + limit: 10000, filterExpression: "isTick='false' and type!='calculateddamage'" ) { data @@ -315,6 +241,7 @@ def get_all_damage_events(report, start_time, end_time, token): startTime: $startTime, endTime: $endTime, dataType: DamageDone, + limit: 10000, filterExpression: "isTick='true' and ability.id != 500000" ) { data @@ -324,6 +251,7 @@ def get_all_damage_events(report, start_time, end_time, token): endTime: $endTime, dataType: Debuffs, hostilityType: Enemies, + limit: 10000, filterExpression: "ability.id not in (1000493, 1001203, 1001195, 1001221)" ) { data @@ -332,6 +260,7 @@ def get_all_damage_events(report, start_time, end_time, token): startTime: $startTime, endTime: $endTime, dataType: Buffs, + limit: 10000, filterExpression: "ability.id in (1000749, 1000501, 1001205, 1000312, 1001869)" ) { data @@ -341,11 +270,6 @@ def get_all_damage_events(report, start_time, end_time, token): } """ - variables = { - 'code': report, - 'startTime': start_time, - 'endTime': end_time, - } data = call_fflogs_api(query, variables, token) base_damages = data['data']['reportData']['report']['damage']['data'] @@ -353,220 +277,10 @@ def get_all_damage_events(report, start_time, end_time, token): tick_events = data['data']['reportData']['report']['tickEvents']['data'] ground_events = data['data']['reportData']['report']['groundEvents']['data'] - raw_combined_ticks = tick_damages + tick_events + ground_events - - combined_tick_events = sorted(raw_combined_ticks, key=lambda tick: (tick['timestamp'], event_priority(tick['type']))) + combined_tick_events = sorted((tick_damages + tick_events + ground_events), key=lambda tick: (tick['timestamp'], event_priority(tick['type']))) - damage_data = { + damage_events = { 'rawDamage': base_damages, 'tickDamage': combined_tick_events, } - return damage_data - -def event_priority(event): - return { - 'applydebuff': 1, - 'applybuff': 1, - 'refreshdebuff': 2, - 'refreshbuff': 2, - 'removedebuff': 4, - 'removebuff': 4, - 'damage': 3, - 'damagesnapshot': 3, - }[event] - -""" -This takes a collection of damage events associated with ticks as well as the - -""" -def sum_tick_damage_snapshots(damage_report): - active_debuffs = {} - summed_tick_damage = [] - - for event in damage_report['tickDamage']: - action = (event['sourceID'], event['targetID'], event['abilityGameID']) - - # these events are either: - # - apply{buff/debuff} - # - reapply{buff,debuff} - # - remove{buff,debuff} (can ignore these) - # - damage - - # damage is summed from the application (apply or reapply) until - # another application event or the end of the data - - # that damage is then reassociated with application event - - if event['type'] in ['applybuff', 'refreshbuff', 'applydebuff', 'refreshdebuff'] and event['timestamp']: - # if it's not an active effect then add it - if action not in active_debuffs: - active_debuffs[action] = { - 'timestamp': event['timestamp'], - 'damage': 0, - } - # if it is an active debuff then add a new damage event associated - # with the sum and restart summing the damage from this event - else: - summed_tick_damage.append({ - 'type': 'damagesnapshot', - 'sourceID': action[0], - 'targetID': action[1], - 'abilityGameID': action[2], - 'amount': active_debuffs[action]['damage'], - 'timestamp': active_debuffs[action]['timestamp'], - }) - active_debuffs[action] = { - 'timestamp': event['timestamp'], - 'damage': 0, - } - elif event['type'] == 'damage': - if action in active_debuffs: - active_debuffs[action]['damage'] += event['amount'] - - # now that we're done we can add the remaining events into the damage array - for action in active_debuffs: - if active_debuffs[action]['damage'] != 0: - summed_tick_damage.append({ - 'type': 'damagesnapshot', - 'sourceID': action[0], - 'targetID': action[1], - 'abilityGameID': action[2], - 'amount': active_debuffs[action]['damage'], - 'timestamp': active_debuffs[action]['timestamp'], - }) - - # finally sort the new array of snapshotdamage events and return it - sorted_tick_damage = sorted(summed_tick_damage, key=lambda tick: tick['timestamp']) - - new_damage_report = { - 'rawDamage': damage_report['rawDamage'], - 'snapshotDamage': sorted_tick_damage, - 'combinedDamage': sorted(sorted_tick_damage + damage_report['rawDamage'], key=lambda tick: tick['timestamp']) - } - - return new_damage_report - -def calculate_total_damage(damage_report, start_time, end_time, players, pets): - total_damage = {} - - # add all raw damage events - for event in damage_report['combinedDamage']: - if event['timestamp'] > start_time and event['timestamp'] < end_time: - if event['sourceID'] in total_damage: - total_damage[event['sourceID']] += event['amount'] - else: - total_damage[event['sourceID']] = event['amount'] - # assume order events and if the current event is after - # the end time then we're done - if event['timestamp'] > end_time: - break - - player_damage = {} - for p in players: - if p in total_damage: - player_damage[p] = total_damage[p] - # else: - # player_damage[p] = 0 - - for p in pets: - if p in total_damage: - if pets[p]['owner'] in player_damage: - player_damage[pets[p]['owner']] += total_damage[p] - else: - player_damage[pets[p]['owner']] = total_damage[p] - - return (total_damage, player_damage) - - -""" -This function is designed to sum the damage done by each actor between the two -timestamp given by start_time and end_time. This involves a simple sum over -the raw damage done and then summing all damage done by tick events applied or -refreshed during the time window - -This should not be used and instead use the calculate_total_damage function -""" -def calculate_total_event_damage(damage_report, start_time, end_time, players, pets): - raw_damage = {} - tick_damage = {} - - # add all raw damage events - for event in damage_report['rawDamage']: - if event['timestamp'] > start_time and event['timestamp'] < end_time: - if event['sourceID'] in raw_damage: - raw_damage[event['sourceID']] += event['amount'] - else: - raw_damage[event['sourceID']] = event['amount'] - # assume order events and if the current event is after - # the end time then we're done - if event['timestamp'] > end_time: - break - - active_debuffs = [] - - for event in damage_report['tickDamage']: - action = (event['sourceID'], event['targetID'], event['abilityGameID']) - - if event['type'] in ['applybuff', 'refreshbuff', 'applydebuff', 'refreshdebuff'] and event['timestamp'] > start_time and event['timestamp'] < end_time: - if action not in active_debuffs: - active_debuffs.append(action) - elif event['type'] in ['applybuff', 'refreshbuff', 'applydebuff', 'refreshdebuff'] and event['timestamp'] > end_time: - if action in active_debuffs: - active_debuffs.remove(action) - # since we removed something we can check if we're done - if len(active_debuffs) == 0 and event['timestamp'] > end_time: - break - elif event['type'] == 'damage': - if action in active_debuffs: - if event['sourceID'] in tick_damage: - tick_damage[event['sourceID']] += event['amount'] - else: - tick_damage[event['sourceID']] = event['amount'] - - combined_damage = {} - for p in players: - combined_damage[p] = 0 - if p in raw_damage: - combined_damage[p] += raw_damage[p] - if p in tick_damage: - combined_damage[p] += tick_damage[p] - - for p in pets: - if p in raw_damage: - combined_damage[pets[p]['owner']] += raw_damage[p] - if p in tick_damage: - combined_damage[pets[p]['owner']] += tick_damage[p] - - return (raw_damage, tick_damage, combined_damage) - -""" -This searches a window of time for the optimal card play - -damage_report: contains all damage instances (both raw and from summing dot snapshots) -start_time: initial value for the search interval to start -end_time: final time that the interval can start -duration: the length of the interval (in milliseconds) -step_size: step_size for the search (in milliseconds) -""" -def search_draw_window(damage_report, start_time, end_time, duration, step_size, players, pets): - - # start searching at the start - interval_start = start_time - interval_end = interval_start + duration - - max_damage = {} - - while interval_start < end_time: - (total_damage, player_damage) = calculate_total_damage(damage_report, interval_start, interval_end, players, pets) - - sorted_damage = sorted(player_damage.items(), key=lambda dmg: dmg[1]) - - max_damage[interval_start] = { - 'id': sorted_damage[0][0], - 'damage': sorted_damage[0][1] - } - - interval_start += step_size - interval_end = interval_start + duration - - return max_damage \ No newline at end of file + return damage_events diff --git a/localtesting.py b/localtesting.py deleted file mode 100644 index a736982..0000000 --- a/localtesting.py +++ /dev/null @@ -1,58 +0,0 @@ -from datetime import datetime -import os -from urllib.parse import urlparse, parse_qs - -from cardcalc import cardcalc, get_last_fight_id, CardCalcException, print_results, get_cards_played, fflogs_api, timedelta, get_friends_and_pets - -LAST_CALC_DATE = datetime.fromtimestamp(1563736200) - -def decompose_url(url): - parts = urlparse(url) - - report_id = [segment for segment in parts.path.split('/') if segment][-1] - try: - fight_id = parse_qs(parts.fragment)['fight'][0] - except KeyError: - raise CardCalcException("Fight ID is required. Select a fight first") - - if fight_id == 'last': - fight_id = get_last_fight_id(report_id) - - fight_id = int(fight_id) - - return report_id, fight_id - - -# local testing here: - -# USE THIS: https://www.fflogs.com/reports/qBxNr4V12gmZz63R#fight=12&type=damage-done - -# Call Order: -# (1) cardcalc - -#zeke's e12s 100: https://www.fflogs.com/reports/r7tnPLDhJb6KYVaf#fight=19&type=damage-done -# report = 'r7tnPLDhJb6KYVaf' -# fight = 19 - -#marielle's recent e9s run: https://www.fflogs.com/reports/byLqHjz8MnphQP3r#fight=1&type=damage-done -# report = 'byLqHjz8MnphQP3r' -# fight = 1 - -#x's e10s #1 parse (1/14/21): https://www.fflogs.com/reports/JkCGX4pqW1N2Fm9h#fight=21&type=damage-done -# report = 'JkCGX4pqW1N2Fm9h' -# fight = 21 - -#zeke's best e10s parse (1/14/21): https://www.fflogs.com/reports/Cpbh94KWTRPtHdam#fight=7&type=damage-done -# report = 'Cpbh94KWTRPtHdam' -# fight = 7 - -#e12p1 pet testing: jyXMVZbC94RB8ADh fight: 25 -report = 'jyXMVZbC94RB8ADh' -fight = 25 - -# overwrite testing: https://www.fflogs.com/reports/Xta1JRmZqDTnjzM7#fight=last -(report, fight) = decompose_url('https://www.fflogs.com/reports/Xta1JRmZqDTnjzM7#fight=last') - -(results, friends, encounter_info, cards) = cardcalc(report, fight) -print_results(results, friends, encounter_info) - diff --git a/plotting.py b/plotting.py new file mode 100644 index 0000000..0a5e2f7 --- /dev/null +++ b/plotting.py @@ -0,0 +1,2 @@ +import plotly.graph_objects as go +import pandas as pd \ No newline at end of file diff --git a/testing.py b/testing.py index 6fd8407..2684397 100644 --- a/testing.py +++ b/testing.py @@ -1,88 +1,55 @@ from datetime import timedelta import os +from urllib.parse import urlparse, parse_qs -from python_graphql_client import GraphqlClient +from fflogsapi import get_bearer_token, get_actor_lists, get_damage_events, get_fight_info +from cardcalc_data import ActorList, FightInfo, SearchWindow +from damagecalc import search_burst_window, calculate_tick_snapshot_damage -# Make sure we have the requests library -try: - import requests -except ImportError: - raise ImportError("FFlogs parsing requires the Requests module for python." - "Run the following to install it:\n python -m pip install requests") +import pandas as pd +import numpy as np +import plotly.io as pio +import plotly.express as px -from requests_oauthlib import OAuth2Session -from oauthlib.oauth2 import BackendApplicationClient +def decompose_url(url): + parts = urlparse(url) -from fflogsapi import get_fight_time, get_all_actors, get_card_play_events, get_cards_played, get_damages, get_all_damage_events, calculate_total_damage, sum_tick_damage_snapshots, search_draw_window + report_id = [segment for segment in parts.path.split('/') if segment][-1] + try: + fight_id = parse_qs(parts.fragment)['fight'][0] + except KeyError: + raise CardCalcException("Fight ID is required. Select a fight first") -client_id = '9281480c-43fe-4fbd-9cd6-3090bee3dba1' -client_pass = 'KfJPAmBM5a0hUjo5vNFaS4cwLGhdtRRsplEElcyQ' -data = 'grant_type=client_credentials' -oauth_url = 'https://www.fflogs.com/oauth/token' + if fight_id == 'last': + fight_id = get_last_fight_id(report_id) -# r = requests.get(oauth_url, auth=(client_id, client_pass)) -# print(r) + fight_id = int(fight_id) + return report_id, fight_id -client = BackendApplicationClient(client_id=client_id) -oauth = OAuth2Session(client=client) -token = oauth.fetch_token(token_url=oauth_url, client_id=client_id, client_secret=client_pass) +token = get_bearer_token() -#https://www.fflogs.com/reports/MQjnkJ7YRwqCaLcN#fight=1 +url = 'https://www.fflogs.com/reports/MQjnkJ7YRwqCaLcN#fight=1' +url = 'https://www.fflogs.com/reports/KaCwVdgTQYhmRAxD#fight=10' +report_id, fight_id = decompose_url(url) -# report_id = 'byLqHjz8MnphQP3r' -report_id = 'MQjnkJ7YRwqCaLcN' -fight = 1 +fight_info = get_fight_info(report_id, fight_id, token) +(players, pets) = get_actor_lists(fight_info, token) -data = get_fight_time(report_id, fight, token) +actors = ActorList(players, pets) -start_time = data['startTime'] -end_time = data['endTime'] +damage_data = get_damage_events(fight_info, token) +damage_report = calculate_tick_snapshot_damage(damage_data) +search_window = SearchWindow(fight_info.start, fight_info.end, 15000, 1000) +burst_damage_collection = search_burst_window(damage_report, search_window, actors) -(players, pets) = get_all_actors(report=report_id, start_time=start_time, end_time=end_time, token=token) +df = pd.DataFrame(damage_report['combinedDamage'], columns=['timestamp', 'type', 'sourceID', 'targetID', 'abilityGameID', 'amount']) -# card_events = get_card_play_events(report_id, start_time, end_time, token) +df['duration'] = df['timestamp'].apply(lambda x: fight_info.Duration(x)) -# cards = get_cards_played(card_events, start_time, end_time) +# fig = px.scatter(df, x='duration', y='amount', color='type') +# fig.update_layout(template='plotly_white') +# fig.update_layout(title='Damage Output Snapshot') +# fig.show() +# pio.write_html(fig, file='index.html', auto_open=True) -# player_cards = [] -# for c in cards: -# if c['target'] not in pets: -# player_cards.append(c) - -# cards = player_cards - -custom_start = 100000 -custom_end = 140000 - -damage_data = get_all_damage_events(report_id, start_time, end_time, token) -damage_report = sum_tick_damage_snapshots(damage_data) - -# (total_damage, player_damage) = calculate_total_damage(damage_report, custom_start, custom_end, players, pets) - -max_damage_windows = search_draw_window(damage_report, start_time, end_time, 5000, 1000, players, pets) - -tabular = '{:<11}{:<24}{:>9}' -print(tabular.format('Time', 'Player', 'Damage')) -print('-' * 50) -for m in max_damage_windows: - print(tabular.format(str(timedelta(milliseconds=(m-start_time)))[2:11], players[max_damage_windows[m]['id']]['name'], max_damage_windows[m]['damage'])) - - -# print('Start Time: {}\nEnd Time: {}\n'.format(str(timedelta(milliseconds=(custom_start - start_time)))[2:11], str(timedelta(milliseconds=(custom_end - start_time)))[2:11])) - -# print("Total Damage") -# for p in total_damage: -# if p in players: -# print('{:<24} - {:>9}'.format(players[p]['name'], total_damage[p])) -# elif p in pets: -# print('{:<24} - {:>9}'.format(pets[p]['name'], total_damage[p])) -# else: -# print('{:<24} - {:>9}'.format('id: ' + str(p), total_damage[p])) - -# print() -# print("Player Damage") -# for p in player_damage: -# if p in players: -# print('{:<24} - {:>9}'.format(players[p]['name'], player_damage[p])) -# else: -# print('{:<24} - {:>9}'.format('id: ' + str(p), player_damage[p])) +actors.PrintAll() From 709f96a61913af58a53eac279a205a10691a2bfa Mon Sep 17 00:00:00 2001 From: Mel Miller Date: Sat, 23 Jan 2021 15:04:25 -0500 Subject: [PATCH 04/11] Update calc date --- app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.py b/app.py index fd4d1dc..3e9e89e 100644 --- a/app.py +++ b/app.py @@ -12,7 +12,7 @@ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy(app) -LAST_CALC_DATE = datetime.fromtimestamp(1563736200) +LAST_CALC_DATE = datetime.fromtimestamp(1611093844) class Report(db.Model): report_id = db.Column(db.String(16), primary_key=True) From 7c730bb073360f31999968a4ec4adb613ce0cd40 Mon Sep 17 00:00:00 2001 From: Mel Miller Date: Sat, 23 Jan 2021 15:05:48 -0500 Subject: [PATCH 05/11] More work on burst window damage calculations, time averaged dps tables for plotting, calculating card windows with new framework, calculating max burst targets in card draw windows --- cardcalc.py | 397 +++++++++++++++++++++++++---------------------- cardcalc_data.py | 202 +++++++++++++++++++----- damagecalc.py | 95 +++++++++--- fflogsapi.py | 45 ++++-- testing.py | 79 ++++++---- 5 files changed, 529 insertions(+), 289 deletions(-) diff --git a/cardcalc.py b/cardcalc.py index 9adf3a3..f743ea2 100644 --- a/cardcalc.py +++ b/cardcalc.py @@ -14,13 +14,13 @@ from datetime import timedelta import os +import pandas as pd -from cardcalc_data import Player, Pet, CardPlay, BurstWindow, DrawWindow, FightInfo, BurstDamageCollection +from cardcalc_data import Player, Pet, CardPlay, BurstWindow, DrawWindow, FightInfo, BurstDamageCollection, CardCalcException, ActorList, SearchWindow -from fflogsapi import +from fflogsapi import get_card_draw_events, get_card_play_events, get_actor_lists, get_fight_info, get_damage_events -class CardCalcException(Exception): - pass +from damagecalc import calculate_tick_snapshot_damage, calculate_total_damage, search_burst_window """ For the initial version of this the following simple rules are use. @@ -33,18 +33,20 @@ class CardCalcException(Exception): def get_draw_windows(card_events, start_time, end_time): last_time = start_time - last_event = DrawWindow.Name(0) + last_event = DrawWindow.GetName(0) + current_source = 0 draw_windows = [] - for events in card_events: + for event in card_events: # check if cast and if it's draw/sleeve/div if event['type'] == 'cast' and event['abilityGameID'] in [3590, 16552, 7448]: - draw_windows.append(DrawWindow(last_time, event['timestamp'], last_event, DrawWindow.Name(event['abilityGameID']))) + current_source = event['sourceID'] + draw_windows.append(DrawWindow(current_source, last_time, event['timestamp'], last_event, DrawWindow.GetName(event['abilityGameID']))) last_time = event['timestamp'] - last_event = DrawWindow.Name(event['abilityGameID']) + last_event = DrawWindow.GetName(event['abilityGameID']) - draw_windows.append(DrawWindow(last_time, end_time, last_event, DrawWindow.Name(-1))) + draw_windows.append(DrawWindow(current_source, last_time, end_time, last_event, DrawWindow.GetName(-1))) return draw_windows @@ -120,198 +122,229 @@ def print_results(results, friends, encounter_info): )) print() -def job_type(job_name): - if job_name in {'DarkKnight', 'Gunbreaker', 'Warrior','Paladin', - 'Dragoon', 'Samurai', 'Ninja', 'Monk'}: - return 'melee' - if job_name in {'Machinist', 'Dancer', 'Bard', 'WhiteMage', 'Scholar', 'Astrologian', 'Summoner', 'BlackMage', 'RedMage'}: - return 'ranged' - return 'n/a' +""" +TODO: this might just need to rewritten mostly from scratch because of all the +changes I've made to the backend it's interacting with +""" -def cardcalc(report, fight_id): +def cardcalc(report, fight_id, token): """ Reads an FFLogs report and solves for optimal Card Usage """ + # get fight info + fight_info = get_fight_info(report, fight_id, token) + + # get actors + actors = get_actor_lists(fight_info, token) - report_data = fflogs_api('fights', report) - - version = report_data['logVersion'] - - fight = [fight for fight in report_data['fights'] if fight['id'] == fight_id][0] - - if not fight: - raise CardCalcException("Fight ID not found in report") - - encounter_start = fight['start_time'] - encounter_end = fight['end_time'] - - encounter_timing = timedelta(milliseconds=fight['end_time']-fight['start_time']) - - encounter_info = { - 'enc_name': fight['name'], - 'enc_time': str(encounter_timing)[2:11], - 'enc_kill': fight['kill'] if 'kill' in fight else False, - # 'enc_dur': int(encounter_timing.total_seconds()), - } - - friends = {friend['id']: friend for friend in report_data['friendlies']} - pets = {pet['id']: pet for pet in report_data['friendlyPets']} - - # Build the list of tether timings - cards = get_cards_played(report, encounter_start, encounter_end) + # Build the list of card plays and draw windows + cards = get_card_play_events(fight_info, token) + draws = get_card_draw_events(fight_info, token) + + # Get all damage event and then sort out tick event into snapshot damage events + damage_events = get_damage_events(fight_info, token) + damage_report = calculate_tick_snapshot_damage(damage_events) if not cards: raise CardCalcException("No cards played in fight") + if not draws: + raise CardCalcException("No draw events in fight") - results = [] - - # remove cards given to pets since the owner damage includes that - for card in cards: - if card['target'] in pets: - # print('Removed pet with ID: {}'.format(card['target'])) - cards.remove(card) - + # remove cards given to pets since the owner's card will account for that for card in cards: - # Easy part: non-dot damage done in window - damages = get_damages(report, card['start'], card['end']) - - # Hard part: snapshotted dot ticks, including wildfire for logVersion <20 - tick_damages = get_tick_damages(report, version, card['start'], card['end']) - - # Pet Tick damage needs to be added to the owner tick damage - # TODO: I think there's a better way to handle this but this - # works for now - # for tick in tick_damages: - # if tick in pets: - # if pets[tick]['petOwner'] in tick_damages: - # tick_damages[pets[tick]['petOwner']] += tick_damages[tick] - # else: - # tick_damages[pets[tick]['petOwner']] = tick_damages[tick] - - # Combine the two - real_damages = get_real_damages(damages, tick_damages, pets) - - # check the type of card and the type of person who received it - mult = 0 - correct_type = False - - if not (card['target'] in friends): - # print('Another pet found, ID: {}'.format(card['target'])) + if card['target'] in actors.pets: cards.remove(card) - continue - - if job_type(friends[card['target']]['type']) == card['type']: - mult = card['bonus'] - correct_type = True - else: - mult = 1 + ((mult-1.0)/2.0) - correct_type = False - - # Correct damage by removing card bonus from player with card - if card['target'] in real_damages: - real_damages[card['target']] = int(real_damages[card['target']] / mult) - - damage_list = sorted(real_damages.items(), key=lambda dmg: dmg[1], reverse=True) - - # correct possible damage from jobs with incorrect - # type by dividing their 'available' damage in half - # - # also checks if anyone in the list already has a card - # and makes a note of it (the damage bonus from that card - # can't be properly negated but this allows the user to - # ignore that individual or at least swap the card usages - # if the damage difference is large enough between the two - # windows) - corrected_damage = [] - - active_cards = [prev_card for prev_card in cards - if prev_card['start'] < card['start'] - and prev_card['end'] > card['start']] - - for damage in damage_list: - mod_dmg = 0 - - has_card = 'No' - for prev_card in active_cards: - if prev_card['start'] < card['start'] and prev_card['end'] > card['start'] and prev_card['target'] == damage[0]: - has_card = 'Yes' - - if card['type'] == job_type(friends[damage[0]]['type']): - mod_dmg = damage[1] - else: - mod_dmg = int(damage[1]/2.0) - corrected_damage.append({ - 'id': damage[0], - 'damage': mod_dmg, - 'rawdamage': damage[1], - 'jobtype': job_type(friends[damage[0]]['type']), - 'bonus': int(mod_dmg * (card['bonus'] - 1)), - 'prevcard': has_card, - }) - corrected_damage_list = sorted(corrected_damage, key=lambda dmg: dmg['damage'], reverse=True) - - # Add to results - timing = timedelta(milliseconds=card['start']-encounter_start) - - # Determine the correct target, the top non-self non-limit combatant - for top in corrected_damage_list: - if friends[top['id']]['type'] != 'LimitBreak' and friends[top['id']]['type'] != 'Limit Break' and top['prevcard'] == 'No': - correct = friends[top['id']]['name'] + # go through each draw windows and calculate the following + # (1.) Find the card played during this window and get the damage dealt by + # each player during that play window + # (2.) Loop through possible play windows form the start of the draw window + # to the end in 1s increments and calculate damage done + # (3.) Return the following: + # (a) table of players/damage done in play window + # (b) table of top damage windows + # i. include top 3/5/8/10 for draw window lasting at least + # 0/4/10/20 seconds + # ii. don't include the same player twice in the same 4s interval + # (c) start/end time of draw window + # (d) start/end events of draw window + # (e) card play time (if present) + # (f) source/target + # (g) correct target in play window + # (h) card played + + cardcalc_data = [] + + for draw in draws: + # find if there was a card played in this window + card = None + for c in cards: + if c.start > draw.start and c.start < draw.end: + card = c break - - if not correct: - correct = 'Nobody?' - results.append({ - 'damages': corrected_damage_list, - 'timing': str(timing)[2:11], - 'duration': timedelta(milliseconds=card['end']-card['start']).total_seconds(), - 'source': card['source'], - 'target': card['target'], - 'card': card['name'], - 'cardtype': card['type'], - 'correct': correct, - 'correctType': correct_type, - }) + # only handle the play window if there was a card played + card_play_data = {} + if card is not None: + # compute damage done during card play window + (damages, _, _) = calculate_total_damage(damage_report, card.start, card.end, actors) + + # check what multiplier should be used to remove the damage bonus + mult = 0 + if Player.GetRole(actors.players[card.target].type) == card.role: + mult = card.bonus + else: + mult = 1 + ((card.bonus-1.0)/2.0) + + # Correct damage by removing card bonus from player with card + if card.target in damages: + damages[card.target] = int(damages[card.target] / mult) + + # now adjust the damage for incorrect roles + corrected_damage = [] + active_cards = [prev_card for prev_card in cards + if prev_card.start < card.start + and prev_card.end > card.start] + + for pid, dmg in damages.items(): + mod_dmg = dmg + has_card = 'No' + for prev_card in active_cards: + if prev_card.start < card.start and prev_card.end > card.start and prev_card.target == pid: + has_card = 'Yes' + + if card.type != actors.players[pid].role: + mod_dmg = int(dmg/2) + + corrected_damage.append({ + 'id': pid, + 'hasCard': has_card, + 'realDamage': dmg, + 'adjustedDamage': mod_dmg, + 'role': actors.players[pid].role, + 'job': actors.players[pid].job, + }) + + # convert to dataframe + damage_table = pd.DataFrame(corrected_damage, index='id') + # get the highest damage target that isn't LimitBreak + optimal_target = damage_table[damage_table['role'] != 'LimitBreak']['adjustedDamage'].idxmax() + + if optimal_target is None: + optimal_target = 'Nobody?' + else: + optimal_target = actors.players[optimal_target].name + + correct = False + if optimal_target == actors.players[card.target].name: + correct = True + + card_play_data = { + 'cardPlayTime': card.start, + 'cardTiming': str(timedelta(milliseconds=card.start-fight_info.start))[2:11], + 'cardDuration': timedelta(milliseconds=card.end-card.start).total_seconds(), + 'cardPlayed': card.name, + 'cardSource': card.source, + 'cardTarget': card.target, + 'cardDamageTable': damage_table, + 'cardOptimalTarget': optimal_target, + 'cardCorrect': correct, + } + else: + card_play_data = { + 'cardPlayTime': 0, + 'cardTiming': 'N/A', + 'cardDuration': 0, + 'cardPlayed': 'None', + 'cardSource': 0, + 'cardTarget': 0, + 'cardDamageTable': None, + 'cardOptimalTarget': 0, + 'cardCorrect': False, + } + # now we can begin compiling data for the draw window as a whole + card_draw_data = {} - # results['duration'] = encounter_info['enc_dur'] - return results, friends, encounter_info, cards - -def get_last_fight_id(report): - """Get the last fight in the report""" - report_data = fflogs_api('fights', report) + # creates a search window from the start of the draw window to the end + # with a 15s duration and 1s step size + search_window = SearchWindow(draw.start, draw.end, 15000, 1000) + draw_window_damage_collection = search_burst_window(damage_report, search_window, actors) - return report_data['fights'][-1]['id'] + draw_window_duration = timedelta(milliseconds=(draw.end-draw.start)).total_seconds() -def get_friends_and_pets(report, fight_id): - """ - Reads an FFLogs report and solves for optimal Card Usage - """ + draw_damage_table = [] - report_data = fflogs_api('fights', report) - - version = report_data['logVersion'] - - fight = [fight for fight in report_data['fights'] if fight['id'] == fight_id][0] - - if not fight: - raise CardCalcException("Fight ID not found in report") - - encounter_start = fight['start_time'] - encounter_end = fight['end_time'] - - encounter_timing = timedelta(milliseconds=fight['end_time']-fight['start_time']) + data_count = 0 + if draw_window_duration < 4.0: + data_count = 3 + elif draw_window_duration < 10.0: + data_count = 5 + elif draw_window_duration < 20.0: + data_count = 8 + else: + data_count = 10 + + (timestamp, pid, damage) = draw_window_damage_collection.GetMax() + collected_count = 1 + draw_damage_table.append({ + 'count': collected_count, + 'id': pid, + 'damage': damage, + 'time': timestamp, + }) - encounter_info = { - 'enc_name': fight['name'], - 'enc_time': str(encounter_timing)[2:11], - 'enc_kill': fight['kill'] if 'kill' in fight else False, - # 'enc_dur': int(encounter_timing.total_seconds()), - } + optimal_time = timestamp + optimal_target = actors.players[pid].name + optimal_damage = damage + optimal_timing = str(timedelta(milliseconds=(timestamp - fight_info.start)))[2:11] + + current_damage = damage + while (collected_count < data_count and current_damage > draw_window_damage_collection.df.min(axis=0).min()): + # get the next lowest damage instance + (time_new, pid_new, damage_new) = draw_window_damage_collection.GetMax(limit=current_damage) + + # update the max damage value we've looked up + current_damage = damage_new + + # if it's the same player in a window that's already + # recorded skip it + ignore_entry = False + for table_entry in draw_damage_table: + if pid_new == table_entry['id'] and abs(time_new - table_entry['time']) < 4: + ignore_entry = True + + if ignore_entry: + continue + + # if the max damage is 0 then we're done and can exit + if damage_new == 0: + break - friends = {friend['id']: friend for friend in report_data['friendlies']} - pets = {pet['id']: pet for pet in report_data['friendlyPets']} + # otherwise we should add the entry to the table + collected_count += 1 + draw_damage_table.append({ + 'count': collected_count, + 'id': pid_new, + 'damage': damage_new, + 'time': time_new, + }) - return (friends, pets) \ No newline at end of file + card_draw_data = { + 'startTime': draw.start, + 'endTime': draw.end, + 'startEvent': draw.startEvent, + 'endEvent': draw.endEvent, + 'drawDamageTable': pd.DataFrame(draw_damage_table), + 'drawOptimalTime': optimal_time, + 'drawOptimalTarget': optimal_target, + 'drawOptimalTiming': optimal_timing, + 'drawOptimalDamage': optimal_damage, + } + + # finally combine the two sets of data and append it to the collection + # of data for each draw window/card play + combined_data = card_draw_data | card_play_data + cardcalc_data.append(combined_data) + + return cardcalc_data, actors diff --git a/cardcalc_data.py b/cardcalc_data.py index b3bde2f..c60a658 100644 --- a/cardcalc_data.py +++ b/cardcalc_data.py @@ -1,10 +1,35 @@ from datetime import timedelta +import pandas as pd +import numpy as np + +class CardCalcException(Exception): + pass class Player: def __init__(self, id, name, job): self.id = id self.name = name self.job = job + self.role = Player.GetRole(job) + + def to_dict(self): + return { + 'id': self.id, + 'name': self.name, + 'job': self.job, + 'role': self.role, + 'owner': self.id, + } + + @staticmethod + def GetRole(job): + if job in {'DarkKnight', 'Gunbreaker', 'Warrior','Paladin', 'Dragoon', 'Samurai', 'Ninja', 'Monk'}: + return 'melee' + if job in {'Machinist', 'Dancer', 'Bard', 'WhiteMage', 'Scholar', 'Astrologian', 'Summoner', 'BlackMage', 'RedMage'}: + return 'ranged' + if job in {'LimitBreak', 'Limit Break'}: + return 'LimitBreak' + return 'n/a' class Pet: def __init__(self, id, name, owner): @@ -12,11 +37,29 @@ def __init__(self, id, name, owner): self.name = name self.owner = owner + def to_dict(self): + return { + 'id': self.id, + 'name': self.name, + 'role': 'pet', + 'owner': self.owner + } + class ActorList: def __init__(self, players: dict, pets: dict): self.players = players self.pets = pets + actors = [] + for _, player in players.items(): + actors.append(player.to_dict()) + + for _, pet in pets.items(): + actors.append(pet.to_dict()) + + self.actors = pd.DataFrame(actors) + self.actors.set_index('id', drop=False, inplace=True) + def PrintAll(self): tabular = '{:<24}{:>4} {}' print('Players') @@ -32,6 +75,22 @@ def PrintAll(self): for _, p in self.pets.items(): print(tabular.format(p.name, p.id, self.players[p.owner].name)) + def PrintPlayers(self): + tabular = '{:<24}{:>4} {}' + print('Players') + print(tabular.format('Name','ID','Job')) + print('-'*40) + for _, p in self.players.items(): + print(tabular.format(p.name, p.id, p.job)) + + def PrintPets(self): + tabular = '{:<24}{:>4} {}' + print('Pets') + print(tabular.format('Name','ID','Owner')) + print('-'*40) + for _, p in self.pets.items(): + print(tabular.format(p.name, p.id, self.players[p.owner].name)) + def GetPlayerID(self, name): for i, p in self.players.items(): if p.name == name: @@ -39,16 +98,29 @@ def GetPlayerID(self, name): return -1 class CardPlay: - def __init__(self, start: int, end: int, source: int, target: int, id: int): + def __init__(self, start = 0: int, end = 0: int, source = 0: int, target = 0: int, id = 0: int): self.start = start self.end = end self.source = source self.target = target self.id = id - self.name = CardPlay.Name(id) - self.type = CardPlay.Type(id) - self.bonus = CardPlay.Bonus(id) + self.name = CardPlay.GetName(id) + self.role = CardPlay.GetRole(id) + self.bonus = CardPlay.GetBonus(id) + + def to_dict(self): + return { + 'source': self.source, + 'target': self.target, + 'type': 'play', + 'start': self.start, + 'end': self.end, + 'id': self.id, + 'name': self.name, + 'role': self.role, + 'bonus': self.bonus, + } def __str__(self): return '{} played {} on {} at {}'.format(self.source, self.name, self.target, self.start) @@ -58,7 +130,7 @@ def String(self, player_list, start_time): @staticmethod - def Name(id): + def GetName(id): return { 1001876: 'Lord of Crowns', 1001877: 'Lady of Crowns', @@ -68,10 +140,11 @@ def Name(id): 1001883: 'The Bole', 1001886: 'The Ewer', 1001887: 'The Spire', + 0: 'None', } [id] @staticmethod - def Type(id): + def GetRole(id): return { 1001876: 'melee', 1001877: 'ranged', @@ -81,10 +154,11 @@ def Type(id): 1001883: 'ranged', 1001886: 'ranged', 1001887: 'ranged', + 0: 'none', } [id] @staticmethod - def Bonus(id): + def GetBonus(id): return { 1001876: 1.08, 1001877: 1.08, @@ -94,6 +168,7 @@ def Bonus(id): 1001883: 1.06, 1001886: 1.06, 1001887: 1.06, + 0: 0, } [id] class SearchWindow: @@ -109,17 +184,28 @@ def __init__(self, start, end): self.end = end class DrawWindow(BurstWindow): - def __init__(self, start, end, startEvent, endEvent): + def __init__(self, source, start, end, startEvent, endEvent): + self.source = source self.start = start self.end = end self.startEvent = startEvent self.endEvent = endEvent + def to_dict(self): + return { + 'soruce': self.source, + 'type': 'draw', + 'start': self.start, + 'end': self.end, + 'startEvent': self.startEvent, + 'endEvent': self.endEvent + } + def Duration(self): return(timedelta(self.end-self.start).total_seconds) @staticmethod - def Name(id): + def GetName(id): return { -1: 'Fight End', 0: 'Fight Start', @@ -130,11 +216,25 @@ def Name(id): }[id] class FightInfo: - def __init__(self, report_id, fight_number, start_time, end_time): + def __init__(self, report_id, fight_number, start_time, end_time, name, kill): self.id = report_id self.index = fight_number self.start = start_time self.end = end_time + self.kill = kill + self.name = name + + def to_dict(self): + return { + 'id': self.id, + 'index': self.index, + 'start': self.start, + 'end': self.end, + 'kill': self.kill, + 'name': self.name, + 'duration': self.Duration(), + 'length': self.ToString(), + } def Duration(self, time = None): if time is not None: @@ -142,44 +242,68 @@ def Duration(self, time = None): else: return timedelta(milliseconds=(self.end-self.start)).total_seconds() - def TimeElapsed(self, time = None): + def ToString(self, time = None): if time is not None: return str(timedelta(milliseconds=(time-self.start)))[2:11] else: return str(timedelta(milliseconds=(self.end-self.start)))[2:11] + def PrintDamageObject(self, actor_list, damage_obj): + format_string = '{:>9} {:<25}...{:>9}' + print(format_string.format(self.TimeElapsed(damage_obj[0]), actor_list.actors.loc[damage_obj[1], 'name'], damage_obj[2] )) + + def TimeElapsed(self, time = None): + if time is not None: + return time-self.start + else: + return self.end-self.start + + def TimeDelta(self, time): + return timedelta(milliseconds=(time - self.start)) + class BurstDamageCollection: - def __init__(self, list, duration): - self.list = list + def __init__(self, df, duration): + self.df = df self.duration = duration # this returns a tuple with the (timestamp, id, damage) set which is the # max - def GetMax(self, id=None, time=None): - # if an ID is given then return the max damage done - # by that person over the duration defined by the collection + def GetMax(self, pid=None, time=None, limit=0): + # Options: + # (1) if there is no time and no id then find overall max + # (2) if there is no time but an id then find max for that id + # (3) if a time is specified then check if it's valid and find the + # overall max at that time + # (4) if there is a time and a player then return their damage at that + # timestamp assuming it's valid - if time is not None: - if time in self.list: - if id is not None: - # print('Getting max value for {} at {}'.format(id,time)) - return (time, id, self.list[time][id]) - else: - # print('Getting max value for any actor at {}'.format(time)) - max_item = sorted(self.list[time].items(), key=lambda dmg: dmg[1], reverse=True )[0] - return(time, max_item[0], max_item[1]) - else: - return (time, 0, 0) + # if a limit is provided (limit > 0) then only search values less than the limit + if limit > 0: + mod_df = self.df.apply(lambda x: [y if y <= limit else 0 for y in x]]) else: - if id is not None: - # get the timestamp where they did the most damage - # print('Getting max value for {}'.format(id)) - max_item = sorted(self.list.items(), key=lambda dmg: dmg[1][id], reverse=True)[0] - # otherwise return the max damage done by anyone - else: - # print('Getting max value for any actor') - # get the timestamp where the most damage was done by anyone - max_item = sorted(self.list.items(), key=lambda dmg: sorted(dmg[1].items(), key=lambda ids: ids[1], reverse=True)[0][1], reverse=True)[0] - # get the id of the person who did the most damage at that time - id = sorted(max_item[1].items(), key=lambda item: item[1], reverse=True)[0][0] - return (max_item[0], id, max_item[1][id]) + mod_df = self.df + + max_dmg = 0 + if time is None and pid is None: + # get overall max damage, person, and time + pid = mod_df.max(axis=0).idxmax() + time = mod_df.max(axis=1).idxmax() + max_dmg = mod_df.loc[time, pid] + elif pid is None and time is not None and time in mod_df.index.values: + # get max damage and the person for this time + pid = mod_df.loc[time, :].idxmax() + max_dmg = mod_df.loc[time, pid] + elif pid is not None and pid in mod_df.columns.values and time is None: + # get the max damage done by this person and at what time + time = mod_df[pid].idxmax() + max_dmg = mod_df.loc[time, pid] + elif pid is not None and pid in mod_df.columns.values and time is not None and time in mod_df.index.values: + # return the damage at time done by the given player + max_dmg = mod_df.loc[time, pid] + else: + # some error + time = 0 + pid = 0 + max_dmg = 0 + + return [int(time), int(pid), int(max_dmg)] \ No newline at end of file diff --git a/damagecalc.py b/damagecalc.py index 089c6a4..70ce587 100644 --- a/damagecalc.py +++ b/damagecalc.py @@ -1,5 +1,8 @@ from cardcalc_data import Player, Pet, SearchWindow, FightInfo, BurstDamageCollection, ActorList +import pandas as pd +import numpy as np + """ This takes a collection of damage events associated with ticks as well as the """ @@ -62,10 +65,35 @@ def calculate_tick_snapshot_damage(damage_events): # finally sort the new array of snapshotdamage events and return it sorted_tick_damage = sorted(summed_tick_damage, key=lambda tick: tick['timestamp']) + combined_damage = pd.DataFrame(sorted(sorted_tick_damage + damage_events['rawDamage'], key=lambda tick: tick['timestamp']), columns=['timestamp', 'type', 'sourceID', 'targetID', 'abilityGameID', 'amount']) + + damage_report = { + 'combinedDamage': combined_damage + } + + return damage_report + +def calculate_tick_damage(damage_events): + instanced_tick_damage = [] + + for event in damage_events['tickDamage']: + if event['type'] == 'damage': + instanced_tick_damage.append({ + 'timestamp': event['timestamp'], + 'sourceID': event['sourceID'], + 'targetID': event['targetID'], + 'amount': event['amount'], + 'type': 'tickdamage', + 'abilityGameID': event['abilityGameID'] + }) + + # finally sort the new array of snapshotdamage events and return it + sorted_tick_damage = sorted(instanced_tick_damage, key=lambda tick: tick['timestamp']) + + combined_damage = pd.DataFrame(sorted(sorted_tick_damage + damage_events['rawDamage'], key=lambda tick: tick['timestamp']), columns=['timestamp', 'type', 'sourceID', 'targetID', 'abilityGameID', 'amount']) + damage_report = { - 'rawDamage': damage_events['rawDamage'], - 'snapshotDamage': sorted_tick_damage, - 'combinedDamage': sorted(sorted_tick_damage + damage_events['rawDamage'], key=lambda tick: tick['timestamp']) + 'combinedDamage': combined_damage } return damage_report @@ -73,17 +101,11 @@ def calculate_tick_snapshot_damage(damage_events): def calculate_total_damage(damage_report, start_time, end_time, actors: ActorList): combined_damage = {} - # add all raw damage events - for event in damage_report['combinedDamage']: - if event['timestamp'] > start_time and event['timestamp'] < end_time: - if event['sourceID'] in combined_damage: - combined_damage[event['sourceID']] += event['amount'] - else: - combined_damage[event['sourceID']] = event['amount'] - # assume order events and if the current event is after - # the end time then we're done - if event['timestamp'] > end_time: - break + # create a dataframe with only the current time window + current_df = damage_report['combinedDamage'].query('timestamp >= {} and timestamp <= {}'.format(start_time, end_time)) + + for actor in current_df['sourceID'].unique(): + combined_damage[actor] = current_df.query('sourceID == {}'.format(actor))['amount'].sum() player_damage = {} for p in actors.players: @@ -116,24 +138,53 @@ def calculate_total_damage(damage_report, start_time, end_time, actors: ActorLis duration: the length of the interval (in milliseconds) step_size: step_size for the search (in milliseconds) """ -def search_burst_window(damage_report, search_window: SearchWindow, actors: ActorList): +def search_burst_window(damage_report, search_window: SearchWindow, actors: ActorList): # start searching at the start interval_start = search_window.start interval_end = interval_start + search_window.duration - damage_collection = {} + damage_collection = [] while interval_start < search_window.end: (total_damage, _, _) = calculate_total_damage(damage_report, interval_start, interval_end, actors) - # sorted_damage = sorted(total_damage.items(), key=lambda dmg: dmg[1]) - # add all values to the collection at this timestamp - damage_collection[interval_start] = {} - for ind in total_damage: - damage_collection[interval_start][ind] = total_damage[ind] + current_damage = total_damage + current_damage['timestamp'] = interval_start + damage_collection.append(current_damage) interval_start += search_window.step interval_end = interval_start + search_window.duration - return BurstDamageCollection(damage_collection, search_window.duration) \ No newline at end of file + damage_df = pd.DataFrame(damage_collection) + damage_df.set_index('timestamp', drop=True, inplace=True) + + return BurstDamageCollection(damage_df, search_window.duration) + + +def time_averaged_dps(damage_report, startTime, endTime, stepSize, timeRange): + + average_dps = [] + df = damage_report['combinedDamage'] + + current_time = startTime + min_time = max(current_time - timeRange, startTime) + max_time = min(current_time + timeRange, endTime) + + # sum up all + while current_time < endTime: + delta = (max_time - min_time)/1000 + + active_events = df.query('timestamp <= {} and timestamp >= {}'.format(max_time, min_time)) + step_damage = active_events['amount'].sum() + + average_dps.append({ + 'timestamp': current_time, + 'dps': step_damage/delta, + }) + + current_time += stepSize + min_time = max(current_time - timeRange, startTime) + max_time = min(current_time + timeRange, endTime) + + return pd.DataFrame(average_dps) \ No newline at end of file diff --git a/fflogsapi.py b/fflogsapi.py index 05af731..46e6790 100644 --- a/fflogsapi.py +++ b/fflogsapi.py @@ -8,10 +8,10 @@ import os # local imports -from cardcalc_data import Player, Pet, FightInfo +from cardcalc_data import Player, Pet, FightInfo, CardCalcException, ActorList # Imports related to making API requests -import requests +# import requests from requests_oauthlib import OAuth2Session from oauthlib.oauth2 import BackendApplicationClient from python_graphql_client import GraphqlClient @@ -29,6 +29,7 @@ def event_priority(event): return { 'applydebuff': 1, 'applybuff': 1, + 'applydebuffstack': 1, 'refreshdebuff': 2, 'refreshbuff': 2, 'removedebuff': 4, @@ -40,18 +41,11 @@ def event_priority(event): # used to obtain a bearer token from the fflogs api def get_bearer_token(): token_client = BackendApplicationClient(client_id=FFLOGS_CLIENT_ID) + oauth = OAuth2Session(client=token_client) token = oauth.fetch_token(token_url=FFLOGS_OAUTH_URL, client_id=FFLOGS_CLIENT_ID, client_secret=FFLOGS_CLIENT_SECRET) return token - headers = { - 'Content-TYpe': 'application/json', - 'Authorization': 'Bearer {}'.format(token['access_token']), - } - response = requests.request('POST', FFLOGS_URL, data=payload, headers=headers) - - return response - # make a request for the data defined in query given a set of # variables def call_fflogs_api(query, variables, token): @@ -63,6 +57,18 @@ def call_fflogs_api(query, variables, token): return data +def decompose_url(url): + parts = urlparse(url) + + report_id = [segment for segment in parts.path.split('/') if segment][-1] + try: + fight_id = parse_qs(parts.fragment)['fight'][0] + except KeyError: + raise CardCalcException("Fight ID is required. Select a fight first") + + return report_id, fight_id + + def get_fight_info(report, fight, token): variables = { 'code': report @@ -75,6 +81,8 @@ def get_fight_info(report, fight, token): id startTime endTime + name + kill } } } @@ -83,17 +91,22 @@ def get_fight_info(report, fight, token): data = call_fflogs_api(query, variables, token) fights = data['data']['reportData']['report']['fights'] - for f in fights: - if f['id'] == fight: - return FightInfo(report_id=report, fight_number=fight, start_time=f['startTime'], end_time=f['endTime']) + if fight == 'last': + f = fights[-1] + return FightInfo(report_id=report, fight_number=f['id'], start_time=f['startTime'], end_time=f['endTime'], name=f['name'], kill=f['kill']) + else: + for f in fights: + if f['id'] == fight: + return FightInfo(report_id=report, fight_number=f['id'], start_time=f['startTime'], end_time=f['endTime'], name=f['name'], kill=f['kill']) + + raise CardCalcException("Fight ID not found in report") def get_actor_lists(fight_info: FightInfo, token): variables = { 'code': fight_info.id, 'startTime': fight_info.start, 'endTime': fight_info.end, - } - + } query = """ query reportData($code: String!, $startTime: Float!, $endTime: Float) { reportData { @@ -129,7 +142,7 @@ def get_actor_lists(fight_info: FightInfo, token): if p['petOwner'] in players: pets[p['id']] = Pet(p['id'], p['name'], p['petOwner']) - return (players, pets) + return ActorList(players, pets) def get_card_play_events(fight_info: FightInfo, token): variables = { diff --git a/testing.py b/testing.py index 2684397..6acfdc3 100644 --- a/testing.py +++ b/testing.py @@ -2,54 +2,73 @@ import os from urllib.parse import urlparse, parse_qs -from fflogsapi import get_bearer_token, get_actor_lists, get_damage_events, get_fight_info -from cardcalc_data import ActorList, FightInfo, SearchWindow -from damagecalc import search_burst_window, calculate_tick_snapshot_damage +from fflogsapi import get_bearer_token, get_actor_lists, get_damage_events, get_fight_info, decompose_url +from cardcalc_data import ActorList, FightInfo, SearchWindow, CardCalcException +from damagecalc import search_burst_window, calculate_tick_snapshot_damage, calculate_tick_damage, time_averaged_dps import pandas as pd import numpy as np + import plotly.io as pio import plotly.express as px +import plotly.graph_objects as go -def decompose_url(url): - parts = urlparse(url) - - report_id = [segment for segment in parts.path.split('/') if segment][-1] - try: - fight_id = parse_qs(parts.fragment)['fight'][0] - except KeyError: - raise CardCalcException("Fight ID is required. Select a fight first") - - if fight_id == 'last': - fight_id = get_last_fight_id(report_id) - - fight_id = int(fight_id) - return report_id, fight_id +import scipy.signal as sig +import scipy as scipy token = get_bearer_token() -url = 'https://www.fflogs.com/reports/MQjnkJ7YRwqCaLcN#fight=1' -url = 'https://www.fflogs.com/reports/KaCwVdgTQYhmRAxD#fight=10' +# url = 'https://www.fflogs.com/reports/MQjnkJ7YRwqCaLcN#fight=1' +# url = 'https://www.fflogs.com/reports/KaCwVdgTQYhmRAxD#fight=10' +# url = 'https://www.fflogs.com/reports/byLqHjz8MnphQP3r#fight=1' +# url = 'https://www.fflogs.com/reports/TmzFDHfWL8bhdMAn#fight=6' +url = 'https://www.fflogs.com/reports/fZXhDbTjw7GWmKLz#fight=2' + report_id, fight_id = decompose_url(url) fight_info = get_fight_info(report_id, fight_id, token) -(players, pets) = get_actor_lists(fight_info, token) - -actors = ActorList(players, pets) +actor_list = get_actor_lists(fight_info, token) damage_data = get_damage_events(fight_info, token) + damage_report = calculate_tick_snapshot_damage(damage_data) +damage_report_base = calculate_tick_damage(damage_data) + search_window = SearchWindow(fight_info.start, fight_info.end, 15000, 1000) -burst_damage_collection = search_burst_window(damage_report, search_window, actors) +burst_damage_collection = search_burst_window(damage_report, search_window, actor_list) + +df_base = damage_report_base['combinedDamage'] +df_snapshot = damage_report['combinedDamage'] + +# df_base.set_index(pd.TimedeltaIndex(data=df_base['timestamp'].apply(lambda x: fight_info.TimeElapsed(x)), unit='ms'), inplace=True) +# df_snapshot.set_index(pd.TimedeltaIndex(data=df_snapshot['timestamp'].apply(lambda x: fight_info.TimeElapsed(x)), unit='ms'), inplace=True) -df = pd.DataFrame(damage_report['combinedDamage'], columns=['timestamp', 'type', 'sourceID', 'targetID', 'abilityGameID', 'amount']) +total_ms = fight_info.end - fight_info.start +step_size = int(total_ms/250) +averaging_size = step_size*4 +print('Step: {}\nAveraging: {}'.format(step_size, averaging_size)) -df['duration'] = df['timestamp'].apply(lambda x: fight_info.Duration(x)) +average_dps = time_averaged_dps(damage_report, fight_info.start, fight_info.end, step_size, averaging_size) +base_average_dps = time_averaged_dps(damage_report_base, fight_info.start, fight_info.end, step_size, averaging_size) + +average_dps.set_index(pd.TimedeltaIndex(data=average_dps['timestamp'].apply(lambda x: fight_info.TimeElapsed(x)), unit='ms'), inplace=True) +base_average_dps.set_index(pd.TimedeltaIndex(data=base_average_dps['timestamp'].apply(lambda x: fight_info.TimeElapsed(x)), unit='ms'), inplace=True) + +average_dps.index = average_dps.index + pd.Timestamp("1970/01/01") +base_average_dps.index = base_average_dps.index + pd.Timestamp("1970/01/01") + +fig = go.Figure() + +fig.add_trace(go.Scatter(name='Snapshot DPS', x=average_dps.index, y=average_dps['dps'], line=go.scatter.Line(shape='spline', smoothing=0.8))) + +fig.add_trace(go.Scatter(name='Base DPS', x=base_average_dps.index, y=base_average_dps['dps'], line=go.scatter.Line(shape='spline', smoothing=0.8))) -# fig = px.scatter(df, x='duration', y='amount', color='type') # fig.update_layout(template='plotly_white') -# fig.update_layout(title='Damage Output Snapshot') -# fig.show() -# pio.write_html(fig, file='index.html', auto_open=True) +fig.update_layout(title='Damage Done') + +fig.update_layout(xaxis = dict(tickformat = '%M:%S', nticks=20)) -actors.PrintAll() +fig.update_layout(yaxis_range=[0,max(average_dps['dps'].max(), base_average_dps['dps'].max())*1.05]) +fig.show() + +# pio.write_html(fig, file='index.html', auto_open=True) From a309f93d602366423049cabd21885080c4ef694e Mon Sep 17 00:00:00 2001 From: Mel Miller Date: Sat, 23 Jan 2021 15:06:47 -0500 Subject: [PATCH 06/11] Added new ignores to .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index b1a3fcf..77a029b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ __pycache__/ sample/ sample/* +.exports +index.html \ No newline at end of file From 0530c5c5f241eb873a21a6d98697bc5c2560aa8a Mon Sep 17 00:00:00 2001 From: Mel Miller Date: Sat, 23 Jan 2021 17:30:49 -0500 Subject: [PATCH 07/11] Fixed missing import for fflogsapi and some syntax errors in cardcalc_data --- cardcalc_data.py | 4 ++-- fflogsapi.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cardcalc_data.py b/cardcalc_data.py index c60a658..8174347 100644 --- a/cardcalc_data.py +++ b/cardcalc_data.py @@ -98,7 +98,7 @@ def GetPlayerID(self, name): return -1 class CardPlay: - def __init__(self, start = 0: int, end = 0: int, source = 0: int, target = 0: int, id = 0: int): + def __init__(self, start: int = 0, end: int = 0, source: int = 0, target: int = 0, id: int = 0): self.start = start self.end = end self.source = source @@ -279,7 +279,7 @@ def GetMax(self, pid=None, time=None, limit=0): # if a limit is provided (limit > 0) then only search values less than the limit if limit > 0: - mod_df = self.df.apply(lambda x: [y if y <= limit else 0 for y in x]]) + mod_df = self.df.apply(lambda x: [y if y <= limit else 0 for y in x]) else: mod_df = self.df diff --git a/fflogsapi.py b/fflogsapi.py index 46e6790..18593b5 100644 --- a/fflogsapi.py +++ b/fflogsapi.py @@ -15,6 +15,7 @@ from requests_oauthlib import OAuth2Session from oauthlib.oauth2 import BackendApplicationClient from python_graphql_client import GraphqlClient +from urllib.parse import urlparse, parse_qs FFLOGS_CLIENT_ID = os.environ['FFLOGS_CLIENT_ID'] FFLOGS_CLIENT_SECRET = os.environ['FFLOGS_CLIENT_SECRET'] From 370610640800012ba890b5893f19bb67dbfb3920 Mon Sep 17 00:00:00 2001 From: Mel Miller Date: Sat, 23 Jan 2021 18:18:35 -0500 Subject: [PATCH 08/11] Updated Pipfile with numpy dependency --- Pipfile | 1 + Pipfile.lock | 54 ++++++++++++++++++++++++++-------------------------- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/Pipfile b/Pipfile index 700a91c..f48a63f 100644 --- a/Pipfile +++ b/Pipfile @@ -22,6 +22,7 @@ gunicorn = "*" "python_graphql_client" = "*" "plotly" = "*" "pandas" = "*" +"numpy" = "*" [requires] diff --git a/Pipfile.lock b/Pipfile.lock index 936f4ca..b72fcde 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "adacce8982b630d22b2d75c5abfc0b206f911a5711aac13e0f99c8455b19814f" + "sha256": "464281d16c70818722d76ff9a1404ae496657a4a299f534c86725dab898f6f6f" }, "pipfile-spec": 6, "requires": { @@ -61,11 +61,11 @@ }, "alembic": { "hashes": [ - "sha256:4e02ed2aa796bd179965041afa092c55b51fb077de19d61835673cc80672c01c", - "sha256:5334f32314fb2a56d86b4c4dd1ae34b08c03cae4cb888bc699942104d66bc245" + "sha256:a4de8d3525a95a96d59342e14b95cab5956c25b0907dce1549bb4e3e7958f4c2", + "sha256:c057488cc8ac7c4d06025ea3907e1a4dd07af70376fa149cf6bd2bc11b43076f" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.4.3" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==1.5.2" }, "async-timeout": { "hashes": [ @@ -115,11 +115,11 @@ }, "flask-migrate": { "hashes": [ - "sha256:4dc4a5cce8cbbb06b8dc963fd86cf8136bd7d875aabe2d840302ea739b243732", - "sha256:a69d508c2e09d289f6e55a417b3b8c7bfe70e640f53d2d9deb0d056a384f37ee" + "sha256:8626af845e6071ef80c70b0dc16d373f761c981f0ad61bb143a529cab649e725", + "sha256:c1601dfd46b9204233935e5d73473cd7fa959db7a4b0e894c7aa7a9e8aeebf0e" ], "index": "pypi", - "version": "==2.5.3" + "version": "==2.6.0" }, "flask-sqlalchemy": { "hashes": [ @@ -287,7 +287,7 @@ "sha256:dbd18bcf4889b720ba13a27ec2f2aac1981bd41203b3a3b27ba7a33f88ae4827", "sha256:df609c82f18c5b9f6cb97271f03315ff0dbe481a2a02e56aeb1b1a985ce38e60" ], - "markers": "python_version >= '3.6'", + "index": "pypi", "version": "==1.19.5" }, "oauthlib": { @@ -300,26 +300,26 @@ }, "pandas": { "hashes": [ - "sha256:0be6102dd99910513e75ed6536284743ead810349c51bdeadd2a5b6649f30abb", - "sha256:272675a98fa4954b9fc0933df775596fc942e50015d7e75d8f19548808a2bfdf", - "sha256:2d8b4f532db37418121831a461fd107d826c240b098f52e7a1b4ab3d5aaa4fb2", - "sha256:33318fa24b192b1a4684347ff76679a7267fd4e547da9f71556a5914f0dc10e7", - "sha256:3bc6d2be03cb75981d8cbeda09503cd9d6d699fc0dc28a65e197165ad527b7b8", - "sha256:43482789c55cbabeed9482263cfc98a11e8fcae900cb63ef038948acb4a72570", - "sha256:616478c1bd8fe1e600f521ae2da434e021c11e7a4e5da3451d02906143d3629a", - "sha256:6c1a57e4d0d6f9633a07817c44e6b36d81c265fe4c52d0c0505513a2d0f7953c", - "sha256:7904ee438549b5223ce8dc008772458dd7c5cf0ccc64cf903e81202400702235", - "sha256:7b54c14130a3448d81eed1348f52429c23e27188d9db6e6d4afeae792bc49c11", - "sha256:8f92b07cdbfa3704d85b4264e52c216cafe6c0059b0d07cdad8cb29e0b90f2b8", - "sha256:91fd0b94e7b98528177a05e6f65efea79d7ef9dec15ee48c7c69fc39fdd87235", - "sha256:9c6692cea6d56da8650847172bdb148622f545e7782d17995822434c79d7a211", - "sha256:9e18631d996fe131de6cb31a8bdae18965cc8f39eb23fdfbbf42808ecc63dabf", - "sha256:cba93d4fd3b0a42858b2b599495aff793fb5d94587979f45a14177d1217ba446", - "sha256:e03386615b970b8b41da6a68afe717626741bb2431cec993640685614c0680e4", - "sha256:f8b87d2f541cd9bc4ecfe85a561abac85c33fe4de4ce70cca36b2768af2611f5" + "sha256:050ed2c9d825ef36738e018454e6d055c63d947c1d52010fbadd7584f09df5db", + "sha256:055647e7f4c5e66ba92c2a7dcae6c2c57898b605a3fb007745df61cc4015937f", + "sha256:23ac77a3a222d9304cb2a7934bb7b4805ff43d513add7a42d1a22dc7df14edd2", + "sha256:2de012a36cc507debd9c3351b4d757f828d5a784a5fc4e6766eafc2b56e4b0f5", + "sha256:30e9e8bc8c5c17c03d943e8d6f778313efff59e413b8dbdd8214c2ed9aa165f6", + "sha256:324e60bea729cf3b55c1bf9e88fe8b9932c26f8669d13b928e3c96b3a1453dff", + "sha256:37443199f451f8badfe0add666e43cdb817c59fa36bceedafd9c543a42f236ca", + "sha256:47ec0808a8357ab3890ce0eca39a63f79dcf941e2e7f494470fe1c9ec43f6091", + "sha256:496fcc29321e9a804d56d5aa5d7ec1320edfd1898eee2f451aa70171cf1d5a29", + "sha256:50e6c0a17ef7f831b5565fd0394dbf9bfd5d615ee4dd4bb60a3d8c9d2e872323", + "sha256:5527c5475d955c0bc9689c56865aaa2a7b13c504d6c44f0aadbf57b565af5ebd", + "sha256:57d5c7ac62925a8d2ab43ea442b297a56cc8452015e71e24f4aa7e4ed6be3d77", + "sha256:9d45f58b03af1fea4b48e44aa38a819a33dccb9821ef9e1d68f529995f8a632f", + "sha256:b26e2dabda73d347c7af3e6fed58483161c7b87a886a4e06d76ccfe55a044aa9", + "sha256:cfd237865d878da9b65cfee883da5e0067f5e2ff839e459466fb90565a77bda3", + "sha256:d7cca42dba13bfee369e2944ae31f6549a55831cba3117e17636955176004088", + "sha256:fe7de6fed43e7d086e3d947651ec89e55ddf00102f9dd5758763d56d182f0564" ], "index": "pypi", - "version": "==1.2.0" + "version": "==1.2.1" }, "plotly": { "hashes": [ From 94392af4b690abf687a522bf8e942a6adc44e183 Mon Sep 17 00:00:00 2001 From: Mel Miller Date: Tue, 26 Jan 2021 14:36:25 -0500 Subject: [PATCH 09/11] Extensive work on front end including adding tabs and cleaning up some of the return value formatting for cardcalc --- app.py | 42 ++++++-------- cardcalc.py | 95 +++++++++++++++++++++---------- cardcalc_data.py | 20 ++++--- damagecalc.py | 16 ++++-- fflogsapi.py | 5 +- templates/base.html | 42 ++++++++++++++ templates/calc.html | 135 ++++++++++++++++++++++++++++++++------------ testing.py | 91 +++++++++++++++++------------ 8 files changed, 308 insertions(+), 138 deletions(-) diff --git a/app.py b/app.py index 3e9e89e..d2dc60c 100644 --- a/app.py +++ b/app.py @@ -5,20 +5,25 @@ from flask import Flask, render_template, request, redirect, send_from_directory, url_for from flask_sqlalchemy import SQLAlchemy from sqlalchemy.exc import IntegrityError -from cardcalc import cardcalc, get_last_fight_id, CardCalcException + +from fflogsapi import decompose_url, get_bearer_token +from cardcalc_data import CardCalcException +from cardcalc import cardcalc app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = os.environ['DATABASE_URL'] app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy(app) -LAST_CALC_DATE = datetime.fromtimestamp(1611093844) +LAST_CALC_DATE = datetime.fromtimestamp(1611676663) + +token = get_bearer_token() class Report(db.Model): report_id = db.Column(db.String(16), primary_key=True) fight_id = db.Column(db.Integer, primary_key=True) results = db.Column(db.JSON) - friends = db.Column(db.JSON) + actors = db.Column(db.JSON) enc_name = db.Column(db.String(64)) enc_time = db.Column(db.String(9)) enc_kill = db.Column(db.Boolean) @@ -28,26 +33,9 @@ class Count(db.Model): count_id = db.Column(db.Integer, primary_key=True) total_reports = db.Column(db.Integer) -def decompose_url(url): - parts = urlparse(url) - - report_id = [segment for segment in parts.path.split('/') if segment][-1] - try: - fight_id = parse_qs(parts.fragment)['fight'][0] - except KeyError: - raise CardCalcException("Fight ID is required. Select a fight first") - - if fight_id == 'last': - fight_id = get_last_fight_id(report_id) - - fight_id = int(fight_id) - - return report_id, fight_id - def increment_count(db): count = Count.query.get(1) - # TODO: Fix this try: count.total_reports = count.total_reports + 1 @@ -120,12 +108,12 @@ def calc(report_id, fight_id): # Recompute if no computed timestamp if not report.computed or report.computed < LAST_CALC_DATE: try: - results, friends, encounter_info, cards = cardcalc(report_id, fight_id) + results, actors, encounter_info = cardcalc(report_id, fight_id, token) except CardCalcException as exception: return render_template('error.html', exception=exception) report.results = results - report.friends = friends + report.actors = actors report.enc_name = encounter_info['enc_name'] report.enc_time = encounter_info['enc_time'] report.enc_kill = encounter_info['enc_kill'] @@ -133,19 +121,21 @@ def calc(report_id, fight_id): db.session.commit() + # TODO: this is gonna cause some issues # These get returned with string keys, so have to massage it some - friends = {int(k):v for k,v in report.friends.items()} + actors = {int(k):v for k,v in report.actors.items()} else: try: - results, friends, encounter_info, cards = cardcalc(report_id, fight_id) + results, actors, encounter_info = cardcalc(report_id, fight_id, token) except CardCalcException as exception: return render_template('error.html', exception=exception) + report = Report( report_id=report_id, fight_id=fight_id, results=results, - friends=friends, + actors=actors, **encounter_info ) try: @@ -164,4 +154,4 @@ def calc(report_id, fight_id): # in which case we don't need to do anything besides redirect pass - return render_template('calc.html', report=report, friends=friends) + return render_template('calc.html', report=report, actors=actors) diff --git a/cardcalc.py b/cardcalc.py index f743ea2..e2c7ea4 100644 --- a/cardcalc.py +++ b/cardcalc.py @@ -73,7 +73,7 @@ def get_cards_played(card_events, start_time, end_time): cards.append(CardPlay(max(event['timestamp'] - 15000, start_time), event['timestamp'], event['sourceID'], event['targetID'], event['abilityGameID'])) for card in cards: if card.end is None: - card.end = min(card['start'] + 15000, end_time) + card.end = min(card.start + 15000, end_time) return cards @@ -137,9 +137,15 @@ def cardcalc(report, fight_id, token): # get actors actors = get_actor_lists(fight_info, token) + # actors.PrintPlayers() + # actors.PrintPets() + # Build the list of card plays and draw windows - cards = get_card_play_events(fight_info, token) - draws = get_card_draw_events(fight_info, token) + card_events = get_card_play_events(fight_info, token) + draw_events = get_card_draw_events(fight_info, token) + + cards = get_cards_played(card_events, fight_info.start, fight_info.end) + draws = get_draw_windows(draw_events, fight_info.start, fight_info.end) # Get all damage event and then sort out tick event into snapshot damage events damage_events = get_damage_events(fight_info, token) @@ -152,7 +158,7 @@ def cardcalc(report, fight_id, token): # remove cards given to pets since the owner's card will account for that for card in cards: - if card['target'] in actors.pets: + if card.target not in actors.players: cards.remove(card) # go through each draw windows and calculate the following @@ -174,8 +180,9 @@ def cardcalc(report, fight_id, token): # (h) card played cardcalc_data = [] - + count = 0 for draw in draws: + count += 1 # find if there was a card played in this window card = None for c in cards: @@ -183,15 +190,19 @@ def cardcalc(report, fight_id, token): card = c break + # print(draw) + # only handle the play window if there was a card played card_play_data = {} if card is not None: + # print(card) # compute damage done during card play window - (damages, _, _) = calculate_total_damage(damage_report, card.start, card.end, actors) - + # print('\tComputing card play damage...') + (_, damages, _) = calculate_total_damage(damage_report, card.start, card.end, actors) + # print('\tDone.') # check what multiplier should be used to remove the damage bonus mult = 0 - if Player.GetRole(actors.players[card.target].type) == card.role: + if actors.players[card.target].role == card.role: mult = card.bonus else: mult = 1 + ((card.bonus-1.0)/2.0) @@ -213,7 +224,7 @@ def cardcalc(report, fight_id, token): if prev_card.start < card.start and prev_card.end > card.start and prev_card.target == pid: has_card = 'Yes' - if card.type != actors.players[pid].role: + if card.role != actors.players[pid].role: mod_dmg = int(dmg/2) corrected_damage.append({ @@ -226,9 +237,11 @@ def cardcalc(report, fight_id, token): }) # convert to dataframe - damage_table = pd.DataFrame(corrected_damage, index='id') + card_damage_table = pd.DataFrame(corrected_damage) + card_damage_table.set_index('id', inplace=True, drop=False) + card_damage_table.sort_values(by='adjustedDamage', ascending=False, inplace=True) # get the highest damage target that isn't LimitBreak - optimal_target = damage_table[damage_table['role'] != 'LimitBreak']['adjustedDamage'].idxmax() + optimal_target = card_damage_table[card_damage_table['role'] != 'LimitBreak']['adjustedDamage'].idxmax() if optimal_target is None: optimal_target = 'Nobody?' @@ -240,13 +253,12 @@ def cardcalc(report, fight_id, token): correct = True card_play_data = { - 'cardPlayTime': card.start, - 'cardTiming': str(timedelta(milliseconds=card.start-fight_info.start))[2:11], + 'cardPlayTime': fight_info.ToString(time=card.start), 'cardDuration': timedelta(milliseconds=card.end-card.start).total_seconds(), 'cardPlayed': card.name, 'cardSource': card.source, 'cardTarget': card.target, - 'cardDamageTable': damage_table, + 'cardDamageTable': card_damage_table.to_dict(orient='records'), 'cardOptimalTarget': optimal_target, 'cardCorrect': correct, } @@ -268,12 +280,14 @@ def cardcalc(report, fight_id, token): # creates a search window from the start of the draw window to the end # with a 15s duration and 1s step size + # print('\tComputing draw window damage...') search_window = SearchWindow(draw.start, draw.end, 15000, 1000) draw_window_damage_collection = search_burst_window(damage_report, search_window, actors) draw_window_duration = timedelta(milliseconds=(draw.end-draw.start)).total_seconds() + # print('\tDone.') - draw_damage_table = [] + draw_damage = [] data_count = 0 if draw_window_duration < 4.0: @@ -285,23 +299,28 @@ def cardcalc(report, fight_id, token): else: data_count = 10 + # print(draw_window_damage_collection.df) + # print('min: {}'.format(draw_window_damage_collection.df.min().min())) + # print('\tPopulating draw window damage table...') + (timestamp, pid, damage) = draw_window_damage_collection.GetMax() collected_count = 1 - draw_damage_table.append({ + draw_damage.append({ 'count': collected_count, 'id': pid, 'damage': damage, - 'time': timestamp, + 'timestamp': timestamp, + 'time': fight_info.ToString(time=timestamp)[:5], }) - + # print('\t\tFound: {}'.format(collected_count)) optimal_time = timestamp optimal_target = actors.players[pid].name optimal_damage = damage - optimal_timing = str(timedelta(milliseconds=(timestamp - fight_info.start)))[2:11] current_damage = damage - while (collected_count < data_count and current_damage > draw_window_damage_collection.df.min(axis=0).min()): + while (collected_count < data_count and current_damage > draw_window_damage_collection.df.min().min()): # get the next lowest damage instance + # print('\t\tCurrent: max value {}'.format(current_damage)) (time_new, pid_new, damage_new) = draw_window_damage_collection.GetMax(limit=current_damage) # update the max damage value we've looked up @@ -310,41 +329,57 @@ def cardcalc(report, fight_id, token): # if it's the same player in a window that's already # recorded skip it ignore_entry = False - for table_entry in draw_damage_table: - if pid_new == table_entry['id'] and abs(time_new - table_entry['time']) < 4: + for table_entry in draw_damage: + # print('Comparing {}/{} and {}/{}'.format(pid_new, time_new, table_entry['pid'], table_entry['timestamp'])) + if pid_new == table_entry['id'] and abs(time_new - table_entry['timestamp']) < 4000: ignore_entry = True if ignore_entry: + # print('\t\tIgnoring...') continue # if the max damage is 0 then we're done and can exit if damage_new == 0: + # print('\t\tNo more results to search...') break # otherwise we should add the entry to the table collected_count += 1 - draw_damage_table.append({ + draw_damage.append({ 'count': collected_count, 'id': pid_new, 'damage': damage_new, - 'time': time_new, + 'timestamp': time_new, + 'time': fight_info.ToString(time=time_new)[:5], }) + # print('\t\tFound: {}'.format(collected_count)) + + draw_damage_table = pd.DataFrame(draw_damage) + draw_damage_table.set_index('id', inplace=True, drop=False) + draw_damage_table.sort_values(by='damage', inplace=True, ascending=False) card_draw_data = { - 'startTime': draw.start, - 'endTime': draw.end, + 'startTime': fight_info.ToString(time=draw.start), + 'endTime': fight_info.ToString(time=draw.end), 'startEvent': draw.startEvent, 'endEvent': draw.endEvent, - 'drawDamageTable': pd.DataFrame(draw_damage_table), - 'drawOptimalTime': optimal_time, + 'drawDamageTable': draw_damage_table.to_dict(orient='records'), + 'drawOptimalTime': fight_info.ToString(time=optimal_time), 'drawOptimalTarget': optimal_target, - 'drawOptimalTiming': optimal_timing, 'drawOptimalDamage': optimal_damage, + 'count': count, } # finally combine the two sets of data and append it to the collection # of data for each draw window/card play combined_data = card_draw_data | card_play_data cardcalc_data.append(combined_data) + # print('\tDone.\n') + - return cardcalc_data, actors + encounter_info = { + 'enc_name': fight_info.name, + 'enc_time': fight_info.ToString(), + 'enc_kill': fight_info.kill, + } + return cardcalc_data, actors.to_dict(), encounter_info diff --git a/cardcalc_data.py b/cardcalc_data.py index 8174347..c8e29d0 100644 --- a/cardcalc_data.py +++ b/cardcalc_data.py @@ -60,6 +60,9 @@ def __init__(self, players: dict, pets: dict): self.actors = pd.DataFrame(actors) self.actors.set_index('id', drop=False, inplace=True) + def to_dict(self): + return self.actors.to_dict(orient='index') + def PrintAll(self): tabular = '{:<24}{:>4} {}' print('Players') @@ -84,12 +87,12 @@ def PrintPlayers(self): print(tabular.format(p.name, p.id, p.job)) def PrintPets(self): - tabular = '{:<24}{:>4} {}' + tabular = '{:<24}{:>4} {:>5} {}' print('Pets') - print(tabular.format('Name','ID','Owner')) + print(tabular.format('Name','ID','OID','Owner')) print('-'*40) for _, p in self.pets.items(): - print(tabular.format(p.name, p.id, self.players[p.owner].name)) + print(tabular.format(p.name, p.id, p.owner, self.players[p.owner].name)) def GetPlayerID(self, name): for i, p in self.players.items(): @@ -109,6 +112,9 @@ def __init__(self, start: int = 0, end: int = 0, source: int = 0, target: int = self.role = CardPlay.GetRole(id) self.bonus = CardPlay.GetBonus(id) + def __str__(self): + return f'{self.source} played {self.name} on {self.target} at {self.start}' + def to_dict(self): return { 'source': self.source, @@ -122,9 +128,6 @@ def to_dict(self): 'bonus': self.bonus, } - def __str__(self): - return '{} played {} on {} at {}'.format(self.source, self.name, self.target, self.start) - def String(self, player_list, start_time): return '{} played {} on {} at {}'.format(player_list[self.source]['name'], self.name, player_list[self.target]['name'], str(timedelta(milliseconds=(self.start-start_time)))[2:11]) @@ -191,6 +194,9 @@ def __init__(self, source, start, end, startEvent, endEvent): self.startEvent = startEvent self.endEvent = endEvent + def __str__(self): + return f'From {self.startEvent} at {self.start} to {self.endEvent} at {self.end}' + def to_dict(self): return { 'soruce': self.source, @@ -279,7 +285,7 @@ def GetMax(self, pid=None, time=None, limit=0): # if a limit is provided (limit > 0) then only search values less than the limit if limit > 0: - mod_df = self.df.apply(lambda x: [y if y <= limit else 0 for y in x]) + mod_df = self.df.apply(lambda x: [y if y < limit else 0 for y in x]) else: mod_df = self.df diff --git a/damagecalc.py b/damagecalc.py index 70ce587..0b12e84 100644 --- a/damagecalc.py +++ b/damagecalc.py @@ -138,15 +138,22 @@ def calculate_total_damage(damage_report, start_time, end_time, actors: ActorLis duration: the length of the interval (in milliseconds) step_size: step_size for the search (in milliseconds) """ -def search_burst_window(damage_report, search_window: SearchWindow, actors: ActorList): +def search_burst_window(damage_report, search_window: SearchWindow, actors: ActorList): + ### + ### TODO: this function is likely the whole computational time + ### of this project right now so any work to optimize this will + ### greatly aid the performance of this project + ### # start searching at the start interval_start = search_window.start interval_end = interval_start + search_window.duration damage_collection = [] + # print('\t\tStarting search in window from {} to {}'.format(search_window.start, search_window.end)) while interval_start < search_window.end: - (total_damage, _, _) = calculate_total_damage(damage_report, interval_start, interval_end, actors) + # print('\t\t\tSearching at {}...'.format(interval_start)) + (_, total_damage, _) = calculate_total_damage(damage_report, interval_start, interval_end, actors) # add all values to the collection at this timestamp current_damage = total_damage @@ -155,10 +162,11 @@ def search_burst_window(damage_report, search_window: SearchWindow, actors: Acto interval_start += search_window.step interval_end = interval_start + search_window.duration - + # print('\t\t\tDone.') + damage_df = pd.DataFrame(damage_collection) damage_df.set_index('timestamp', drop=True, inplace=True) - + # print('\t\tDone with full search.') return BurstDamageCollection(damage_df, search_window.duration) diff --git a/fflogsapi.py b/fflogsapi.py index 18593b5..a7e9edc 100644 --- a/fflogsapi.py +++ b/fflogsapi.py @@ -66,6 +66,9 @@ def decompose_url(url): fight_id = parse_qs(parts.fragment)['fight'][0] except KeyError: raise CardCalcException("Fight ID is required. Select a fight first") + + if (fight_id != 'last'): + fight_id = int(fight_id) return report_id, fight_id @@ -186,7 +189,7 @@ def get_card_draw_events(fight_info: FightInfo, token): draws: events( startTime: $startTime, endTime: $endTime, - filterExpression: "ability.id in (3590, 7448, 3593, 1000915, 1000913, 1000914, 1000917, 1000916, 1000918)" + filterExpression: "ability.id in (3590, 7448, 16552, 3593, 1000915, 1000913, 1000914, 1000917, 1000916, 1000918)" ) { data } diff --git a/templates/base.html b/templates/base.html index 55fc358..6de369f 100644 --- a/templates/base.html +++ b/templates/base.html @@ -63,6 +63,48 @@ 100% { transform: rotate(360deg); } } + /* the following is for the tabs */ + .tabs { + position: relative; + display: block; + min-height: 650px; + /* This part sucks */ + clear: both; + margin: 25px 0; + } + .tab { + float: left; + } + .tab label { + background: #eee; + padding: 5px; + border: 1px solid #ccc; + margin-left: 1px; + left: 1px; + } + .tab [type=radio] { + display: none; + } + .content { + position: absolute; + display: flex; + top: 28px; + left: 0; + background: white; + right: 0; + bottom: 28px; + padding: 20px; + border: 1px solid #ccc; + } + [type=radio]:checked ~ label { + background: white; + border-bottom: 1px solid white; + z-index: 2; + } + [type=radio]:checked ~ label ~ .content { + z-index: 1; + } + diff --git a/templates/calc.html b/templates/calc.html index 9b35c53..419ff5e 100644 --- a/templates/calc.html +++ b/templates/calc.html @@ -8,44 +8,109 @@
{{ report.enc_time }}
Original log + {% for result in report.results %} -
-
-

- {{ friends[result['source']]['name'] }} - played - {{ result.card }} - on - {{ friends[result['target']]['name'] }} - at - {{ result.timing }} -

-

The correct target was {{ result['correct'] }}

+
+
+ + +
+ {% if result.cardPlayed != 'None' %} +
+
+

+ {{ actors[result['cardSource']]['name'] }} + played + {{ result.cardPlayed }} + on + {{ actors[result['cardTarget']]['name'] }} + at + {{ result.cardPlayTime }} +

+

The correct target was {{ result['cardOptimalTarget'] }}

+ + + + + + + + + + + + {% for damage in result['cardDamageTable'] %} + {% if actors[damage['id']]['role'] != 'LimitBreak' %} + + + + + + + {% endif %} + {% endfor %} + +
PlayerJobAdjusted DamageRaw Damage
{{ actors[damage['id']]['name'] }}{{ actors[damage['id']]['job'] }}{{ damage['adjustedDamage'] }}{{ damage['realDamage'] }}
+
+
+
+ {% else %} +
+ No card was played in this draw window (likely a result of Divination being cast) +
+ {% endif %} +
+
+ +
+ + + +
+
+
+

+ From + {{ result['startEvent'] }} + at + {{ result['startTime'] }} + until + {{ result['endEvent'] }} + at + {{ result['endTime'] }} +

+

+ The optimal target was {{ result['drawOptimalTarget'] }} + at {{ result['drawOptimalTime'] }}. +

+ + + + + + + + + + + + {% for damage in result['drawDamageTable'] %} + + + + + + + {% endfor %} + +
Play TimePlayerJobDamage
{{ damage['time'] }}{{ actors[damage['id']]['name'] }}{{ actors[damage['id']]['job'] }}{{ damage['damage'] }}
+
+
+
+
- - - - - - - - - - - {% for damage in result['damages'] %} - {% if friends[damage['id']]['type'] != 'LimitBreak' %} - - - - - - - {% endif %} - {% endfor %} - -
PlayerJobAdjusted DamageRaw Damage
{{ friends[damage['id']]['name'] }}{{ friends[damage['id']]['type'] }}{{ damage['damage'] }}{{ damage['rawdamage'] }}
-
{% endfor %} + {% endblock %} \ No newline at end of file diff --git a/testing.py b/testing.py index 6acfdc3..479be6f 100644 --- a/testing.py +++ b/testing.py @@ -5,6 +5,7 @@ from fflogsapi import get_bearer_token, get_actor_lists, get_damage_events, get_fight_info, decompose_url from cardcalc_data import ActorList, FightInfo, SearchWindow, CardCalcException from damagecalc import search_burst_window, calculate_tick_snapshot_damage, calculate_tick_damage, time_averaged_dps +from cardcalc import cardcalc import pandas as pd import numpy as np @@ -16,6 +17,47 @@ import scipy.signal as sig import scipy as scipy +def test_to_dict_damage_table(card_damage_table): + print(card_damage_table) + +def test_to_dict_actor_list(actor_list): + test_dict = actor_list.to_dict() + print(test_dict) + +def test_plotting(): + # df_base.set_index(pd.TimedeltaIndex(data=df_base['timestamp'].apply(lambda x: fight_info.TimeElapsed(x)), unit='ms'), inplace=True) + # df_snapshot.set_index(pd.TimedeltaIndex(data=df_snapshot['timestamp'].apply(lambda x: fight_info.TimeElapsed(x)), unit='ms'), inplace=True) + + total_ms = fight_info.end - fight_info.start + step_size = int(total_ms/250) + averaging_size = step_size*4 + print('Step: {}\nAveraging: {}'.format(step_size, averaging_size)) + + average_dps = time_averaged_dps(damage_report, fight_info.start, fight_info.end, step_size, averaging_size) + base_average_dps = time_averaged_dps(damage_report_base, fight_info.start, fight_info.end, step_size, averaging_size) + + average_dps.set_index(pd.TimedeltaIndex(data=average_dps['timestamp'].apply(lambda x: fight_info.TimeElapsed(x)), unit='ms'), inplace=True) + base_average_dps.set_index(pd.TimedeltaIndex(data=base_average_dps['timestamp'].apply(lambda x: fight_info.TimeElapsed(x)), unit='ms'), inplace=True) + + average_dps.index = average_dps.index + pd.Timestamp("1970/01/01") + base_average_dps.index = base_average_dps.index + pd.Timestamp("1970/01/01") + + fig = go.Figure() + + fig.add_trace(go.Scatter(name='Snapshot DPS', x=average_dps.index, y=average_dps['dps'], line=go.scatter.Line(shape='spline', smoothing=0.8))) + + fig.add_trace(go.Scatter(name='Base DPS', x=base_average_dps.index, y=base_average_dps['dps'], line=go.scatter.Line(shape='spline', smoothing=0.8))) + + # fig.update_layout(template='plotly_white') + fig.update_layout(title='Damage Done') + + fig.update_layout(xaxis = dict(tickformat = '%M:%S', nticks=20)) + + fig.update_layout(yaxis_range=[0,max(average_dps['dps'].max(), base_average_dps['dps'].max())*1.05]) + fig.show() + + # pio.write_html(fig, file='index.html', auto_open=True) + token = get_bearer_token() # url = 'https://www.fflogs.com/reports/MQjnkJ7YRwqCaLcN#fight=1' @@ -26,49 +68,28 @@ report_id, fight_id = decompose_url(url) -fight_info = get_fight_info(report_id, fight_id, token) -actor_list = get_actor_lists(fight_info, token) - -damage_data = get_damage_events(fight_info, token) - -damage_report = calculate_tick_snapshot_damage(damage_data) -damage_report_base = calculate_tick_damage(damage_data) - -search_window = SearchWindow(fight_info.start, fight_info.end, 15000, 1000) -burst_damage_collection = search_burst_window(damage_report, search_window, actor_list) - -df_base = damage_report_base['combinedDamage'] -df_snapshot = damage_report['combinedDamage'] - -# df_base.set_index(pd.TimedeltaIndex(data=df_base['timestamp'].apply(lambda x: fight_info.TimeElapsed(x)), unit='ms'), inplace=True) -# df_snapshot.set_index(pd.TimedeltaIndex(data=df_snapshot['timestamp'].apply(lambda x: fight_info.TimeElapsed(x)), unit='ms'), inplace=True) +# print('Report: {} ({})\nFight: {} ({})'.format(report_id, type(report_id), fight_id, type(fight_id))) -total_ms = fight_info.end - fight_info.start -step_size = int(total_ms/250) -averaging_size = step_size*4 -print('Step: {}\nAveraging: {}'.format(step_size, averaging_size)) +cardcalc_data, actors, _ = cardcalc(report_id, fight_id, token) -average_dps = time_averaged_dps(damage_report, fight_info.start, fight_info.end, step_size, averaging_size) -base_average_dps = time_averaged_dps(damage_report_base, fight_info.start, fight_info.end, step_size, averaging_size) +dmg_tbl = cardcalc_data[0]['cardDamageTable'] -average_dps.set_index(pd.TimedeltaIndex(data=average_dps['timestamp'].apply(lambda x: fight_info.TimeElapsed(x)), unit='ms'), inplace=True) -base_average_dps.set_index(pd.TimedeltaIndex(data=base_average_dps['timestamp'].apply(lambda x: fight_info.TimeElapsed(x)), unit='ms'), inplace=True) +test_to_dict_damage_table(dmg_tbl) -average_dps.index = average_dps.index + pd.Timestamp("1970/01/01") -base_average_dps.index = base_average_dps.index + pd.Timestamp("1970/01/01") +# fight_info = get_fight_info(report_id, fight_id, token) +# actor_list = get_actor_lists(fight_info, token) -fig = go.Figure() +# damage_data = get_damage_events(fight_info, token) -fig.add_trace(go.Scatter(name='Snapshot DPS', x=average_dps.index, y=average_dps['dps'], line=go.scatter.Line(shape='spline', smoothing=0.8))) +# damage_report = calculate_tick_snapshot_damage(damage_data) +# damage_report_base = calculate_tick_damage(damage_data) -fig.add_trace(go.Scatter(name='Base DPS', x=base_average_dps.index, y=base_average_dps['dps'], line=go.scatter.Line(shape='spline', smoothing=0.8))) +# search_window = SearchWindow(fight_info.start, fight_info.end, 15000, 1000) +# burst_damage_collection = search_burst_window(damage_report, search_window, actor_list) -# fig.update_layout(template='plotly_white') -fig.update_layout(title='Damage Done') +# df_base = damage_report_base['combinedDamage'] +# df_snapshot = damage_report['combinedDamage'] -fig.update_layout(xaxis = dict(tickformat = '%M:%S', nticks=20)) +print(actors) -fig.update_layout(yaxis_range=[0,max(average_dps['dps'].max(), base_average_dps['dps'].max())*1.05]) -fig.show() -# pio.write_html(fig, file='index.html', auto_open=True) From c1584c6e382ebe71ae0080ee113d1e6798528b11 Mon Sep 17 00:00:00 2001 From: Mel Miller Date: Tue, 26 Jan 2021 15:14:16 -0500 Subject: [PATCH 10/11] Reworked how display tabs work to make them resize correctly and look a little nicer --- templates/base.html | 125 +++++++++++++++++++++---------- templates/calc.html | 174 ++++++++++++++++++++++++-------------------- 2 files changed, 183 insertions(+), 116 deletions(-) diff --git a/templates/base.html b/templates/base.html index 6de369f..4edc017 100644 --- a/templates/base.html +++ b/templates/base.html @@ -64,45 +64,96 @@ } /* the following is for the tabs */ - .tabs { - position: relative; - display: block; - min-height: 650px; - /* This part sucks */ - clear: both; - margin: 25px 0; - } - .tab { - float: left; - } - .tab label { - background: #eee; - padding: 5px; - border: 1px solid #ccc; - margin-left: 1px; - left: 1px; - } - .tab [type=radio] { - display: none; - } - .content { - position: absolute; - display: flex; - top: 28px; - left: 0; - background: white; - right: 0; - bottom: 28px; - padding: 20px; - border: 1px solid #ccc; + .tabset { + margin-top: 15px; + margin-bottom: 15px; } - [type=radio]:checked ~ label { - background: white; - border-bottom: 1px solid white; - z-index: 2; + + .tabset > input { + display:block; /* "enable" hidden elements in IE/edge */ + position:absolute; /* then hide them off-screen */ + left:-100%; + } + + .tabset > ul { + position:static; + z-index:999; + list-style:none; + display:flex; + margin-bottom:-9px; + margin-left: -40px; + } + + .tabset > ul label, + .tabset > div { + /* border:1px solid hsl(0, 0%, 40%); */ + border:1px solid rgba(29, 49, 44, 0.733); + } + + .tabset > ul label { + display:inline-block; + padding:0.5em 2em; + background:hsl(0, 0%, 100%); + border-right-width:0; + } + + .tabset > ul li:first-child label { + border-radius:0.3em 0 0 0; } - [type=radio]:checked ~ label ~ .content { - z-index: 1; + + .tabset > ul li:last-child label { + border-right-width:1px; + border-radius:0 0.3em 0 0; + } + + .tabset > div { + position:relative; + background:hsl(0, 0%, 100%); + border-radius:0em 0.3em 0.3em 0.3em; + } + + .tabset > input:nth-child(1):checked ~ ul li:nth-child(1) label, + .tabset > input:nth-child(2):checked ~ ul li:nth-child(2) label, + .tabset > input:nth-child(3):checked ~ ul li:nth-child(3) label, + .tabset > input:nth-child(4):checked ~ ul li:nth-child(4) label, + .tabset > input:nth-child(5):checked ~ ul li:nth-child(5) label, + .tabset > input:nth-child(6):checked ~ ul li:nth-child(6) label, + .tabset > input:nth-child(7):checked ~ ul li:nth-child(7) label, + .tabset > input:nth-child(8):checked ~ ul li:nth-child(8) label, + .tabset > input:nth-child(9):checked ~ ul li:nth-child(9) label { + border-bottom-color:hsl(0, 0%, 83%); + background: rgba(120, 194, 173, 0.5) + } + + .tabset > div > section, + .tabset > div > section h2 { + position:absolute; + top:-999em; + left:-999em; + } + .tabset > div > section { + padding:1em 1em 0; + } + + .tabset > input:nth-child(1):checked ~ div > section:nth-child(1), + .tabset > input:nth-child(2):checked ~ div > section:nth-child(2), + .tabset > input:nth-child(3):checked ~ div > section:nth-child(3), + .tabset > input:nth-child(4):checked ~ div > section:nth-child(4), + .tabset > input:nth-child(5):checked ~ div > section:nth-child(5), + .tabset > input:nth-child(6):checked ~ div > section:nth-child(6), + .tabset > input:nth-child(7):checked ~ div > section:nth-child(7), + .tabset > input:nth-child(8):checked ~ div > section:nth-child(8), + .tabset > input:nth-child(9):checked ~ div > section:nth-child(9) { + position:Static; + } + + .tabset > ul label { + -webkit-touch-callout:none; + -webkit-user-select:none; + -khtml-user-select:none; + -moz-user-select:none; + -ms-user-select:none; + user-select:none; } diff --git a/templates/calc.html b/templates/calc.html index 419ff5e..8560f7d 100644 --- a/templates/calc.html +++ b/templates/calc.html @@ -10,105 +10,121 @@
{{ report.enc_time }}
{% for result in report.results %} -
-
- - -
- {% if result.cardPlayed != 'None' %} -
-
-

- {{ actors[result['cardSource']]['name'] }} - played - {{ result.cardPlayed }} - on - {{ actors[result['cardTarget']]['name'] }} - at - {{ result.cardPlayTime }} -

-

The correct target was {{ result['cardOptimalTarget'] }}

- - - - - - - - - - - - {% for damage in result['cardDamageTable'] %} - {% if actors[damage['id']]['role'] != 'LimitBreak' %} - - - - - - - {% endif %} - {% endfor %} - -
PlayerJobAdjusted DamageRaw Damage
{{ actors[damage['id']]['name'] }}{{ actors[damage['id']]['job'] }}{{ damage['adjustedDamage'] }}{{ damage['realDamage'] }}
-
-
-
- {% else %} -
- No card was played in this draw window (likely a result of Divination being cast) -
- {% endif %} -
-
- -
- - - -
-
+
+ + + +
+
+

Card Play

+
+ {% if result.cardPlayed != 'None' %} +

- From - {{ result['startEvent'] }} - at - {{ result['startTime'] }} - until - {{ result['endEvent'] }} + {{ actors[result['cardSource']]['name'] }} + played + {{ result.cardPlayed }} + on + {{ actors[result['cardTarget']]['name'] }} at - {{ result['endTime'] }} -

-

- The optimal target was {{ result['drawOptimalTarget'] }} - at {{ result['drawOptimalTime'] }}. + {{ result.cardPlayTime }}

+

The correct target was {{ result['cardOptimalTarget'] }}

- - + + - {% for damage in result['drawDamageTable'] %} - - - - - - + {% for damage in result['cardDamageTable'] %} + {% if actors[damage['id']]['role'] != 'LimitBreak' %} + + + + + + + {% endif %} {% endfor %}
Play Time Player JobDamageAdjusted DamageRaw Damage
{{ damage['time'] }}{{ actors[damage['id']]['name'] }}{{ actors[damage['id']]['job'] }}{{ damage['damage'] }}
{{ actors[damage['id']]['name'] }}{{ actors[damage['id']]['job'] }}{{ damage['adjustedDamage'] }}{{ damage['realDamage'] }}

+
+ {% else %} +
+ No card was played in this draw window (likely a result of Divination being cast) +
+ {% endif %}
-
+ + +
+

Card Draw

+
+
+
+

+ From + {{ result['startEvent'] }} + at + {{ result['startTime'] }} + until + {{ result['endEvent'] }} + at + {{ result['endTime'] }} +

+

+ The optimal target was {{ result['drawOptimalTarget'] }} + at {{ result['drawOptimalTime'] }}. +

+ + + + + + + + + + + + {% for damage in result['drawDamageTable'] %} + + + + + + + {% endfor %} + +
Play TimePlayerJobDamage
{{ damage['time'] }}{{ actors[damage['id']]['name'] }}{{ actors[damage['id']]['job'] }}{{ damage['damage'] }}
+
+
+
+
+
{% endfor %} From dbafc7f729904014b1b63add1b2b73d84635b42c Mon Sep 17 00:00:00 2001 From: Mel Miller Date: Tue, 26 Jan 2021 17:24:23 -0500 Subject: [PATCH 11/11] Fixed cards being included in burst window searches, updated help page, fixed empty DataFrame errors, various other minor changes --- app.py | 2 +- cardcalc.py | 22 ++++++++++---- damagecalc.py | 46 +++++++++++++++++++--------- fflogsapi.py | 41 ++++++++++++++++++------- templates/about.html | 71 +++++++++++++++++++++++++++++++++----------- templates/base.html | 3 ++ testing.py | 36 ++++++++-------------- 7 files changed, 149 insertions(+), 72 deletions(-) diff --git a/app.py b/app.py index d2dc60c..2cda725 100644 --- a/app.py +++ b/app.py @@ -61,7 +61,7 @@ def homepage(): if request.method == 'POST': report_url = request.form['report_url'] try: - report_id, fight_id = decompose_url(report_url) + report_id, fight_id = decompose_url(report_url, token) except CardCalcException as exception: return render_template('error.html', exception=exception) diff --git a/cardcalc.py b/cardcalc.py index e2c7ea4..67dcb0e 100644 --- a/cardcalc.py +++ b/cardcalc.py @@ -20,7 +20,7 @@ from fflogsapi import get_card_draw_events, get_card_play_events, get_actor_lists, get_fight_info, get_damage_events -from damagecalc import calculate_tick_snapshot_damage, calculate_total_damage, search_burst_window +from damagecalc import calculate_tick_snapshot_damage, calculate_total_damage, search_burst_window, remove_card_damage """ For the initial version of this the following simple rules are use. @@ -151,6 +151,8 @@ def cardcalc(report, fight_id, token): damage_events = get_damage_events(fight_info, token) damage_report = calculate_tick_snapshot_damage(damage_events) + non_card_damage_report = remove_card_damage(damage_report, cards, actors) + if not cards: raise CardCalcException("No cards played in fight") if not draws: @@ -164,9 +166,12 @@ def cardcalc(report, fight_id, token): # go through each draw windows and calculate the following # (1.) Find the card played during this window and get the damage dealt by # each player during that play window - # (2.) Loop through possible play windows form the start of the draw window + # (2.) Remove damage bonuses from any active cards during the current + # window + # (3.) Loop through possible play windows form the start of the draw + # window # to the end in 1s increments and calculate damage done - # (3.) Return the following: + # (4.) Return the following: # (a) table of players/damage done in play window # (b) table of top damage windows # i. include top 3/5/8/10 for draw window lasting at least @@ -278,11 +283,18 @@ def cardcalc(report, fight_id, token): # now we can begin compiling data for the draw window as a whole card_draw_data = {} + # check for any cards that are active during the current search window + active_cards = [] + for c in cards: + if c.end > draw.start or c.start < draw.end: + active_cards.append(c) + # creates a search window from the start of the draw window to the end # with a 15s duration and 1s step size - # print('\tComputing draw window damage...') search_window = SearchWindow(draw.start, draw.end, 15000, 1000) - draw_window_damage_collection = search_burst_window(damage_report, search_window, actors) + + # this uses the damage report with all card bonuses removed + draw_window_damage_collection = search_burst_window(non_card_damage_report, search_window, actors) draw_window_duration = timedelta(milliseconds=(draw.end-draw.start)).total_seconds() # print('\tDone.') diff --git a/damagecalc.py b/damagecalc.py index 0b12e84..3af8a06 100644 --- a/damagecalc.py +++ b/damagecalc.py @@ -65,12 +65,8 @@ def calculate_tick_snapshot_damage(damage_events): # finally sort the new array of snapshotdamage events and return it sorted_tick_damage = sorted(summed_tick_damage, key=lambda tick: tick['timestamp']) - combined_damage = pd.DataFrame(sorted(sorted_tick_damage + damage_events['rawDamage'], key=lambda tick: tick['timestamp']), columns=['timestamp', 'type', 'sourceID', 'targetID', 'abilityGameID', 'amount']) + damage_report = pd.DataFrame(sorted(sorted_tick_damage + damage_events['rawDamage'], key=lambda tick: tick['timestamp']), columns=['timestamp', 'type', 'sourceID', 'targetID', 'abilityGameID', 'amount']) - damage_report = { - 'combinedDamage': combined_damage - } - return damage_report def calculate_tick_damage(damage_events): @@ -90,19 +86,42 @@ def calculate_tick_damage(damage_events): # finally sort the new array of snapshotdamage events and return it sorted_tick_damage = sorted(instanced_tick_damage, key=lambda tick: tick['timestamp']) - combined_damage = pd.DataFrame(sorted(sorted_tick_damage + damage_events['rawDamage'], key=lambda tick: tick['timestamp']), columns=['timestamp', 'type', 'sourceID', 'targetID', 'abilityGameID', 'amount']) + damage_report = pd.DataFrame(sorted(sorted_tick_damage + damage_events['rawDamage'], key=lambda tick: tick['timestamp']), columns=['timestamp', 'type', 'sourceID', 'targetID', 'abilityGameID', 'amount']) + + return damage_report + +def remove_card_damage(damage_report, cards, actors): + for card in cards: + # check the real bonus received + eff_bonus = 1.0 + + print(card) + + if card.target in actors.players: + if card.role == actors.players[card.target].role: + eff_bonus = card.bonus + else: + eff_bonus = 1.0 + ((card.bonus - 1.0)/2.0) + elif card.target in actors.pets: + if card.role == actors.players[actors.pets[card.target].owner].role: + eff_bonus = card.bonus + else: + eff_bonus = 1.0 + ((card.bonus - 1.0)/2.0) + + # check if there are any valid damage values for the active card holder during it's time window (this should be non-empty but especially for pets may sometimes not be) + if damage_report.loc[lambda df: (df['timestamp'] > card.start) & (df['timestamp'] < card.end) & (df['sourceID'] == card.target), 'amount'].empty: + next + else: + # modifiy all values with the correct sourceID that lie between the start event and end event times for the card + damage_report.loc[lambda df: (df['timestamp'] > card.start) & (df['timestamp'] < card.end) & (df['sourceID'] == card.target), 'amount'] = damage_report.loc[lambda df: (df['timestamp'] > card.start) & (df['timestamp'] < card.end) & (df['sourceID'] == card.target), 'amount'].transform(lambda x: int(x/eff_bonus)) - damage_report = { - 'combinedDamage': combined_damage - } - return damage_report def calculate_total_damage(damage_report, start_time, end_time, actors: ActorList): combined_damage = {} # create a dataframe with only the current time window - current_df = damage_report['combinedDamage'].query('timestamp >= {} and timestamp <= {}'.format(start_time, end_time)) + current_df = damage_report.query('timestamp >= {} and timestamp <= {}'.format(start_time, end_time)) for actor in current_df['sourceID'].unique(): combined_damage[actor] = current_df.query('sourceID == {}'.format(actor))['amount'].sum() @@ -173,8 +192,7 @@ def search_burst_window(damage_report, search_window: SearchWindow, actors: Acto def time_averaged_dps(damage_report, startTime, endTime, stepSize, timeRange): average_dps = [] - df = damage_report['combinedDamage'] - + current_time = startTime min_time = max(current_time - timeRange, startTime) max_time = min(current_time + timeRange, endTime) @@ -183,7 +201,7 @@ def time_averaged_dps(damage_report, startTime, endTime, stepSize, timeRange): while current_time < endTime: delta = (max_time - min_time)/1000 - active_events = df.query('timestamp <= {} and timestamp >= {}'.format(max_time, min_time)) + active_events = damage_report.query('timestamp <= {} and timestamp >= {}'.format(max_time, min_time)) step_damage = active_events['amount'].sum() average_dps.append({ diff --git a/fflogsapi.py b/fflogsapi.py index a7e9edc..feb35e8 100644 --- a/fflogsapi.py +++ b/fflogsapi.py @@ -58,7 +58,29 @@ def call_fflogs_api(query, variables, token): return data -def decompose_url(url): +def get_last_fight(report, token): + variables = { + 'code': report + } + query = """ +query reportData($code: String!) { + reportData { + report(code: $code) { + fights { + id + startTime + endTime + name + kill + } + } + } +} +""" + data = call_fflogs_api(query, variables, token) + return data['data']['reportData']['report']['fights'][-1]['id'] + +def decompose_url(url, token): parts = urlparse(url) report_id = [segment for segment in parts.path.split('/') if segment][-1] @@ -67,8 +89,9 @@ def decompose_url(url): except KeyError: raise CardCalcException("Fight ID is required. Select a fight first") - if (fight_id != 'last'): - fight_id = int(fight_id) + if fight_id == 'last': + fight_id = get_last_fight(report_id, token) + fight_id = int(fight_id) return report_id, fight_id @@ -95,15 +118,11 @@ def get_fight_info(report, fight, token): data = call_fflogs_api(query, variables, token) fights = data['data']['reportData']['report']['fights'] - if fight == 'last': - f = fights[-1] - return FightInfo(report_id=report, fight_number=f['id'], start_time=f['startTime'], end_time=f['endTime'], name=f['name'], kill=f['kill']) - else: - for f in fights: - if f['id'] == fight: - return FightInfo(report_id=report, fight_number=f['id'], start_time=f['startTime'], end_time=f['endTime'], name=f['name'], kill=f['kill']) + for f in fights: + if f['id'] == fight: + return FightInfo(report_id=report, fight_number=f['id'], start_time=f['startTime'], end_time=f['endTime'], name=f['name'], kill=f['kill']) - raise CardCalcException("Fight ID not found in report") + raise CardCalcException("Fight ID not found in report") def get_actor_lists(fight_info: FightInfo, token): variables = { diff --git a/templates/about.html b/templates/about.html index bc4b649..341b224 100644 --- a/templates/about.html +++ b/templates/about.html @@ -3,7 +3,7 @@ {% block content %}

What is this?

-

This is a tool to calculate the optimal target for Astrologian cards in Final Fantasy XIV

+

This is a tool to calculate more optimal targets for Astrologian cards in Final Fantasy XIV. This is broken down into two different searchs. The first looking for the optimal target with each window where a card was actually played in the fight ('Card Play' window). The second search looks at possible times between each Draw/Sleeve Draw/Divination where players were doing the most damage during a 15s window ('Card Draw' window).

Does it account for...

@@ -16,37 +16,75 @@

Does it account for...

  • Includes the portion of Wildfire damage generated by damage inside the tether window (Stormblood logs only)
  • Includes damage from ground effect DoTs (Shadow Flare, Doton, Salted Earth, Flamethrower)
  • Includes damage from Radiant Shield (when applicable)
  • -
  • Excludes the 3%/4%/6%/8% damage buff from the card
  • +
  • Excludes the 3%/4%/6%/8% damage buff from the active card
  • Accounts for the correct melee/ranged bonus associated with the card
  • +
  • During a 'Card Play' window any targets who already have an active card at the time of cast is ignored for recomendation as the optimal target
  • +
  • While searching for optimal cards during a 'Card Draw' window all damage from card buffs is ignored
  • How it works

    -

    This is the methodology used by the script for each tether window

    +

    This is the methodology used by the script for each 'Card Play' window

    1. Get all of the non-tick, direct damage inside the card window (typically 15s) for each player
    2. -
    3. Walk through each debuff and tick event. +
    4. Find the start of all tick application or reapplication events that occur during the card window and then sum the associated damage events (even if those occur outside of the card window)
    5. +
    6. Take out the appropriate bonus to the player that actually receieved the card based on what card was played and the job of the receiving player
    7. +
    + +

    After doing all of that, the result is a fairly accurate representation of the total amount of damage that would have been buffed by the card, if that player were its target at the time it was originally played.

    + +

    Separately for each 'Card Draw' window the script uses the following methodology:

    +
      +
    1. Get all of the non-tick, direct damage inside the card window (typically 15s) for each player
    2. +
    3. For every tick application or reapplication the damage events associated with that event are summed up and listed as a new damage event at the application time with a total amount associated with the summed damage value
    4. +
    5. The damage bonus associated with every card played throughout the fight is removed
    6. +
    7. Starting at the time of the initial event:
      • -
      • While inside the window, keep a list of every debuff application we see. Remove debuffs that fade
      • -
      • For every tick, add it only if there's been an application of that debuff inside the window
      • -
      • For Stormblood wildfire, take 25% of the damage done inside the card window, if card was only up for some part of it
      • +
      • The damage for each player is summed up as if a card was played at this time lasting for 15s
      • +
      • The starting time is incremented in 1s timesteps while checking that it is still before the time of final event for the window
      • +
      • A number of maximal damage values are selected from this collection based on the size of the window
      • + + + + + + + + + + + + + + + + + + + + + +
        DurationCount Reported
        0s - 4s3
        4s - 10s5
        10s - 20s8
        20s+10
        +
      • Another total damage amount for the same character will not be listed again if it starts within 4s of a previously listed damage window with higher total damage
      - -
    8. Take out the appropriate bonus to the player that actually receieved the card based on what card was played and the job of the receiving player
    -

    After doing all of that, the result is an accurate representation of the total amount of damage that would have been buffed by the card, if that player were its target at the time it was originally played.

    +

    This output should help to accurately predict where it might have been possible to play a card on a player while they are doing the maximum amount. However, it does have some limitations discussed below

    Current limitations

    -

    Currently the analysis of whether the card was played "optimally" is fairly simple. It doesn't do any of the following (yet):

    +

    Some notes on the current analysis with regards to the optimal target for each 'Card Play' window:

      -
    • Look for a better time to play the card during the same Draw window
    • -
    • Look for whether a card overwrote another
    • -
    • Look at whether a Lord/Lady usage should have been swapped with a normal card usage elsewhere in the same Divination window
    • -
    • Look at whether you should have tried to Redraw for a card of the opposite melee/ranged type
    • -
    • There is no consideration of what seals you have currently or need for your next Divination
    • +
    • Cards overwriting other cards isn't looked for explicitly but instead just results in shorted card play windows
    • +
    • The optimal target is based on the role (melee or ranged) of the card you played and there is no consideration at whether you should have tried to Redraw for a card of the opposite type
    • If multiple cards are active at the same time players that have cards are ignored for the later card plays without checking if an alternative play order would have been more optimal
    +

    While searching for the maximum damage output available during each potential 'Card Draw' window the following considerations are not taken into account:

    +
      +
    • There is no check for whether you could have held a card past a Divination thus you need to separately check the following window from Divination until the next event to examine possible card targets there
    • +
    • Within each draw window there is no consideration for what card you have drawn, what seals you have or need, whether it would be reasonable to redraw for a different role or seal
    • +
    • No consideration is given to your GCD speed, when you can weave a card play, or potential healing when calculating the initial time for possible play times so it is possible that it would not be possible to reasonably play a card a specified time
    • +
    • The search is performed in 1s increments so it is possible that two damage instances which are nearly 15s apart will never end up considered together despite there being a possible card which would cover these. Thus it's possible that the reported maximum damage play time is incorrect and could be higher if the playtime was shifted by less than a second in either direction
    • +

    Who made this?

    @@ -54,6 +92,5 @@

    Who made this?

    If you notice any issues, feel free to DM me through Discord (Melody ♫#1653), or open issues and pull requests on the GitHub repo for this site.

    Changes

    -

    2021-01-15: Fixed pet tick damage (such as SMN Ifrit Enkindle DoT) not being correctly included in the damage total for that player. Fixed in git commit 1eafff2

    {% endblock %} diff --git a/templates/base.html b/templates/base.html index 4edc017..de85d25 100644 --- a/templates/base.html +++ b/templates/base.html @@ -24,6 +24,9 @@ margin-bottom: 180px; } } + table, th, td { + padding: 5px; + } .badge { font-size: 100%; } diff --git a/testing.py b/testing.py index 479be6f..d4f66d0 100644 --- a/testing.py +++ b/testing.py @@ -64,32 +64,20 @@ def test_plotting(): # url = 'https://www.fflogs.com/reports/KaCwVdgTQYhmRAxD#fight=10' # url = 'https://www.fflogs.com/reports/byLqHjz8MnphQP3r#fight=1' # url = 'https://www.fflogs.com/reports/TmzFDHfWL8bhdMAn#fight=6' -url = 'https://www.fflogs.com/reports/fZXhDbTjw7GWmKLz#fight=2' +# url = 'https://www.fflogs.com/reports/fZXhDbTjw7GWmKLz#fight=2' +url = 'https://www.fflogs.com/reports/p47GRHQBvaZXq1xk#fight=last' -report_id, fight_id = decompose_url(url) +report_id, fight_id = decompose_url(url, token) -# print('Report: {} ({})\nFight: {} ({})'.format(report_id, type(report_id), fight_id, type(fight_id))) +fight_info = get_fight_info(report_id, fight_id, token) +actor_list = get_actor_lists(fight_info, token) -cardcalc_data, actors, _ = cardcalc(report_id, fight_id, token) - -dmg_tbl = cardcalc_data[0]['cardDamageTable'] - -test_to_dict_damage_table(dmg_tbl) - -# fight_info = get_fight_info(report_id, fight_id, token) -# actor_list = get_actor_lists(fight_info, token) - -# damage_data = get_damage_events(fight_info, token) - -# damage_report = calculate_tick_snapshot_damage(damage_data) -# damage_report_base = calculate_tick_damage(damage_data) - -# search_window = SearchWindow(fight_info.start, fight_info.end, 15000, 1000) -# burst_damage_collection = search_burst_window(damage_report, search_window, actor_list) - -# df_base = damage_report_base['combinedDamage'] -# df_snapshot = damage_report['combinedDamage'] - -print(actors) +damage_data = get_damage_events(fight_info, token) +damage_report = calculate_tick_snapshot_damage(damage_data) +print(damage_report.loc[lambda df: df['timestamp'] > 1489968]) +print('Intentionally Empty') +print(damage_report.loc[lambda df: (df['timestamp'] > 1489968) & (df['sourceID'] == 3)]) +cardcalc_data, actors, _ = cardcalc(report_id, fight_id, token) +print(cardcalc_data)