<commit_before>import SPELLS from 'common/SPELLS';
import Analyzer, { SELECTED_PLAYER, Options } from 'parser/core/Analyzer';
import StatTracker from 'parser/shared/modules/StatTracker';

import Events, { ApplyBuffEvent, RefreshBuffEvent, RemoveBuffEvent, CastEvent, DamageEvent } from 'parser/core/Events';

import BlackoutCombo from './BlackoutCombo';
import SharedBrews from '../core/SharedBrews';

const TIGER_PALM_REDUCTION = 1000;

class TigerPalm extends Analyzer {
  static dependencies = {
    boc: BlackoutCombo,
    brews: SharedBrews,
    statTracker: StatTracker,
  };

  protected boc!: BlackoutCombo;
  protected brews!: SharedBrews;
  protected statTracker!: StatTracker;

  totalCasts = 0;
  normalHits = 0;
  bocHits = 0;
  lastAttackPower = 0;

  cdr = 0;
  wastedCDR = 0;

  bocBuffActive = false;
  bocApplyToTP = false;

  get totalBocHits() {
    return this.bocHits;
  }

  get bocEmpoweredThreshold() {
    if(!this.selectedCombatant.hasTalent(SPELLS.BLACKOUT_COMBO_TALENT.id)) {
      return null;
    }
    return {
      actual: this.totalBocHits / this.totalCasts,
      isLessThan: {
        minor: 0.95,
        average: 0.9,
        major: 0.85,
      },
      style: 'percentage',
    };
  }

  constructor(options: Options){
    super(options);
    this.addEventListener(Events.applybuff.by(SELECTED_PLAYER).spell(SPELLS.BLACKOUT_COMBO_BUFF), this.onGainBOC);
    this.addEventListener(Events.refreshbuff.by(SELECTED_PLAYER).spell(SPELLS.BLACKOUT_COMBO_BUFF), this.onGainBOC);
    this.addEventListener(Events.removebuff.by(SELECTED_PLAYER).spell(SPELLS.BLACKOUT_COMBO_BUFF), this.onLoseBOC);
    this.addEventListener(Events.cast.by(SELECTED_PLAYER).spell(SPELLS.TIGER_PALM), this.onCast);
    this.addEventListener(Events.damage.by(SELECTED_PLAYER).spell(SPELLS.TIGER_PALM), this.onDamage);
    this.addEventListener(Events.damage.to(SELECTED_PLAYER), this.onDamageTaken);
  }

  onGainBOC(event: ApplyBuffEvent | RefreshBuffEvent){
    this.bocBuffActive = true;
  }

  onLoseBOC(event: RemoveBuffEvent){
    this.bocBuffActive = false;
  }

  onCast(event: CastEvent) {
    this.totalCasts += 1;
    this.bocApplyToTP = this.bocBuffActive;
  }

  onDamage(event: DamageEvent) {
    // OK SO we have a hit, lets reduce the CD by the base amount...
    const actualReduction = this.brews.reduceCooldown(TIGER_PALM_REDUCTION);
    this.cdr += actualReduction;
    this.wastedCDR += TIGER_PALM_REDUCTION - actualReduction;

    if (this.bocApplyToTP) {
      this.bocHits += 1;
    } else {
      this.normalHits += 1;
    }
  }

  onDamageTaken(event: DamageEvent) {
    // record the last known AP to estimate the amount of damage a
    // non-FP TP should do
    if (event.attackPower !== undefined && event.attackPower > 0) {
      this.lastAttackPower = event.attackPower;
    }
  }
}

export default TigerPalm;<commit_msg>added bad TP cast annotations to timeline<commit_after>import React from 'react';
import SPELLS from 'common/SPELLS';
import SpellLink from 'common/SpellLink';
import Analyzer, { SELECTED_PLAYER, Options } from 'parser/core/Analyzer';
import StatTracker from 'parser/shared/modules/StatTracker';
import Combatant from 'parser/core/Combatant';
import { SpellInfo } from 'parser/core/EventFilter';
import Events, { ApplyBuffEvent, RefreshBuffEvent, RemoveBuffEvent, CastEvent, DamageEvent } from 'parser/core/Events';
import SpellUsable from 'parser/shared/modules/SpellUsable';

import BlackoutCombo from './BlackoutCombo';
import SharedBrews from '../core/SharedBrews';

const TIGER_PALM_REDUCTION = 1000;

interface ConditionalSpell {
  spell: SpellInfo,
  when: (c: Combatant) => boolean,
}

function isConditional(spell: ConditionalSpell | SpellInfo): spell is ConditionalSpell {
  return Object.prototype.hasOwnProperty.call(spell, 'when');
}

const BETTER_SPELLS: Array<SpellInfo | ConditionalSpell> = [
  SPELLS.KEG_SMASH,
  SPELLS.BLACKOUT_KICK,
  SPELLS.BREATH_OF_FIRE,
  {
    spell: SPELLS.RUSHING_JADE_WIND,
    when: (combatant: Combatant) => combatant.hasTalent(SPELLS.RUSHING_JADE_WIND.id) && !combatant.hasBuff(SPELLS.RUSHING_JADE_WIND.id),
  },
  {
    spell: SPELLS.CHI_BURST_TALENT,
    when: (combatant: Combatant) => combatant.hasTalent(SPELLS.CHI_BURST_TALENT.id),
  },
  {
    spell: SPELLS.CHI_WAVE_TALENT,
    when: (combatant: Combatant) => combatant.hasTalent(SPELLS.CHI_WAVE_TALENT.id),
  },
];

