<commit_before>import {extent, min, max} from "d3-array";
import {nest} from "d3-collection";
import * as scales from "d3-scale";
import {stack} from "d3-shape";
import {mouse} from "d3-selection";

import {AxisBottom, AxisLeft} from "d3plus-axis";
import {assign} from "d3plus-color";
import {accessor, closest, constant, elem} from "d3plus-common";
import * as shapes from "d3plus-shape";
import {Viz} from "d3plus-viz";

import {default as CircleBuffer} from "./buffers/Circle.js";
import {default as RectBuffer} from "./buffers/Rect.js";

/**
    @class Plot
    @extends Viz
    @desc Creates an x/y plot based on an array of data.
*/
export default class Plot extends Viz {

  /**
      @memberof Plot
      @desc Invoked when creating a new class instance, and sets any default parameters.
      @private
  */
  constructor() {

    super();
    this._buffer = {
      Circle: CircleBuffer,
      Rect: RectBuffer
    };
    this._shape = constant("Circle");
    this._shapeConfig = Object.assign(this._shapeConfig, {
      Circle: {
        r: constant(5)
      },
      Line: {
        fill: constant("none"),
        label: false,
        stroke: (d, i) => assign(this._id(d, i)),
        strokeWidth: constant(1)
      },
      Rect: {
        height: constant(10),
        width: constant(10)
      }
    });
    this._x = accessor("x");
    this._xAxis = new AxisBottom().align("end");
    this._xTest = new AxisBottom().align("end").gridSize(0);
    this._xConfig = {
      title: "X Axis"
    };
    this._y = accessor("y");
    this._yAxis = new AxisLeft().align("start");
    this._yTest = new AxisLeft().align("start").gridSize(0);
    this._yConfig = {
      title: "Y Axis"
    };

  }

  /**
      Extends the render behavior of the abstract Viz class.
      @private
  */
  render(callback) {

    super.render(callback);

    const data = this._filteredData.map((d, i) => ({
            data: d,
            i,
            id: this._id(d, i),
            shape: this._shape(d, i),
            x: this._x(d, i),
            y: this._y(d, i)
          })),
          height = this._height - this._margin.top - this._margin.bottom,
          opp = this._discrete ? this._discrete === "x" ? "y" : "x" : undefined,
          parent = this._select,
          that = this,
          transform = `translate(${this._margin.left}, ${this._margin.top})`,
          transition = this._transition,
          width = this._width - this._margin.left - this._margin.right;

    let domains, stackData, stackKeys;
    if (this._stacked) {

      stackKeys = Array.from(new Set(data.map(d => d.id)));
      stackData = stack()
        .keys(stackKeys)
        .value((group, key) => group.filter(d => d.id === key)[0][opp])
        (nest().key(d => d[this._discrete]).entries(data).map(d => d.values));

      domains = {
        [this._discrete]: extent(data, d => d[this._discrete]),
        [opp]: [min(stackData.map(g => min(g.map(p => p[1])))), max(stackData.map(g => max(g.map(p => p[1]))))]
      };

    }
    else domains = {x: extent(data, d => d.x), y: extent(data, d => d.y)};

    const xDomain = this._xDomain ? this._xDomain.slice() : domains.x;
    if (xDomain[0] === void 0) xDomain[0] = domains.x[0];
    if (xDomain[1] === void 0) xDomain[1] = domains.x[1];
    const yDomain = this._yDomain ? this._yDomain.slice() : domains.y;
    if (yDomain[0] === void 0) yDomain[0] = domains.y[0];
    if (yDomain[1] === void 0) yDomain[1] = domains.y[1];
    domains = {x: xDomain, y: yDomain};

    if (opp && this._baseline !== void 0) {
      const b = this._baseline;
      if (domains[opp][0] > b) domains[opp][0] = b;
      else if (domains[opp][1] < b) domains[opp][1] = b;
    }

    let x = scales.scaleLinear().domain(domains.x).range([0, width]),
        y = scales.scaleLinear().domain(domains.y.reverse()).range([0, height]);

    const shapeData = nest().key(d => d.shape).entries(data);
    shapeData.forEach(d => {
      if (this._buffer[d.key]) {
        const res = this._buffer[d.key](d.values, x, y, this._shapeConfig[d.key]);
        x = res[0];
        y = res[1];
      }
    });

    const xTicks = this._discrete === "x" && (!this._time || this._x(data[0], 0) !== this._time(data[0], 0))
                 ? Array.from(new Set(data.map(d => d.x))) : undefined,
          yTicks = this._discrete === "y" && (!this._time || this._y(data[0], 0) !== this._time(data[0], 0))
                 ? Array.from(new Set(data.map(d => d.y))) : undefined;

    this._yTest
      .domain(y.domain())
      .height(height)
      .select(elem("g.d3plus-plot-test", {enter: {opacity: 0}, parent: this._select}).node())
      .ticks(yTicks)
      .width(width)
      .config(this._yConfig)
      .render();

    const xOffset = this._yTest.outerBounds().width + this._yTest.padding();

    this._xTest
      .domain(x.domain())
      .height(height)
      .range([xOffset, undefined])
      .select(elem("g.d3plus-plot-test", {enter: {opacity: 0}, parent: this._select}).node())
      .ticks(xTicks)
      .width(width)
      .config(this._xConfig)
      .render();

    this._xAxis
      .domain(x.domain())
      .height(height)
      .range([xOffset, undefined])
      .select(elem("g.d3plus-plot-x-axis", {parent, transition, enter: {transform}, update: {transform}}).node())
      .ticks(xTicks)
      .width(width)
      .config(this._xConfig)
      .render();

    x = this._xAxis._d3Scale;

    this._yAxis
      .domain(y.domain())
      .height(height)
      .range([this._xAxis.outerBounds().y, this._xTest.outerBounds().y])
      .select(elem("g.d3plus-plot-y-axis", {parent, transition, enter: {transform}, update: {transform}}).node())
      .ticks(yTicks)
      .width(x.range()[1] + this._xAxis.padding())
      .config(this._yConfig)
      .render();

    y = this._yAxis._d3Scale;

    let shapeConfig = {
      duration: this._duration,
      fill: d => this._shapeConfig.fill(d.data, d.i),
      label: d => this._drawLabel(d.data, d.i),
      opacity: d => this._shapeConfig.opacity(d.data, d.i),
      select: elem("g.d3plus-plot-shapes", {parent, transition}).node(),
      stroke: d => this._shapeConfig.stroke(d.data, d.i),
      strokeWidth: d => this._shapeConfig.strokeWidth(d.data, d.i),
      x: d => x(d.x),
      y: d => y(d.y)
    };

    const positions = {
      x0: this._discrete === "x" ? shapeConfig.x : x(0),
      x1: this._discrete === "x" ? null : shapeConfig.x,
      y0: this._discrete === "y" ? shapeConfig.y : y(0),
      y1: this._discrete === "y" ? null : shapeConfig.y
    };

    if (this._stacked) {
      positions[`${opp}0`] = (d, i) => (opp === "x" ? x : y)(stackData[stackKeys.indexOf(d.id)][i][0]);
      positions[`${opp}1`] = (d, i) => (opp === "x" ? x : y)(stackData[stackKeys.indexOf(d.id)][i][1]);
    }

    shapeConfig = Object.assign(shapeConfig, positions);

    /**
        @desc Handles mouse events for nested shapes, finding the closest discrete data point to send to the defined event function.
        @private
    */
    function mouseEvent(d) {
      if (d.nested && d.values) {
        const axis = that._discrete,
              cursor = mouse(that._select.node())[axis === "x" ? 0 : 1],
              values = d.values.map(d => shapeConfig[axis](d));
        d = d.values[values.indexOf(closest(cursor, values))];
      }
      return this(d.data, d.i);
    }

    const events = Object.keys(this._on);
    shapeData.forEach(d => {

      const s = new shapes[d.key]().config(shapeConfig).data(d.values);
      const classEvents = events.filter(e => e.includes(`.${d.key}`)),
            globalEvents = events.filter(e => !e.includes(".")),
            shapeEvents = events.filter(e => e.includes(".shape"));
      for (let e = 0; e < globalEvents.length; e++) s.on(globalEvents[e], mouseEvent.bind(this._on[globalEvents[e]]));
      for (let e = 0; e < shapeEvents.length; e++) s.on(shapeEvents[e], mouseEvent.bind(this._on[shapeEvents[e]]));
      for (let e = 0; e < classEvents.length; e++) s.on(classEvents[e], mouseEvent.bind(this._on[classEvents[e]]));
      s.config(this._shapeConfig[d.key] || {}).render();
      this._shapes.push(s);

    });

    return this;

  }

  /**
      @memberof Plot
      @desc If *value* is specified, sets the baseline for the x/y plot and returns the current class instance. If *value* is not specified, returns the current baseline.
      @param {Number} [*value*]
  */
  baseline(_) {
    return arguments.length ? (this._baseline = _, this) : this._baseline;
  }

  /**
      @memberof Plot
      @desc If *value* is specified, toggles shape stacking and returns the current class instance. If *value* is not specified, returns the current stack value.
      @param {Boolean} [*value* = false]
  */
  stacked(_) {
    return arguments.length ? (this._stacked = _, this) : this._stacked;
  }

  /**
      @memberof Plot
      @desc If *value* is specified, sets the x accessor to the specified function or number and returns the current class instance. If *value* is not specified, returns the current x accessor.
      @param {Function|Number} [*value*]
  */
  x(_) {
    return arguments.length ? (this._x = typeof _ === "function" ? _ : constant(_), this) : this._x;
  }

  /**
      @memberof Plot
      @desc If *value* is specified, sets the x domain to the specified array and returns the current class instance. If *value* is not specified, returns the current x domain. Additionally, if either value of the array is undefined, it will be calculated from the data.
      @param {Array} [*value*]
  */
  xDomain(_) {
    return arguments.length ? (this._xDomain = _, this) : this._xDomain;
  }

  /**
      @memberof Plot
      @desc If *value* is specified, sets the y accessor to the specified function or number and returns the current class instance. If *value* is not specified, returns the current y accessor.
      @param {Function|Number} [*value*]
  */
  y(_) {
    return arguments.length ? (this._y = typeof _ === "function" ? _ : constant(_), this) : this._y;
  }

  /**
      @memberof Plot
      @desc If *value* is specified, sets the y domain to the specified array and returns the current class instance. If *value* is not specified, returns the current y domain. Additionally, if either value of the array is undefined, it will be calculated from the data.
      @param {Array} [*value*]
  */
  yDomain(_) {
    return arguments.length ? (this._yDomain = _, this) : this._yDomain;
  }

}
<commit_msg>cleans up date axes<commit_after>import {extent, min, max} from "d3-array";
import {nest} from "d3-collection";
import * as scales from "d3-scale";
import {stack} from "d3-shape";
import {mouse} from "d3-selection";

import {AxisBottom, AxisLeft, date} from "d3plus-axis";
import {assign} from "d3plus-color";
import {accessor, closest, constant, elem} from "d3plus-common";
import * as shapes from "d3plus-shape";
import {Viz} from "d3plus-viz";

import {default as CircleBuffer} from "./buffers/Circle.js";
import {default as RectBuffer} from "./buffers/Rect.js";

/**
    @class Plot
    @extends Viz
    @desc Creates an x/y plot based on an array of data.
*/
export default class Plot extends Viz {

  /**
      @memberof Plot
      @desc Invoked when creating a new class instance, and sets any default parameters.
      @private
  */
  constructor() {

    super();
    this._buffer = {
      Circle: CircleBuffer,
      Rect: RectBuffer
    };
    this._shape = constant("Circle");
    this._shapeConfig = Object.assign(this._shapeConfig, {
      Circle: {
        r: constant(5)
      },
      Line: {
        fill: constant("none"),
        label: false,
        stroke: (d, i) => assign(this._id(d, i)),
        strokeWidth: constant(1)
      },
      Rect: {
        height: constant(10),
        width: constant(10)
      }
    });
    this._x = accessor("x");
    this._xAxis = new AxisBottom().align("end");
    this._xTest = new AxisBottom().align("end").gridSize(0);
    this._xConfig = {
      title: "X Axis"
    };
    this._y = accessor("y");
    this._yAxis = new AxisLeft().align("start");
    this._yTest = new AxisLeft().align("start").gridSize(0);
    this._yConfig = {
      title: "Y Axis"
    };

  }

