Refactor filters to make them not stall if one output is blocked

This commit is contained in:
tobspr 2020-09-19 19:30:04 +02:00
parent 518d9b9f6f
commit 26cd38b68c
16 changed files with 471 additions and 365 deletions

View File

@ -30,7 +30,7 @@
transform: scale(0.9) !important; transform: scale(0.9) !important;
} }
opacity: 0.5; opacity: 0.7;
&:hover { &:hover {
opacity: 0.9 !important; opacity: 0.9 !important;
} }

View File

@ -1,11 +1,7 @@
import { enumDirection, Vector } from "../../core/vector"; import { enumDirection, Vector } from "../../core/vector";
import { FilterComponent } from "../components/filter";
import { ItemAcceptorComponent } from "../components/item_acceptor"; import { ItemAcceptorComponent } from "../components/item_acceptor";
import { ItemEjectorComponent } from "../components/item_ejector"; import { ItemEjectorComponent } from "../components/item_ejector";
import {
enumItemProcessorRequirements,
enumItemProcessorTypes,
ItemProcessorComponent,
} from "../components/item_processor";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins"; import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity"; import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building"; import { MetaBuilding } from "../meta_building";
@ -79,12 +75,6 @@ export class MetaFilterBuilding extends MetaBuilding {
}) })
); );
entity.addComponent( entity.addComponent(new FilterComponent());
new ItemProcessorComponent({
processorType: enumItemProcessorTypes.filter,
inputsPerCharge: 1,
processingRequirement: enumItemProcessorRequirements.filter,
})
);
} }
} }

View File

@ -17,6 +17,7 @@ import { LeverComponent } from "./components/lever";
import { WireTunnelComponent } from "./components/wire_tunnel"; import { WireTunnelComponent } from "./components/wire_tunnel";
import { DisplayComponent } from "./components/display"; import { DisplayComponent } from "./components/display";
import { BeltReaderComponent } from "./components/belt_reader"; import { BeltReaderComponent } from "./components/belt_reader";
import { FilterComponent } from "./components/filter";
export function initComponentRegistry() { export function initComponentRegistry() {
gComponentRegistry.register(StaticMapEntityComponent); gComponentRegistry.register(StaticMapEntityComponent);
@ -37,6 +38,7 @@ export function initComponentRegistry() {
gComponentRegistry.register(WireTunnelComponent); gComponentRegistry.register(WireTunnelComponent);
gComponentRegistry.register(DisplayComponent); gComponentRegistry.register(DisplayComponent);
gComponentRegistry.register(BeltReaderComponent); gComponentRegistry.register(BeltReaderComponent);
gComponentRegistry.register(FilterComponent);
// IMPORTANT ^^^^^ UPDATE ENTITY COMPONENT STORAGE AFTERWARDS // IMPORTANT ^^^^^ UPDATE ENTITY COMPONENT STORAGE AFTERWARDS

View File

@ -0,0 +1,55 @@
import { types } from "../../savegame/serialization";
import { BaseItem } from "../base_item";
import { Component } from "../component";
import { typeItemSingleton } from "../item_resolver";
/**
* @typedef {{
* item: BaseItem,
* progress: number
* }} PendingFilterItem
*/
export class FilterComponent extends Component {
static getId() {
return "Filter";
}
duplicateWithoutContents() {
return new FilterComponent();
}
static getSchema() {
return {
pendingItemsToLeaveThrough: types.array(
types.structured({
item: typeItemSingleton,
progress: types.ufloat,
})
),
pendingItemsToReject: types.array(
types.structured({
item: typeItemSingleton,
progress: types.ufloat,
})
),
};
}
constructor() {
super();
/**
* Items in queue to leave through
* @type {Array<PendingFilterItem>}
*/
this.pendingItemsToLeaveThrough = [];
/**
* Items in queue to reject
* @type {Array<PendingFilterItem>}
*/
this.pendingItemsToReject = [];
}
}

View File

@ -24,7 +24,6 @@ export const enumItemProcessorTypes = {
/** @enum {string} */ /** @enum {string} */
export const enumItemProcessorRequirements = { export const enumItemProcessorRequirements = {
painterQuad: "painterQuad", painterQuad: "painterQuad",
filter: "filter",
}; };
/** @typedef {{ /** @typedef {{

View File

@ -17,6 +17,7 @@ import { LeverComponent } from "./components/lever";
import { WireTunnelComponent } from "./components/wire_tunnel"; import { WireTunnelComponent } from "./components/wire_tunnel";
import { DisplayComponent } from "./components/display"; import { DisplayComponent } from "./components/display";
import { BeltReaderComponent } from "./components/belt_reader"; import { BeltReaderComponent } from "./components/belt_reader";
import { FilterComponent } from "./components/filter";
/* typehints:end */ /* typehints:end */
/** /**
@ -81,6 +82,9 @@ export class EntityComponentStorage {
/** @type {BeltReaderComponent} */ /** @type {BeltReaderComponent} */
this.BeltReader; this.BeltReader;
/** @type {FilterComponent} */
this.Filter;
/* typehints:end */ /* typehints:end */
} }
} }

View File

@ -22,6 +22,7 @@ import { LeverSystem } from "./systems/lever";
import { DisplaySystem } from "./systems/display"; import { DisplaySystem } from "./systems/display";
import { ItemProcessorOverlaysSystem } from "./systems/item_processor_overlays"; import { ItemProcessorOverlaysSystem } from "./systems/item_processor_overlays";
import { BeltReaderSystem } from "./systems/belt_reader"; import { BeltReaderSystem } from "./systems/belt_reader";
import { FilterSystem } from "./systems/filter";
const logger = createLogger("game_system_manager"); const logger = createLogger("game_system_manager");
@ -92,6 +93,9 @@ export class GameSystemManager {
/** @type {BeltReaderSystem} */ /** @type {BeltReaderSystem} */
beltReader: null, beltReader: null,
/** @type {FilterSystem} */
filter: null,
/* typehints:end */ /* typehints:end */
}; };
this.systemUpdateOrder = []; this.systemUpdateOrder = [];
@ -124,6 +128,8 @@ export class GameSystemManager {
add("itemProcessor", ItemProcessorSystem); add("itemProcessor", ItemProcessorSystem);
add("filter", FilterSystem);
add("itemEjector", ItemEjectorSystem); add("itemEjector", ItemEjectorSystem);
add("mapResources", MapResourcesSystem); add("mapResources", MapResourcesSystem);

View File

@ -401,7 +401,6 @@ export class HubGoals extends BasicSerializableObject {
return 1e30; return 1e30;
case enumItemProcessorTypes.splitter: case enumItemProcessorTypes.splitter:
return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt * 2; return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt * 2;
case enumItemProcessorTypes.filter:
case enumItemProcessorTypes.reader: case enumItemProcessorTypes.reader:
return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt; return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt;

View File

@ -65,7 +65,7 @@ export class DisplaySystem extends GameSystemWithFilter {
const pinsComp = entity.components.WiredPins; const pinsComp = entity.components.WiredPins;
const network = pinsComp.slots[0].linkedNetwork; const network = pinsComp.slots[0].linkedNetwork;
if (!network || !network.currentValue) { if (!network || !network.hasValue()) {
continue; continue;
} }

View File

@ -0,0 +1,85 @@
import { globalConfig } from "../../core/config";
import { BaseItem } from "../base_item";
import { FilterComponent } from "../components/filter";
import { Entity } from "../entity";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { BOOL_TRUE_SINGLETON } from "../items/boolean_item";
const MAX_ITEMS_IN_QUEUE = 2;
export class FilterSystem extends GameSystemWithFilter {
constructor(root) {
super(root, [FilterComponent]);
}
update() {
const progress =
this.root.dynamicTickrate.deltaSeconds *
this.root.hubGoals.getBeltBaseSpeed() *
globalConfig.itemSpacingOnBelts;
const requiredProgress = 1 - progress;
for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i];
const filterComp = entity.components.Filter;
const ejectorComp = entity.components.ItemEjector;
// Process payloads
const slotsAndLists = [filterComp.pendingItemsToLeaveThrough, filterComp.pendingItemsToReject];
for (let slotIndex = 0; slotIndex < slotsAndLists.length; ++slotIndex) {
const pendingItems = slotsAndLists[slotIndex];
for (let j = 0; j < pendingItems.length; ++j) {
const nextItem = pendingItems[j];
// Advance next item
nextItem.progress = Math.min(requiredProgress, nextItem.progress + progress);
// Check if it's ready to eject
if (nextItem.progress >= requiredProgress - 1e-5) {
if (ejectorComp.tryEject(slotIndex, nextItem.item)) {
pendingItems.shift();
}
}
}
}
}
}
/**
*
* @param {Entity} entity
* @param {number} slot
* @param {BaseItem} item
*/
tryAcceptItem(entity, slot, item) {
const network = entity.components.WiredPins.slots[0].linkedNetwork;
if (!network || !network.hasValue()) {
// Filter is not connected
return false;
}
const value = network.currentValue;
const filterComp = entity.components.Filter;
assert(filterComp, "entity is no filter");
// Figure out which list we have to check
let listToCheck;
if (value.equals(BOOL_TRUE_SINGLETON) || value.equals(item)) {
listToCheck = filterComp.pendingItemsToLeaveThrough;
} else {
listToCheck = filterComp.pendingItemsToReject;
}
if (listToCheck.length >= MAX_ITEMS_IN_QUEUE) {
// Busy
return false;
}
// Actually accept item
listToCheck.push({
item,
progress: 0.0,
});
return true;
}
}

View File

@ -282,6 +282,15 @@ export class ItemEjectorSystem extends GameSystemWithFilter {
return false; return false;
} }
const filterComp = receiver.components.Filter;
if (filterComp) {
// It's a filter! Unfortunately the filter has to know a lot about it's
// surrounding state and components, so it can't be within the component itself.
if (this.root.systemMgr.systems.filter.tryAcceptItem(receiver, slotIndex, item)) {
return true;
}
}
return false; return false;
} }

