The WoWAnalyzer framework - WoWAnalyzer/WoWAnalyzer GitHub Wiki

We use TypeScript for all new code. This is a modern variant of JavaScript that supports typechecking before the code hits the browser. Code is transpiled to JS with the npm start command whenever you make changes to anything, and the browser then automatically refreshes. This usually takes 1 to 3 seconds.

If you want to learn (modern) JavaScript these book series are recommended: https://github.com/getify/You-Dont-Know-JS. Most of this is applicable to TypeScript, which uses very similar syntax and patterns.

Overview

The below image attempts to give you an overview of the app setup. If you're going to be working on a spec specific analysis you will only be working on the blue boxes shown in the image for that specific spec. Each module (any of the blue boxes except that CombatLogParser) is an isolated class, usually found in the "Modules" folder of a spec. Most modules have dependencies on other modules, most commonly Combatants which contains information about the selected player, such as equipped gear and talents.

App overview

Analyzers and Normalizers

At this time there are two types of modules: Analyzers and Normalizers.

Analyzers

An Analyzer is an object that responds to events and produces some kind of analytical output. It is worth looking at a very simple example before going further. Let's look at an example Analyzer that just calculates the average number of targets hit by an AoE spell (Keg Smash) over the course of a fight.

import Analyzer, { Options, SELECTED_PLAYER } from 'parser/core/Analyzer';
import Events, { CastEvent, DamageEvent } from 'parser/core/Events';
import talents from 'common/TALENTS/monk';

// 1 - all Analyzers extend this class, or one of its subclasses.
export default class KegSmash extends Analyzer {
  private totalCasts = 0;
  private totalHits = 0;

  constructor(options: Options) {
    // 2 - setting the `active` property controls whether the analyzer is shown
    this.active = this.selectedCombatant.hasTalent(talents.KEG_SMASH_BREWMASTER_TALENT);
    if (!this.active) {
      return;
    }

    // 3 - in order to receive events, we need to add event listeners
    this.addEventListener(Events.cast.spell(talents.KEG_SMASH_BREWMASTER_TALENT).by(SELECTED_PLAYER), this.recordCast);
    this.addEventListener(Events.damage.spell(talents.KEG_SMASH_BREWMASTER_TALENT).by(SELECTED_PLAYER), this.recordDamage);
  }

  // 4 - these event listeners can do (almost) anything. you will frequently need to track some state to handle complex log data.
  private recordCast(event: CastEvent) {
    this.totalCasts += 1;
  }

  private recordDamage(event: DamageEvent) {
   this.totalHits += 1;
  }

  // 5 - we can use the tracked data any way that we want. here, we're just going to calculate the average.
  get averageHitsPerCast() {
    if (this.totalCasts === 0) {
      return 0;
    }

    return this.totalHits / this.totalCasts;
  }

  // 6 - to show results on the Statistics page, we implement the `statistic` function
  statistic() {
    // ...but we're not going to do that in this document :)
  }
}

This breaks down the structure that most analyzers will have:

  1. Start with a simple class that extends the Analyzer base class.
  2. If you're working with an optional spell or item, you should check that the player has it here. We don't want to run analysis for every trinket and talent in the game when only a few can be selected!
  3. Add any event listeners that you're going to need to do your analysis. Often, it won't be clear what you need until you get further in development. A good way to get started is by finding a similar spell and looking at how analysis for it is implemented.
  4. Implement the bodies of the event listeners. These can start empty and get filled in later.
  5. Do something with the analysis!

This leaves out (6), which is showing the analysis on the Statistics page. The best way to learn how to implement one of these is to look for other spells that are similar and seeing what they do. There is a lot of flexibility in what you can show---far more than we can cover here!

Dependencies

It is also possible to have your analysis depend on another module. To do that, you set up a dependencies object. The required modules will be injected into your class when the analysis is being run. For example, if we wanted out KegSmash analyzer to depend on the output of the StormstoutsLastKeg analyzer, we would do:

// 1 - add the import
import StormstoutsLastKeg from 'analysis/retail/monk/brewmaster/modules/talents/StormstoutsLastKeg';

export default class KegSmash extends Analyzer {
  // 2 - add the dependency to the class
  // dependencies are conventionally listed first.
  static dependencies = {
    stormstouts: StormstoutsLastKeg,
  };

  // 3 - declare the field type for the typechecker
  protected stormstouts!: StormstoutsLastKeg;

  // 4 - use the field somewhere!
  myAnalysis() {
    const value = this.stormstouts.getSomeAnalysisResult();
  }

  // ...
}

Where to Find Examples

There are a few classes that have been updated for Dragonflight (to some degree) already that will be useful for examples.

Normalizers

There are a lot of bugs in combat logs that make analysis harder. Normalizers are used to work around these issues. They receive the full events list prior to analysis and change its order, values or even fabricate new events to make it more consistent, less buggy and easier to analyze. You usually won't have to make your own normalizer and should always try to solve things without one first.

Common Normalizers

  • EventOrderNormalizer (Example Use) reorders events so that they occur in a consistent order. For example: you could fix it so that a cast always occurs before the buff you get from the cast.