  /**
      Extends the render behavior of the abstract Viz class.
      @private
  */
  render(callback) {

    super.render(callback);

    const data = this._filteredData.map((d, i) => ({
            data: d,
            i,
            id: this._id(d, i),
            shape: this._shape(d, i),
            x: this._x(d, i),
            y: this._y(d, i)
          })),
          height = this._height - this._margin.top - this._margin.bottom,
          opp = this._discrete ? this._discrete === "x" ? "y" : "x" : undefined,
          parent = this._select,
          that = this,
          transform = `translate(${this._margin.left}, ${this._margin.top})`,
          transition = this._transition,
          width = this._width - this._margin.left - this._margin.right;

    let domains, stackData, stackKeys;
    if (this._stacked) {

      stackKeys = Array.from(new Set(data.map(d => d.id)));
      stackData = stack()
        .keys(stackKeys)
        .value((group, key) => group.filter(d => d.id === key)[0][opp])
        (nest().key(d => d[this._discrete]).entries(data).map(d => d.values));

      domains = {
        [this._discrete]: extent(data, d => d[this._discrete]),
        [opp]: [min(stackData.map(g => min(g.map(p => p[1])))), max(stackData.map(g => max(g.map(p => p[1]))))]
      };

    }
    else domains = {x: extent(data, d => d.x), y: extent(data, d => d.y)};

    const xTime = this._time && this._x(data[0], 0) === this._time(data[0], 0),
          yTime = this._time && this._y(data[0], 0) === this._time(data[0], 0);

    if (xTime || yTime) {
      data.forEach(d => {
        if (xTime) d.x = date(d.x);
        if (yTime) d.y = date(d.y);
      });
    }

    let xDomain = this._xDomain ? this._xDomain.slice() : domains.x;
    if (xDomain[0] === void 0) xDomain[0] = domains.x[0];
    if (xDomain[1] === void 0) xDomain[1] = domains.x[1];
    if (xTime) xDomain = xDomain.map(date);

    let yDomain = this._yDomain ? this._yDomain.slice() : domains.y;
    if (yDomain[0] === void 0) yDomain[0] = domains.y[0];
    if (yDomain[1] === void 0) yDomain[1] = domains.y[1];
    if (yTime) yDomain = yDomain.map(date);

    domains = {x: xDomain, y: yDomain};

    if (opp && this._baseline !== void 0) {
      const b = this._baseline;
      if (domains[opp][0] > b) domains[opp][0] = b;
      else if (domains[opp][1] < b) domains[opp][1] = b;
    }

    let x = scales[`scale${xTime ? "Time" : "Linear"}`]().domain(domains.x).range([0, width]),
        y = scales[`scale${yTime ? "Time" : "Linear"}`]().domain(domains.y.reverse()).range([0, height]);

    const shapeData = nest().key(d => d.shape).entries(data);
    shapeData.forEach(d => {
      if (this._buffer[d.key]) {
        const res = this._buffer[d.key](d.values, x, y, this._shapeConfig[d.key]);
        x = res[0];
        y = res[1];
      }
    });

    const xTicks = this._discrete === "x" && !xTime ? Array.from(new Set(data.map(d => d.x))) : undefined,
          yTicks = this._discrete === "y" && !yTime ? Array.from(new Set(data.map(d => d.y))) : undefined;

    this._yTest
      .domain(yDomain)
      .height(height)
      .scale(yTime ? "time" : "linear")
      .select(elem("g.d3plus-plot-test", {enter: {opacity: 0}, parent: this._select}).node())
      .ticks(yTicks)
      .width(width)
      .config(this._yConfig)
      .render();

    const xOffset = this._yTest.outerBounds().width + this._yTest.padding();

    this._xTest
      .domain(xDomain)
      .height(height)
      .range([xOffset, undefined])
      .scale(xTime ? "time" : "linear")
      .select(elem("g.d3plus-plot-test", {enter: {opacity: 0}, parent: this._select}).node())
      .ticks(xTicks)
      .width(width)
      .config(this._xConfig)
      .render();

    this._xAxis
      .domain(xDomain)
      .height(height)
      .range([xOffset, undefined])
      .scale(xTime ? "time" : "linear")
      .select(elem("g.d3plus-plot-x-axis", {parent, transition, enter: {transform}, update: {transform}}).node())
      .ticks(xTicks)
      .width(width)
      .config(this._xConfig)
      .render();

    x = this._xAxis._d3Scale;

    this._yAxis
      .domain(yDomain)
      .height(height)
      .range([this._xAxis.outerBounds().y, this._xTest.outerBounds().y])
      .scale(yTime ? "time" : "linear")
      .select(elem("g.d3plus-plot-y-axis", {parent, transition, enter: {transform}, update: {transform}}).node())
      .ticks(yTicks)
      .width(x.range()[1] + this._xAxis.padding())
      .config(this._yConfig)
      .render();

    y = this._yAxis._d3Scale;

    let shapeConfig = {
      duration: this._duration,
      fill: d => this._shapeConfig.fill(d.data, d.i),
      label: d => this._drawLabel(d.data, d.i),
      opacity: d => this._shapeConfig.opacity(d.data, d.i),
      select: elem("g.d3plus-plot-shapes", {parent, transition}).node(),
      stroke: d => this._shapeConfig.stroke(d.data, d.i),
      strokeWidth: d => this._shapeConfig.strokeWidth(d.data, d.i),
      x: d => x(d.x),
      y: d => y(d.y)
    };

    const positions = {
      x0: this._discrete === "x" ? shapeConfig.x : x(0),
      x1: this._discrete === "x" ? null : shapeConfig.x,
      y0: this._discrete === "y" ? shapeConfig.y : y(0),
      y1: this._discrete === "y" ? null : shapeConfig.y
    };

    if (this._stacked) {
      positions[`${opp}0`] = (d, i) => (opp === "x" ? x : y)(stackData[stackKeys.indexOf(d.id)][i][0]);
      positions[`${opp}1`] = (d, i) => (opp === "x" ? x : y)(stackData[stackKeys.indexOf(d.id)][i][1]);
    }

    shapeConfig = Object.assign(shapeConfig, positions);

    /**
        @desc Handles mouse events for nested shapes, finding the closest discrete data point to send to the defined event function.
        @private
    */
    function mouseEvent(d) {
      if (d.nested && d.values) {
        const axis = that._discrete,
              cursor = mouse(that._select.node())[axis === "x" ? 0 : 1],
              values = d.values.map(d => shapeConfig[axis](d));
        d = d.values[values.indexOf(closest(cursor, values))];
      }
      return this(d.data, d.i);
    }

    const events = Object.keys(this._on);
    shapeData.forEach(d => {

      const s = new shapes[d.key]().config(shapeConfig).data(d.values);
      const classEvents = events.filter(e => e.includes(`.${d.key}`)),
            globalEvents = events.filter(e => !e.includes(".")),
            shapeEvents = events.filter(e => e.includes(".shape"));
      for (let e = 0; e < globalEvents.length; e++) s.on(globalEvents[e], mouseEvent.bind(this._on[globalEvents[e]]));
      for (let e = 0; e < shapeEvents.length; e++) s.on(shapeEvents[e], mouseEvent.bind(this._on[shapeEvents[e]]));
      for (let e = 0; e < classEvents.length; e++) s.on(classEvents[e], mouseEvent.bind(this._on[classEvents[e]]));
      s.config(this._shapeConfig[d.key] || {}).render();
      this._shapes.push(s);

    });

    return this;

  }

  /**
      @memberof Plot
      @desc If *value* is specified, sets the baseline for the x/y plot and returns the current class instance. If *value* is not specified, returns the current baseline.
      @param {Number} [*value*]
  */
  baseline(_) {
    return arguments.length ? (this._baseline = _, this) : this._baseline;
  }

  /**
      @memberof Plot
      @desc If *value* is specified, toggles shape stacking and returns the current class instance. If *value* is not specified, returns the current stack value.
      @param {Boolean} [*value* = false]
  */
  stacked(_) {
    return arguments.length ? (this._stacked = _, this) : this._stacked;
  }

  /**
      @memberof Plot
      @desc If *value* is specified, sets the x accessor to the specified function or number and returns the current class instance. If *value* is not specified, returns the current x accessor.
      @param {Function|Number} [*value*]
  */
  x(_) {
    return arguments.length ? (this._x = typeof _ === "function" ? _ : constant(_), this) : this._x;
  }

  /**
      @memberof Plot
      @desc If *value* is specified, sets the x domain to the specified array and returns the current class instance. If *value* is not specified, returns the current x domain. Additionally, if either value of the array is undefined, it will be calculated from the data.
      @param {Array} [*value*]
  */
  xDomain(_) {
    return arguments.length ? (this._xDomain = _, this) : this._xDomain;
  }

  /**
      @memberof Plot
      @desc If *value* is specified, sets the y accessor to the specified function or number and returns the current class instance. If *value* is not specified, returns the current y accessor.
      @param {Function|Number} [*value*]
  */
  y(_) {
    return arguments.length ? (this._y = typeof _ === "function" ? _ : constant(_), this) : this._y;
  }

  /**
      @memberof Plot
      @desc If *value* is specified, sets the y domain to the specified array and returns the current class instance. If *value* is not specified, returns the current y domain. Additionally, if either value of the array is undefined, it will be calculated from the data.
      @param {Array} [*value*]
  */
  yDomain(_) {
    return arguments.length ? (this._yDomain = _, this) : this._yDomain;
  }

}
<|endoftext|>


<commit_before>var Master = function(){
    var main = "";

    return {
        init: function(){
            $('main').html(main);
        }
    };
}();
<commit_msg>Update master.js<commit_after>var Master = function(){
    var main = "";

    return {
        init: function(){

            console.log(main);

            $('main').html(main);
        }
    };
}();
<|endoftext|>


<commit_before>const gulp = require('gulp');
const webpack = require('webpack-stream');
const preset = require('babel-preset-react');
const babel = require('babel-loader');
const mocha = require('gulp-mocha');

gulp.task('html:dev', () => {
  gulp.src(__dirname + '/app/**/*.html')
    .pipe(gulp.dest(__dirname + '/build'));
});

gulp.task('webpack:dev', () => {
gulp.src(__dirname + '/app/js/client.js')
  .pipe(webpack({
    output: {
      filename: 'bundle.js'
    },
    module: {
      loaders: [
        {
          test: /\.js$/,
          exclude: /(node_modules)/,
          loaders: [ 'babel-loader?presets[]=react' ]
        }
      ]
    }
  }))
  .pipe(gulp.dest('build/'));
});

gulp.task('tests:dev', () => {
  gulp.src(__dirname + '/test/**/*.js')
    .pipe(mocha({reporter: 'nyan'}))
});

gulp.task('build:dev', ['tests:dev', 'webpack:dev', 'html:dev']);

gulp.task('default', ['build:dev']);
<commit_msg>added linting<commit_after>const gulp = require('gulp');
const webpack = require('webpack-stream');
const preset = require('babel-preset-react');
const babel = require('babel-loader');
const mocha = require('gulp-mocha');

gulp.task('html:dev', () => {
  gulp.src(__dirname + '/app/**/*.html')
    .pipe(gulp.dest(__dirname + '/build'));
});

gulp.task('webpack:dev', () => {
gulp.src(__dirname + '/app/js/client.js')
  .pipe(webpack({
    output: {
      filename: 'bundle.js'
    },
    module: {
      loaders: [
        {
          test: /\.js$/,
          exclude: /(node_modules)/,
          loaders: [ 'babel-loader?presets[]=react' ]
        }
      ]
    }
  }))
  .pipe(gulp.dest('build/'));
});