View File

@ -58,7 +58,6 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
[enumItemProcessorTypes.painterDouble]: this.process_PAINTER_DOUBLE, [enumItemProcessorTypes.painterDouble]: this.process_PAINTER_DOUBLE,
[enumItemProcessorTypes.painterQuad]: this.process_PAINTER_QUAD, [enumItemProcessorTypes.painterQuad]: this.process_PAINTER_QUAD,
[enumItemProcessorTypes.hub]: this.process_HUB, [enumItemProcessorTypes.hub]: this.process_HUB,
[enumItemProcessorTypes.filter]: this.process_FILTER,
[enumItemProcessorTypes.reader]: this.process_READER, [enumItemProcessorTypes.reader]: this.process_READER,
}; };
@ -162,24 +161,13 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
// Check the network value at the given slot // Check the network value at the given slot
const network = pinsComp.slots[slotIndex - 1].linkedNetwork; const network = pinsComp.slots[slotIndex - 1].linkedNetwork;
const slotIsEnabled = network && isTruthyItem(network.currentValue); const slotIsEnabled = network && network.hasValue() && isTruthyItem(network.currentValue);
if (!slotIsEnabled) { if (!slotIsEnabled) {
return false; return false;
} }
return true; return true;
} }
case enumItemProcessorRequirements.filter: {
const network = pinsComp.slots[0].linkedNetwork;
if (!network || !network.currentValue) {
// Item filter is not connected
return false;
}
// Otherwise, all good
return true;
}
// By default, everything is accepted // By default, everything is accepted
default: default:
return true; return true;
@ -222,9 +210,8 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
// Check which slots are enabled // Check which slots are enabled
for (let i = 0; i < 4; ++i) { for (let i = 0; i < 4; ++i) {
// Extract the network value on the Nth pin // Extract the network value on the Nth pin
const networkValue = pinsComp.slots[i].linkedNetwork const network = pinsComp.slots[i].linkedNetwork;
? pinsComp.slots[i].linkedNetwork.currentValue const networkValue = network && network.hasValue() ? network.currentValue : null;
: null;
// If there is no "1" on that slot, don't paint there // If there is no "1" on that slot, don't paint there
if (!isTruthyItem(networkValue)) { if (!isTruthyItem(networkValue)) {
@ -257,18 +244,6 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
return true; return true;
} }
// FILTER
// Double check with linked network
case enumItemProcessorRequirements.filter: {
const network = entity.components.WiredPins.slots[0].linkedNetwork;
if (!network || !network.currentValue) {
// Item filter is not connected
return false;
}
return processorComp.inputSlots.length >= processorComp.inputsPerCharge;
}
default: default:
assertAlways(false, "Unknown requirement for " + processorComp.processingRequirement); assertAlways(false, "Unknown requirement for " + processorComp.processingRequirement);
} }
@ -553,38 +528,6 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
}); });
} }
/**
* @param {ProcessorImplementationPayload} payload
*/
process_FILTER(payload) {
const item = payload.itemsBySlot[0];
const network = payload.entity.components.WiredPins.slots[0].linkedNetwork;
if (!network || !network.currentValue) {
payload.outItems.push({
item,
requiredSlot: 1,
doNotTrack: true,
});
return;
}
const value = network.currentValue;
if (value.equals(BOOL_TRUE_SINGLETON) || value.equals(item)) {
payload.outItems.push({
item,
requiredSlot: 0,
doNotTrack: true,
});
} else {
payload.outItems.push({
item,
requiredSlot: 1,
doNotTrack: true,
});
}
}
/** /**
* @param {ProcessorImplementationPayload} payload * @param {ProcessorImplementationPayload} payload
*/ */

View File

@ -34,34 +34,40 @@ export class ItemProcessorOverlaysSystem extends GameSystem {
for (let i = 0; i < contents.length; ++i) { for (let i = 0; i < contents.length; ++i) {
const entity = contents[i]; const entity = contents[i];
const processorComp = entity.components.ItemProcessor; const processorComp = entity.components.ItemProcessor;
if (!processorComp) { const filterComp = entity.components.Filter;
continue;
}
const requirement = processorComp.processingRequirement; // Draw processor overlays
if (!requirement && processorComp.type !== enumItemProcessorTypes.reader) { if (processorComp) {
continue; const requirement = processorComp.processingRequirement;
} if (!requirement && processorComp.type !== enumItemProcessorTypes.reader) {
continue;
if (this.drawnUids.has(entity.uid)) {
continue;
}
this.drawnUids.add(entity.uid);
switch (requirement) {
case enumItemProcessorRequirements.painterQuad: {
this.drawConnectedSlotRequirement(parameters, entity, { drawIfFalse: true });
break;
} }
case enumItemProcessorRequirements.filter: {
this.drawConnectedSlotRequirement(parameters, entity, { drawIfFalse: false }); if (this.drawnUids.has(entity.uid)) {
break; continue;
}
this.drawnUids.add(entity.uid);
switch (requirement) {
case enumItemProcessorRequirements.painterQuad: {
this.drawConnectedSlotRequirement(parameters, entity, { drawIfFalse: true });
break;
}
}
if (processorComp.type === enumItemProcessorTypes.reader) {
this.drawReaderOverlays(parameters, entity);
} }
} }
if (processorComp.type === enumItemProcessorTypes.reader) { // Draw filter overlays
this.drawReaderOverlays(parameters, entity); else if (filterComp) {
if (this.drawnUids.has(entity.uid)) {
continue;
}
this.drawnUids.add(entity.uid);
this.drawConnectedSlotRequirement(parameters, entity, { drawIfFalse: false });
} }
} }
} }
@ -111,7 +117,7 @@ export class ItemProcessorOverlaysSystem extends GameSystem {
for (let i = 0; i < pinsComp.slots.length; ++i) { for (let i = 0; i < pinsComp.slots.length; ++i) {
const slot = pinsComp.slots[i]; const slot = pinsComp.slots[i];
const network = slot.linkedNetwork; const network = slot.linkedNetwork;
if (network && network.currentValue) { if (network && network.hasValue()) {
anySlotConnected = true; anySlotConnected = true;
if (isTruthyItem(network.currentValue) || !drawIfFalse) { if (isTruthyItem(network.currentValue) || !drawIfFalse) {

View File

@ -47,13 +47,13 @@ export class LogicGateSystem extends GameSystemWithFilter {
if (slot.type !== enumPinSlotType.logicalAcceptor) { if (slot.type !== enumPinSlotType.logicalAcceptor) {
continue; continue;
} }
if (slot.linkedNetwork) { const network = slot.linkedNetwork;
if (slot.linkedNetwork.valueConflict) { if (network) {
if (network.valueConflict) {
anyConflict = true; anyConflict = true;
break; break;
} }
slotValues.push(network.currentValue);
slotValues.push(slot.linkedNetwork.currentValue);
} else { } else {
slotValues.push(null); slotValues.push(null);
} }

View File

@ -79,6 +79,14 @@ export class WireNetwork {
*/ */
this.uid = ++networkUidCounter; this.uid = ++networkUidCounter;
} }
/**
* Returns whether this network currently has a value
* @returns {boolean}
*/
hasValue() {
return !!this.currentValue && !this.valueConflict;
}
} }
export class WireSystem extends GameSystemWithFilter { export class WireSystem extends GameSystemWithFilter {

View File

@ -1,261 +1,261 @@
import { globalConfig } from "../../core/config"; import { globalConfig } from "../../core/config";
import { createLogger } from "../../core/logging"; import { createLogger } from "../../core/logging";
import { GameRoot } from "../../game/root"; import { GameRoot } from "../../game/root";
import { InGameState } from "../../states/ingame"; import { InGameState } from "../../states/ingame";
import { GameAnalyticsInterface } from "../game_analytics"; import { GameAnalyticsInterface } from "../game_analytics";
import { FILE_NOT_FOUND } from "../storage"; import { FILE_NOT_FOUND } from "../storage";
import { blueprintShape, UPGRADES } from "../../game/upgrades"; import { blueprintShape, UPGRADES } from "../../game/upgrades";
import { tutorialGoals } from "../../game/tutorial_goals"; import { tutorialGoals } from "../../game/tutorial_goals";
import { BeltComponent } from "../../game/components/belt"; import { BeltComponent } from "../../game/components/belt";
import { StaticMapEntityComponent } from "../../game/components/static_map_entity"; import { StaticMapEntityComponent } from "../../game/components/static_map_entity";
const logger = createLogger("game_analytics"); const logger = createLogger("game_analytics");
const analyticsUrl = G_IS_DEV ? "http://localhost:8001" : "https://analytics.shapez.io"; const analyticsUrl = G_IS_DEV ? "http://localhost:8001" : "https://analytics.shapez.io";
// Be sure to increment the ID whenever it changes to make sure all // Be sure to increment the ID whenever it changes to make sure all
// users are tracked // users are tracked
const analyticsLocalFile = "shapez_token_123.bin"; const analyticsLocalFile = "shapez_token_123.bin";
export class ShapezGameAnalytics extends GameAnalyticsInterface { export class ShapezGameAnalytics extends GameAnalyticsInterface {
get environment() { get environment() {
if (G_IS_DEV) { if (G_IS_DEV) {
return "dev"; return "dev";
} }
if (G_IS_STANDALONE) { if (G_IS_STANDALONE) {
return "steam"; return "steam";
} }
if (G_IS_RELEASE) { if (G_IS_RELEASE) {
return "prod"; return "prod";
} }
return "beta"; return "beta";
} }
/** /**
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
initialize() { initialize() {
this.syncKey = null; this.syncKey = null;
setInterval(() => this.sendTimePoints(), 60 * 1000); setInterval(() => this.sendTimePoints(), 60 * 1000);
// Retrieve sync key from player // Retrieve sync key from player
return this.app.storage.readFileAsync(analyticsLocalFile).then( return this.app.storage.readFileAsync(analyticsLocalFile).then(
syncKey => { syncKey => {
this.syncKey = syncKey; this.syncKey = syncKey;
logger.log("Player sync key read:", this.syncKey); logger.log("Player sync key read:", this.syncKey);
}, },
error => { error => {
// File was not found, retrieve new key // File was not found, retrieve new key
if (error === FILE_NOT_FOUND) { if (error === FILE_NOT_FOUND) {
logger.log("Retrieving new player key"); logger.log("Retrieving new player key");
// Perform call to get a new key from the API // Perform call to get a new key from the API
this.sendToApi("/v1/register", { this.sendToApi("/v1/register", {
environment: this.environment, environment: this.environment,
}) })
.then(res => { .then(res => {
// Try to read and parse the key from the api // Try to read and parse the key from the api
if (res.key && typeof res.key === "string" && res.key.length === 40) { if (res.key && typeof res.key === "string" && res.key.length === 40) {
this.syncKey = res.key; this.syncKey = res.key;
logger.log("Key retrieved:", this.syncKey); logger.log("Key retrieved:", this.syncKey);
this.app.storage.writeFileAsync(analyticsLocalFile, res.key); this.app.storage.writeFileAsync(analyticsLocalFile, res.key);
} else { } else {
throw new Error("Bad response from analytics server: " + res); throw new Error("Bad response from analytics server: " + res);
} }
}) })
.catch(err => { .catch(err => {
logger.error("Failed to register on analytics api:", err); logger.error("Failed to register on analytics api:", err);
}); });
} else { } else {
logger.error("Failed to read ga key:", error); logger.error("Failed to read ga key:", error);
} }
return; return;
} }
); );
} }
/** /**
* Sends a request to the api * Sends a request to the api
* @param {string} endpoint Endpoint without base url * @param {string} endpoint Endpoint without base url
* @param {object} data payload * @param {object} data payload
* @returns {Promise<any>} * @returns {Promise<any>}
*/ */
sendToApi(endpoint, data) { sendToApi(endpoint, data) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject("Request to " + endpoint + " timed out"), 20000); const timeout = setTimeout(() => reject("Request to " + endpoint + " timed out"), 20000);
fetch(analyticsUrl + endpoint, { fetch(analyticsUrl + endpoint, {
method: "POST", method: "POST",
mode: "cors", mode: "cors",
cache: "no-cache", cache: "no-cache",
referrer: "no-referrer", referrer: "no-referrer",
credentials: "omit", credentials: "omit",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"Accept": "application/json", "Accept": "application/json",
"x-api-key": globalConfig.info.analyticsApiKey, "x-api-key": globalConfig.info.analyticsApiKey,
}, },
body: JSON.stringify(data), body: JSON.stringify(data),
}) })
.then(res => { .then(res => {
clearTimeout(timeout); clearTimeout(timeout);
if (!res.ok || res.status !== 200) { if (!res.ok || res.status !== 200) {
reject("Fetch error: Bad status " + res.status); reject("Fetch error: Bad status " + res.status);
} else { } else {
return res.json(); return res.json();
} }
}) })
.then(resolve) .then(resolve)
.catch(reason => { .catch(reason => {
clearTimeout(timeout); clearTimeout(timeout);
reject(reason); reject(reason);
}); });
}); });
} }
/** /**
* Sends a game event to the analytics * Sends a game event to the analytics
* @param {string} category * @param {string} category
* @param {string} value * @param {string} value
*/ */
sendGameEvent(category, value) { sendGameEvent(category, value) {
if (!this.syncKey) { if (!this.syncKey) {
logger.warn("Can not send event due to missing sync key"); logger.warn("Can not send event due to missing sync key");
return; return;
} }
const gameState = this.app.stateMgr.currentState; const gameState = this.app.stateMgr.currentState;
if (!(gameState instanceof InGameState)) { if (!(gameState instanceof InGameState)) {
logger.warn("Trying to send analytics event outside of ingame state"); logger.warn("Trying to send analytics event outside of ingame state");
return; return;
} }
const savegame = gameState.savegame; const savegame = gameState.savegame;
if (!savegame) { if (!savegame) {
logger.warn("Ingame state has empty savegame"); logger.warn("Ingame state has empty savegame");
return; return;
} }
const savegameId = savegame.internalId; const savegameId = savegame.internalId;
if (!gameState.core) { if (!gameState.core) {
logger.warn("Game state has no core"); logger.warn("Game state has no core");
return; return;
} }
const root = gameState.core.root; const root = gameState.core.root;
if (!root) { if (!root) {
logger.warn("Root is not initialized"); logger.warn("Root is not initialized");
return; return;
} }
logger.log("Sending event", category, value); logger.log("Sending event", category, value);
this.sendToApi("/v1/game-event", { this.sendToApi("/v1/game-event", {
playerKey: this.syncKey, playerKey: this.syncKey,
gameKey: savegameId, gameKey: savegameId,
ingameTime: root.time.now(), ingameTime: root.time.now(),
environment: this.environment, environment: this.environment,
category, category,
value, value,
version: G_BUILD_VERSION, version: G_BUILD_VERSION,
level: root.hubGoals.level, level: root.hubGoals.level,
gameDump: this.generateGameDump(root), gameDump: this.generateGameDump(root),
}); });
} }
sendTimePoints() { sendTimePoints() {
const gameState = this.app.stateMgr.currentState; const gameState = this.app.stateMgr.currentState;
if (gameState instanceof InGameState) { if (gameState instanceof InGameState) {
logger.log("Syncing analytics"); logger.log("Syncing analytics");
this.sendGameEvent("sync", ""); this.sendGameEvent("sync", "");
} }
} }
/** /**
* Returns true if the shape is interesting * Returns true if the shape is interesting
* @param {string} key * @param {string} key
*/ */
isInterestingShape(key) { isInterestingShape(key) {
if (key === blueprintShape) { if (key === blueprintShape) {
return true; return true;
} }
// Check if its a story goal // Check if its a story goal
for (let i = 0; i < tutorialGoals.length; ++i) { for (let i = 0; i < tutorialGoals.length; ++i) {
if (key === tutorialGoals[i].shape) { if (key === tutorialGoals[i].shape) {
return true; return true;
} }
} }
// Check if its required to unlock an upgrade // Check if its required to unlock an upgrade
for (const upgradeKey in UPGRADES) { for (const upgradeKey in UPGRADES) {
const handle = UPGRADES[upgradeKey]; const handle = UPGRADES[upgradeKey];
const tiers = handle.tiers; const tiers = handle.tiers;
for (let i = 0; i < tiers.length; ++i) { for (let i = 0; i < tiers.length; ++i) {
const tier = tiers[i]; const tier = tiers[i];
const required = tier.required; const required = tier.required;
for (let k = 0; k < required.length; ++k) { for (let k = 0; k < required.length; ++k) {
if (required[k].shape === key) { if (required[k].shape === key) {
return true; return true;
} }
} }
} }
} }
return false; return false;
} }
/** /**
* Generates a game dump * Generates a game dump
* @param {GameRoot} root * @param {GameRoot} root
*/ */
generateGameDump(root) { generateGameDump(root) {
const shapeIds = Object.keys(root.hubGoals.storedShapes).filter(this.isInterestingShape.bind(this)); const shapeIds = Object.keys(root.hubGoals.storedShapes).filter(this.isInterestingShape.bind(this));
let shapes = {}; let shapes = {};
for (let i = 0; i < shapeIds.length; ++i) { for (let i = 0; i < shapeIds.length; ++i) {
shapes[shapeIds[i]] = root.hubGoals.storedShapes[shapeIds[i]]; shapes[shapeIds[i]] = root.hubGoals.storedShapes[shapeIds[i]];
} }
return { return {
shapes, shapes,
upgrades: root.hubGoals.upgradeLevels, upgrades: root.hubGoals.upgradeLevels,
belts: root.entityMgr.getAllWithComponent(BeltComponent).length, belts: root.entityMgr.getAllWithComponent(BeltComponent).length,
buildings: buildings:
root.entityMgr.getAllWithComponent(StaticMapEntityComponent).length - root.entityMgr.getAllWithComponent(StaticMapEntityComponent).length -
root.entityMgr.getAllWithComponent(BeltComponent).length, root.entityMgr.getAllWithComponent(BeltComponent).length,
}; };
} }
/** /**
*/ */
handleGameStarted() { handleGameStarted() {
this.sendGameEvent("game_start", ""); this.sendGameEvent("game_start", "");
} }
/** /**
*/ */
handleGameResumed() { handleGameResumed() {
this.sendTimePoints(); this.sendTimePoints();
} }
/** /**
* Handles the given level completed * Handles the given level completed
* @param {number} level * @param {number} level
*/ */
handleLevelCompleted(level) { handleLevelCompleted(level) {
logger.log("Complete level", level); logger.log("Complete level", level);
this.sendGameEvent("level_complete", "" + level); this.sendGameEvent("level_complete", "" + level);
} }
/** /**
* Handles the given upgrade completed * Handles the given upgrade completed
* @param {string} id * @param {string} id
* @param {number} level * @param {number} level
*/ */
handleUpgradeUnlocked(id, level) { handleUpgradeUnlocked(id, level) {
logger.log("Unlock upgrade", id, level); logger.log("Unlock upgrade", id, level);
this.sendGameEvent("upgrade_unlock", id + "@" + level); this.sendGameEvent("upgrade_unlock", id + "@" + level);
} }
} }