From bf79f17776cfb433ba6f3380df42e55c9cf04299 Mon Sep 17 00:00:00 2001 From: tobspr Date: Thu, 14 May 2020 08:03:24 +0200 Subject: [PATCH] Refactor statistics and fix crash --- src/js/game/hud/parts/statistics.js | 233 ++------------------- src/js/game/hud/parts/statistics_handle.js | 217 +++++++++++++++++++ 2 files changed, 230 insertions(+), 220 deletions(-) create mode 100644 src/js/game/hud/parts/statistics_handle.js diff --git a/src/js/game/hud/parts/statistics.js b/src/js/game/hud/parts/statistics.js index 2e0e3dfb..dc90d6eb 100644 --- a/src/js/game/hud/parts/statistics.js +++ b/src/js/game/hud/parts/statistics.js @@ -1,224 +1,11 @@ -import { BaseHUDPart } from "../base_hud_part"; -import { - makeDiv, - makeButton, - formatBigNumber, - clamp, - removeAllChildren, - waitNextFrame, -} from "../../../core/utils"; -import { DynamicDomAttach } from "../dynamic_dom_attach"; +import { Math_min } from "../../../core/builtins"; import { InputReceiver } from "../../../core/input_receiver"; +import { makeButton, makeDiv, removeAllChildren } from "../../../core/utils"; import { KeyActionMapper } from "../../key_action_mapper"; -import { ShapeDefinition } from "../../shape_definition"; -import { GameRoot } from "../../root"; -import { freeCanvas, makeOffscreenBuffer } from "../../../core/buffer_utils"; import { enumAnalyticsDataSource } from "../../production_analytics"; -import { globalConfig } from "../../../core/config"; -import { Math_floor, Math_min } from "../../../core/builtins"; - -/** @enum {string} */ -const enumDisplayMode = { - icons: "icons", - detailed: "detailed", -}; - -/** - * Simple wrapper for a shape definition - */ -class ShapeStatisticsHandle { - /** - * @param {GameRoot} root - * @param {ShapeDefinition} definition - * @param {IntersectionObserver} intersectionObserver - */ - constructor(root, definition, intersectionObserver) { - this.definition = definition; - this.root = root; - this.intersectionObserver = intersectionObserver; - - this.visible = false; - } - - initElement() { - this.element = document.createElement("div"); - this.element.setAttribute("data-shape-key", this.definition.getHash()); - - this.counter = document.createElement("span"); - this.counter.classList.add("counter"); - this.element.appendChild(this.counter); - } - - /** - * Sets whether the shape handle is visible currently - * @param {boolean} visibility - */ - setVisible(visibility) { - if (visibility === this.visible) { - return; - } - this.visible = visibility; - if (visibility) { - if (!this.shapeCanvas) { - // Create elements - this.shapeCanvas = this.definition.generateAsCanvas(100); - this.shapeCanvas.classList.add("icon"); - this.element.appendChild(this.shapeCanvas); - } - } else { - // Drop elements - if (this.shapeCanvas) { - this.shapeCanvas.remove(); - delete this.shapeCanvas; - } - if (this.graphCanvas) { - this.graphCanvas.remove(); - delete this.graphCanvas; - delete this.graphContext; - } - } - } - - /** - * - * @param {enumDisplayMode} displayMode - * @param {enumAnalyticsDataSource} dataSource - * @param {boolean=} forced - */ - update(displayMode, dataSource, forced = false) { - if (!this.element) { - return; - } - if (!this.visible && !forced) { - return; - } - - switch (dataSource) { - case enumAnalyticsDataSource.stored: { - this.counter.innerText = formatBigNumber( - this.root.hubGoals.storedShapes[this.definition.getHash()] || 0 - ); - break; - } - case enumAnalyticsDataSource.delivered: - case enumAnalyticsDataSource.produced: { - let rate = - (this.root.productionAnalytics.getCurrentShapeRate(dataSource, this.definition) / - globalConfig.analyticsSliceDurationSeconds) * - 60; - this.counter.innerText = formatBigNumber(rate) + " / m"; - break; - } - } - - if (displayMode === enumDisplayMode.detailed) { - const graphDpi = globalConfig.statisticsGraphDpi; - - const w = 300; - const h = 40; - - if (!this.graphCanvas) { - const [canvas, context] = makeOffscreenBuffer(w * graphDpi, h * graphDpi, { - smooth: true, - reusable: false, - label: "statgraph-" + this.definition.getHash(), - }); - context.scale(graphDpi, graphDpi); - canvas.classList.add("graph"); - this.graphCanvas = canvas; - this.graphContext = context; - this.element.appendChild(this.graphCanvas); - } - - this.graphContext.clearRect(0, 0, w, h); - - this.graphContext.fillStyle = "#bee0db"; - this.graphContext.strokeStyle = "#66ccbc"; - this.graphContext.lineWidth = 1.5; - - const sliceWidth = w / globalConfig.statisticsGraphSlices; - - let values = []; - let maxValue = 1; - - for (let i = 0; i < globalConfig.statisticsGraphSlices - 1; ++i) { - const value = this.root.productionAnalytics.getPastShapeRate( - dataSource, - this.definition, - globalConfig.statisticsGraphSlices - i - 1 - ); - if (value > maxValue) { - maxValue = value; - } - values.push(value); - } - - this.graphContext.beginPath(); - this.graphContext.moveTo(0.75, h + 5); - for (let i = 0; i < values.length; ++i) { - const yValue = clamp((1 - values[i] / maxValue) * h, 0.75, h - 0.75); - const x = i * sliceWidth; - if (i === 0) { - this.graphContext.lineTo(0.75, yValue); - } - this.graphContext.lineTo(x, yValue); - if (i === values.length - 1) { - this.graphContext.lineTo(w + 100, yValue); - this.graphContext.lineTo(w + 100, h + 5); - } - } - - this.graphContext.closePath(); - this.graphContext.stroke(); - this.graphContext.fill(); - } else { - if (this.graphCanvas) { - this.graphCanvas.remove(); - delete this.graphCanvas; - delete this.graphContext; - } - } - } - - /** - * Attaches the handle - * @param {HTMLElement} parent - */ - attach(parent) { - if (!this.element) { - this.initElement(); - } - if (this.element.parentElement !== parent) { - parent.appendChild(this.element); - this.intersectionObserver.observe(this.element); - } - } - - /** - * Detaches the handle - */ - detach() { - if (this.element && this.element.parentElement) { - this.element.parentElement.removeChild(this.element); - this.intersectionObserver.unobserve(this.element); - } - } - - /** - * Destroys the handle - */ - destroy() { - if (this.element) { - this.intersectionObserver.unobserve(this.element); - this.shapeCanvas.remove(); - - this.element.remove(); - delete this.element; - delete this.counter; - delete this.shapeCanvas; - } - } -} +import { BaseHUDPart } from "../base_hud_part"; +import { DynamicDomAttach } from "../dynamic_dom_attach"; +import { enumDisplayMode, HUDShapeStatisticsHandle } from "./statistics_handle"; export class HUDStatistics extends BaseHUDPart { createElements(parent) { @@ -285,7 +72,7 @@ export class HUDStatistics extends BaseHUDPart { this.keyActionMapper.getBinding("back").add(this.close, this); this.keyActionMapper.getBinding("menu_open_stats").add(this.close, this); - /** @type {Object.} */ + /** @type {Object.} */ this.activeHandles = {}; this.setDataSource(enumAnalyticsDataSource.produced); @@ -342,6 +129,9 @@ export class HUDStatistics extends BaseHUDPart { } } + /** + * Performs a partial rerender, only updating graphs and counts + */ rerenderPartial() { for (const key in this.activeHandles) { const handle = this.activeHandles[key]; @@ -349,6 +139,9 @@ export class HUDStatistics extends BaseHUDPart { } } + /** + * Performs a full rerender, regenerating everything + */ rerenderFull() { removeAllChildren(this.contentDiv); @@ -369,7 +162,7 @@ export class HUDStatistics extends BaseHUDPart { let handle = this.activeHandles[shapeKey]; if (!handle) { const definition = this.root.shapeDefinitionMgr.getShapeFromShortKey(shapeKey); - handle = this.activeHandles[shapeKey] = new ShapeStatisticsHandle( + handle = this.activeHandles[shapeKey] = new HUDShapeStatisticsHandle( this.root, definition, this.intersectionObserver diff --git a/src/js/game/hud/parts/statistics_handle.js b/src/js/game/hud/parts/statistics_handle.js new file mode 100644 index 00000000..35326b6b --- /dev/null +++ b/src/js/game/hud/parts/statistics_handle.js @@ -0,0 +1,217 @@ +import { GameRoot } from "../../root"; +import { ShapeDefinition } from "../../shape_definition"; +import { enumAnalyticsDataSource } from "../../production_analytics"; +import { formatBigNumber, clamp } from "../../../core/utils"; +import { globalConfig } from "../../../core/config"; +import { makeOffscreenBuffer } from "../../../core/buffer_utils"; + +/** @enum {string} */ +export const enumDisplayMode = { + icons: "icons", + detailed: "detailed", +}; + +/** + * Simple wrapper for a shape definition within the shape statistics + */ +export class HUDShapeStatisticsHandle { + /** + * @param {GameRoot} root + * @param {ShapeDefinition} definition + * @param {IntersectionObserver} intersectionObserver + */ + constructor(root, definition, intersectionObserver) { + this.definition = definition; + this.root = root; + this.intersectionObserver = intersectionObserver; + + this.visible = false; + } + + initElement() { + this.element = document.createElement("div"); + this.element.setAttribute("data-shape-key", this.definition.getHash()); + + this.counter = document.createElement("span"); + this.counter.classList.add("counter"); + this.element.appendChild(this.counter); + } + + /** + * Sets whether the shape handle is visible currently + * @param {boolean} visibility + */ + setVisible(visibility) { + if (visibility === this.visible) { + return; + } + this.visible = visibility; + if (visibility) { + if (!this.shapeCanvas) { + // Create elements + this.shapeCanvas = this.definition.generateAsCanvas(100); + this.shapeCanvas.classList.add("icon"); + this.element.appendChild(this.shapeCanvas); + } + } else { + // Drop elements + this.cleanupChildElements(); + } + } + + /** + * + * @param {enumDisplayMode} displayMode + * @param {enumAnalyticsDataSource} dataSource + * @param {boolean=} forced + */ + update(displayMode, dataSource, forced = false) { + if (!this.element) { + return; + } + if (!this.visible && !forced) { + return; + } + + switch (dataSource) { + case enumAnalyticsDataSource.stored: { + this.counter.innerText = formatBigNumber( + this.root.hubGoals.storedShapes[this.definition.getHash()] || 0 + ); + break; + } + case enumAnalyticsDataSource.delivered: + case enumAnalyticsDataSource.produced: { + let rate = + (this.root.productionAnalytics.getCurrentShapeRate(dataSource, this.definition) / + globalConfig.analyticsSliceDurationSeconds) * + 60; + this.counter.innerText = formatBigNumber(rate) + " / m"; + break; + } + } + + if (displayMode === enumDisplayMode.detailed) { + const graphDpi = globalConfig.statisticsGraphDpi; + + const w = 300; + const h = 40; + + if (!this.graphCanvas) { + const [canvas, context] = makeOffscreenBuffer(w * graphDpi, h * graphDpi, { + smooth: true, + reusable: false, + label: "statgraph-" + this.definition.getHash(), + }); + context.scale(graphDpi, graphDpi); + canvas.classList.add("graph"); + this.graphCanvas = canvas; + this.graphContext = context; + this.element.appendChild(this.graphCanvas); + } + + this.graphContext.clearRect(0, 0, w, h); + + this.graphContext.fillStyle = "#bee0db"; + this.graphContext.strokeStyle = "#66ccbc"; + this.graphContext.lineWidth = 1.5; + + const sliceWidth = w / globalConfig.statisticsGraphSlices; + + let values = []; + let maxValue = 1; + + for (let i = 0; i < globalConfig.statisticsGraphSlices - 1; ++i) { + const value = this.root.productionAnalytics.getPastShapeRate( + dataSource, + this.definition, + globalConfig.statisticsGraphSlices - i - 1 + ); + if (value > maxValue) { + maxValue = value; + } + values.push(value); + } + + this.graphContext.beginPath(); + this.graphContext.moveTo(0.75, h + 5); + for (let i = 0; i < values.length; ++i) { + const yValue = clamp((1 - values[i] / maxValue) * h, 0.75, h - 0.75); + const x = i * sliceWidth; + if (i === 0) { + this.graphContext.lineTo(0.75, yValue); + } + this.graphContext.lineTo(x, yValue); + if (i === values.length - 1) { + this.graphContext.lineTo(w + 100, yValue); + this.graphContext.lineTo(w + 100, h + 5); + } + } + + this.graphContext.closePath(); + this.graphContext.stroke(); + this.graphContext.fill(); + } else { + if (this.graphCanvas) { + this.graphCanvas.remove(); + delete this.graphCanvas; + delete this.graphContext; + } + } + } + + /** + * Attaches the handle + * @param {HTMLElement} parent + */ + attach(parent) { + if (!this.element) { + this.initElement(); + } + if (this.element.parentElement !== parent) { + parent.appendChild(this.element); + this.intersectionObserver.observe(this.element); + } + } + + /** + * Detaches the handle + */ + detach() { + if (this.element && this.element.parentElement) { + this.element.parentElement.removeChild(this.element); + this.intersectionObserver.unobserve(this.element); + } + } + + /** + * Cleans up all child elements + */ + cleanupChildElements() { + if (this.shapeCanvas) { + this.shapeCanvas.remove(); + delete this.shapeCanvas; + } + + if (this.graphCanvas) { + this.graphCanvas.remove(); + delete this.graphCanvas; + delete this.graphContext; + } + } + + /** + * Destroys the handle + */ + destroy() { + this.cleanupChildElements(); + if (this.element) { + this.intersectionObserver.unobserve(this.element); + this.element.remove(); + delete this.element; + + // Remove handle + delete this.counter; + } + } +}