Refactor filters to make them not stall if one output is blocked
This commit is contained in:
parent
518d9b9f6f
commit
26cd38b68c
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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 = [];
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {{
|
||||||
|
|
|
@ -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 */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Reference in New Issue