gulp.task('tests:dev', () => {
  gulp.src(__dirname + '/test/**/*.js')
    .pipe(mocha({reporter: 'nyan'}))
});
gulp.task('lint:test', ['lint:nontest'], () => {
  return gulp
  .src('./test/**/*test.js')
  .pipe(mocha({reporter: 'nyan'}))
  .pipe(eslint())
  .pipe(eslint.format());
});
gulp.task('lint:nontest', () => {
  return gulp
  .src(files)
  .pipe(eslint())
  .pipe(eslint.format());
});
gulp.task('build:dev', ['tests:dev', 'webpack:dev', 'html:dev']);

gulp.task('default', ['build:dev', 'lint:test', 'lint:nontest']);
<|endoftext|>


<commit_before>
/**
* get information about Submitty's releases from the GitHub API
*
* @param {String} current_tag - tag Submitty is currently running on
*/
async function getReleases(current_tag) {

    const text = document.getElementById("text");

    try{
        const response = await fetch("https://api.github.com/repos/Submitty/Submitty/releases");

        if(response.status !== 200){
            displayMessage(" Failed to get latest version info." +
                " (Status Code : " + response.status.toString() + ")
" +
                "Message : " + response.statusText , "error"
            );
            console.error("Got bad response:", response);
        }else{
            response.json().then(data => {
                updateReleaseNotes(data, current_tag)
            });
        }
    }catch(error){
        console.error(error);
        displayMessage(error.toString(), "error");
    }
}

/**
* Update the page with latest available release and its release notes
*
* @param {Array} data - response from GitHub API, array of last few releases
* @param {String} current_tag - tag Submitty is currently running on
*/
function updateReleaseNotes(data, current_tag){
    //collect all of the release notes submitty is behind into one large string
    //release notes are separated with \n-END-\n to help with parsing later
    let updates = '';
    let releases_behind = 0;
    while (releases_behind < data.length && data[releases_behind].tag_name !== current_tag) {
        updates += `# ${data[releases_behind].tag_name}\n${data[releases_behind].body}\n\n-END-\n`;
        releases_behind++;
    }

    const latest = data[0];

    $.ajax({
        url: buildUrl(['markdown']),
        type: 'POST',
        data: {
            content: addPRLinks(updates),
            csrf_token: csrfToken
        },
        success: function(markdown_data) {
            //Now that we have the basic markdown, we have to do some extra work to add functionality into the HTML
            //since normally the markdown is just plain HTML. We can't make an extension off of the markdown engine
            //because these are highly specific changes needed for just this page (plus JQuery is very helpful).

            //split markdown payload by the custom marker we inserted to separate out releases from each other
            const release_notes = markdown_data.split('-END-');

            //due to the splitting of the single markdown payload, there will always be an extra empty release at the
            //end, so don't include it in the loop
            for(let i = 0; i < release_notes.length-1; i++) {
                //have to wrap the output in

 since we are using 1 ajax call to process multiple payloads
                //that way they all come back at the same time & faster
                $('.content').append(`<div class="box release"><div class="markdown">${injectStyling(release_notes[i])}</div></div>`);
            }

            //mark release sections that have no items with a special class
            $('.section').each( (index, section) => {
                if (!$(section).find('li').length && $(section).text().includes('None')) {
                    $(section).addClass('no-content');
                }
            });

            //add collapse buttons to each release
            $('.version-header').append('<button class="btn btn-default btn-toggle-release" onclick="toggleRelease(this, event)">Collapse</button>');
            //collapse all releases
            $('#toggle-releases').addClass('collapsed');
            $('#toggle-releases').trigger('click');

            //initially expand the latest release if there is only 1
            if ($('.btn-toggle-release').length === 1) {
                $('.btn-toggle-release').eq(0).trigger('click');
            }

            //mark security & sysadmin releases with badges
            $('.release').each( (i, release) => {
                const security_notes = $(release).find('.update-security');
                const sysadmin_notes = $(release).find('.update-sysadmin');
                //highlight the collapsed release if it contains sysadmin or security notes
                if (security_notes.length && !security_notes.hasClass('no-content')) {
                    $('SECURITY').insertAfter($(release).find('.version-header').find('h1'));
                }

                if (sysadmin_notes.length && !sysadmin_notes.hasClass('no-content')) {
                    $('SYSADMIN ACTION').insertAfter($(release).find('.version-header').find('h1'));
                }
            });

            //set info about state of submitty
            $('#tag').html(`Most Recent Version Available: <a href="${latest['html_url']}" target="_blank">${latest['tag_name']}</a>`);
            if (current_tag === latest['tag_name']) {
                $('#text').html('Submitty is up to date!');
            }
            else {
                const important_message = $('.update-important').length > 0 ? `<strong class="important-text"><em>THERE ${$('.update-important').length === 1 ? "IS" : "ARE"} ${$('.update-important').length} SECURITY/SYSADMIN UPDATES</em></strong>` : '';
                $('#text').html(`A new version of Submitty is available

                                Submitty is ${releases_behind} releases behind.

                                ${important_message}`);
            }

            //hide loading text
            $('#loading-text').hide();
        },
        error: function() {
            displayErrorMessage('Something went wrong while trying to render markdown. Please try again.');
        }
    });
}

/**
 * Expands/Collapses all releases collectively. Ex. if the button is in expand mode, all releases
 * will be expanded, even those that are already expanded.
 * @param {HTMLElement} toggleAllButton
 */
function toggleAllReleases(toggleAllButton) {
    //in this case, collapsed class controls what action the button should take
    //  if toggleAllButton has collapsed - will collapse all
    //  if toggleAllButton doesn't have collapsed - will expand all
    $('.btn-toggle-release').each( (index, button) => {
        if($(toggleAllButton).hasClass('collapsed') != $(button).hasClass('collapsed')) {
            $(button).trigger('click');
        }
    });

    //toggle collapsed class and switch button text accordingly
    $(toggleAllButton).toggleClass('collapsed');
    if($(toggleAllButton).hasClass('collapsed')) {
        $(toggleAllButton).html('Collapse All');
    }
    else {
        $(toggleAllButton).html('Expand All');
    }
}

/**
 *
 * @param {HTMLElement} button HTMLElement of the button that was clicked to trigger this function
 * @param {Event} event Event context of the click event that triggered this function
 */
function toggleRelease(button, event) {
    const release = $(button).closest('.box');
    $(button).toggleClass('collapsed');

    //COLLAPSE
    if ($(button).hasClass('collapsed')) {
        //hide everything that is not in the header
        release.find(':not(div.markdown, div.version-header, div.version-header *)').hide();
        const security_notes = release.find('.update-security');
        const sysadmin_notes = release.find('.update-sysadmin');
        //highlight the collapsed release if it contains sysadmin or security notes
        if ((security_notes.length && !security_notes.hasClass('no-content')) || (sysadmin_notes.length && !sysadmin_notes.hasClass('no-content'))) {
            release.addClass('update-important');
            $(button).addClass('btn-primary');
            $(button).removeClass('btn-default');
        }
        $(button).html('Expand');
    }
    //EXPAND
    else {
        //show everything that was hidden (not in the header)
        release.find(':not(div.markdown, div.version-header, div.version-header *)').show();
        //important class is only helpful when release is collapsed
        release.removeClass('update-important');
        $(button).removeClass('btn-primary');
        $(button).addClass('btn-default');
        $(button).html('Collapse');
    }
    //stop bubbling of event
    event.stopPropagation();
}

/**
 * finds all PR numbers in a string and adds markdown formatting so that they will be rendered as links when
 * converted into HTML markdown
 * @param {string} raw_str Pre-markdown processed string
 * @returns {string} String with link formatting in place.
 */
