Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import collections
- import functools
- import itertools
- import json
- import math
- import urllib.parse
- import pandas
- import scrapy
- REPORTS = ["fflogs report IDs goes here", "e.g.,", "YA4QvwPCft1qdxBX"]
- API_KEY = "fflogs API key goes here"
- PHASES = ["Twin", "Nael", "Quickmarch", "Blackfire", "Fellruin", "Heavensfall", "Tenstrike", "Octet", "Adds", "Golden"] + \
- ["Garuda (not woken)", "Garuda (woken)", "Ifrit (not woken)", "Ifrit (woken)", "Titan (not woken)", "Titan (woken)", "LB Phase", "Predation", "Annihilation", "Suppression", "Final"] + \
- ["Pepsiman", "Limit Cut", "CC + BJ", "Time Stop + Inception", "Wormhole", "Perfect Alex"]
- TRIOS = {
- 9954: "Quickmarch",
- 9955: "Blackfire",
- 9956: "Fellruin",
- 9957: "Heavensfall",
- 9958: "Tenstrike",
- 9959: "Octet",
- }
- ULTIMATES = {
- 11126: "Predation",
- 11596: "Annihilation",
- 11597: "Suppression",
- 11151: "Final", # 3x Viscous Aetheroplasm
- }
- WOKEN_DEBUFF = 1000000 + 1529;
- TRIO_FILTER = "type = 'cast' AND (" + " OR ".join(map(lambda t: "ability.id = " + str(t), TRIOS.keys())) + ")"
- ULTIMATE_FILTER = "(type = 'cast' AND (" + " OR ".join(map(lambda t: "ability.id = " + str(t), ULTIMATES.keys())) + "))" + \
- " OR (type = 'applydebuff' AND ability.id = " + str(WOKEN_DEBUFF) + ")"
- TITAN_GAOL_FILTER = "(type = 'death' AND target.disposition = 'friendly') OR ability.id = 11115 OR ability.id = 11116 OR (type = 'applydebuff' AND ability.id = 1000292)"
- class FFLogsSpider(scrapy.Spider):
- name = "FFLogs"
- custom_settings = {
- "COOKIES_ENABLED": False,
- }
- fightData = {}
- uwuRoleDps = {}
- ucobRoleDps = {}
- titanGaolData = {
- "deaths": collections.Counter(),
- "success": collections.Counter(),
- "failPositioning": collections.Counter(),
- "failDeathCount": 0,
- "failPositioningCount": 0,
- }
- def start_requests(self):
- for report in REPORTS:
- yield scrapy.Request(
- "https://www.fflogs.com/v1/report/fights/{}?api_key={}&translate=true".format(report, API_KEY),
- functools.partial(self.fights, report)
- )
- def fights(self, reportId, response):
- data = json.loads(response.body_as_unicode())
- data["actors"] = {}
- for actor in itertools.chain(data["friendlies"], data["enemies"]):
- data["actors"][actor["id"]] = actor
- uwuStart = math.inf
- uwuEnd = -math.inf
- uwuFights = []
- teaStart = math.inf
- teaEnd = -math.inf
- teaFights = []
- for fight in data["fights"]:
- fight["report"] = data
- fight["reportId"] = reportId
- if fight["zoneName"] == "The Unending Coil Of Bahamut (Ultimate)":
- if "lastPhaseForPercentageDisplay" not in fight or fight["lastPhaseForPercentageDisplay"] == 1:
- self.fightData.setdefault(reportId, []).append((fight, "Twin"))
- elif fight["lastPhaseForPercentageDisplay"] == 2:
- self.fightData.setdefault(reportId, []).append((fight, "Nael"))
- elif fight["lastPhaseForPercentageDisplay"] == 3:
- yield response.follow(
- "https://www.fflogs.com/v1/report/events/{}?".format(reportId) +
- urllib.parse.urlencode({
- "api_key": API_KEY,
- "start": fight["start_time"],
- "end": fight["end_time"],
- "filter": TRIO_FILTER
- }),
- functools.partial(self.ucobEvents, fight)
- )
- elif fight["lastPhaseForPercentageDisplay"] == 4:
- self.fightData.setdefault(reportId, []).append((fight, "Adds"))
- elif fight["lastPhaseForPercentageDisplay"] == 5:
- self.fightData.setdefault(reportId, []).append((fight, "Golden"))
- #if fight["bossPercentage"] <= 2000:
- if "kill" in fight and fight["kill"]:
- yield scrapy.Request(
- "https://www.fflogs.com/v1/report/tables/damage-done/{}?".format(fight["reportId"]) +
- urllib.parse.urlencode({
- "api_key": API_KEY,
- "start": fight["start_time"],
- "end": fight["end_time"],
- "filter": "IN RANGE FROM type = 'begincast' AND ability.id = 9964 TO type = 'cast' AND ability.id = 9965 END"
- }),
- functools.partial(self.ucobDamageDone, fight)
- )
- else:
- assert(False)
- elif fight["zoneName"] == "The Weapon's Refrain (Ultimate)":
- if "lastPhaseForPercentageDisplay" not in fight:
- self.fightData.setdefault(reportId, []).append((fight, "Garuda (not woken)"))
- else:
- uwuStart = min(uwuStart, fight["start_time"])
- uwuEnd = max(uwuEnd, fight["end_time"])
- uwuFights.append(fight)
- elif fight["zoneName"] == "The Epic of Alexander (Ultimate)":
- if "lastPhaseForPercentageDisplay" not in fight or fight["lastPhaseForPercentageDisplay"] == 1:
- if not fight.get("lastPhaseIsIntermission"):
- self.fightData.setdefault(reportId, []).append((fight, "Pepsiman"))
- else:
- self.fightData.setdefault(reportId, []).append((fight, "Limit Cut"))
- elif (fight["lastPhaseForPercentageDisplay"] == 4 or
- (fight["lastPhaseForPercentageDisplay"] == 3 and fight.get("lastPhaseIsIntermission"))):
- self.fightData.setdefault(reportId, []).append((fight, "Perfect Alex"))
- else:
- teaStart = min(teaStart, fight["start_time"])
- teaEnd = max(teaEnd, fight["end_time"])
- teaFights.append(fight)
- if uwuStart < uwuEnd:
- yield self.getAllEvents(
- reportId, uwuStart, uwuEnd,
- {"filter": "({}) OR ({})".format(ULTIMATE_FILTER, TITAN_GAOL_FILTER)},
- functools.partial(self.uwuEvents, uwuFights)
- )
- if teaStart < teaEnd:
- yield self.getAllEvents(
- reportId, teaStart, teaEnd,
- {"filter": "type = 'cast' AND (" +
- "ability.id = 18494 OR " + # Judgement Nisi
- "ability.id = 18522 OR " + # Temporal Stasis
- "ability.id = 18542" + # Wormhole
- ")"},
- functools.partial(self.teaEvents, teaFights)
- )
- def getAllEvents(self, reportId, start, end, args, callback):
- args.update({
- "api_key": API_KEY,
- "start": start,
- "end": end,
- })
- events = []
- def continuation(response):
- nonlocal args, events
- data = json.loads(response.body_as_unicode())
- events.extend(data["events"])
- if len(data["events"]) != 0:
- args["start"] = data["events"][-1]["timestamp"] + 1
- return response.follow(
- "https://www.fflogs.com/v1/report/events/{}?".format(reportId) +
- urllib.parse.urlencode(args),
- continuation
- )
- else:
- events = pandas.Series(events, index = map(lambda e: e["timestamp"], events))
- return callback(events)
- return scrapy.Request(
- "https://www.fflogs.com/v1/report/events/{}?".format(reportId) +
- urllib.parse.urlencode(args),
- continuation
- )
- def ucobEvents(self, fight, response):
- data = json.loads(response.body_as_unicode())
- assert("nextPageTimestamp" not in data)
- if len(data["events"]) == 0:
- # Probably died to phase transition
- self.fightData.setdefault(fight["reportId"], []).append((fight, "Nael"))
- else:
- self.fightData.setdefault(fight["reportId"], []).append((fight, TRIOS[data["events"][-1]["ability"]["guid"]]))
- def ucobDamageDone(self, fight, response):
- data = json.loads(response.body_as_unicode())
- activeTime = max(e["activeTime"] for e in data["entries"]) / 1000
- print("\nFight {}:{} Golden -> 1st Enrage hit DPS".format(fight["reportId"], fight["id"]))
- for entry in data["entries"]:
- if entry["type"] == "LimitBreak" and entry["name"] != "Limit Break":
- continue
- self.ucobRoleDps.setdefault(entry["type"], []).append(entry["total"] / activeTime)
- print("{}: {:.0f} dps".format(entry["type"], entry["total"] / activeTime))
- print("")
- def uwuEvents(self, fights, allEvents):
- def rfindAbility(events, abilities):
- for e, event in events.iloc[::-1].items():
- if "ability" not in event:
- continue
- if event["ability"]["guid"] in abilities:
- return event
- return None
- for fight in fights:
- events = allEvents.loc[fight["start_time"]:fight["end_time"]]
- phase = None
- lastWokenTarget = None
- if fight["lastPhaseForPercentageDisplay"] == 4:
- phase = "LB Phase"
- elif fight["lastPhaseForPercentageDisplay"] == 5:
- ultimateEvent = rfindAbility(events, ULTIMATES)
- phase = ULTIMATES[ultimateEvent["ability"]["guid"]] if ultimateEvent else "LB Phase"
- #if fight["bossPercentage"] <= 2000:
- if "kill" in fight and fight["kill"]:
- yield scrapy.Request(
- "https://www.fflogs.com/v1/report/tables/damage-done/{}?".format(fight["reportId"]) +
- urllib.parse.urlencode({
- "api_key": API_KEY,
- "start": fight["start_time"],
- "end": fight["end_time"],
- "filter": "IN RANGE FROM encounterPhase = 5 AND type = 'begincast' AND ability.id = 11147 TO ability.id = 1000201 END"
- }),
- functools.partial(self.uwuDamageDone, fight)
- )
- else:
- lastWokenEvent = rfindAbility(events, {WOKEN_DEBUFF})
- lastWokenTarget = fight["report"]["actors"][lastWokenEvent["targetID"]]["name"] if lastWokenEvent else None
- if fight["lastPhaseForPercentageDisplay"] == 1:
- phase = "Garuda (woken)" if lastWokenTarget == "Garuda" else "Garuda (not woken)"
- elif fight["lastPhaseForPercentageDisplay"] == 2:
- phase = "Ifrit (woken)" if lastWokenTarget == "Ifrit" else "Ifrit (not woken)"
- elif fight["lastPhaseForPercentageDisplay"] == 3:
- phase = "Titan (woken)" if lastWokenTarget == "Titan" else "Titan (not woken)"
- if fight["lastPhaseForPercentageDisplay"] >= 3:
- isTitanWoken = lastWokenTarget == "Titan" or fight["lastPhaseForPercentageDisplay"] == 5
- self.titanGaolEvents(fight, events.tolist(), isTitanWoken)
- assert(phase != None)
- self.fightData.setdefault(fight["reportId"], []).append((fight, phase))
- def uwuDamageDone(self, fight, response):
- data = json.loads(response.body_as_unicode())
- activeTime = max(e["activeTime"] for e in data["entries"]) / 1000
- print("\nFight {}:{} Ultima -> Stun DPS".format(fight["reportId"], fight["id"]))
- for entry in data["entries"]:
- if entry["type"] == "LimitBreak" and entry["name"] != "Limit Break":
- continue
- self.uwuRoleDps.setdefault(entry["type"], []).append(entry["total"] / activeTime)
- print("{}: {:.0f} dps".format(entry["type"], entry["total"] / activeTime))
- print("")
- def titanGaolEvents(self, fight, allEvents, isWoken):
- start = -1
- end = -1
- gaols = 0
- for e, event in enumerate(allEvents):
- if "ability" not in event:
- continue
- if start == -1 and event["ability"]["guid"] in {11115, 11116}:
- start = e
- if gaols < 3 and event["ability"]["guid"] == 1000292:
- gaols += 1
- end = e + 1
- if start == -1 or end < start + 6:
- return
- events = allEvents[start:end]
- targeted = set()
- for event in events[:3]:
- if "ability" not in event or event["ability"]["guid"] not in {11115, 11116}:
- return
- targeted.add(event["targetID"])
- deaths = set()
- for event in events[3:-3]:
- assert(event["type"] == "death")
- deaths.add(event["targetID"])
- for event in reversed(events[:start]):
- if event["timestamp"] + 5000 < allEvents[start]["timestamp"]:
- break
- if event["type"] == "death":
- deaths.add(event["targetID"])
- gaoled = set()
- for event in events[-3:]:
- if "ability" not in event or event["ability"]["guid"] != 1000292:
- return
- gaoled.add(event["targetID"])
- self.titanGaolData["deaths"].update(map(lambda id: fight["report"]["actors"][id]["name"], deaths))
- if targeted == gaoled:
- if isWoken:
- self.titanGaolData["success"].update(map(lambda id: fight["report"]["actors"][id]["name"], targeted))
- else:
- self.titanGaolData["failPositioning"].update(map(lambda id: fight["report"]["actors"][id]["name"], targeted))
- self.titanGaolData["failPositioningCount"] += 1
- else:
- self.titanGaolData["failDeathCount"] += 1
- def teaEvents(self, fights, allEvents):
- for fight in fights:
- events = allEvents.loc[fight["start_time"]:fight["end_time"]]
- if len(events) == 0:
- # Before Nisi
- self.fightData.setdefault(fight["reportId"], []).append((fight, "Limit Cut"))
- elif events.iloc[-1]["ability"]["guid"] == 18494: # Judgement Nisi
- self.fightData.setdefault(fight["reportId"], []).append((fight, "CC + BJ"))
- elif events.iloc[-1]["ability"]["guid"] == 18522: # Temporal Stasis
- self.fightData.setdefault(fight["reportId"], []).append((fight, "Time Stop + Inception"))
- elif events.iloc[-1]["ability"]["guid"] == 18542: # Wormhole
- self.fightData.setdefault(fight["reportId"], []).append((fight, "Wormhole"))
- else:
- assert(False)
- def closed(self, reason):
- MSPERHOUR = 1000 * 60 * 60
- totalDuration = 0
- phaseCounts = dict.fromkeys(PHASES, 0)
- phaseDurations = dict.fromkeys(PHASES, 0)
- phaseIntervals = {}
- weekStarts = [0]
- lastWeek = None
- clears = []
- for report in REPORTS:
- for fight, phase in sorted(self.fightData[report], key = lambda e: e[0]["start_time"]):
- duration = fight["end_time"] - fight["start_time"]
- phaseCounts[phase] += 1
- phaseDurations[phase] += duration
- phaseIntervals.setdefault(phase, []).append((totalDuration, totalDuration + duration))
- if lastWeek is None:
- lastWeek = fight["report"]["start"] + fight["start_time"]
- if lastWeek + MSPERHOUR * 24 * 7 < fight["report"]["start"] + fight["end_time"]:
- weekStarts.append(totalDuration)
- while lastWeek + MSPERHOUR * 24 * 7 < fight["report"]["start"] + fight["end_time"]:
- lastWeek += MSPERHOUR * 24 * 7
- isClear = "kill" in fight and fight["kill"]
- if isClear:
- clears.append(totalDuration + duration)
- totalDuration += duration
- print("{}:{}: {}{}".format(fight["reportId"], fight["id"], phase, " (Clear)" if isClear else ""))
- print("")
- print("legends = {{{}}};".format(
- ",".join(map(
- lambda p: "\"{}\"".format(p),
- filter(lambda p: p in phaseIntervals, PHASES)
- ))
- ))
- print("data = {{{}}};".format(
- ",".join(map(
- lambda p: "{{{}}}".format(
- ",".join(map(lambda i: "{{{:.2f}, {:.2f}}}".format(i[0] / MSPERHOUR, i[1] / MSPERHOUR), phaseIntervals[p]))
- ),
- filter(lambda p: p in phaseIntervals, PHASES)
- ))
- ))
- print("gridLines = {{{{{}}}, None}};".format(
- ",".join(itertools.chain(
- map(lambda c: "{{{:.2f}, Directive[Red, Thick]}}".format(c / MSPERHOUR), clears),
- map(lambda w: "{:.2f}".format(w / MSPERHOUR), weekStarts)
- ))
- ))
- print("""
- NumberLinePlot[
- Interval @@ # & /@ data, PlotLegends -> legends,
- PlotStyle -> Directive[Thickness[0.01], CapForm[None]],
- AspectRatio -> 1 / 5, ImageSize -> 1000
- ] /. Point[a_] -> Point[{-100, 1}]""")
- print("""
- filling = Join[{{1 -> Axis}}, Table[{n -> {n - 1}}, {n, 2, Length[data]}]];
- windowSize = 4;
- smooth[fn_] := MovingAverage[Table[If[fn, 1, 0], {x, 0 - windowSize/2, Max[data] + windowSize/2, 0.01}], 100 windowSize];
- envelope = smooth[0 <= x < Max[data]];
- ParallelMap[smooth[Or @@ (#[[1]] <= x < #[[2]] & /@ #)] / envelope &, data];
- ListLinePlot[
- Accumulate[%], Filling -> filling, DataRange -> {0, Max[data]},
- PlotRange -> All, PlotLegends -> legends,
- GridLines -> gridLines, GridLinesStyle -> Dashed,
- PlotLabel -> "Time spent in pull wiping to phase x",
- AxesLabel -> {"Hours in pull", "Ratio"},
- ImageSize -> 1000
- ]""")
- print("")
- for phase in PHASES:
- print("{}: {} pulls, {:.1f}% in duration".format(phase, phaseCounts[phase], phaseDurations[phase] / totalDuration * 100))
- if len(self.ucobRoleDps) > 0:
- print("")
- print("{{{}}}".format(",".join("plot[{0}, \"{0}\", {{{1}}}]".format(role, ",".join(map(str, dps))) for role, dps in self.ucobRoleDps.items())))
- if len(self.uwuRoleDps) > 0:
- print("")
- print("{{{}}}".format(",".join("plot[{0}, \"{0}\", {{{1}}}]".format(role, ",".join(map(str, dps))) for role, dps in self.uwuRoleDps.items())))
- if len(self.titanGaolData["deaths"]) > 0:
- print("\nTitan Knockback Deaths")
- print("\n".join(map(lambda e: "{}: {}".format(e[0], e[1]), self.titanGaolData["deaths"].most_common())))
- if len(self.titanGaolData["failPositioning"]) > 0:
- failureRate = collections.Counter()
- for id, failCount in self.titanGaolData["failPositioning"].items():
- successCount = self.titanGaolData["success"][id]
- failureRate[id] = failCount / (failCount + successCount)
- print("\nTitan Gaol Positioning Failure Rate")
- print("\n".join(map(lambda e: "{}: {:.3f}".format(e[0], e[1]), failureRate.most_common())))
- totalFailCount = self.titanGaolData["failDeathCount"] + self.titanGaolData["failPositioningCount"]
- if totalFailCount > 0:
- print("\nTitan Non-woken Reason:")
- print("Knockback Death: {:.3f}".format(self.titanGaolData["failDeathCount"] / totalFailCount))
- print("Gaol Positioning: {:.3f}".format(self.titanGaolData["failPositioningCount"] / totalFailCount))
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement