Statistics tab

pull/33/head
tobspr 4 years ago
parent f335a48927
commit 9898916807

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

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);
}
}
}
}
}

@ -35,11 +35,12 @@
@import "ingame_hud/dialogs";
@import "ingame_hud/mass_selector";
@import "ingame_hud/vignette_overlay";
@import "ingame_hud/statistics";
// Z-Index
$elements: ingame_Canvas, ingame_VignetteOverlay, ingame_HUD_building_placer, ingame_HUD_buildings_toolbar,
ingame_HUD_GameMenu, ingame_HUD_KeybindingOverlay, ingame_HUD_Shop, ingame_HUD_BetaOverlay,
ingame_HUD_MassSelector, ingame_HUD_UnlockNotification;
ingame_HUD_GameMenu, ingame_HUD_KeybindingOverlay, ingame_HUD_Shop, ingame_HUD_Statistics,
ingame_HUD_BetaOverlay, ingame_HUD_MassSelector, ingame_HUD_UnlockNotification;
$zindex: 100;

@ -18,6 +18,9 @@ export const globalConfig = {
assetsSharpness: 1.2,
shapesSharpness: 1.4,
statisticsGraphDpi: 2.5,
statisticsGraphSlices: 100,
// [Calculated] physics step size
physicsDeltaMs: 0,
physicsDeltaSeconds: 0,
@ -38,6 +41,8 @@ export const globalConfig = {
undergroundBeltMaxTiles: 5,
analyticsSliceDurationSeconds: 10,
buildingSpeeds: {
cutter: 1 / 4,
rotater: 1 / 1,

@ -48,9 +48,26 @@ function stringPolyfills() {
}
}
function objectPolyfills() {
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries
// @ts-ignore
if (!Object.entries) {
// @ts-ignore
Object.entries = function (obj) {
var ownProps = Object.keys(obj),
i = ownProps.length,
resArray = new Array(i); // preallocate the Array
while (i--) resArray[i] = [ownProps[i], obj[ownProps[i]]];
return resArray;
};
}
}
function initPolyfills() {
mathPolyfills();
stringPolyfills();
objectPolyfills();
}
function initExtensions() {

@ -725,6 +725,23 @@ export function makeDiv(parent, id = null, classes = [], innerHTML = "") {
return div;
}
/**
* Helper method to create a new button
* @param {Element} parent
* @param {Array<string>=} classes
* @param {string=} innerHTML
*/
export function makeButton(parent, classes = [], innerHTML = "") {
const element = document.createElement("button");
for (let i = 0; i < classes.length; ++i) {
element.classList.add(classes[i]);
}
element.classList.add("styledButton");
element.innerHTML = innerHTML;
parent.appendChild(element);
return element;
}
/**
* Removes all children of the given element
* @param {Element} elem

@ -30,6 +30,7 @@ import { GameRoot } from "./root";
import { ShapeDefinitionManager } from "./shape_definition_manager";
import { SoundProxy } from "./sound_proxy";
import { GameTime } from "./time/game_time";
import { ProductionAnalytics } from "./production_analytics";
const logger = createLogger("ingame/core");
@ -109,13 +110,11 @@ export class GameCore {
root.entityMgr = new EntityManager(root);
root.systemMgr = new GameSystemManager(root);
root.shapeDefinitionMgr = new ShapeDefinitionManager(root);
root.mapNoiseGenerator = new PerlinNoise(Math.random()); // TODO: Save seed
root.hubGoals = new HubGoals(root);
root.productionAnalytics = new ProductionAnalytics(root);
root.mapNoiseGenerator = new PerlinNoise(Math.random()); // TODO: Save seed
root.buffers = new BufferMaintainer(root);
// root.particleMgr = new ParticleManager(root);
// root.uiParticleMgr = new ParticleManager(root);
// Initialize the hud once everything is loaded
this.root.hud.initialize();
@ -260,6 +259,9 @@ export class GameCore {
// root.uiParticleMgr.update();
}
// Update analytics
root.productionAnalytics.update();
// Update automatic save after everything finished
root.automaticSave.update();

@ -1,7 +1,7 @@
import { BasicSerializableObject } from "../savegame/serialization";
import { GameRoot } from "./root";
import { ShapeDefinition, enumSubShape } from "./shape_definition";
import { enumColors } from "./colors";
import { enumColors, enumShortcodeToColor, enumColorToShortcode } from "./colors";
import { randomChoice, clamp, randomInt, findNiceIntegerValue } from "../core/utils";
import { tutorialGoals, enumHubGoalRewards } from "./tutorial_goals";
import { createLogger } from "../core/logging";
@ -114,6 +114,8 @@ export class HubGoals extends BasicSerializableObject {
const hash = definition.getHash();
this.storedShapes[hash] = (this.storedShapes[hash] || 0) + 1;
this.root.signals.shapeDelivered.dispatch(definition);
// Check if we have enough for the next level
const targetHash = this.currentGoal.definition.getHash();
if (
@ -133,9 +135,7 @@ export class HubGoals extends BasicSerializableObject {
const { shape, required, reward } = tutorialGoals[storyIndex];
this.currentGoal = {
/** @type {ShapeDefinition} */
definition: this.root.shapeDefinitionMgr.registerOrReturnHandle(
ShapeDefinition.fromShortKey(shape)
),
definition: this.root.shapeDefinitionMgr.getShapeFromShortKey(shape),
required,
reward,
};
@ -320,14 +320,14 @@ export class HubGoals extends BasicSerializableObject {
case enumItemProcessorTypes.hub:
return 1e30;
case enumItemProcessorTypes.splitter:
return (2 / globalConfig.beltSpeedItemsPerSecond) * this.upgradeImprovements.belt;
return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt * 2;
case enumItemProcessorTypes.cutter:
case enumItemProcessorTypes.rotater:
case enumItemProcessorTypes.stacker:
case enumItemProcessorTypes.mixer:
case enumItemProcessorTypes.painter:
return (
(1 / globalConfig.beltSpeedItemsPerSecond) *
globalConfig.beltSpeedItemsPerSecond *
this.upgradeImprovements.processors *
globalConfig.buildingSpeeds[processorType]
);

@ -15,6 +15,7 @@ import { HUDShop } from "./parts/shop";
import { IS_MOBILE } from "../../core/config";
import { HUDMassSelector } from "./parts/mass_selector";
import { HUDVignetteOverlay } from "./parts/vignette_overlay";
import { HUDStatistics } from "./parts/statistics";
export class GameHUD {
/**
@ -45,6 +46,7 @@ export class GameHUD {
massSelector: new HUDMassSelector(this.root),
shop: new HUDShop(this.root),
statistics: new HUDStatistics(this.root),
vignetteOverlay: new HUDVignetteOverlay(this.root),

@ -16,7 +16,7 @@ export class HUDGameMenu extends BaseHUDPart {
{
id: "stats",
label: "Stats",
handler: () => null,
handler: () => this.root.hud.parts.statistics.show(),
keybinding: "menu_open_stats",
},
];

@ -88,9 +88,7 @@ export class HUDShop extends BaseHUDPart {
tierHandle.required.forEach(({ shape, amount }) => {
const requireDiv = makeDiv(handle.elemRequirements, null, ["requirement"]);
const shapeDef = this.root.shapeDefinitionMgr.registerOrReturnHandle(
ShapeDefinition.fromShortKey(shape)
);
const shapeDef = this.root.shapeDefinitionMgr.getShapeFromShortKey(shape);
const shapeCanvas = shapeDef.generateAsCanvas(120);
shapeCanvas.classList.add();
requireDiv.appendChild(shapeCanvas);

@ -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();
}
}
}

@ -27,6 +27,7 @@ import { CanvasClickInterceptor } from "./canvas_click_interceptor";
import { PerlinNoise } from "../core/perlin_noise";
import { HubGoals } from "./hub_goals";
import { BufferMaintainer } from "../core/buffer_maintainer";
import { ProductionAnalytics } from "./production_analytics";
/* typehints:end */
const logger = createLogger("game/root");
@ -125,6 +126,9 @@ export class GameRoot {
/** @type {ShapeDefinitionManager} */
this.shapeDefinitionMgr = null;
/** @type {ProductionAnalytics} */
this.productionAnalytics = null;
this.signals = {
// Entities
entityAdded: new Signal(/* entity */),
@ -150,6 +154,9 @@ export class GameRoot {
// Can be used to trigger an async task
performAsync: new Signal(),
shapeDelivered: new Signal(/* definition */),
shapeProduced: new Signal(/* definition */),
};
// RNG's

@ -197,7 +197,7 @@ export class ShapeDefinition extends BasicSerializableObject {
* Generates this shape as a canvas
* @param {number} size
*/
generateAsCanvas(size = 20) {
generateAsCanvas(size = 120) {
const [canvas, context] = makeOffscreenBuffer(size, size, {
smooth: true,
label: "definition-canvas-cache-" + this.getHash(),

@ -26,6 +26,19 @@ export class ShapeDefinitionManager extends BasicSerializableObject {
this.operationCache = {};
}
/**
*
* @param {string} hash
* @returns {ShapeDefinition}
*/
getShapeFromShortKey(hash) {
const cached = this.shapeKeyToDefinition[hash];
if (cached) {
return cached;
}
return (this.shapeKeyToDefinition[hash] = ShapeDefinition.fromShortKey(hash));
}
/**
* Registers a new shape definition
* @param {ShapeDefinition} definition

@ -157,9 +157,11 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
item: new ShapeItem(cutDefinition1),
requiredSlot: 0,
});
this.root.signals.shapeProduced.dispatch(cutDefinition1);
}
if (!cutDefinition2.isEntirelyEmpty()) {
this.root.signals.shapeProduced.dispatch(cutDefinition2);
outItems.push({
item: new ShapeItem(cutDefinition2),
requiredSlot: 1,
@ -176,6 +178,7 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
const inputDefinition = inputItem.definition;
const rotatedDefinition = this.root.shapeDefinitionMgr.shapeActionRotateCW(inputDefinition);
this.root.signals.shapeProduced.dispatch(rotatedDefinition);
outItems.push({
item: new ShapeItem(rotatedDefinition),
});
@ -197,6 +200,7 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
lowerItem.definition,
upperItem.definition
);
this.root.signals.shapeProduced.dispatch(stackedDefinition);
outItems.push({
item: new ShapeItem(stackedDefinition),
});
@ -249,6 +253,7 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
colorItem.color
);
this.root.signals.shapeProduced.dispatch(colorizedDefinition);
outItems.push({
item: new ShapeItem(colorizedDefinition),
});

@ -3,6 +3,7 @@ import { DrawParameters } from "../../core/draw_parameters";
import { MinerComponent } from "../components/miner";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { MapChunkView } from "../map_chunk_view";
import { ShapeItem } from "../items/shape_item";
export class MinerSystem extends GameSystemWithFilter {
constructor(root) {
@ -36,6 +37,11 @@ export class MinerSystem extends GameSystemWithFilter {
continue;
}
// Analytics hook
if (lowerLayerItem instanceof ShapeItem) {
this.root.signals.shapeProduced.dispatch(lowerLayerItem.definition);
}
// Try actually ejecting
if (!ejectComp.tryEject(0, lowerLayerItem)) {
assert(false, "Failed to eject");

@ -138,6 +138,10 @@ declare interface Element {
innerHTML: string;
}
declare interface Object {
entries(obj: object): Array<[string, any]>;
}
declare interface Math {
radians(number): number;
degrees(number): number;

Loading…
Cancel
Save