def test_get_cast_query_spells(self): #inputs spells = [WowSpell(spell_id=101), WowSpell(spell_id=102)] # run and test q = self.actor.get_cast_query(spells) assert q == "type='cast' and ability.id in (101,102)"
def test_get_buff_query_spells(self): #inputs spells = [WowSpell(spell_id=101), WowSpell(spell_id=102)] expected = "type in ('applybuff','removebuff','applydebuff','removedebuff') and ability.id in (101,102)" # run and test q = self.actor.get_buff_query(spells) assert q == expected
def setUp(self): self.boss = warcraftlogs_boss.Boss() # setup an example boss self.example_boss = mock.MagicMock() self.example_boss.spells = [ WowSpell(spell_id=101), WowSpell(spell_id=102) ] self.example_boss.buffs = [ WowSpell(spell_id=201), WowSpell(spell_id=202) ]
def get_filter(self) -> str: """Filter that is applied to the query.""" filter_healing_cds = self._get_healing_cooldowns() raid_cds = self._get_raid_cds() raid_cds_str = WowSpell.spell_ids_str(raid_cds) raid_cds_filter = f"type='cast' and ability.id in ({raid_cds_str})" raid_buffs = self._get_raid_buffs() raid_buffs_str = WowSpell.spell_ids_str(raid_buffs) raid_buffs_filter = f"type in ('applybuff', 'removebuff') and ability.id in ({raid_buffs_str})" return self.combine_queries(filter_healing_cds, raid_cds_filter, raid_buffs_filter)
def get_cast_query(self, spells=typing.List[WowSpell]): if not spells: return "" spell_ids = WowSpell.spell_ids_str(spells) cast_filter = f"type='cast' and ability.id in ({spell_ids})" return cast_filter
def add_buff(self, spell_id, **kwargs) -> WowSpell: kwargs.setdefault("spell_type", self.full_name_slug) spell = WowSpell(spell_id=spell_id, **kwargs) self.buffs.append(spell) return spell
def add_spell(self, **kwargs): kwargs.setdefault("color", self.color) kwargs.setdefault("spell_type", self.name_slug) spell = WowSpell(**kwargs) self.spells.append(spell) return spell
def has_own_casts(self): """Return true if a player has own casts (eg.: exclude raid wide buffs like bloodlust).""" for cast in self.casts: spell = WowSpell.get(spell_id=cast.spell_id) if spell.spell_type != WowSpell.TYPE_BUFFS: return True return False
def add_debuff(self, spell: WowSpell = None, **kwargs): if not spell: kwargs.setdefault("color", self.color) kwargs.setdefault("spell_type", self.name_slug) spell = WowSpell(**kwargs) self.debuffs.append(spell)
def add_spell(self, spell: WowSpell = None, **kwargs): if not spell: kwargs.setdefault("color", self.wow_class.color) kwargs.setdefault("spell_type", self.full_name_slug) spell = WowSpell(**kwargs) self.spells.append(spell) # Important to keep a ref in memory return spell
def add_buff(self, spell: WowSpell = None, **kwargs): if not spell: kwargs.setdefault("color", self.wow_class.color) kwargs.setdefault("spell_type", self.full_name_slug) spell = WowSpell(**kwargs) self.buffs.append(spell) return spell
def add_event( self, **kwargs ): # event_type, spell_id, name: str, icon: str, duration: int = 0): kwargs.setdefault("event_type", "cast") # track the event (for query) self.events.append(kwargs) # dedicated "stop" event, for events with non static timers.. eg: intermissions end_event = kwargs.get("until", {}) if end_event: self.events.append(end_event) # spell instance used for UI things kwargs.setdefault("spell_type", self.full_name_slug) spell = WowSpell(**kwargs) spell.specs = [self] self.event_spells.append(spell)
def _build_buff_query(spells: typing.List[WowSpell], event_types: typing.List[str]): if not spells: return "" spell_ids = WowSpell.spell_ids_str(spells) event_types = [f"'{event}'" for event in event_types ] # wrap each into single quotes event_types_combined = ",".join(event_types) return f"type in ({event_types_combined}) and ability.id in ({spell_ids})"
def setUp(self): self.player = warcraftlogs_actor.Player( spec_slug=MOCK_SPEC.full_name_slug ) self.spells = [WowSpell(spell_id=101)] self.cast_query_patch = mock.patch("lorgs.models.warcraftlogs_actor.BaseActor.get_cast_query") self.cast_query_mock = self.cast_query_patch.start() self.cast_query_mock.return_value = "CAST_QUERY" self.buff_query_patch = mock.patch("lorgs.models.warcraftlogs_actor.BaseActor.get_buff_query") self.buff_query_mock = self.buff_query_patch.start() self.buff_query_mock.return_value = "BUFF_QUERY"
def _get_healing_cooldowns(self) -> str: # typing.List[WowSpell]: """All Spells that are considered Healing-Cooldowns. Right now, this simply returns every spell healers have TODO: share logic with <BaseActor> ? """ def join(*parts: str): return " and ".join(parts) queries: typing.List[str] = [] healers: typing.List[WowSpec] = [ spec for spec in WowSpec.all if spec.role.code == "heal" ] # Casts casts: typing.List[WowSpell] = utils.flatten(spec.all_spells for spec in healers) casts = [cast for cast in casts if cast.is_healing_cooldown()] if casts: cast_ids = WowSpell.spell_ids_str(casts) buffs_q = join("source.role='healer'", "type='cast'", f"ability.id in ({cast_ids})") queries.append(buffs_q) # Buffs buffs: typing.List[WowSpell] = utils.flatten(spec.all_buffs for spec in healers) buffs = [buff for buff in buffs if buff.is_healing_cooldown()] if buffs: buffs_ids = WowSpell.spell_ids_str(buffs) buffs_q = join("target.role='healer'", "type in ('applybuff', 'removebuff')", f"ability.id in ({buffs_ids})") queries.append(buffs_q) return self.combine_queries(*queries)
def get_event_query(self, spells=typing.List[WowSpell]): if not spells: return "" # TODO: group spells by event_type spell_ids = WowSpell.spell_ids_str(spells) parts = [] for spell in spells: part = f"(type='{spell.event_type.value}' and ability.id in ({spell_ids}))" parts.append(part) return " or ".join(parts)
def process_event_resurrect(self, event): fight_start = self.fight.start_time_rel if self.fight else 0 data = {} data["ts"] = event.get("timestamp", 0) - fight_start spell_id = event.get("abilityGameID", -1) spell = WowSpell.get(spell_id=spell_id) if spell: data["spell_name"] = spell.name data["spell_icon"] = spell.icon source_id = event.get("sourceID", 0) source_player: Player = self.fight.report.players.get(str(source_id)) if source_player: data["source_name"] = source_player.name data["source_class"] = source_player.class_slug self.resurrects.append(data)
def test_is_healing_cooldown__other(): spell = WowSpell(spell_id=5) assert spell.is_healing_cooldown() == True
icon="spell_shadow_summonfelguard.jpg") WARLOCK_DEMONOLOGY.add_spell(spell_id=264119, cooldown=45, duration=15, color="#69b851", name="Summon Vilefiend", icon="inv_argusfelstalkermount.jpg") WARLOCK_DEMONOLOGY.add_spell(spell_id=267217, cooldown=180, duration=15, name="Nether Portal", icon="inv_netherportal.jpg") WARLOCK_DESTRUCTION.add_spell(spell_id=1122, cooldown=180, duration=30, color="#91c45a", name="Summon Infernal", icon="spell_shadow_summoninfernal.jpg") WARLOCK_DESTRUCTION.add_spell(spell_id=113858, cooldown=120, duration=20, color="#c35ec4", name="Dark Soul: Instability", icon="spell_warlock_soulburn.jpg") # Additional Spells (not tracked) SOULSTONE_RESURRECTION = WowSpell(spell_id=95750, name="Soulstone", icon="spell_shadow_soulgem.jpg")
def process_query_result(self, query_result): """Process the result of a casts-query to create Cast objects.""" # save unwrap the data query_result = query_result.get("reportData") or query_result query_result = query_result.get("report") or query_result query_result = query_result.get("events") or query_result casts_data = query_result.get("data") or [] if not casts_data: logger.warning("casts_data is empty") return # track buffs/debuff: spell id -> start cast active_buffs: typing.Dict[int, Cast] = {} fight_start = self.fight.start_time_rel if self.fight else 0 for cast_data in casts_data: self.process_event(cast_data) cast_type: str = cast_data.get("type") or "unknown" cast_actor_id = cast_data.get("sourceID") if cast_type in ("applybuff", "removebuff", "resurrect"): cast_actor_id = cast_data.get("targetID") if self._has_source_id and (cast_actor_id != self.source_id): continue # resurrect are dealt with in `process_event` if cast_type == "resurrect": continue # Create the Cast Object cast = Cast() cast.spell_id = cast_data.get("abilityGameID") cast.spell_id = WowSpell.resolve_spell_id(cast.spell_id) cast.timestamp = cast_data.get("timestamp", 0) - fight_start cast.duration = cast_data.get("duration") if cast_type in ("cast", "damage"): cast.stacks = 1 # new buff, or buff stack if cast_type in ("applybuff", "applydebuff"): # check if the buff/debuff is already active. cast = active_buffs.get(cast.spell_id) or cast cast.stacks += 1 if cast_type in ("removebuff", "removedebuff"): start_cast = active_buffs.get(cast.spell_id) # special case for buffs that are applied pre pull # meaning.. the buff was already present at the start of the fight, if not start_cast: start_cast = cast spell = WowSpell.get( spell_id=cast.spell_id ) # get the duration from the spell defintion start_cast.timestamp -= ( spell.duration * 1000 ) # and calculate back the start time continue start_cast.stacks -= 1 # no stacks left --> buff/debuff ends if start_cast.stacks == 0: start_cast.duration = (cast.timestamp - start_cast.timestamp) start_cast.duration *= 0.001 active_buffs[cast.spell_id] = None if cast.stacks == 1: # only add new buffs on their first application # track applied buffs active_buffs[cast.spell_id] = cast self.casts.append(cast) # Filter out same event at the same time (eg.: raid wide debuff apply) self.casts = utils.uniqify( self.casts, key=lambda cast: (cast.spell_id, math.floor(cast.timestamp / 1000))) self.casts = list( self.casts ) # `utils.uniqify` returns dict values, which mongoengine doesn't like # make sure casts are sorted correctly # avoids weird UI overlaps, and just feels cleaner self.casts = sorted(self.casts, key=lambda cast: cast.timestamp)
def spell(self) -> WowSpell: return WowSpell.get(spell_id=self.spell_id)
def test_spell_ids_str(): spells = [WowSpell(spell_id=5), WowSpell(spell_id=3), WowSpell(spell_id=10)] result = WowSpell.spell_ids_str(spells) assert result == "3,5,10"
def test_is_item_spell__true(): spell = WowSpell(spell_id=5, spell_type=WowSpell.TYPE_TRINKET) assert spell.is_item_spell() == True
def test_is_healing_cooldown__personal(): spell = WowSpell(spell_id=5, spell_type=WowSpell.TYPE_PERSONAL) assert spell.is_healing_cooldown() == False
def test_is_healing_cooldown__item_spell(): spell = WowSpell(spell_id=5, spell_type=WowSpell.TYPE_TRINKET) assert spell.is_healing_cooldown() == False
def test_is_item_spell__false(): spell = WowSpell(spell_id=5) assert spell.is_item_spell() == False
icon="ability_deathknight_pillaroffrost.jpg", show=False) DEATHKNIGHT_FROST.add_spell(spell_id=46585, cooldown=120, duration=60, color="#c7ba28", name="Raise Dead", icon="inv_pet_ghoul.jpg", show=False) DEATHKNIGHT_FROST.add_spell(spell_id=47568, cooldown=120, duration=20, color="#88e8f2", name="Empower Rune Weapon", icon="inv_sword_62.jpg") DEATHKNIGHT_FROST.add_spell(spell_id=152279, cooldown=120, duration=30, color="#52abff", name="Breath of Sindragosa", icon="spell_deathknight_breathofsindragosa.jpg") DEATHKNIGHT_FROST.add_spell(spell_id=279302, cooldown=180, name="Frostwyrm's Fury", icon="achievement_boss_sindragosa.jpg") # Additional Spells (not tracked) RAISE_ALLY = WowSpell(spell_id=61999, name="Raise Ally", icon="spell_shadow_deadofnight.jpg")
def test_spell_ids(): spells = [WowSpell(spell_id=5), WowSpell(spell_id=3), WowSpell(spell_id=10)] result = WowSpell.spell_ids(spells) assert result == [3, 5, 10]
show=False) # Defensive MONK.add_spell(spell_id=122278, cooldown=120, duration=10, color="#fcba03", name="Dampen Harm", icon="ability_monk_dampenharm.jpg", show=False) # MW and WW get a reduced CD with rank2 FORT_BREW = WowSpell(spell_type=MONK.name_slug, spell_id=243435, cooldown=180, duration=15, color="#ffb145", name="Fortifying Brew", icon="ability_monk_fortifyingale_new.jpg", show=False) DIFFUSE = WowSpell(spell_type=MONK.name_slug, spell_id=122783, cooldown=90, duration=6, color=MONK.color, name="Diffuse Magic", icon="spell_monk_diffusemagic.jpg", show=False) MONK_MISTWEAVER.add_spells(FORT_BREW, DIFFUSE) MONK_WINDWALKER.add_spells(FORT_BREW, DIFFUSE)
# DRUID_BALANCE = WowSpec(role=RDPS, wow_class=DRUID, name="Balance") DRUID_FERAL = WowSpec(role=MDPS, wow_class=DRUID, name="Feral") DRUID_GUARDIAN = WowSpec(role=TANK, wow_class=DRUID, name="Guardian") DRUID_RESTORATION = WowSpec(role=HEAL, wow_class=DRUID, name="Restoration", short_name="Resto") ################################################################################ # Spells # DRUID.add_spell( spell_id=323764, cooldown=60, duration=4, color=COL_NF, name="Convoke the Spirits", icon="ability_ardenweald_druid.jpg") DRUID.add_spell( spell_id=323546, cooldown=180, duration=20, color=COL_VENTR, name="Ravenous Frenzy", icon="ability_revendreth_druid.jpg", show=False) # Defensives DRUID.add_spell( spell_id=22812, cooldown=60, duration=12, name="Barkskin", icon="spell_nature_stoneclawtotem.jpg", show=False) BEAR_FORM = WowSpell(spell_id=5487, name="Bear Form", icon="ability_racial_bearform.jpg", show=False) BEAR_FORM.spell_type = DRUID.name_slug BEAR_FORM.color = DRUID.color DRUID_BALANCE.add_buff(BEAR_FORM) DRUID_FERAL.add_buff(BEAR_FORM) DRUID_RESTORATION.add_buff(BEAR_FORM) # Offensive DRUID_BALANCE.add_spell( spell_id=194223, cooldown=180, duration=20, name="Celestial Alignment", icon="spell_nature_natureguardian.jpg") DRUID_BALANCE.add_spell( spell_id=102560, cooldown=180, duration=30, name="Incarnation: Chosen of Elune", icon="spell_druid_incarnation.jpg") DRUID_BALANCE.add_spell( spell_id=205636, cooldown=60, duration=10, name="Force of Nature", icon="ability_druid_forceofnature.jpg", show=False) DRUID_BALANCE.add_spell( spell_id=202770, cooldown=60, duration=8, name="Fury of Elune", icon="ability_druid_dreamstate.jpg", show=False) DRUID_FERAL.add_spell( spell_id=106951, cooldown=180, duration=15, name="Berserk", icon="ability_druid_berserk.jpg") DRUID_FERAL.add_spell( spell_id=58984, cooldown=120, color="#999999", name="Shadowmeld ", icon="ability_ambush.jpg", show=False)