function addPRLinks(raw_str) {
    return raw_str.replace(/#(\d+)/g, '[#$1](https://github.com/Submitty/Submitty/pull/$1)');
}

/**
 * Converts parsed markdown release notes into a more useful form to assist with the other functionalities of the page
 * @param {string} markdown_data HTML string that has been processed and turned into markdown
 * @returns {string} HTML string with helpful wrappers/classes applied
 */
function injectStyling(markdown_data) {
    //add
 after each
 or
 tag
    markdown_data = markdown_data.replace(/<\/ul>|<\/em><\/p>|<\/a><\/p>/g, '$&
');
    //replace normal
 contents with spans with classes
    markdown_data = markdown_data.replace(/
(\[\w+:(\w+)\].+)(?=<\/li>)|
(\[([^\]]+)\].+)(?=<\/li>)/g, (match, p1, p2, p3, p4) => `<li class="release-item"><span class="update-${`${p2}${p4}`.toLowerCase()}}">${p1}${p3}</span>`);
    //add class and wrapper to version headers
    markdown_data = markdown_data.replace(/
.+?<\/h1>/g, `<div class="version-header" onclick="$(this).find('.btn-toggle-release').trigger('click')">$&</div>`);
    //wrap release sections in a

    markdown_data = markdown_data.replace(/
([^<\n\s]+)[^<\n]*?[\s\S]+?(?:
)/g, (match, p1) => `<div class="section update-${p1.toLowerCase()}">${match}</div>`);

    return markdown_data;
}

function removeFilterFromHTML(html) {
    return html.replace(/([\s\S]+?)<\/span>/g, '$1');
}

/**
 * Handles special filter case to save time. All releases/sections/items are shown, all releases are collapsed,
 * and all filter highlighting is removed.
 */
function clearFilter() {
    $('#release-notes-filter').val('');
    $('.release').show();
    $('.section').show();
    $('.release-item').show();

    //remove leftover highlighting from release items
    $('.release-item').each( (i, list_item) => {
        const no_filter = removeFilterFromHTML($(list_item).html());
        $(list_item).html(no_filter);
    });

    //collapse all
    $('#toggle-releases').addClass('collapsed');
    $('#toggle-releases').trigger('click');
}

/**
 * Searches through release notes and only shows releases/items that contain matches
 * to the given filter string
 * @param {string} filter The substring to filter by
 *
 */
function filterReleaseNotes(filter) {
    //handle special case of empty filter to save time
    if(filter === '') {
        clearFilter();
        return;
    }

    $('#no-filter-results').hide();

    let releases_shown = 0;
    const releases = $('.release');
    releases.each( (i, release) => {
        //get all sections on this release
        const sections = $(release).find('.section');
        let found_at_least_one = false;
        sections.each( (j, section) => {
            //initially hide the section
            $(section).hide();
            //skip to next section early if there is no content in this one
            if ($(section).hasClass('no-content')) {
                return;
            }
            //if the filter exists in the section's text somewhere
            if($(section).text().toLowerCase().includes(filter.toLowerCase())){
                //show the section and make sure the release that the section is in is shown
                $(section).show();
                $(release).show();

                //loop through all release items in section
                $(section).find('.release-item').each( (k, release_item) => {
                    //remove old filter highlighting
                    const no_filter = removeFilterFromHTML($(release_item).html());
                    $(release_item).html(no_filter);

                    //initially hide release item
                    $(release_item).hide();

                    //if the filter text can be found in this release item
                    if($(release_item).text().toLowerCase().includes(filter.toLowerCase())){
                        found_at_least_one = true;
                        releases_shown++;
                        //show the release item
                        $(release_item).show();
                        //expand the release this release item belongs to if it isn't already expanded
                        const button = $(release).find('.btn-toggle-release');
                        if(button.hasClass('collapsed')) {
                            button.trigger('click');
                        }
                        //replace all instances of the filter text that is not inside an html tag's attributes
                        //with a span wrapper for styling
                        const matches = $(release_item).html().replace(new RegExp(`${filter}(?=[^<>]+<)`, 'gi'), '$&');
                        $(release_item).html(matches);
                    }
                });
            }
        });

        //if no release items matched with the filter in this release, hide it
        if(!found_at_least_one) {
            $(release).hide();
        }
    });

    if (releases_shown === 0) {
        $('#no-filter-results').show();
    }
}<commit_msg>[Bugfix:System] Fix Undefined Error on Release Notes (#6714)<commit_after>
/**
* get information about Submitty's releases from the GitHub API
*
* @param {String} current_tag - tag Submitty is currently running on
*/
async function getReleases(current_tag) {

    const text = document.getElementById("text");

    try{
        const response = await fetch("https://api.github.com/repos/Submitty/Submitty/releases");

        if(response.status !== 200){
            displayMessage(" Failed to get latest version info." +
                " (Status Code : " + response.status.toString() + ")
" +
                "Message : " + response.statusText , "error"
            );
            console.error("Got bad response:", response);
        }else{
            response.json().then(data => {
                updateReleaseNotes(data, current_tag)
            });
        }
    }catch(error){
        console.error(error);
        displayMessage(error.toString(), "error");
    }
}

/**
* Update the page with latest available release and its release notes
*
* @param {Array} data - response from GitHub API, array of last few releases
* @param {String} current_tag - tag Submitty is currently running on
*/
function updateReleaseNotes(data, current_tag){
    //collect all of the release notes submitty is behind into one large string
    //release notes are separated with \n-END-\n to help with parsing later
    let updates = '';
    let releases_behind = 0;
    while (releases_behind < data.length && data[releases_behind].tag_name !== current_tag) {
        updates += `# ${data[releases_behind].tag_name}\n${data[releases_behind].body}\n\n-END-\n`;
        releases_behind++;
    }

    const latest = data[0];

    $.ajax({
        url: buildUrl(['markdown']),
        type: 'POST',
        data: {
            content: addPRLinks(updates),
            csrf_token: csrfToken
        },
        success: function(markdown_data) {
            //Now that we have the basic markdown, we have to do some extra work to add functionality into the HTML
            //since normally the markdown is just plain HTML. We can't make an extension off of the markdown engine
            //because these are highly specific changes needed for just this page (plus JQuery is very helpful).

            //split markdown payload by the custom marker we inserted to separate out releases from each other
            const release_notes = markdown_data.split('-END-');

            //due to the splitting of the single markdown payload, there will always be an extra empty release at the
            //end, so don't include it in the loop
            for(let i = 0; i < release_notes.length-1; i++) {
                //have to wrap the output in

 since we are using 1 ajax call to process multiple payloads
                //that way they all come back at the same time & faster
                $('.content').append(`<div class="box release"><div class="markdown">${injectStyling(release_notes[i])}</div></div>`);
            }

            //mark release sections that have no items with a special class
            $('.section').each( (index, section) => {
                if (!$(section).find('li').length && $(section).text().includes('None')) {
                    $(section).addClass('no-content');
                }
            });

            //add collapse buttons to each release
            $('.version-header').append('<button class="btn btn-default btn-toggle-release" onclick="toggleRelease(this, event)">Collapse</button>');
            //collapse all releases
            $('#toggle-releases').addClass('collapsed');
            $('#toggle-releases').trigger('click');

            //initially expand the latest release if there is only 1
            if ($('.btn-toggle-release').length === 1) {
                $('.btn-toggle-release').eq(0).trigger('click');
            }

            //mark security & sysadmin releases with badges
            $('.release').each( (i, release) => {
                const security_notes = $(release).find('.update-security');
                const sysadmin_notes = $(release).find('.update-sysadmin');
                //highlight the collapsed release if it contains sysadmin or security notes
                if (security_notes.length && !security_notes.hasClass('no-content')) {
                    $('SECURITY').insertAfter($(release).find('.version-header').find('h1'));
                }

                if (sysadmin_notes.length && !sysadmin_notes.hasClass('no-content')) {
                    $('SYSADMIN ACTION').insertAfter($(release).find('.version-header').find('h1'));
                }
            });

            //set info about state of submitty
            $('#tag').html(`Most Recent Version Available: <a href="${latest['html_url']}" target="_blank">${latest['tag_name']}</a>`);
            if (current_tag === latest['tag_name']) {
                $('#text').html('Submitty is up to date!');
            }
            else {
                const important_message = $('.update-important').length > 0 ? `<strong class="important-text"><em>THERE ${$('.update-important').length === 1 ? "IS" : "ARE"} ${$('.update-important').length} SECURITY/SYSADMIN UPDATES</em></strong>` : '';
                $('#text').html(`A new version of Submitty is available

                                Submitty is ${releases_behind} releases behind.

                                ${important_message}`);
            }

            //hide loading text
            $('#loading-text').hide();
        },
        error: function() {
            displayErrorMessage('Something went wrong while trying to render markdown. Please try again.');
        }
    });
}

/**
 * Expands/Collapses all releases collectively. Ex. if the button is in expand mode, all releases
 * will be expanded, even those that are already expanded.
 * @param {HTMLElement} toggleAllButton
 */
function toggleAllReleases(toggleAllButton) {
    //in this case, collapsed class controls what action the button should take
    //  if toggleAllButton has collapsed - will collapse all
    //  if toggleAllButton doesn't have collapsed - will expand all
    $('.btn-toggle-release').each( (index, button) => {
        if($(toggleAllButton).hasClass('collapsed') != $(button).hasClass('collapsed')) {
            $(button).trigger('click');
        }
    });

    //toggle collapsed class and switch button text accordingly
    $(toggleAllButton).toggleClass('collapsed');
    if($(toggleAllButton).hasClass('collapsed')) {
        $(toggleAllButton).html('Collapse All');
    }
    else {
        $(toggleAllButton).html('Expand All');
    }
}

/**
 *
 * @param {HTMLElement} button HTMLElement of the button that was clicked to trigger this function
 * @param {Event} event Event context of the click event that triggered this function
 */
function toggleRelease(button, event) {
    const release = $(button).closest('.box');
    $(button).toggleClass('collapsed');

    //COLLAPSE
    if ($(button).hasClass('collapsed')) {
        //hide everything that is not in the header
        release.find(':not(div.markdown, div.version-header, div.version-header *)').hide();
        const security_notes = release.find('.update-security');
        const sysadmin_notes = release.find('.update-sysadmin');
        //highlight the collapsed release if it contains sysadmin or security notes
        if ((security_notes.length && !security_notes.hasClass('no-content')) || (sysadmin_notes.length && !sysadmin_notes.hasClass('no-content'))) {
            release.addClass('update-important');
            $(button).addClass('btn-primary');
            $(button).removeClass('btn-default');
        }
        $(button).html('Expand');
    }
    //EXPAND
    else {
        //show everything that was hidden (not in the header)
        release.find(':not(div.markdown, div.version-header, div.version-header *)').show();
        //important class is only helpful when release is collapsed
        release.removeClass('update-important');
        $(button).removeClass('btn-primary');
        $(button).addClass('btn-default');
        $(button).html('Collapse');
    }
    //stop bubbling of event
    event.stopPropagation();
}

/**
 * finds all PR numbers in a string and adds markdown formatting so that they will be rendered as links when
 * converted into HTML markdown
 * @param {string} raw_str Pre-markdown processed string
 * @returns {string} String with link formatting in place.
 */
function addPRLinks(raw_str) {
    return raw_str.replace(/#(\d+)/g, '[#$1](https://github.com/Submitty/Submitty/pull/$1)');
}

/**
 * Converts parsed markdown release notes into a more useful form to assist with the other functionalities of the page
 * @param {string} markdown_data HTML string that has been processed and turned into markdown
 * @returns {string} HTML string with helpful wrappers/classes applied
 */
function injectStyling(markdown_data) {
    //add
 after each
 or
 tag
    markdown_data = markdown_data.replace(/<\/ul>|<\/em><\/p>|<\/a><\/p>/g, '$&
');
    //replace normal
 contents with spans with classes
    markdown_data = markdown_data.replace(/
(\[\w+:(\w+)\].+)(?=<\/li>)|
(\[([^\]]+)\].+)(?=<\/li>)/g, (match, p1, p2, p3, p4) => `<li class="release-item"><span class="update-${`${p2 ? p2 : p4}`.toLowerCase()}">${p1 ? p1 : p3}</span>`);
    //add class and wrapper to version headers
    markdown_data = markdown_data.replace(/
.+?<\/h1>/g, `<div class="version-header" onclick="$(this).find('.btn-toggle-release').trigger('click')">$&</div>`);
    //wrap release sections in a

    markdown_data = markdown_data.replace(/
([^<\n\s]+)[^<\n]*?[\s\S]+?(?:
)/g, (match, p1) => `<div class="section update-${p1.toLowerCase()}">${match}</div>`);

    return markdown_data;
}

function removeFilterFromHTML(html) {
    return html.replace(/([\s\S]+?)<\/span>/g, '$1');
}

/**
 * Handles special filter case to save time. All releases/sections/items are shown, all releases are collapsed,
 * and all filter highlighting is removed.
 */
function clearFilter() {
    $('#release-notes-filter').val('');
    $('.release').show();
    $('.section').show();
    $('.release-item').show();

    //remove leftover highlighting from release items
    $('.release-item').each( (i, list_item) => {
        const no_filter = removeFilterFromHTML($(list_item).html());
        $(list_item).html(no_filter);
    });

    //collapse all
    $('#toggle-releases').addClass('collapsed');
    $('#toggle-releases').trigger('click');
}

/**
 * Searches through release notes and only shows releases/items that contain matches
 * to the given filter string
 * @param {string} filter The substring to filter by
 *
 */
function filterReleaseNotes(filter) {
    //handle special case of empty filter to save time
    if(filter === '') {
        clearFilter();
        return;
    }

    $('#no-filter-results').hide();

    let releases_shown = 0;
    const releases = $('.release');
    releases.each( (i, release) => {
        //get all sections on this release
        const sections = $(release).find('.section');
        let found_at_least_one = false;
        sections.each( (j, section) => {
            //initially hide the section
            $(section).hide();
            //skip to next section early if there is no content in this one
            if ($(section).hasClass('no-content')) {
                return;
            }
            //if the filter exists in the section's text somewhere
            if($(section).text().toLowerCase().includes(filter.toLowerCase())){
                //show the section and make sure the release that the section is in is shown
                $(section).show();
                $(release).show();

                //loop through all release items in section
                $(section).find('.release-item').each( (k, release_item) => {
                    //remove old filter highlighting
                    const no_filter = removeFilterFromHTML($(release_item).html());
                    $(release_item).html(no_filter);

                    //initially hide release item
                    $(release_item).hide();

                    //if the filter text can be found in this release item
                    if($(release_item).text().toLowerCase().includes(filter.toLowerCase())){
                        found_at_least_one = true;
                        releases_shown++;
                        //show the release item
                        $(release_item).show();
                        //expand the release this release item belongs to if it isn't already expanded
                        const button = $(release).find('.btn-toggle-release');
                        if(button.hasClass('collapsed')) {
                            button.trigger('click');
                        }
                        //replace all instances of the filter text that is not inside an html tag's attributes
                        //with a span wrapper for styling
                        const matches = $(release_item).html().replace(new RegExp(`${filter}(?=[^<>]+<)`, 'gi'), '$&');
                        $(release_item).html(matches);
                    }
                });
            }
        });

        //if no release items matched with the filter in this release, hide it
        if(!found_at_least_one) {
            $(release).hide();
        }
    });

    if (releases_shown === 0) {
        $('#no-filter-results').show();
    }
}<|endoftext|>


<commit_before>//import //

var _elm_lang$core$Native_Utils = function() {

// COMPARISONS

function eq(rootX, rootY)
{
	var stack = [{ x: rootX, y: rootY }];
	while (stack.length > 0)
	{
		var front = stack.pop();
		var x = front.x;
		var y = front.y;
		if (x === y)
		{
			continue;
		}
		if (typeof x === 'object')
		{
			var c = 0;
			for (var key in x)
			{
				++c;
				if (!(key in y))
				{
					return false;
				}
				if (key === 'ctor')
				{
					continue;
				}
				stack.push({ x: x[key], y: y[key] });
			}
			if ('ctor' in x)
			{
				stack.push({ x: x.ctor, y: y.ctor});
			}
			if (c !== Object.keys(y).length)
			{
				return false;
			}
		}
		else if (typeof x === 'function')
		{
			throw new Error('Equality error: general function equality is ' +
							'undecidable, and therefore, unsupported');
		}
		else
		{
			return false;
		}
	}
	return true;
}

// Code in Generate/JavaScript.hs, Basics.js, and List.js depends on
// the particular integer values assigned to LT, EQ, and GT.

var LT = -1, EQ = 0, GT = 1;

function cmp(x, y)
{
	var ord;
	if (typeof x !== 'object')
	{
		return x === y ? EQ : x < y ? LT : GT;
	}
	else if (x instanceof String)
	{
		var a = x.valueOf();
		var b = y.valueOf();
		return a === b
			? EQ
			: a < b
				? LT
				: GT;
	}
	else if (x.ctor === '::' || x.ctor === '[]')
	{
		while (true)
		{
			if (x.ctor === '[]' && y.ctor === '[]')
			{
				return EQ;
			}
			if (x.ctor !== y.ctor)
			{
				return x.ctor === '[]' ? LT : GT;
			}
			ord = cmp(x._0, y._0);
			if (ord !== EQ)
			{
				return ord;
			}
			x = x._1;
			y = y._1;
		}
	}
	else if (x.ctor.slice(0, 6) === '_Tuple')
	{
		var n = x.ctor.slice(6) - 0;
		var err = 'cannot compare tuples with more than 6 elements.';
		if (n === 0) return EQ;
		if (n >= 1) { ord = cmp(x._0, y._0); if (ord !== EQ) return ord;
		if (n >= 2) { ord = cmp(x._1, y._1); if (ord !== EQ) return ord;
		if (n >= 3) { ord = cmp(x._2, y._2); if (ord !== EQ) return ord;
		if (n >= 4) { ord = cmp(x._3, y._3); if (ord !== EQ) return ord;
		if (n >= 5) { ord = cmp(x._4, y._4); if (ord !== EQ) return ord;
		if (n >= 6) { ord = cmp(x._5, y._5); if (ord !== EQ) return ord;
		if (n >= 7) throw new Error('Comparison error: ' + err); } } } } } }
		return EQ;
	}
	else
	{
		throw new Error('Comparison error: comparison is only defined on ints, ' +
						'floats, times, chars, strings, lists of comparable values, ' +
						'and tuples of comparable values.');
	}
}

// COMMON VALUES

var Tuple0 = {
	ctor: '_Tuple0'
};

function Tuple2(x, y)
{
	return {
		ctor: '_Tuple2',
		_0: x,
		_1: y
	};
}

function chr(c)
{
	return new String(c);
}

// GUID

var count = 0;
function guid(_)
{
	return count++;
}

// RECORDS

function update(oldRecord, updatedFields)
{
	var newRecord = {};
	for (var key in oldRecord)
	{
		var value = (key in updatedFields) ? updatedFields[key] : oldRecord[key];
		newRecord[key] = value;
	}
	return newRecord;
}

//// LIST STUFF ////

var Nil = { ctor: '[]' };

function Cons(hd, tl)
{
	return {
		ctor: '::',
		_0: hd,
		_1: tl
	};
}

function append(xs, ys)
{
	// append Strings
	if (typeof xs === 'string')
	{
		return xs + ys;
	}

	// append Lists
	if (xs.ctor === '[]')
	{
		return ys;
	}
	var root = Cons(xs._0, Nil);
	var curr = root;
	xs = xs._1;
	while (xs.ctor !== '[]')
	{
		curr._1 = Cons(xs._0, Nil);
		xs = xs._1;
		curr = curr._1;
	}
	curr._1 = ys;
	return root;
}

// CRASHES

function crash(moduleName, region)
{
	return function(message) {
		throw new Error(
			'Ran into a `Debug.crash` in module `' + moduleName + '` ' + regionToString(region) + '\n'
			+ 'The message provided by the code author is:\n\n    '
			+ message
		);
	};
}

function crashCase(moduleName, region, value)
{
	return function(message) {
		throw new Error(
			'Ran into a `Debug.crash` in module `' + moduleName + '`\n\n'
			+ 'This was caused by the `case` expression ' + regionToString(region) + '.\n'
			+ 'One of the branches ended with a crash and the following value got through:\n\n    ' + toString(value) + '\n\n'
			+ 'The message provided by the code author is:\n\n    '
			+ message
		);
	};
}

function regionToString(region)
{
	if (region.start.line == region.end.line)
	{
		return 'on line ' + region.start.line;
	}
	return 'between lines ' + region.start.line + ' and ' + region.end.line;
}

// TO STRING

function toString(v)
{
	var type = typeof v;
	if (type === 'function')
	{
		var name = v.func ? v.func.name : v.name;
		return '<function' + (name === '' ? '' : ':') + name + '>';
	}

	if (type === 'boolean')
	{
		return v ? 'True' : 'False';
	}

	if (type === 'number')
	{
		return v + '';
	}

	if (v instanceof String)
	{
		return '\'' + addSlashes(v, true) + '\'';
	}

	if (type === 'string')
	{
		return '"' + addSlashes(v, false) + '"';
	}

	if (v === null)
	{
		return 'null';
	}

	if (type === 'object' && 'ctor' in v)
	{
		var ctorStarter = v.ctor.substring(0, 5);

		if (ctorStarter === '_Tupl')
		{
			var output = [];
			for (var k in v)
			{
				if (k === 'ctor') continue;
				output.push(toString(v[k]));
			}
			return '(' + output.join(',') + ')';
		}

		if (ctorStarter === '_Task')
		{
			return '<task>'
		}

		if (v.ctor === '_Array')
		{
			var list = _elm_lang$core$Array$toList(v);
			return 'Array.fromList ' + toString(list);
		}

		if (v.ctor === '<decoder>')
		{
			return '<decoder>';
		}

		if (v.ctor === '_Process')
		{
			return '<process:' + v.id + '>';
		}

		if (v.ctor === '::')
		{
			var output = '[' + toString(v._0);
			v = v._1;
			while (v.ctor === '::')
			{
				output += ',' + toString(v._0);
				v = v._1;
			}
			return output + ']';
		}

		if (v.ctor === '[]')
		{
			return '[]';
		}

		if (v.ctor === 'RBNode_elm_builtin' || v.ctor === 'RBEmpty_elm_builtin' || v.ctor === 'Set_elm_builtin')
		{
			var name, list;
			if (v.ctor === 'Set_elm_builtin')
			{
				name = 'Set';
				list = A2(
					_elm_lang$core$List$map,
					function(x) {return x._0; },
					_elm_lang$core$Dict$toList(v._0)
				);
			}
			else
			{
				name = 'Dict';
				list = _elm_lang$core$Dict$toList(v);
			}
			return name + '.fromList ' + toString(list);
		}

		var output = '';
		for (var i in v)
		{
			if (i === 'ctor') continue;
			var str = toString(v[i]);
			var c0 = str[0];
			var parenless = c0 === '{' || c0 === '(' || c0 === '<' || c0 === '"' || str.indexOf(' ') < 0;
			output += ' ' + (parenless ? str : '(' + str + ')');
		}
		return v.ctor + output;
	}

	if (type === 'object')
	{
		var output = [];
		for (var k in v)
		{
			output.push(k + ' = ' + toString(v[k]));
		}
		if (output.length === 0)
		{
			return '{}';
		}
		return '{ ' + output.join(', ') + ' }';
	}

	return '<internal structure>';
}

function addSlashes(str, isChar)
{
	var s = str.replace(/\\/g, '\\\\')
			  .replace(/\n/g, '\\n')
			  .replace(/\t/g, '\\t')
			  .replace(/\r/g, '\\r')
			  .replace(/\v/g, '\\v')
			  .replace(/\0/g, '\\0');
	if (isChar)
	{
		return s.replace(/\'/g, '\\\'');
	}
	else
	{
		return s.replace(/\"/g, '\\"');
	}
}

return {
	eq: eq,
	cmp: cmp,
	Tuple0: Tuple0,
	Tuple2: Tuple2,
	chr: chr,
	update: update,
	guid: guid,

	append: F2(append),

	crash: crash,
	crashCase: crashCase,

	toString: toString
};

}();<commit_msg>Improve (==) implementation<commit_after>//import //

var _elm_lang$core$Native_Utils = function() {

// COMPARISONS

function eq(x, y)
{
	var stack = [];
	var isEqual = eqHelp(x, y, 0, stack);
	var pair;
	while (isEqual && (pair = stack.pop()))
	{
		isEqual = eqHelp(pair.x, pair.y, 0, stack);
	}
	return isEqual;
}

function eqHelp(x, y, depth, stack)
{
	if (depth > 100)
	{
		stack.push({ x: x, y: y });
		return true;
	}

	if (x === y)
	{
		return true;
	}

	if (typeof x !== 'object')
	{
		if (typeof x === 'function')
		{
			throw new Error(
				'Trying to use `(==)` on functions. There is no way to know if functions are "the same" in the Elm sense.'
				+ ' Read more about this at http://package.elm-lang.org/packages/elm-lang/core/latest/Basics#=='
				+ ' which describes why it is this way and what the better version will look like.'
			);
		}
		return false;
	}

	if (x === null || y === null)
	{
		return false
	}

	if (!('ctor' in x))
	{
		for (var key in x)
		{
			if (!eqHelp(x[key], y[key], depth + 1, stack))
			{
				return false;
			}
		}
		return true;
	}

	// convert Dicts and Sets to lists
	if (x.ctor === 'RBNode_elm_builtin' || x.ctor === 'RBEmpty_elm_builtin')
	{
		x = _elm_lang$core$Dict$toList(x);
		y = _elm_lang$core$Dict$toList(y);
	}

	// check if lists are equal without recursion
	if (x.ctor === '::')
	{
		var a = x;
		var b = y;
		while (a.ctor === '::' && b.ctor === '::')
		{
			if (!eqHelp(a._0, b._0, depth + 1, stack))
			{
				return false;
			}
			a = a._1;
			b = b._1;
		}
		return a.ctor === b.ctor;
	}

	// check if Arrays are equal
	if (x.ctor === '_Array')
	{
		var xs = _elm_lang$core$Native_Array.toJSArray(x);
		var ys = _elm_lang$core$Native_Array.toJSArray(y);
		if (xs.length !== ys.length)
		{
			return false;
		}
		for (var i = 0; i < xs.length; i++)
		{
			if (!eqHelp(xs[i], ys[i], depth + 1, stack))
			{
				return false;
			}
		}
		return true;
	}

	if (x.ctor !== y.ctor)
	{
		return false;
	}

	for (var key in x)
	{
		if (!eqHelp(x[key], y[key], depth + 1, stack))
		{
			return false;
		}
	}
	return true;
}

// Code in Generate/JavaScript.hs, Basics.js, and List.js depends on
// the particular integer values assigned to LT, EQ, and GT.

var LT = -1, EQ = 0, GT = 1;

function cmp(x, y)
{
	var ord;
	if (typeof x !== 'object')
	{
		return x === y ? EQ : x < y ? LT : GT;
	}
	else if (x instanceof String)
	{
		var a = x.valueOf();
		var b = y.valueOf();
		return a === b
			? EQ
			: a < b
				? LT
				: GT;
	}
	else if (x.ctor === '::' || x.ctor === '[]')
	{
		while (true)
		{
			if (x.ctor === '[]' && y.ctor === '[]')
			{
				return EQ;
			}
			if (x.ctor !== y.ctor)
			{
				return x.ctor === '[]' ? LT : GT;
			}
			ord = cmp(x._0, y._0);
			if (ord !== EQ)
			{
				return ord;
			}
			x = x._1;
			y = y._1;
		}
	}
	else if (x.ctor.slice(0, 6) === '_Tuple')
	{
		var n = x.ctor.slice(6) - 0;
		var err = 'cannot compare tuples with more than 6 elements.';
		if (n === 0) return EQ;
		if (n >= 1) { ord = cmp(x._0, y._0); if (ord !== EQ) return ord;
		if (n >= 2) { ord = cmp(x._1, y._1); if (ord !== EQ) return ord;
		if (n >= 3) { ord = cmp(x._2, y._2); if (ord !== EQ) return ord;
		if (n >= 4) { ord = cmp(x._3, y._3); if (ord !== EQ) return ord;
		if (n >= 5) { ord = cmp(x._4, y._4); if (ord !== EQ) return ord;
		if (n >= 6) { ord = cmp(x._5, y._5); if (ord !== EQ) return ord;
		if (n >= 7) throw new Error('Comparison error: ' + err); } } } } } }
		return EQ;
	}
	else
	{
		throw new Error('Comparison error: comparison is only defined on ints, ' +
						'floats, times, chars, strings, lists of comparable values, ' +
						'and tuples of comparable values.');
	}
}

// COMMON VALUES

var Tuple0 = {
	ctor: '_Tuple0'
};

function Tuple2(x, y)
{
	return {
		ctor: '_Tuple2',
		_0: x,
		_1: y
	};
}

function chr(c)
{
	return new String(c);
}

// GUID

var count = 0;
function guid(_)
{
	return count++;
}

// RECORDS

function update(oldRecord, updatedFields)
{
	var newRecord = {};
	for (var key in oldRecord)
	{
		var value = (key in updatedFields) ? updatedFields[key] : oldRecord[key];
		newRecord[key] = value;
	}
	return newRecord;
}

//// LIST STUFF ////

var Nil = { ctor: '[]' };

function Cons(hd, tl)
{
	return {
		ctor: '::',
		_0: hd,
		_1: tl
	};
}

function append(xs, ys)
{
	// append Strings
	if (typeof xs === 'string')
	{
		return xs + ys;
	}

	// append Lists
	if (xs.ctor === '[]')
	{
		return ys;
	}
	var root = Cons(xs._0, Nil);
	var curr = root;
	xs = xs._1;
	while (xs.ctor !== '[]')
	{
		curr._1 = Cons(xs._0, Nil);
		xs = xs._1;
		curr = curr._1;
	}
	curr._1 = ys;
	return root;
}

// CRASHES

function crash(moduleName, region)
{
	return function(message) {
		throw new Error(
			'Ran into a `Debug.crash` in module `' + moduleName + '` ' + regionToString(region) + '\n'
			+ 'The message provided by the code author is:\n\n    '
			+ message
		);
	};
}

function crashCase(moduleName, region, value)
{
	return function(message) {
		throw new Error(
			'Ran into a `Debug.crash` in module `' + moduleName + '`\n\n'
			+ 'This was caused by the `case` expression ' + regionToString(region) + '.\n'
			+ 'One of the branches ended with a crash and the following value got through:\n\n    ' + toString(value) + '\n\n'
			+ 'The message provided by the code author is:\n\n    '
			+ message
		);
	};
}

function regionToString(region)
{
	if (region.start.line == region.end.line)
	{
		return 'on line ' + region.start.line;
	}
	return 'between lines ' + region.start.line + ' and ' + region.end.line;
}

// TO STRING

function toString(v)
{
	var type = typeof v;
	if (type === 'function')
	{
		var name = v.func ? v.func.name : v.name;
		return '<function' + (name === '' ? '' : ':') + name + '>';
	}

	if (type === 'boolean')
	{
		return v ? 'True' : 'False';
	}

	if (type === 'number')
	{
		return v + '';
	}

	if (v instanceof String)
	{
		return '\'' + addSlashes(v, true) + '\'';
	}

	if (type === 'string')
	{
		return '"' + addSlashes(v, false) + '"';
	}

	if (v === null)
	{
		return 'null';
	}

	if (type === 'object' && 'ctor' in v)
	{
		var ctorStarter = v.ctor.substring(0, 5);

		if (ctorStarter === '_Tupl')
		{
			var output = [];
			for (var k in v)
			{
				if (k === 'ctor') continue;
				output.push(toString(v[k]));
			}
			return '(' + output.join(',') + ')';
		}

		if (ctorStarter === '_Task')
		{
			return '<task>'
		}

		if (v.ctor === '_Array')
		{
			var list = _elm_lang$core$Array$toList(v);
			return 'Array.fromList ' + toString(list);
		}

		if (v.ctor === '<decoder>')
		{
			return '<decoder>';
		}

		if (v.ctor === '_Process')
		{
			return '<process:' + v.id + '>';
		}

		if (v.ctor === '::')
		{
			var output = '[' + toString(v._0);
			v = v._1;
			while (v.ctor === '::')
			{
				output += ',' + toString(v._0);
				v = v._1;
			}
			return output + ']';
		}

		if (v.ctor === '[]')
		{
			return '[]';
		}

		if (v.ctor === 'RBNode_elm_builtin' || v.ctor === 'RBEmpty_elm_builtin' || v.ctor === 'Set_elm_builtin')
		{
			var name, list;
			if (v.ctor === 'Set_elm_builtin')
			{
				name = 'Set';
				list = A2(
					_elm_lang$core$List$map,
					function(x) {return x._0; },
					_elm_lang$core$Dict$toList(v._0)
				);
			}
			else
			{
				name = 'Dict';
				list = _elm_lang$core$Dict$toList(v);
			}
			return name + '.fromList ' + toString(list);
		}

		var output = '';
		for (var i in v)
		{
			if (i === 'ctor') continue;
			var str = toString(v[i]);
			var c0 = str[0];
			var parenless = c0 === '{' || c0 === '(' || c0 === '<' || c0 === '"' || str.indexOf(' ') < 0;
			output += ' ' + (parenless ? str : '(' + str + ')');
		}
		return v.ctor + output;
	}

	if (type === 'object')
	{
		var output = [];
		for (var k in v)
		{
			output.push(k + ' = ' + toString(v[k]));
		}
		if (output.length === 0)
		{
			return '{}';
		}
		return '{ ' + output.join(', ') + ' }';
	}

	return '<internal structure>';
}

function addSlashes(str, isChar)
{
	var s = str.replace(/\\/g, '\\\\')
			  .replace(/\n/g, '\\n')
			  .replace(/\t/g, '\\t')
			  .replace(/\r/g, '\\r')
			  .replace(/\v/g, '\\v')
			  .replace(/\0/g, '\\0');
	if (isChar)
	{
		return s.replace(/\'/g, '\\\'');
	}
	else
	{
		return s.replace(/\"/g, '\\"');
	}
}

return {
	eq: eq,
	cmp: cmp,
	Tuple0: Tuple0,
	Tuple2: Tuple2,
	chr: chr,
	update: update,
	guid: guid,

	append: F2(append),

	crash: crash,
	crashCase: crashCase,

	toString: toString
};

}();<|endoftext|>


<commit_before>import React from 'react';
import SPELLS from 'common/SPELLS';
import SpellLink from 'common/SpellLink';
import SpellIcon from 'common/SpellIcon';
import Wrapper from 'common/Wrapper';
import { formatMilliseconds, formatPercentage } from 'common/format';
import StatisticBox, { STATISTIC_ORDER } from 'Main/StatisticBox';
import Combatants from 'Parser/Core/Modules/Combatants';
import Analyzer from 'Parser/Core/Analyzer';
import AbilityTracker from 'Parser/Core/Modules/AbilityTracker';
import EnemyInstances, { encodeTargetString } from 'Parser/Core/Modules/EnemyInstances';

const debug = false;

const GUARANTEE_CRIT_SPELLS = [
  SPELLS.FIRE_BLAST.id,
  SPELLS.PHOENIXS_FLAMES.id,
];

class HeatingUp extends Analyzer {
  static dependencies = {
    combatants: Combatants,
    abilityTracker: AbilityTracker,
    enemies: EnemyInstances,
  };

  fireBlastWithoutHeatingUp = 0;
  phoenixFlamesWithoutHeatingUp = 0;
  fireBlastWithHotStreak = 0;
  phoenixFlamesWithHotStreak = 0;
  targetId = 0;

  on_byPlayer_cast(event) {
    const spellId = event.ability.guid;
    if (spellId !== SPELLS.PHOENIXS_FLAMES.id) {
      return;
    }
    this.targetId = encodeTargetString(event.targetID, event.targetInstance);
  }

  on_byPlayer_damage(event) {
    const spellId = event.ability.guid;
    const damageTarget = encodeTargetString(event.targetID, event.targetInstance);
    if (!GUARANTEE_CRIT_SPELLS.includes(spellId) || (spellId === SPELLS.PHOENIXS_FLAMES.id && (this.targetId !== damageTarget))) {
      return;
    }
    const hasHeatingUp = this.combatants.selected.hasBuff(SPELLS.HEATING_UP.id);
    const hasHotStreak = this.combatants.selected.hasBuff(SPELLS.HOT_STREAK.id);

    if (spellId === SPELLS.FIRE_BLAST.id) {
      if (hasHotStreak) {
        this.fireBlastWithHotStreak += 1;
        debug && console.log("Fire Blast with Hot Streak @ " + formatMilliseconds(event.timestamp - this.owner.fight.start_time));
      } else if (!hasHeatingUp) {
        this.fireBlastWithoutHeatingUp += 1;
        debug && console.log("Fire Blast without Heating Up @ " + formatMilliseconds(event.timestamp - this.owner.fight.start_time));
      }
    } else if (spellId === SPELLS.PHOENIXS_FLAMES.id) {
        if (hasHotStreak) {
          this.phoenixFlamesWithHotStreak += 1;
          debug && console.log("Phoenix Flames with Hot Streak @ " + formatMilliseconds(event.timestamp - this.owner.fight.start_time));
        } else if (!hasHeatingUp) {
          this.phoenixFlamesWithoutHeatingUp += 1;
          debug && console.log("Phoenix Flames without Heating Up @ " + formatMilliseconds(event.timestamp - this.owner.fight.start_time));
        }
    }
  }

  get fireBlastWasted() {
    return this.fireBlastWithoutHeatingUp + this.fireBlastWithHotStreak;
  }

  get phoenixFlamesWasted() {
    return this.phoenixFlamesWithoutHeatingUp + this.phoenixFlamesWithHotStreak;
  }

  get totalWasted() {
    return this.fireBlastWasted + this.phoenixFlamesWasted;
  }

  get fireBlastUtil() {
    return 1 - this.fireBlastMissedPercent;
  }

  get phoenixFlamesUtil() {
    return 1 - this.phoenixFlamesMissedPercent;
  }

  get fireBlastMissedPercent() {
    return this.fireBlastWasted / this.abilityTracker.getAbility(SPELLS.FIRE_BLAST.id).casts;
  }

  get phoenixFlamesMissedPercent() {
    return this.phoenixFlamesWasted / this.abilityTracker.getAbility(SPELLS.PHOENIXS_FLAMES.id).casts;
  }

  get fireBlastUtilSuggestionThresholds() {
    return {
      actual: this.fireBlastUtil,
      isLessThan: {
        minor: 0.95,
        average: 0.85,
        major: 0.70,
      },
      style: 'percentage',
    };
  }

  get phoenixFlamesUtilSuggestionThresholds() {
    return {
      actual: this.phoenixFlamesUtil,
      isLessThan: {
        minor: 0.90,
        average: 0.80,
        major: 0.70,
      },
      style: 'percentage',
    };
  }

  suggestions(when) {
		when(this.fireBlastMissedPercent).isLessThan(this.fireBlastUtilSuggestionThresholds.isLessThan.minor)
			.addSuggestion((suggest, actual, recommended) => {
				return suggest(<Wrapper>You cast <SpellLink id={SPELLS.FIRE_BLAST.id} /> {this.fireBlastWithHotStreak} times while <SpellLink id={SPELLS.HOT_STREAK.id}/> was active and {this.fireBlastWithoutHeatingUp} times while you didnt have <SpellLink id={SPELLS.HEATING_UP.id}/>. Make sure that you are only using Fire Blast to convert Heating Up into Hot Streak.</Wrapper>)
					.icon(SPELLS.FIRE_BLAST.icon)
					.actual(`${formatPercentage(this.fireBlastMissedPercent)}% missed`)
					.recommended(`<${formatPercentage(recommended)}% is recommended`)
					.regular(this.fireBlastUtilSuggestionThresholds.isLessThan.average).major(this.fireBlastUtilSuggestionThresholds.isLessThan.major);
			});
    when(this.phoenixFlamesMissedPercent).isLessThan(this.phoenixFlamesUtilSuggestionThresholds.isLessThan.minor)
			.addSuggestion((suggest, actual, recommended) => {
				return suggest(<Wrapper>You cast <SpellLink id={SPELLS.PHOENIXS_FLAMES.id} /> {this.phoenixFlamesWithHotStreak} times while <SpellLink id={SPELLS.HOT_STREAK.id}/> was active and {this.phoenixFlamesWithoutHeatingUp} times while you didnt have <SpellLink id={SPELLS.HEATING_UP.id}/>. While ideally you should only be using these to convert Heating Up into Hot Streak, there are some minor circumstances where it is acceptable (i.e. If you are about to cap on Phoenixs Flames charges or when used alongside <SpellLink id={SPELLS.FIREBALL.id}/> to bait Heating Up or Hot Streak just before <SpellLink id={SPELLS.COMBUSTION.id}/>.</Wrapper>)
					.icon(SPELLS.PHOENIXS_FLAMES.icon)
					.actual(`${formatPercentage(this.phoenixFlamesMissedPercent)}% missed`)
					.recommended(`<${formatPercentage(recommended)}% is recommended`)
					.regular(this.phoenixFlamesUtilSuggestionThresholds.isLessThan.average).major(this.phoenixFlamesUtilSuggestionThresholds.isLessThan.major);
			});
	}

  statistic() {
    return (
      <StatisticBox
        icon={<SpellIcon id={SPELLS.HEATING_UP.id} />}
        value={(

            <SpellIcon
              id={SPELLS.FIRE_BLAST.id}
              style={{
                height: '1.2em',
                marginBottom: '.15em',
              }}
            />
            {' '}{formatPercentage(this.fireBlastUtil, 0)}{' %'}

            <SpellIcon
              id={SPELLS.PHOENIXS_FLAMES.id}
              style={{
                height: '1.2em',
                marginBottom: '.15em',
              }}
            />
            {' '}{formatPercentage(this.phoenixFlamesUtil, 0)}{' %'}

        )}
        label="Heating Up Utilization"
        tooltip={`Spells that are guaranteed to crit like Fire Blast and Phoenix's Flames should only be used to convert Heating Up to Hot Streak. While there are minor exceptions to this (like if you are about to cap on Phoenixs Flames charges or using Fireball & Phoenixs Flames to bait Heating Up/Hot Streak just before Combustion), the goal should be to waste as few of these as possible.

Fireblast Used with no procs: ${this.fireBlastWithoutHeatingUp}

Fireblast used during Hot Streak: ${this.fireBlastWithHotStreak}

Phoenix's Flames used with no procs: ${this.phoenixFlamesWithoutHeatingUp}

Phoenix's Flames used during Hot Streak: ${this.phoenixFlamesWithHotStreak}

`}
      />
    );
  }
  statisticOrder = STATISTIC_ORDER.CORE(14);

}

export default HeatingUp;
<commit_msg>redo suggestions<commit_after>import React from 'react';
import SPELLS from 'common/SPELLS';
import SpellLink from 'common/SpellLink';
import SpellIcon from 'common/SpellIcon';
import Wrapper from 'common/Wrapper';
import { formatMilliseconds, formatPercentage } from 'common/format';
import StatisticBox, { STATISTIC_ORDER } from 'Main/StatisticBox';
import Combatants from 'Parser/Core/Modules/Combatants';
import Analyzer from 'Parser/Core/Analyzer';
import AbilityTracker from 'Parser/Core/Modules/AbilityTracker';
import EnemyInstances, { encodeTargetString } from 'Parser/Core/Modules/EnemyInstances';

const debug = false;

const GUARANTEE_CRIT_SPELLS = [
  SPELLS.FIRE_BLAST.id,
  SPELLS.PHOENIXS_FLAMES.id,
];

class HeatingUp extends Analyzer {
  static dependencies = {
    combatants: Combatants,
    abilityTracker: AbilityTracker,
    enemies: EnemyInstances,
  };

  fireBlastWithoutHeatingUp = 0;
  phoenixFlamesWithoutHeatingUp = 0;
  fireBlastWithHotStreak = 0;
  phoenixFlamesWithHotStreak = 0;
  targetId = 0;

  on_byPlayer_cast(event) {
    const spellId = event.ability.guid;
    if (spellId !== SPELLS.PHOENIXS_FLAMES.id) {
      return;
    }
    this.targetId = encodeTargetString(event.targetID, event.targetInstance);
  }

  on_byPlayer_damage(event) {
    const spellId = event.ability.guid;
    const damageTarget = encodeTargetString(event.targetID, event.targetInstance);
    if (!GUARANTEE_CRIT_SPELLS.includes(spellId) || (spellId === SPELLS.PHOENIXS_FLAMES.id && (this.targetId !== damageTarget))) {
      return;
    }
    const hasHeatingUp = this.combatants.selected.hasBuff(SPELLS.HEATING_UP.id);
    const hasHotStreak = this.combatants.selected.hasBuff(SPELLS.HOT_STREAK.id);

    if (spellId === SPELLS.FIRE_BLAST.id) {
      if (hasHotStreak) {
        this.fireBlastWithHotStreak += 1;
        debug && console.log("Fire Blast with Hot Streak @ " + formatMilliseconds(event.timestamp - this.owner.fight.start_time));
      } else if (!hasHeatingUp) {
        this.fireBlastWithoutHeatingUp += 1;
        debug && console.log("Fire Blast without Heating Up @ " + formatMilliseconds(event.timestamp - this.owner.fight.start_time));
      }
    } else if (spellId === SPELLS.PHOENIXS_FLAMES.id) {
        if (hasHotStreak) {
          this.phoenixFlamesWithHotStreak += 1;
          debug && console.log("Phoenix Flames with Hot Streak @ " + formatMilliseconds(event.timestamp - this.owner.fight.start_time));
        } else if (!hasHeatingUp) {
          this.phoenixFlamesWithoutHeatingUp += 1;
          debug && console.log("Phoenix Flames without Heating Up @ " + formatMilliseconds(event.timestamp - this.owner.fight.start_time));
        }
    }
  }

  get fireBlastWasted() {
    return this.fireBlastWithoutHeatingUp + this.fireBlastWithHotStreak;
  }

  get phoenixFlamesWasted() {
    return this.phoenixFlamesWithoutHeatingUp + this.phoenixFlamesWithHotStreak;
  }

  get totalWasted() {
    return this.fireBlastWasted + this.phoenixFlamesWasted;
  }

  get fireBlastUtil() {
    return 1 - this.fireBlastMissedPercent;
  }

  get phoenixFlamesUtil() {
    return 1 - this.phoenixFlamesMissedPercent;
  }

  get fireBlastMissedPercent() {
    return this.fireBlastWasted / this.abilityTracker.getAbility(SPELLS.FIRE_BLAST.id).casts;
  }

  get phoenixFlamesMissedPercent() {
    return this.phoenixFlamesWasted / this.abilityTracker.getAbility(SPELLS.PHOENIXS_FLAMES.id).casts;
  }

  get fireBlastUtilSuggestionThresholds() {
    return {
      actual: this.fireBlastUtil,
      isLessThan: {
        minor: 0.95,
        average: 0.85,
        major: 0.70,
      },
      style: 'percentage',
    };
  }

  get phoenixFlamesUtilSuggestionThresholds() {
    return {
      actual: this.phoenixFlamesUtil,
      isLessThan: {
        minor: 0.90,
        average: 0.80,
        major: 0.70,
      },
      style: 'percentage',
    };
  }

  suggestions(when) {
		when(this.fireBlastUtilSuggestionThresholds)
			.addSuggestion((suggest, actual, recommended) => {
				return suggest(<Wrapper>You cast <SpellLink id={SPELLS.FIRE_BLAST.id} /> {this.fireBlastWithHotStreak} times while <SpellLink id={SPELLS.HOT_STREAK.id}/> was active and {this.fireBlastWithoutHeatingUp} times while you didnt have <SpellLink id={SPELLS.HEATING_UP.id}/>. Make sure that you are only using Fire Blast to convert Heating Up into Hot Streak.</Wrapper>)
					.icon(SPELLS.FIRE_BLAST.icon)
					.actual(`${formatPercentage(this.fireBlastUtil)}% Utilization`)
					.recommended(`<${formatPercentage(recommended)}% is recommended`);
			});
    when(this.phoenixFlamesUtilSuggestionThresholds)
			.addSuggestion((suggest, actual, recommended) => {
				return suggest(<Wrapper>You cast <SpellLink id={SPELLS.PHOENIXS_FLAMES.id} /> {this.phoenixFlamesWithHotStreak} times while <SpellLink id={SPELLS.HOT_STREAK.id}/> was active and {this.phoenixFlamesWithoutHeatingUp} times while you didnt have <SpellLink id={SPELLS.HEATING_UP.id}/>. While ideally you should only be using these to convert Heating Up into Hot Streak, there are some minor circumstances where it is acceptable (i.e. If you are about to cap on Phoenixs Flames charges or when used alongside <SpellLink id={SPELLS.FIREBALL.id}/> to bait Heating Up or Hot Streak just before <SpellLink id={SPELLS.COMBUSTION.id}/>.</Wrapper>)
					.icon(SPELLS.PHOENIXS_FLAMES.icon)
					.actual(`${formatPercentage(this.phoenixFlamesUtil)}% Utilization`)
					.recommended(`<${formatPercentage(recommended)}% is recommended`);
			});
	}

  statistic() {
    return (
      <StatisticBox
        icon={<SpellIcon id={SPELLS.HEATING_UP.id} />}
        value={(

            <SpellIcon
              id={SPELLS.FIRE_BLAST.id}
              style={{
                height: '1.2em',
                marginBottom: '.15em',
              }}
            />
            {' '}{formatPercentage(this.fireBlastUtil, 0)}{' %'}

            <SpellIcon
              id={SPELLS.PHOENIXS_FLAMES.id}
              style={{
                height: '1.2em',
                marginBottom: '.15em',
              }}
            />
            {' '}{formatPercentage(this.phoenixFlamesUtil, 0)}{' %'}

        )}
        label="Heating Up Utilization"
        tooltip={`Spells that are guaranteed to crit like Fire Blast and Phoenix's Flames should only be used to convert Heating Up to Hot Streak. While there are minor exceptions to this (like if you are about to cap on Phoenixs Flames charges or using Fireball & Phoenixs Flames to bait Heating Up/Hot Streak just before Combustion), the goal should be to waste as few of these as possible.

Fireblast Used with no procs: ${this.fireBlastWithoutHeatingUp}

Fireblast used during Hot Streak: ${this.fireBlastWithHotStreak}

Phoenix's Flames used with no procs: ${this.phoenixFlamesWithoutHeatingUp}

Phoenix's Flames used during Hot Streak: ${this.phoenixFlamesWithHotStreak}

`}
      />
    );
  }
  statisticOrder = STATISTIC_ORDER.CORE(14);

}

export default HeatingUp;
<|endoftext|>


<commit_before>/*
 * Paper.js - The Swiss Army Knife of Vector Graphics Scripting.
 * http://paperjs.org/
 *
 * Copyright (c) 2011 - 2013, Juerg Lehni & Jonathan Puckey
 * http://lehni.org/ & http://jonathanpuckey.com/
 *
 * Distributed under the MIT license. See LICENSE file for details.
 *
 * All rights reserved.
 */

/**
 * @name CurveLocation
 *
 * @class CurveLocation objects describe a location on {@link Curve}
 * objects, as defined by the curve {@link #parameter}, a value between
 * {@code 0} (beginning of the curve) and {@code 1} (end of the curve). If
 * the curve is part of a {@link Path} item, its {@link #index} inside the
 * {@link Path#curves} array is also provided.
 *
 * The class is in use in many places, such as
 * {@link Path#getLocationAt(offset, isParameter)},
 * {@link Path#getLocationOf(point)},
 * {@link Path#getNearestLocation(point),
 * {@link PathItem#getIntersections(path)},
 * etc.
 */
var CurveLocation = this.CurveLocation = Base.extend(/** @lends CurveLocation# */{
	// DOCS: CurveLocation class description: add these back when the  mentioned
	// functioned have been added: {@link Path#split(location)}
	/**
	 * Creates a new CurveLocation object.
	 *
	 * @param {Curve} curve
	 * @param {Number} parameter
	 * @param {Point} point
	 */
	initialize: function(curve, parameter, point, distance) {
		this._curve = curve;
		// Also store references to segment1 and segment2, in case path
		// splitting / dividing is going to happen, in which case the segments
		// can be used to determine the new curves, see #getCurve(true)
		this._segment1 = curve._segment1;
		this._segment2 = curve._segment2;
		this._parameter = parameter;
		this._point = point;
		this._distance = distance;
	},

	/**
	 * The segment of the curve which is closer to the described location.
	 *
	 * @type Segment
	 * @bean
	 */
	getSegment: function() {
		if (!this._segment) {
			var curve = this.getCurve(),
				parameter = this.getParameter();
			if (parameter == 0) {
				this._segment = curve._segment1;
			} else if (parameter == 1) {
				this._segment = curve._segment2;
			} else if (parameter == null) {
				return null;
			} else {
				// Determine the closest segment by comparing curve lengths
				this._segment = curve.getLength(0, parameter)
					< curve.getLength(parameter, 1)
						? curve._segment1
						: curve._segment2;
			}
		}
		return this._segment;
	},

	/**
	 * The curve by which the location is defined.
	 *
	 * @type Curve
	 * @bean
	 */
	getCurve: function(/* uncached */) {
		if (!this._curve || arguments[0]) {
			// If we're asked to get the curve uncached, access current curve
			// objects through segment1 / segment2. Since path splitting or
			// dividing might have happened in the meantime, try segment1's
			// curve, and see if _point lies on it still, otherwise assume it's
			// the curve before segment2.
			this._curve = this._segment1.getCurve();
			if (this._curve.getParameterOf(this._point) == null)
				this._curve = this._segment2.getPrevious().getCurve();
		}
		return this._curve;
	},

	/**
	 * The path this curve belongs to, if any.
	 *
	 * @type Item
	 * @bean
	 */
	getPath: function() {
		var curve = this.getCurve();
		return curve && curve._path;
	},

	/**
	 * The index of the curve within the {@link Path#curves} list, if the
	 * curve is part of a {@link Path} item.
	 *
	 * @type Index
	 * @bean
	 */
	getIndex: function() {
		var curve = this.getCurve();
		return curve && curve.getIndex();
	},

	/**
	 * The length of the path from its beginning up to the location described
	 * by this object.
	 *
	 * @type Number
	 * @bean
	 */
	getOffset: function() {
		var path = this.getPath();
		return path && path._getOffset(this);
	},

	/**
	 * The length of the curve from its beginning up to the location described
	 * by this object.
	 *
	 * @type Number
	 * @bean
	 */
	getCurveOffset: function() {
		var curve = this.getCurve(),
			parameter = this.getParameter();
		return parameter != null && curve && curve.getLength(0, parameter);
	},

	/**
	 * The curve parameter, as used by various bezier curve calculations. It is
	 * value between {@code 0} (beginning of the curve) and {@code 1} (end of
	 * the curve).
	 *
	 * @type Number
	 * @bean
	 */
	getParameter: function(/* uncached */) {
		if ((this._parameter == null || arguments[0]) && this._point) {
			var curve = this.getCurve(arguments[0] && this._point);
			this._parameter = curve && curve.getParameterOf(this._point);
		}
		return this._parameter;
	},

	/**
	 * The point which is defined by the {@link #curve} and
	 * {@link #parameter}.
	 *
	 * @type Point
	 * @bean
	 */
	getPoint: function() {
		if (!this._point && this._parameter != null) {
			var curve = this.getCurve();
			this._point = curve && curve.getPointAt(this._parameter, true);
		}
		return this._point;
	},

	/**
	 * The tangential vector to the {@link #curve} at the given location.
	 *
	 * @type Point
	 * @bean
	 */
	getTangent: function() {
		var parameter = this.getParameter(),
			curve = this.getCurve();
		return parameter != null && curve && curve.getTangentAt(parameter, true);
	},

	/**
	 * The normal vector to the {@link #curve} at the given location.
	 *
	 * @type Point
	 * @bean
	 */
	getNormal: function() {
		var parameter = this.getParameter(),
			curve = this.getCurve();
		return parameter != null && curve && curve.getNormalAt(parameter, true);
	},

	/**
	 * The distance from the queried point to the returned location.
	 *
	 * @type Number
	 * @bean
	 */
	getDistance: function() {
		return this._distance;
	},

	divide: function() {
		var curve = this.getCurve(true);
		return curve && curve.divide(this.getParameter(true));
	},

	split: function() {
		var curve = this.getCurve(true);
		return curve && curve.split(this.getParameter(true));
	},

	/**
	 * @return {String} A string representation of the curve location.
	 */
	toString: function() {
		var parts = [],
			point = this.getPoint(),
			f = Formatter.instance;
		if (point)
			parts.push('point: ' + point);
		var index = this.getIndex();
		if (index != null)
			parts.push('index: ' + index);
		var parameter = this.getParameter();
		if (parameter != null)
			parts.push('parameter: ' + f.number(parameter));
		if (this._distance != null)
			parts.push('distance: ' + f.number(this._distance));
		return '{ ' + parts.join(', ') + ' }';
	}
});
<commit_msg>Add unique ids to CurveLocation.<commit_after>/*
 * Paper.js - The Swiss Army Knife of Vector Graphics Scripting.
 * http://paperjs.org/
 *
 * Copyright (c) 2011 - 2013, Juerg Lehni & Jonathan Puckey
 * http://lehni.org/ & http://jonathanpuckey.com/
 *
 * Distributed under the MIT license. See LICENSE file for details.
 *
 * All rights reserved.
 */

/**
 * @name CurveLocation
 *
 * @class CurveLocation objects describe a location on {@link Curve}
 * objects, as defined by the curve {@link #parameter}, a value between
 * {@code 0} (beginning of the curve) and {@code 1} (end of the curve). If
 * the curve is part of a {@link Path} item, its {@link #index} inside the
 * {@link Path#curves} array is also provided.
 *
 * The class is in use in many places, such as
 * {@link Path#getLocationAt(offset, isParameter)},
 * {@link Path#getLocationOf(point)},
 * {@link Path#getNearestLocation(point),
 * {@link PathItem#getIntersections(path)},
 * etc.
 */
var CurveLocation = this.CurveLocation = Base.extend(/** @lends CurveLocation# */{
	// DOCS: CurveLocation class description: add these back when the  mentioned
	// functioned have been added: {@link Path#split(location)}
	/**
	 * Creates a new CurveLocation object.
	 *
	 * @param {Curve} curve
	 * @param {Number} parameter
	 * @param {Point} point
	 */
	initialize: function(curve, parameter, point, distance) {
		// Define this CurveLocation's unique id.
		this._id = CurveLocation._id = (CurveLocation._id || 0) + 1;
		this._curve = curve;
		// Also store references to segment1 and segment2, in case path
		// splitting / dividing is going to happen, in which case the segments
		// can be used to determine the new curves, see #getCurve(true)
		this._segment1 = curve._segment1;
		this._segment2 = curve._segment2;
		this._parameter = parameter;
		this._point = point;
		this._distance = distance;
	},

	/**
	 * The segment of the curve which is closer to the described location.
	 *
	 * @type Segment
	 * @bean
	 */
	getSegment: function() {
		if (!this._segment) {
			var curve = this.getCurve(),
				parameter = this.getParameter();
			if (parameter == 0) {
				this._segment = curve._segment1;
			} else if (parameter == 1) {
				this._segment = curve._segment2;
			} else if (parameter == null) {
				return null;
			} else {
				// Determine the closest segment by comparing curve lengths
				this._segment = curve.getLength(0, parameter)
					< curve.getLength(parameter, 1)
						? curve._segment1
						: curve._segment2;
			}
		}
		return this._segment;
	},

	/**
	 * The curve by which the location is defined.
	 *
	 * @type Curve
	 * @bean
	 */
	getCurve: function(/* uncached */) {
		if (!this._curve || arguments[0]) {
			// If we're asked to get the curve uncached, access current curve
			// objects through segment1 / segment2. Since path splitting or
			// dividing might have happened in the meantime, try segment1's
			// curve, and see if _point lies on it still, otherwise assume it's
			// the curve before segment2.
			this._curve = this._segment1.getCurve();
			if (this._curve.getParameterOf(this._point) == null)
				this._curve = this._segment2.getPrevious().getCurve();
		}
		return this._curve;
	},

	/**
	 * The path this curve belongs to, if any.
	 *
	 * @type Item
	 * @bean
	 */
	getPath: function() {
		var curve = this.getCurve();
		return curve && curve._path;
	},

	/**
	 * The index of the curve within the {@link Path#curves} list, if the
	 * curve is part of a {@link Path} item.
	 *
	 * @type Index
	 * @bean
	 */
	getIndex: function() {
		var curve = this.getCurve();
		return curve && curve.getIndex();
	},

	/**
	 * The length of the path from its beginning up to the location described
	 * by this object.
	 *
	 * @type Number
	 * @bean
	 */
	getOffset: function() {
		var path = this.getPath();
		return path && path._getOffset(this);
	},

	/**
	 * The length of the curve from its beginning up to the location described
	 * by this object.
	 *
	 * @type Number
	 * @bean
	 */
	getCurveOffset: function() {
		var curve = this.getCurve(),
			parameter = this.getParameter();
		return parameter != null && curve && curve.getLength(0, parameter);
	},

	/**
	 * The curve parameter, as used by various bezier curve calculations. It is
	 * value between {@code 0} (beginning of the curve) and {@code 1} (end of
	 * the curve).
	 *
	 * @type Number
	 * @bean
	 */
	getParameter: function(/* uncached */) {
		if ((this._parameter == null || arguments[0]) && this._point) {
			var curve = this.getCurve(arguments[0] && this._point);
			this._parameter = curve && curve.getParameterOf(this._point);
		}
		return this._parameter;
	},

	/**
	 * The point which is defined by the {@link #curve} and
	 * {@link #parameter}.
	 *
	 * @type Point
	 * @bean
	 */
	getPoint: function() {
		if (!this._point && this._parameter != null) {
			var curve = this.getCurve();
			this._point = curve && curve.getPointAt(this._parameter, true);
		}
		return this._point;
	},

	/**
	 * The tangential vector to the {@link #curve} at the given location.
	 *
	 * @type Point
	 * @bean
	 */
	getTangent: function() {
		var parameter = this.getParameter(),
			curve = this.getCurve();
		return parameter != null && curve && curve.getTangentAt(parameter, true);
	},

	/**
	 * The normal vector to the {@link #curve} at the given location.
	 *
	 * @type Point
	 * @bean
	 */
	getNormal: function() {
		var parameter = this.getParameter(),
			curve = this.getCurve();
		return parameter != null && curve && curve.getNormalAt(parameter, true);
	},

	/**
	 * The distance from the queried point to the returned location.
	 *
	 * @type Number
	 * @bean
	 */
	getDistance: function() {
		return this._distance;
	},

	divide: function() {
		var curve = this.getCurve(true);
		return curve && curve.divide(this.getParameter(true));
	},

	split: function() {
		var curve = this.getCurve(true);
		return curve && curve.split(this.getParameter(true));
	},

	/**
	 * @return {String} A string representation of the curve location.
	 */
	toString: function() {
		var parts = [],
			point = this.getPoint(),
			f = Formatter.instance;
		if (point)
			parts.push('point: ' + point);
		var index = this.getIndex();
		if (index != null)
			parts.push('index: ' + index);
		var parameter = this.getParameter();
		if (parameter != null)
			parts.push('parameter: ' + f.number(parameter));
		if (this._distance != null)
			parts.push('distance: ' + f.number(this._distance));
		return '{ ' + parts.join(', ') + ' }';
	}
});
<|endoftext|>