parent
f335a48927
commit
9898916807
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 838 B |
@ -0,0 +1,163 @@
|
|||||||
|
#ingame_HUD_Statistics {
|
||||||
|
.filterHeader {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
align-items: center;
|
||||||
|
justify-items: end;
|
||||||
|
|
||||||
|
.filtersDataSource {
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtersDisplayMode {
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
@include S(height, 20px);
|
||||||
|
@include S(padding, 1px, 10px);
|
||||||
|
border: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: 0;
|
||||||
|
@include IncreasedClickArea(1px);
|
||||||
|
@include S(min-width, 30px);
|
||||||
|
color: #fff;
|
||||||
|
opacity: 0.25;
|
||||||
|
@include S(margin-left, 1px);
|
||||||
|
|
||||||
|
&.displayIcons,
|
||||||
|
&.displayDetailed {
|
||||||
|
background: uiResource("icons/display_list.png") center center / #{D(15px)} no-repeat;
|
||||||
|
&.displayIcons {
|
||||||
|
background-image: uiResource("icons/display_icons.png");
|
||||||
|
background-size: #{D(11.5px)};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
background-color: #44484a !important;
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filtersDataSource,
|
||||||
|
.filtersDisplayMode {
|
||||||
|
display: flex;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
:first-child {
|
||||||
|
border-radius: #{D(2px)} 0 0 #{D(2px)} !important;
|
||||||
|
margin-left: 0 !important;
|
||||||
|
}
|
||||||
|
:last-child {
|
||||||
|
margin-right: 0 !important;
|
||||||
|
border-radius: 0 #{D(2px)} #{D(2px)} 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
@include S(margin-top, 10px);
|
||||||
|
@include S(height, 350px);
|
||||||
|
overflow-y: scroll;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
@include S(padding-right, 4px);
|
||||||
|
|
||||||
|
> .noEntries {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
@include PlainText;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
background: #f4f4f4;
|
||||||
|
@include S(border-radius, 2px);
|
||||||
|
@include S(margin-bottom, 4px);
|
||||||
|
display: grid;
|
||||||
|
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
@include S(padding, 5px);
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.icon {
|
||||||
|
grid-column: 1 / 2;
|
||||||
|
grid-row: 1 / 2;
|
||||||
|
@include S(width, 40px);
|
||||||
|
@include S(height, 40px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter {
|
||||||
|
@include SuperSmallText;
|
||||||
|
|
||||||
|
@include S(border-radius, 2px);
|
||||||
|
@include S(padding, 0, 3px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialogInner {
|
||||||
|
&[data-displaymode="detailed"] .displayDetailed,
|
||||||
|
&[data-displaymode="icons"] .displayIcons,
|
||||||
|
&[data-datasource="produced"] .modeProduced,
|
||||||
|
&[data-datasource="delivered"] .modeDelivered,
|
||||||
|
&[data-datasource="stored"] .modeStored {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-displaymode="icons"] .content.hasEntries {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(6, 1fr);
|
||||||
|
grid-auto-rows: #{D(73px)};
|
||||||
|
align-items: flex-start;
|
||||||
|
@include S(grid-column-gap, 3px);
|
||||||
|
> div {
|
||||||
|
@include S(grid-row-gap, 5px);
|
||||||
|
@include S(height, 60px);
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: 1fr auto;
|
||||||
|
justify-items: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.counter {
|
||||||
|
grid-column: 1 / 2;
|
||||||
|
grid-row: 2 / 3;
|
||||||
|
background: rgba(0, 10, 20, 0.05);
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&[data-displaymode="detailed"] .content.hasEntries {
|
||||||
|
> div {
|
||||||
|
@include S(padding, 10px);
|
||||||
|
@include S(height, 40px);
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
@include S(grid-column-gap, 15px);
|
||||||
|
|
||||||
|
.counter {
|
||||||
|
grid-column: 3 / 4;
|
||||||
|
grid-row: 1 / 2;
|
||||||
|
@include Heading;
|
||||||
|
color: #55595a;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.graph {
|
||||||
|
@include S(width, 300px);
|
||||||
|
@include S(height, 40px);
|
||||||
|
@include S(border-radius, 0, 0, 2px, 2px);
|
||||||
|
$color: rgba(0, 10, 20, 0.04);
|
||||||
|
// background: $color;
|
||||||
|
border: #{D(4px)} solid transparent;
|
||||||
|
// @include S(border-width, 1px, 0, 1px, 0);
|
||||||
|
@include S(margin-top, -3px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,397 @@
|
|||||||
|
import { BaseHUDPart } from "../base_hud_part";
|
||||||
|
import {
|
||||||
|
makeDiv,
|
||||||
|
makeButton,
|
||||||
|
formatBigNumber,
|
||||||
|
clamp,
|
||||||
|
removeAllChildren,
|
||||||
|
waitNextFrame,
|
||||||
|
} from "../../../core/utils";
|
||||||
|
import { DynamicDomAttach } from "../dynamic_dom_attach";
|
||||||
|
import { InputReceiver } from "../../../core/input_receiver";
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HUDStatistics extends BaseHUDPart {
|
||||||
|
createElements(parent) {
|
||||||
|
this.background = makeDiv(parent, "ingame_HUD_Statistics", ["ingameDialog"]);
|
||||||
|
|
||||||
|
// DIALOG Inner / Wrapper
|
||||||
|
this.dialogInner = makeDiv(this.background, null, ["dialogInner"]);
|
||||||
|
this.title = makeDiv(this.dialogInner, null, ["title"], `statistics`);
|
||||||
|
this.closeButton = makeDiv(this.title, null, ["closeButton"]);
|
||||||
|
this.trackClicks(this.closeButton, this.close);
|
||||||
|
|
||||||
|
this.filterHeader = makeDiv(this.dialogInner, null, ["filterHeader"]);
|
||||||
|
|
||||||
|
this.filtersDataSource = makeDiv(this.filterHeader, null, ["filtersDataSource"]);
|
||||||
|
this.filtersDisplayMode = makeDiv(this.filterHeader, null, ["filtersDisplayMode"]);
|
||||||
|
|
||||||
|
const buttonModeProduced = makeButton(this.filtersDataSource, ["modeProduced"], "Produced");
|
||||||
|
const buttonModeDelivered = makeButton(this.filtersDataSource, ["modeDelivered"], "Delivered");
|
||||||
|
const buttonModeStored = makeButton(this.filtersDataSource, ["modeStored"], "Stored");
|
||||||
|
|
||||||
|
this.trackClicks(buttonModeProduced, () => this.setDataSource(enumAnalyticsDataSource.produced));
|
||||||
|
this.trackClicks(buttonModeStored, () => this.setDataSource(enumAnalyticsDataSource.stored));
|
||||||
|
this.trackClicks(buttonModeDelivered, () => this.setDataSource(enumAnalyticsDataSource.delivered));
|
||||||
|
|
||||||
|
const buttonDisplayDetailed = makeButton(this.filtersDisplayMode, ["displayDetailed"]);
|
||||||
|
const buttonDisplayIcons = makeButton(this.filtersDisplayMode, ["displayIcons"]);
|
||||||
|
|
||||||
|
this.trackClicks(buttonDisplayIcons, () => this.setDisplayMode(enumDisplayMode.icons));
|
||||||
|
this.trackClicks(buttonDisplayDetailed, () => this.setDisplayMode(enumDisplayMode.detailed));
|
||||||
|
|
||||||
|
this.contentDiv = makeDiv(this.dialogInner, null, ["content"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {enumAnalyticsDataSource} source
|
||||||
|
*/
|
||||||
|
setDataSource(source) {
|
||||||
|
this.dataSource = source;
|
||||||
|
this.dialogInner.setAttribute("data-datasource", source);
|
||||||
|
if (this.visible) {
|
||||||
|
this.rerenderFull();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {enumDisplayMode} mode
|
||||||
|
*/
|
||||||
|
setDisplayMode(mode) {
|
||||||
|
this.displayMode = mode;
|
||||||
|
this.dialogInner.setAttribute("data-displaymode", mode);
|
||||||
|
if (this.visible) {
|
||||||
|
this.rerenderFull();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
this.domAttach = new DynamicDomAttach(this.root, this.background, {
|
||||||
|
attachClass: "visible",
|
||||||
|
});
|
||||||
|
|
||||||
|
this.inputReciever = new InputReceiver("statistics");
|
||||||
|
this.keyActionMapper = new KeyActionMapper(this.root, this.inputReciever);
|
||||||
|
|
||||||
|
this.keyActionMapper.getBinding("back").add(this.close, this);
|
||||||
|
this.keyActionMapper.getBinding("menu_open_stats").add(this.close, this);
|
||||||
|
|
||||||
|
/** @type {Object.<string, ShapeStatisticsHandle>} */
|
||||||
|
this.activeHandles = {};
|
||||||
|
|
||||||
|
this.setDataSource(enumAnalyticsDataSource.produced);
|
||||||
|
this.setDisplayMode(enumDisplayMode.detailed);
|
||||||
|
|
||||||
|
this.intersectionObserver = new IntersectionObserver(this.intersectionCallback.bind(this), {
|
||||||
|
root: this.contentDiv,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.lastFullRerender = 0;
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
this.rerenderFull();
|
||||||
|
}
|
||||||
|
|
||||||
|
intersectionCallback(entries) {
|
||||||
|
for (let i = 0; i < entries.length; ++i) {
|
||||||
|
const entry = entries[i];
|
||||||
|
const handle = this.activeHandles[entry.target.getAttribute("data-shape-key")];
|
||||||
|
if (handle) {
|
||||||
|
handle.setVisible(entry.intersectionRatio > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
document.body.classList.remove("ingameDialogOpen");
|
||||||
|
}
|
||||||
|
|
||||||
|
show() {
|
||||||
|
this.visible = true;
|
||||||
|
document.body.classList.add("ingameDialogOpen");
|
||||||
|
this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever);
|
||||||
|
this.rerenderFull();
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.visible = false;
|
||||||
|
document.body.classList.remove("ingameDialogOpen");
|
||||||
|
this.root.app.inputMgr.makeSureDetached(this.inputReciever);
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
this.domAttach.update(this.visible);
|
||||||
|
if (this.visible) {
|
||||||
|
if (this.root.time.now() - this.lastFullRerender > 1) {
|
||||||
|
this.lastFullRerender = this.root.time.now();
|
||||||
|
this.lastPartialRerender = this.root.time.now();
|
||||||
|
this.rerenderFull();
|
||||||
|
}
|
||||||
|
this.rerenderPartial();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rerenderPartial() {
|
||||||
|
for (const key in this.activeHandles) {
|
||||||
|
const handle = this.activeHandles[key];
|
||||||
|
handle.update(this.displayMode, this.dataSource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rerenderFull() {
|
||||||
|
removeAllChildren(this.contentDiv);
|
||||||
|
|
||||||
|
// Now, attach new ones
|
||||||
|
const entries = Object.entries(this.root.hubGoals.storedShapes);
|
||||||
|
entries.sort((a, b) => b[1] - a[1]);
|
||||||
|
|
||||||
|
let rendered = new Set();
|
||||||
|
|
||||||
|
for (let i = 0; i < Math_min(entries.length, 200); ++i) {
|
||||||
|
const entry = entries[i];
|
||||||
|
const shapeKey = entry[0];
|
||||||
|
const amount = entry[1];
|
||||||
|
if (amount < 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let handle = this.activeHandles[shapeKey];
|
||||||
|
if (!handle) {
|
||||||
|
const definition = this.root.shapeDefinitionMgr.getShapeFromShortKey(shapeKey);
|
||||||
|
handle = this.activeHandles[shapeKey] = new ShapeStatisticsHandle(
|
||||||
|
this.root,
|
||||||
|
definition,
|
||||||
|
this.intersectionObserver
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
rendered.add(shapeKey);
|
||||||
|
handle.attach(this.contentDiv);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in this.activeHandles) {
|
||||||
|
if (!rendered.has(key)) {
|
||||||
|
this.activeHandles[key].destroy();
|
||||||
|
delete this.activeHandles[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entries.length === 0) {
|
||||||
|
this.contentDiv.innerHTML = `
|
||||||
|
<strong class="noEntries">No shapes have been produced so far.</strong>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.contentDiv.classList.toggle("hasEntries", entries.length > 0);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,116 @@
|
|||||||
|
import { GameRoot } from "./root";
|
||||||
|
import { ShapeDefinition } from "./shape_definition";
|
||||||
|
import { globalConfig } from "../core/config";
|
||||||
|
|
||||||
|
/** @enum {string} */
|
||||||
|
export const enumAnalyticsDataSource = {
|
||||||
|
produced: "produced",
|
||||||
|
stored: "stored",
|
||||||
|
delivered: "delivered",
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ProductionAnalytics {
|
||||||
|
/**
|
||||||
|
* @param {GameRoot} root
|
||||||
|
*/
|
||||||
|
constructor(root) {
|
||||||
|
this.root = root;
|
||||||
|
|
||||||
|
this.history = {
|
||||||
|
[enumAnalyticsDataSource.produced]: [],
|
||||||
|
[enumAnalyticsDataSource.stored]: [],
|
||||||
|
[enumAnalyticsDataSource.delivered]: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < globalConfig.statisticsGraphSlices; ++i) {
|
||||||
|
this.startNewSlice();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.root.signals.shapeDelivered.add(this.onShapeDelivered, this);
|
||||||
|
this.root.signals.shapeProduced.add(this.onShapeProduced, this);
|
||||||
|
|
||||||
|
this.lastAnalyticsSlice = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ShapeDefinition} definition
|
||||||
|
*/
|
||||||
|
onShapeDelivered(definition) {
|
||||||
|
const key = definition.getHash();
|
||||||
|
const entry = this.history[enumAnalyticsDataSource.delivered];
|
||||||
|
entry[entry.length - 1][key] = (entry[entry.length - 1][key] || 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ShapeDefinition} definition
|
||||||
|
*/
|
||||||
|
onShapeProduced(definition) {
|
||||||
|
const key = definition.getHash();
|
||||||
|
const entry = this.history[enumAnalyticsDataSource.produced];
|
||||||
|
entry[entry.length - 1][key] = (entry[entry.length - 1][key] || 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a new time slice
|
||||||
|
*/
|
||||||
|
startNewSlice() {
|
||||||
|
for (const key in this.history) {
|
||||||
|
if (key === enumAnalyticsDataSource.stored) {
|
||||||
|
// Copy stored data
|
||||||
|
this.history[key].push(Object.assign({}, this.root.hubGoals.storedShapes));
|
||||||
|
} else {
|
||||||
|
this.history[key].push({});
|
||||||
|
}
|
||||||
|
while (this.history[key].length > globalConfig.statisticsGraphSlices) {
|
||||||
|
this.history[key].shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ShapeDefinition} definition
|
||||||
|
*/
|
||||||
|
getCurrentShapeProductionRate(definition) {
|
||||||
|
const slices = this.history[enumAnalyticsDataSource.produced];
|
||||||
|
return slices[slices.length - 2][definition.getHash()] || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ShapeDefinition} definition
|
||||||
|
*/
|
||||||
|
getCurrentShapeDeliverRate(definition) {
|
||||||
|
const slices = this.history[enumAnalyticsDataSource.delivered];
|
||||||
|
return slices[slices.length - 2][definition.getHash()] || 0;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {enumAnalyticsDataSource} dataSource
|
||||||
|
* @param {ShapeDefinition} definition
|
||||||
|
*/
|
||||||
|
getCurrentShapeRate(dataSource, definition) {
|
||||||
|
const slices = this.history[dataSource];
|
||||||
|
return slices[slices.length - 2][definition.getHash()] || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {enumAnalyticsDataSource} dataSource
|
||||||
|
* @param {ShapeDefinition} definition
|
||||||
|
* @param {number} historyOffset
|
||||||
|
*/
|
||||||
|
getPastShapeRate(dataSource, definition, historyOffset) {
|
||||||
|
assertAlways(
|
||||||
|
historyOffset >= 0 && historyOffset < globalConfig.statisticsGraphSlices,
|
||||||
|
"Invalid slice offset: " + historyOffset
|
||||||
|
);
|
||||||
|
|
||||||
|
const slices = this.history[dataSource];
|
||||||
|
return slices[slices.length - 1 - historyOffset][definition.getHash()] || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
if (this.root.time.now() - this.lastAnalyticsSlice > globalConfig.analyticsSliceDurationSeconds) {
|
||||||
|
this.lastAnalyticsSlice = this.root.time.now();
|
||||||
|
this.startNewSlice();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in new issue