class TigerPalm extends Analyzer {
  static dependencies = {
    boc: BlackoutCombo,
    brews: SharedBrews,
    statTracker: StatTracker,
    spellUsable: SpellUsable,
  };

  protected boc!: BlackoutCombo;
  protected brews!: SharedBrews;
  protected statTracker!: StatTracker;
  protected spellUsable!: SpellUsable;

  totalCasts = 0;
  badCasts = 0;
  normalHits = 0;
  bocHits = 0;
  lastAttackPower = 0;

  cdr = 0;
  wastedCDR = 0;

  bocBuffActive = false;
  bocApplyToTP = false;

  get totalBocHits() {
    return this.bocHits;
  }

  get bocEmpoweredThreshold() {
    if(!this.selectedCombatant.hasTalent(SPELLS.BLACKOUT_COMBO_TALENT.id)) {
      return null;
    }
    return {
      actual: this.totalBocHits / this.totalCasts,
      isLessThan: {
        minor: 0.95,
        average: 0.9,
        major: 0.85,
      },
      style: 'percentage',
    };
  }

  constructor(options: Options){
    super(options);
    this.addEventListener(Events.applybuff.by(SELECTED_PLAYER).spell(SPELLS.BLACKOUT_COMBO_BUFF), this.onGainBOC);
    this.addEventListener(Events.refreshbuff.by(SELECTED_PLAYER).spell(SPELLS.BLACKOUT_COMBO_BUFF), this.onGainBOC);
    this.addEventListener(Events.removebuff.by(SELECTED_PLAYER).spell(SPELLS.BLACKOUT_COMBO_BUFF), this.onLoseBOC);
    this.addEventListener(Events.cast.by(SELECTED_PLAYER).spell(SPELLS.TIGER_PALM), this.onCast);
    this.addEventListener(Events.cast.by(SELECTED_PLAYER).spell(SPELLS.TIGER_PALM), this.checkBadTP);
    this.addEventListener(Events.damage.by(SELECTED_PLAYER).spell(SPELLS.TIGER_PALM), this.onDamage);
    this.addEventListener(Events.damage.to(SELECTED_PLAYER), this.onDamageTaken);
  }

  onGainBOC(event: ApplyBuffEvent | RefreshBuffEvent){
    this.bocBuffActive = true;
  }

  onLoseBOC(event: RemoveBuffEvent){
    this.bocBuffActive = false;
  }

  onCast(event: CastEvent) {
    this.totalCasts += 1;
    this.bocApplyToTP = this.bocBuffActive;
  }

  onDamage(event: DamageEvent) {
    // OK SO we have a hit, lets reduce the CD by the base amount...
    const actualReduction = this.brews.reduceCooldown(TIGER_PALM_REDUCTION);
    this.cdr += actualReduction;
    this.wastedCDR += TIGER_PALM_REDUCTION - actualReduction;

    if (this.bocApplyToTP) {
      this.bocHits += 1;
    } else {
      this.normalHits += 1;
    }
  }

  onDamageTaken(event: DamageEvent) {
    // record the last known AP to estimate the amount of damage a
    // non-FP TP should do
    if (event.attackPower !== undefined && event.attackPower > 0) {
      this.lastAttackPower = event.attackPower;
    }
  }

  // a Tiger Palm cast is bad if it is cast while one of the `BETTER_SPELLS` is
  // off cooldown, or if casting it delayed Keg Smash due to energy starvation.
  checkBadTP(event: CastEvent) {
    if(this.bocBuffActive) {
      return; // TP+BoC is highest prio
    }

    const availableSpells: SpellInfo[] = BETTER_SPELLS.filter(entry => {
      if (isConditional(entry)) {
        return entry.when(this.selectedCombatant) && this.spellUsable.isAvailable(entry.spell.id);
      } else {
        return this.spellUsable.isAvailable(entry.id);
      }
    }).map(entry => isConditional(entry) ? entry.spell : entry);

    if (availableSpells.length === 0) {
      return; // nothing better to cast, so this is OK
    }

    // this clobbers the timeline metadata, but at this time nothing sets it
    // outside of individual analyzers.
    event.meta = {
      isInefficientCast: true,
      inefficientCastReason: (
        <>
          The following better spells were available during this GCD:
          <ul>
        {availableSpells.map(({ id }) => <li key={id}><SpellLink id={id} /></li>)}
          </ul>
        </>
      )
    };

    this.badCasts += 1;
  }
}

export default TigerPalm;
{'fraction_non_alphanumeric': 0.11330472103004292, 'fraction_numerical': 0.0057633353770692826, 'mean_word_length': 3.681974741676234, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 20, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5103927', 'n_tokens_mistral': 2987, 'n_tokens_neox': 2890, 'n_words': 546}