After Width: | Height: | Size: 7.8 KiB |
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
Before Width: | Height: | Size: 269 KiB After Width: | Height: | Size: 272 KiB |
Before Width: | Height: | Size: 653 KiB After Width: | Height: | Size: 659 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 4.9 KiB |
@ -1,136 +1,136 @@
|
||||
import { queryParamOptions } from "./query_parameters";
|
||||
|
||||
export const IS_DEBUG =
|
||||
G_IS_DEV &&
|
||||
typeof window !== "undefined" &&
|
||||
window.location.port === "3005" &&
|
||||
(window.location.host.indexOf("localhost:") >= 0 || window.location.host.indexOf("192.168.0.") >= 0) &&
|
||||
window.location.search.indexOf("nodebug") < 0;
|
||||
|
||||
export const IS_DEMO = queryParamOptions.fullVersion
|
||||
? false
|
||||
: (!G_IS_DEV && !G_IS_STANDALONE) ||
|
||||
(typeof window !== "undefined" && window.location.search.indexOf("demo") >= 0);
|
||||
|
||||
export const SUPPORT_TOUCH = false;
|
||||
|
||||
const smoothCanvas = true;
|
||||
|
||||
export const THIRDPARTY_URLS = {
|
||||
discord: "https://discord.gg/HN7EVzV",
|
||||
github: "https://github.com/tobspr/shapez.io",
|
||||
reddit: "https://www.reddit.com/r/shapezio",
|
||||
|
||||
standaloneStorePage: "https://store.steampowered.com/app/1318690/shapezio/",
|
||||
};
|
||||
|
||||
export const globalConfig = {
|
||||
// Size of a single tile in Pixels.
|
||||
// NOTICE: Update webpack.production.config too!
|
||||
tileSize: 32,
|
||||
halfTileSize: 16,
|
||||
|
||||
// Which dpi the assets have
|
||||
assetsDpi: 192 / 32,
|
||||
assetsSharpness: 1.5,
|
||||
shapesSharpness: 1.4,
|
||||
|
||||
// Production analytics
|
||||
statisticsGraphDpi: 2.5,
|
||||
statisticsGraphSlices: 100,
|
||||
analyticsSliceDurationSeconds: G_IS_DEV ? 1 : 10,
|
||||
|
||||
minimumTickRate: 25,
|
||||
maximumTickRate: 500,
|
||||
|
||||
// Map
|
||||
mapChunkSize: 16,
|
||||
mapChunkOverviewMinZoom: 0.9,
|
||||
mapChunkWorldSize: null, // COMPUTED
|
||||
|
||||
// Belt speeds
|
||||
// NOTICE: Update webpack.production.config too!
|
||||
beltSpeedItemsPerSecond: 2,
|
||||
minerSpeedItemsPerSecond: 0, // COMPUTED
|
||||
|
||||
defaultItemDiameter: 20,
|
||||
|
||||
itemSpacingOnBelts: 0.63,
|
||||
|
||||
wiresSpeedItemsPerSecond: 6,
|
||||
|
||||
undergroundBeltMaxTilesByTier: [5, 8],
|
||||
|
||||
buildingSpeeds: {
|
||||
cutter: 1 / 4,
|
||||
cutterQuad: 1 / 4,
|
||||
rotater: 1 / 1,
|
||||
rotaterCCW: 1 / 1,
|
||||
rotaterFL: 1 / 1,
|
||||
painter: 1 / 6,
|
||||
painterDouble: 1 / 8,
|
||||
painterQuad: 1 / 8,
|
||||
mixer: 1 / 5,
|
||||
stacker: 1 / 6,
|
||||
advancedProcessor: 1 / 3,
|
||||
filter: 1,
|
||||
},
|
||||
|
||||
// Zooming
|
||||
initialZoom: 1.9,
|
||||
minZoomLevel: 0.1,
|
||||
maxZoomLevel: 3,
|
||||
|
||||
// Global game speed
|
||||
gameSpeed: 1,
|
||||
|
||||
warmupTimeSecondsFast: 0.1,
|
||||
warmupTimeSecondsRegular: 1,
|
||||
|
||||
smoothing: {
|
||||
smoothMainCanvas: smoothCanvas && true,
|
||||
quality: "low", // Low is CRUCIAL for mobile performance!
|
||||
},
|
||||
|
||||
rendering: {},
|
||||
debug: require("./config.local").default,
|
||||
|
||||
// Secret vars
|
||||
info: {
|
||||
// Binary file salt
|
||||
file: "Ec'])@^+*9zMevK3uMV4432x9%iK'=",
|
||||
|
||||
// Savegame salt
|
||||
sgSalt: "}95Q3%8/.837Lqym_BJx%q7)pAHJbF",
|
||||
|
||||
// Analytics key
|
||||
analyticsApiKey: "baf6a50f0cc7dfdec5a0e21c88a1c69a4b34bc4a",
|
||||
},
|
||||
};
|
||||
|
||||
export const IS_MOBILE = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
||||
|
||||
// Automatic calculations
|
||||
globalConfig.minerSpeedItemsPerSecond = globalConfig.beltSpeedItemsPerSecond / 5;
|
||||
|
||||
globalConfig.mapChunkWorldSize = globalConfig.mapChunkSize * globalConfig.tileSize;
|
||||
|
||||
// Dynamic calculations
|
||||
if (globalConfig.debug.disableMapOverview) {
|
||||
globalConfig.mapChunkOverviewMinZoom = 0;
|
||||
}
|
||||
|
||||
// Stuff for making the trailer
|
||||
if (G_IS_DEV && globalConfig.debug.renderForTrailer) {
|
||||
globalConfig.debug.framePausesBetweenTicks = 32;
|
||||
// globalConfig.mapChunkOverviewMinZoom = 0.0;
|
||||
// globalConfig.debug.instantBelts = true;
|
||||
// globalConfig.debug.instantProcessors = true;
|
||||
// globalConfig.debug.instantMiners = true;
|
||||
globalConfig.debug.disableSavegameWrite = true;
|
||||
// globalConfig.beltSpeedItemsPerSecond *= 2;
|
||||
}
|
||||
|
||||
if (globalConfig.debug.fastGameEnter) {
|
||||
globalConfig.debug.noArtificalDelays = true;
|
||||
}
|
||||
import { queryParamOptions } from "./query_parameters";
|
||||
|
||||
export const IS_DEBUG =
|
||||
G_IS_DEV &&
|
||||
typeof window !== "undefined" &&
|
||||
window.location.port === "3005" &&
|
||||
(window.location.host.indexOf("localhost:") >= 0 || window.location.host.indexOf("192.168.0.") >= 0) &&
|
||||
window.location.search.indexOf("nodebug") < 0;
|
||||
|
||||
export const IS_DEMO = queryParamOptions.fullVersion
|
||||
? false
|
||||
: (!G_IS_DEV && !G_IS_STANDALONE) ||
|
||||
(typeof window !== "undefined" && window.location.search.indexOf("demo") >= 0);
|
||||
|
||||
export const SUPPORT_TOUCH = false;
|
||||
|
||||
const smoothCanvas = true;
|
||||
|
||||
export const THIRDPARTY_URLS = {
|
||||
discord: "https://discord.gg/HN7EVzV",
|
||||
github: "https://github.com/tobspr/shapez.io",
|
||||
reddit: "https://www.reddit.com/r/shapezio",
|
||||
|
||||
standaloneStorePage: "https://store.steampowered.com/app/1318690/shapezio/",
|
||||
};
|
||||
|
||||
export const globalConfig = {
|
||||
// Size of a single tile in Pixels.
|
||||
// NOTICE: Update webpack.production.config too!
|
||||
tileSize: 32,
|
||||
halfTileSize: 16,
|
||||
|
||||
// Which dpi the assets have
|
||||
assetsDpi: 192 / 32,
|
||||
assetsSharpness: 1.5,
|
||||
shapesSharpness: 1.4,
|
||||
|
||||
// Production analytics
|
||||
statisticsGraphDpi: 2.5,
|
||||
statisticsGraphSlices: 100,
|
||||
analyticsSliceDurationSeconds: G_IS_DEV ? 1 : 10,
|
||||
|
||||
minimumTickRate: 25,
|
||||
maximumTickRate: 500,
|
||||
|
||||
// Map
|
||||
mapChunkSize: 16,
|
||||
mapChunkOverviewMinZoom: 0.9,
|
||||
mapChunkWorldSize: null, // COMPUTED
|
||||
|
||||
// Belt speeds
|
||||
// NOTICE: Update webpack.production.config too!
|
||||
beltSpeedItemsPerSecond: 2,
|
||||
minerSpeedItemsPerSecond: 0, // COMPUTED
|
||||
|
||||
defaultItemDiameter: 20,
|
||||
|
||||
itemSpacingOnBelts: 0.63,
|
||||
|
||||
wiresSpeedItemsPerSecond: 6,
|
||||
|
||||
undergroundBeltMaxTilesByTier: [5, 8],
|
||||
|
||||
readerAnalyzeIntervalSeconds: 10,
|
||||
|
||||
buildingSpeeds: {
|
||||
cutter: 1 / 4,
|
||||
cutterQuad: 1 / 4,
|
||||
rotater: 1 / 1,
|
||||
rotaterCCW: 1 / 1,
|
||||
rotaterFL: 1 / 1,
|
||||
painter: 1 / 6,
|
||||
painterDouble: 1 / 8,
|
||||
painterQuad: 1 / 8,
|
||||
mixer: 1 / 5,
|
||||
stacker: 1 / 6,
|
||||
},
|
||||
|
||||
// Zooming
|
||||
initialZoom: 1.9,
|
||||
minZoomLevel: 0.1,
|
||||
maxZoomLevel: 3,
|
||||
|
||||
// Global game speed
|
||||
gameSpeed: 1,
|
||||
|
||||
warmupTimeSecondsFast: 0.1,
|
||||
warmupTimeSecondsRegular: 1,
|
||||
|
||||
smoothing: {
|
||||
smoothMainCanvas: smoothCanvas && true,
|
||||
quality: "low", // Low is CRUCIAL for mobile performance!
|
||||
},
|
||||
|
||||
rendering: {},
|
||||
debug: require("./config.local").default,
|
||||
|
||||
// Secret vars
|
||||
info: {
|
||||
// Binary file salt
|
||||
file: "Ec'])@^+*9zMevK3uMV4432x9%iK'=",
|
||||
|
||||
// Savegame salt
|
||||
sgSalt: "}95Q3%8/.837Lqym_BJx%q7)pAHJbF",
|
||||
|
||||
// Analytics key
|
||||
analyticsApiKey: "baf6a50f0cc7dfdec5a0e21c88a1c69a4b34bc4a",
|
||||
},
|
||||
};
|
||||
|
||||
export const IS_MOBILE = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
||||
|
||||
// Automatic calculations
|
||||
globalConfig.minerSpeedItemsPerSecond = globalConfig.beltSpeedItemsPerSecond / 5;
|
||||
|
||||
globalConfig.mapChunkWorldSize = globalConfig.mapChunkSize * globalConfig.tileSize;
|
||||
|
||||
// Dynamic calculations
|
||||
if (globalConfig.debug.disableMapOverview) {
|
||||
globalConfig.mapChunkOverviewMinZoom = 0;
|
||||
}
|
||||
|
||||
// Stuff for making the trailer
|
||||
if (G_IS_DEV && globalConfig.debug.renderForTrailer) {
|
||||
globalConfig.debug.framePausesBetweenTicks = 32;
|
||||
// globalConfig.mapChunkOverviewMinZoom = 0.0;
|
||||
// globalConfig.debug.instantBelts = true;
|
||||
// globalConfig.debug.instantProcessors = true;
|
||||
// globalConfig.debug.instantMiners = true;
|
||||
globalConfig.debug.disableSavegameWrite = true;
|
||||
// globalConfig.beltSpeedItemsPerSecond *= 2;
|
||||
}
|
||||
|
||||
if (globalConfig.debug.fastGameEnter) {
|
||||
globalConfig.debug.noArtificalDelays = true;
|
||||
}
|
||||
|
@ -0,0 +1,101 @@
|
||||
import { enumDirection, Vector } from "../../core/vector";
|
||||
import { ItemAcceptorComponent } from "../components/item_acceptor";
|
||||
import { ItemEjectorComponent } from "../components/item_ejector";
|
||||
import { enumItemProcessorTypes, ItemProcessorComponent } from "../components/item_processor";
|
||||
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
|
||||
import { Entity } from "../entity";
|
||||
import { MetaBuilding } from "../meta_building";
|
||||
import { GameRoot } from "../root";
|
||||
import { BeltUnderlaysComponent } from "../components/belt_underlays";
|
||||
import { BeltReaderComponent } from "../components/belt_reader";
|
||||
|
||||
export class MetaReaderBuilding extends MetaBuilding {
|
||||
constructor() {
|
||||
super("reader");
|
||||
}
|
||||
|
||||
getSilhouetteColor() {
|
||||
return "#25fff2";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {GameRoot} root
|
||||
*/
|
||||
getIsUnlocked(root) {
|
||||
// @todo
|
||||
return true;
|
||||
}
|
||||
|
||||
getDimensions() {
|
||||
return new Vector(1, 1);
|
||||
}
|
||||
|
||||
getShowWiresLayerPreview() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the entity at the given location
|
||||
* @param {Entity} entity
|
||||
*/
|
||||
setupEntityComponents(entity) {
|
||||
entity.addComponent(
|
||||
new WiredPinsComponent({
|
||||
slots: [
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.right,
|
||||
type: enumPinSlotType.logicalEjector,
|
||||
},
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.left,
|
||||
type: enumPinSlotType.logicalEjector,
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
entity.addComponent(
|
||||
new ItemAcceptorComponent({
|
||||
slots: [
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
directions: [enumDirection.bottom],
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
entity.addComponent(
|
||||
new ItemEjectorComponent({
|
||||
slots: [
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.top,
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
entity.addComponent(
|
||||
new ItemProcessorComponent({
|
||||
processorType: enumItemProcessorTypes.reader,
|
||||
inputsPerCharge: 1,
|
||||
})
|
||||
);
|
||||
|
||||
entity.addComponent(
|
||||
new BeltUnderlaysComponent({
|
||||
underlays: [
|
||||
{
|
||||
pos: new Vector(0, 0),
|
||||
direction: enumDirection.top,
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
entity.addComponent(new BeltReaderComponent());
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import { Component } from "../component";
|
||||
import { BaseItem } from "../base_item";
|
||||
|
||||
export class BeltReaderComponent extends Component {
|
||||
static getId() {
|
||||
return "BeltReader";
|
||||
}
|
||||
|
||||
duplicateWithoutContents() {
|
||||
return new BeltReaderComponent();
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
/**
|
||||
* Which items went through the reader, we only store the time
|
||||
* @type {Array<number>}
|
||||
*/
|
||||
this.lastItemTimes = [];
|
||||
|
||||
/**
|
||||
* Which item passed the reader last
|
||||
* @type {BaseItem}
|
||||
*/
|
||||
this.lastItem = null;
|
||||
|
||||
/**
|
||||
* Stores the last throughput we computed
|
||||
* @type {number}
|
||||
*/
|
||||
this.lastThroughput = 0;
|
||||
|
||||
/**
|
||||
* Stores when we last computed the throughput
|
||||
* @type {number}
|
||||
*/
|
||||
this.lastThroughputComputation = 0;
|
||||
}
|
||||
}
|
@ -1,82 +1,86 @@
|
||||
/* typehints:start */
|
||||
import { BeltComponent } from "./components/belt";
|
||||
import { BeltUnderlaysComponent } from "./components/belt_underlays";
|
||||
import { HubComponent } from "./components/hub";
|
||||
import { ItemAcceptorComponent } from "./components/item_acceptor";
|
||||
import { ItemEjectorComponent } from "./components/item_ejector";
|
||||
import { ItemProcessorComponent } from "./components/item_processor";
|
||||
import { MinerComponent } from "./components/miner";
|
||||
import { StaticMapEntityComponent } from "./components/static_map_entity";
|
||||
import { StorageComponent } from "./components/storage";
|
||||
import { UndergroundBeltComponent } from "./components/underground_belt";
|
||||
import { WiredPinsComponent } from "./components/wired_pins";
|
||||
import { WireComponent } from "./components/wire";
|
||||
import { ConstantSignalComponent } from "./components/constant_signal";
|
||||
import { LogicGateComponent } from "./components/logic_gate";
|
||||
import { LeverComponent } from "./components/lever";
|
||||
import { WireTunnelComponent } from "./components/wire_tunnel";
|
||||
import { DisplayComponent } from "./components/display";
|
||||
/* typehints:end */
|
||||
|
||||
/**
|
||||
* Typedefs for all entity components. These are not actually present on the entity,
|
||||
* thus they are undefined by default
|
||||
*/
|
||||
export class EntityComponentStorage {
|
||||
constructor() {
|
||||
/* typehints:start */
|
||||
|
||||
/** @type {StaticMapEntityComponent} */
|
||||
this.StaticMapEntity;
|
||||
|
||||
/** @type {BeltComponent} */
|
||||
this.Belt;
|
||||
|
||||
/** @type {ItemEjectorComponent} */
|
||||
this.ItemEjector;
|
||||
|
||||
/** @type {ItemAcceptorComponent} */
|
||||
this.ItemAcceptor;
|
||||
|
||||
/** @type {MinerComponent} */
|
||||
this.Miner;
|
||||
|
||||
/** @type {ItemProcessorComponent} */
|
||||
this.ItemProcessor;
|
||||
|
||||
/** @type {UndergroundBeltComponent} */
|
||||
this.UndergroundBelt;
|
||||
|
||||
/** @type {HubComponent} */
|
||||
this.Hub;
|
||||
|
||||
/** @type {StorageComponent} */
|
||||
this.Storage;
|
||||
|
||||
/** @type {WiredPinsComponent} */
|
||||
this.WiredPins;
|
||||
|
||||
/** @type {BeltUnderlaysComponent} */
|
||||
this.BeltUnderlays;
|
||||
|
||||
/** @type {WireComponent} */
|
||||
this.Wire;
|
||||
|
||||
/** @type {ConstantSignalComponent} */
|
||||
this.ConstantSignal;
|
||||
|
||||
/** @type {LogicGateComponent} */
|
||||
this.LogicGate;
|
||||
|
||||
/** @type {LeverComponent} */
|
||||
this.Lever;
|
||||
|
||||
/** @type {WireTunnelComponent} */
|
||||
this.WireTunnel;
|
||||
|
||||
/** @type {DisplayComponent} */
|
||||
this.Display;
|
||||
|
||||
/* typehints:end */
|
||||
}
|
||||
}
|
||||
/* typehints:start */
|
||||
import { BeltComponent } from "./components/belt";
|
||||
import { BeltUnderlaysComponent } from "./components/belt_underlays";
|
||||
import { HubComponent } from "./components/hub";
|
||||
import { ItemAcceptorComponent } from "./components/item_acceptor";
|
||||
import { ItemEjectorComponent } from "./components/item_ejector";
|
||||
import { ItemProcessorComponent } from "./components/item_processor";
|
||||
import { MinerComponent } from "./components/miner";
|
||||
import { StaticMapEntityComponent } from "./components/static_map_entity";
|
||||
import { StorageComponent } from "./components/storage";
|
||||
import { UndergroundBeltComponent } from "./components/underground_belt";
|
||||
import { WiredPinsComponent } from "./components/wired_pins";
|
||||
import { WireComponent } from "./components/wire";
|
||||
import { ConstantSignalComponent } from "./components/constant_signal";
|
||||
import { LogicGateComponent } from "./components/logic_gate";
|
||||
import { LeverComponent } from "./components/lever";
|
||||
import { WireTunnelComponent } from "./components/wire_tunnel";
|
||||
import { DisplayComponent } from "./components/display";
|
||||
import { BeltReaderComponent } from "./components/belt_reader";
|
||||
/* typehints:end */
|
||||
|
||||
/**
|
||||
* Typedefs for all entity components. These are not actually present on the entity,
|
||||
* thus they are undefined by default
|
||||
*/
|
||||
export class EntityComponentStorage {
|
||||
constructor() {
|
||||
/* typehints:start */
|
||||
|
||||
/** @type {StaticMapEntityComponent} */
|
||||
this.StaticMapEntity;
|
||||
|
||||
/** @type {BeltComponent} */
|
||||
this.Belt;
|
||||
|
||||
/** @type {ItemEjectorComponent} */
|
||||
this.ItemEjector;
|
||||
|
||||
/** @type {ItemAcceptorComponent} */
|
||||
this.ItemAcceptor;
|
||||
|
||||
/** @type {MinerComponent} */
|
||||
this.Miner;
|
||||
|
||||
/** @type {ItemProcessorComponent} */
|
||||
this.ItemProcessor;
|
||||
|
||||
/** @type {UndergroundBeltComponent} */
|
||||
this.UndergroundBelt;
|
||||
|
||||
/** @type {HubComponent} */
|
||||
this.Hub;
|
||||
|
||||
/** @type {StorageComponent} */
|
||||
this.Storage;
|
||||
|
||||
/** @type {WiredPinsComponent} */
|
||||
this.WiredPins;
|
||||
|
||||
/** @type {BeltUnderlaysComponent} */
|
||||
this.BeltUnderlays;
|
||||
|
||||
/** @type {WireComponent} */
|
||||
this.Wire;
|
||||
|
||||
/** @type {ConstantSignalComponent} */
|
||||
this.ConstantSignal;
|
||||
|
||||
/** @type {LogicGateComponent} */
|
||||
this.LogicGate;
|
||||
|
||||
/** @type {LeverComponent} */
|
||||
this.Lever;
|
||||
|
||||
/** @type {WireTunnelComponent} */
|
||||
this.WireTunnel;
|
||||
|
||||
/** @type {DisplayComponent} */
|
||||
this.Display;
|
||||
|
||||
/** @type {BeltReaderComponent} */
|
||||
this.BeltReader;
|
||||
|
||||
/* typehints:end */
|
||||
}
|
||||
}
|
||||
|
@ -1,447 +1,448 @@
|
||||
import { globalConfig } from "../core/config";
|
||||
import { clamp, findNiceIntegerValue, randomChoice, randomInt } from "../core/utils";
|
||||
import { BasicSerializableObject, types } from "../savegame/serialization";
|
||||
import { enumColors } from "./colors";
|
||||
import { enumItemProcessorTypes } from "./components/item_processor";
|
||||
import { GameRoot } from "./root";
|
||||
import { enumSubShape, ShapeDefinition } from "./shape_definition";
|
||||
import { enumHubGoalRewards, tutorialGoals } from "./tutorial_goals";
|
||||
import { UPGRADES } from "./upgrades";
|
||||
|
||||
export class HubGoals extends BasicSerializableObject {
|
||||
static getId() {
|
||||
return "HubGoals";
|
||||
}
|
||||
|
||||
static getSchema() {
|
||||
return {
|
||||
level: types.uint,
|
||||
storedShapes: types.keyValueMap(types.uint),
|
||||
upgradeLevels: types.keyValueMap(types.uint),
|
||||
|
||||
currentGoal: types.structured({
|
||||
definition: types.knownType(ShapeDefinition),
|
||||
required: types.uint,
|
||||
reward: types.nullable(types.enum(enumHubGoalRewards)),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
deserialize(data) {
|
||||
const errorCode = super.deserialize(data);
|
||||
if (errorCode) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
// Compute gained rewards
|
||||
for (let i = 0; i < this.level - 1; ++i) {
|
||||
if (i < tutorialGoals.length) {
|
||||
const reward = tutorialGoals[i].reward;
|
||||
this.gainedRewards[reward] = (this.gainedRewards[reward] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Compute upgrade improvements
|
||||
for (const upgradeId in UPGRADES) {
|
||||
const upgradeHandle = UPGRADES[upgradeId];
|
||||
const level = this.upgradeLevels[upgradeId] || 0;
|
||||
let totalImprovement = upgradeHandle.baseValue || 1;
|
||||
for (let i = 0; i < level; ++i) {
|
||||
totalImprovement += upgradeHandle.tiers[i].improvement;
|
||||
}
|
||||
this.upgradeImprovements[upgradeId] = totalImprovement;
|
||||
}
|
||||
|
||||
// Compute current goal
|
||||
const goal = tutorialGoals[this.level - 1];
|
||||
if (goal) {
|
||||
this.currentGoal = {
|
||||
/** @type {ShapeDefinition} */
|
||||
definition: this.root.shapeDefinitionMgr.getShapeFromShortKey(goal.shape),
|
||||
required: goal.required,
|
||||
reward: goal.reward,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {GameRoot} root
|
||||
*/
|
||||
constructor(root) {
|
||||
super();
|
||||
|
||||
this.root = root;
|
||||
|
||||
this.level = 1;
|
||||
|
||||
/**
|
||||
* Which story rewards we already gained
|
||||
* @type {Object.<string, number>}
|
||||
*/
|
||||
this.gainedRewards = {};
|
||||
|
||||
/**
|
||||
* Mapping from shape hash -> amount
|
||||
* @type {Object<string, number>}
|
||||
*/
|
||||
this.storedShapes = {};
|
||||
|
||||
/**
|
||||
* Stores the levels for all upgrades
|
||||
* @type {Object<string, number>}
|
||||
*/
|
||||
this.upgradeLevels = {};
|
||||
|
||||
/**
|
||||
* Stores the improvements for all upgrades
|
||||
* @type {Object<string, number>}
|
||||
*/
|
||||
this.upgradeImprovements = {};
|
||||
for (const key in UPGRADES) {
|
||||
this.upgradeImprovements[key] = UPGRADES[key].baseValue || 1;
|
||||
}
|
||||
|
||||
this.createNextGoal();
|
||||
|
||||
// Allow quickly switching goals in dev mode
|
||||
if (G_IS_DEV) {
|
||||
window.addEventListener("keydown", ev => {
|
||||
if (ev.key === "b") {
|
||||
// root is not guaranteed to exist within ~0.5s after loading in
|
||||
if (this.root && this.root.app && this.root.app.gameAnalytics) {
|
||||
this.onGoalCompleted();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns how much of the current shape is stored
|
||||
* @param {ShapeDefinition} definition
|
||||
* @returns {number}
|
||||
*/
|
||||
getShapesStored(definition) {
|
||||
return this.storedShapes[definition.getHash()] || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {number} amount
|
||||
*/
|
||||
takeShapeByKey(key, amount) {
|
||||
assert(this.getShapesStoredByKey(key) >= amount, "Can not afford: " + key + " x " + amount);
|
||||
assert(amount >= 0, "Amount < 0 for " + key);
|
||||
assert(Number.isInteger(amount), "Invalid amount: " + amount);
|
||||
this.storedShapes[key] = (this.storedShapes[key] || 0) - amount;
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns how much of the current shape is stored
|
||||
* @param {string} key
|
||||
* @returns {number}
|
||||
*/
|
||||
getShapesStoredByKey(key) {
|
||||
return this.storedShapes[key] || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns how much of the current goal was already delivered
|
||||
*/
|
||||
getCurrentGoalDelivered() {
|
||||
return this.getShapesStored(this.currentGoal.definition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current level of a given upgrade
|
||||
* @param {string} upgradeId
|
||||
*/
|
||||
getUpgradeLevel(upgradeId) {
|
||||
return this.upgradeLevels[upgradeId] || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given reward is already unlocked
|
||||
* @param {enumHubGoalRewards} reward
|
||||
*/
|
||||
isRewardUnlocked(reward) {
|
||||
if (G_IS_DEV && globalConfig.debug.allBuildingsUnlocked) {
|
||||
return true;
|
||||
}
|
||||
return !!this.gainedRewards[reward];
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the given definition, by either accounting it towards the
|
||||
* goal or otherwise granting some points
|
||||
* @param {ShapeDefinition} definition
|
||||
*/
|
||||
handleDefinitionDelivered(definition) {
|
||||
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 (
|
||||
this.storedShapes[targetHash] >= this.currentGoal.required ||
|
||||
(G_IS_DEV && globalConfig.debug.rewardsInstant)
|
||||
) {
|
||||
this.onGoalCompleted();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the next goal
|
||||
*/
|
||||
createNextGoal() {
|
||||
const storyIndex = this.level - 1;
|
||||
if (storyIndex < tutorialGoals.length) {
|
||||
const { shape, required, reward } = tutorialGoals[storyIndex];
|
||||
this.currentGoal = {
|
||||
/** @type {ShapeDefinition} */
|
||||
definition: this.root.shapeDefinitionMgr.getShapeFromShortKey(shape),
|
||||
required,
|
||||
reward,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentGoal = {
|
||||
/** @type {ShapeDefinition} */
|
||||
definition: this.createRandomShape(),
|
||||
required: 10000 + findNiceIntegerValue(this.level * 2000),
|
||||
reward: enumHubGoalRewards.no_reward_freeplay,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the level was completed
|
||||
*/
|
||||
onGoalCompleted() {
|
||||
const reward = this.currentGoal.reward;
|
||||
this.gainedRewards[reward] = (this.gainedRewards[reward] || 0) + 1;
|
||||
|
||||
this.root.app.gameAnalytics.handleLevelCompleted(this.level);
|
||||
++this.level;
|
||||
this.createNextGoal();
|
||||
|
||||
this.root.signals.storyGoalCompleted.dispatch(this.level - 1, reward);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether we are playing in free-play
|
||||
*/
|
||||
isFreePlay() {
|
||||
return this.level >= tutorialGoals.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a given upgrade can be unlocked
|
||||
* @param {string} upgradeId
|
||||
*/
|
||||
canUnlockUpgrade(upgradeId) {
|
||||
const handle = UPGRADES[upgradeId];
|
||||
const currentLevel = this.getUpgradeLevel(upgradeId);
|
||||
|
||||
if (currentLevel >= handle.tiers.length) {
|
||||
// Max level
|
||||
return false;
|
||||
}
|
||||
|
||||
if (G_IS_DEV && globalConfig.debug.upgradesNoCost) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const tierData = handle.tiers[currentLevel];
|
||||
|
||||
for (let i = 0; i < tierData.required.length; ++i) {
|
||||
const requirement = tierData.required[i];
|
||||
if ((this.storedShapes[requirement.shape] || 0) < requirement.amount) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of available upgrades
|
||||
* @returns {number}
|
||||
*/
|
||||
getAvailableUpgradeCount() {
|
||||
let count = 0;
|
||||
for (const upgradeId in UPGRADES) {
|
||||
if (this.canUnlockUpgrade(upgradeId)) {
|
||||
++count;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to unlock the given upgrade
|
||||
* @param {string} upgradeId
|
||||
* @returns {boolean}
|
||||
*/
|
||||
tryUnlockUpgrade(upgradeId) {
|
||||
if (!this.canUnlockUpgrade(upgradeId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const handle = UPGRADES[upgradeId];
|
||||
const currentLevel = this.getUpgradeLevel(upgradeId);
|
||||
|
||||
const tierData = handle.tiers[currentLevel];
|
||||
if (!tierData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (G_IS_DEV && globalConfig.debug.upgradesNoCost) {
|
||||
// Dont take resources
|
||||
} else {
|
||||
for (let i = 0; i < tierData.required.length; ++i) {
|
||||
const requirement = tierData.required[i];
|
||||
|
||||
// Notice: Don't have to check for hash here
|
||||
this.storedShapes[requirement.shape] -= requirement.amount;
|
||||
}
|
||||
}
|
||||
|
||||
this.upgradeLevels[upgradeId] = (this.upgradeLevels[upgradeId] || 0) + 1;
|
||||
this.upgradeImprovements[upgradeId] += tierData.improvement;
|
||||
|
||||
this.root.signals.upgradePurchased.dispatch(upgradeId);
|
||||
|
||||
this.root.app.gameAnalytics.handleUpgradeUnlocked(upgradeId, currentLevel);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {ShapeDefinition}
|
||||
*/
|
||||
createRandomShape() {
|
||||
const layerCount = clamp(this.level / 25, 2, 4);
|
||||
/** @type {Array<import("./shape_definition").ShapeLayer>} */
|
||||
let layers = [];
|
||||
|
||||
const randomColor = () => randomChoice(Object.values(enumColors));
|
||||
const randomShape = () => randomChoice(Object.values(enumSubShape));
|
||||
|
||||
let anyIsMissingTwo = false;
|
||||
|
||||
for (let i = 0; i < layerCount; ++i) {
|
||||
/** @type {import("./shape_definition").ShapeLayer} */
|
||||
const layer = [null, null, null, null];
|
||||
|
||||
for (let quad = 0; quad < 4; ++quad) {
|
||||
layer[quad] = {
|
||||
subShape: randomShape(),
|
||||
color: randomColor(),
|
||||
};
|
||||
}
|
||||
|
||||
// Sometimes shapes are missing
|
||||
if (Math.random() > 0.85) {
|
||||
layer[randomInt(0, 3)] = null;
|
||||
}
|
||||
|
||||
// Sometimes they actually are missing *two* ones!
|
||||
// Make sure at max only one layer is missing it though, otherwise we could
|
||||
// create an uncreateable shape
|
||||
if (Math.random() > 0.95 && !anyIsMissingTwo) {
|
||||
layer[randomInt(0, 3)] = null;
|
||||
anyIsMissingTwo = true;
|
||||
}
|
||||
|
||||
layers.push(layer);
|
||||
}
|
||||
|
||||
const definition = new ShapeDefinition({ layers });
|
||||
return this.root.shapeDefinitionMgr.registerOrReturnHandle(definition);
|
||||
}
|
||||
|
||||
////////////// HELPERS
|
||||
|
||||
/**
|
||||
* Belt speed
|
||||
* @returns {number} items / sec
|
||||
*/
|
||||
getBeltBaseSpeed() {
|
||||
return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Underground belt speed
|
||||
* @returns {number} items / sec
|
||||
*/
|
||||
getUndergroundBeltBaseSpeed() {
|
||||
return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Miner speed
|
||||
* @returns {number} items / sec
|
||||
*/
|
||||
getMinerBaseSpeed() {
|
||||
return globalConfig.minerSpeedItemsPerSecond * this.upgradeImprovements.miner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processor speed
|
||||
* @param {enumItemProcessorTypes} processorType
|
||||
* @returns {number} items / sec
|
||||
*/
|
||||
getProcessorBaseSpeed(processorType) {
|
||||
switch (processorType) {
|
||||
case enumItemProcessorTypes.splitterWires:
|
||||
return globalConfig.wiresSpeedItemsPerSecond * 2;
|
||||
|
||||
case enumItemProcessorTypes.trash:
|
||||
case enumItemProcessorTypes.hub:
|
||||
return 1e30;
|
||||
case enumItemProcessorTypes.splitter:
|
||||
return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt * 2;
|
||||
case enumItemProcessorTypes.filter:
|
||||
return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt;
|
||||
|
||||
case enumItemProcessorTypes.mixer:
|
||||
case enumItemProcessorTypes.painter:
|
||||
case enumItemProcessorTypes.painterDouble:
|
||||
case enumItemProcessorTypes.painterQuad: {
|
||||
assert(
|
||||
globalConfig.buildingSpeeds[processorType],
|
||||
"Processor type has no speed set in globalConfig.buildingSpeeds: " + processorType
|
||||
);
|
||||
return (
|
||||
globalConfig.beltSpeedItemsPerSecond *
|
||||
this.upgradeImprovements.painting *
|
||||
globalConfig.buildingSpeeds[processorType]
|
||||
);
|
||||
}
|
||||
|
||||
case enumItemProcessorTypes.cutter:
|
||||
case enumItemProcessorTypes.cutterQuad:
|
||||
case enumItemProcessorTypes.rotater:
|
||||
case enumItemProcessorTypes.rotaterCCW:
|
||||
case enumItemProcessorTypes.rotaterFL:
|
||||
case enumItemProcessorTypes.stacker: {
|
||||
assert(
|
||||
globalConfig.buildingSpeeds[processorType],
|
||||
"Processor type has no speed set in globalConfig.buildingSpeeds: " + processorType
|
||||
);
|
||||
return (
|
||||
globalConfig.beltSpeedItemsPerSecond *
|
||||
this.upgradeImprovements.processors *
|
||||
globalConfig.buildingSpeeds[processorType]
|
||||
);
|
||||
}
|
||||
default:
|
||||
assertAlways(false, "invalid processor type: " + processorType);
|
||||
}
|
||||
|
||||
return 1 / globalConfig.beltSpeedItemsPerSecond;
|
||||
}
|
||||
}
|
||||
import { globalConfig } from "../core/config";
|
||||
import { clamp, findNiceIntegerValue, randomChoice, randomInt } from "../core/utils";
|
||||
import { BasicSerializableObject, types } from "../savegame/serialization";
|
||||
import { enumColors } from "./colors";
|
||||
import { enumItemProcessorTypes } from "./components/item_processor";
|
||||
import { GameRoot } from "./root";
|
||||
import { enumSubShape, ShapeDefinition } from "./shape_definition";
|
||||
import { enumHubGoalRewards, tutorialGoals } from "./tutorial_goals";
|
||||
import { UPGRADES } from "./upgrades";
|
||||
|
||||
export class HubGoals extends BasicSerializableObject {
|
||||
static getId() {
|
||||
return "HubGoals";
|
||||
}
|
||||
|
||||
static getSchema() {
|
||||
return {
|
||||
level: types.uint,
|
||||
storedShapes: types.keyValueMap(types.uint),
|
||||
upgradeLevels: types.keyValueMap(types.uint),
|
||||
|
||||
currentGoal: types.structured({
|
||||
definition: types.knownType(ShapeDefinition),
|
||||
required: types.uint,
|
||||
reward: types.nullable(types.enum(enumHubGoalRewards)),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
deserialize(data) {
|
||||
const errorCode = super.deserialize(data);
|
||||
if (errorCode) {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
// Compute gained rewards
|
||||
for (let i = 0; i < this.level - 1; ++i) {
|
||||
if (i < tutorialGoals.length) {
|
||||
const reward = tutorialGoals[i].reward;
|
||||
this.gainedRewards[reward] = (this.gainedRewards[reward] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Compute upgrade improvements
|
||||
for (const upgradeId in UPGRADES) {
|
||||
const upgradeHandle = UPGRADES[upgradeId];
|
||||
const level = this.upgradeLevels[upgradeId] || 0;
|
||||
let totalImprovement = upgradeHandle.baseValue || 1;
|
||||
for (let i = 0; i < level; ++i) {
|
||||
totalImprovement += upgradeHandle.tiers[i].improvement;
|
||||
}
|
||||
this.upgradeImprovements[upgradeId] = totalImprovement;
|
||||
}
|
||||
|
||||
// Compute current goal
|
||||
const goal = tutorialGoals[this.level - 1];
|
||||
if (goal) {
|
||||
this.currentGoal = {
|
||||
/** @type {ShapeDefinition} */
|
||||
definition: this.root.shapeDefinitionMgr.getShapeFromShortKey(goal.shape),
|
||||
required: goal.required,
|
||||
reward: goal.reward,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {GameRoot} root
|
||||
*/
|
||||
constructor(root) {
|
||||
super();
|
||||
|
||||
this.root = root;
|
||||
|
||||
this.level = 1;
|
||||
|
||||
/**
|
||||
* Which story rewards we already gained
|
||||
* @type {Object.<string, number>}
|
||||
*/
|
||||
this.gainedRewards = {};
|
||||
|
||||
/**
|
||||
* Mapping from shape hash -> amount
|
||||
* @type {Object<string, number>}
|
||||
*/
|
||||
this.storedShapes = {};
|
||||
|
||||
/**
|
||||
* Stores the levels for all upgrades
|
||||
* @type {Object<string, number>}
|
||||
*/
|
||||
this.upgradeLevels = {};
|
||||
|
||||
/**
|
||||
* Stores the improvements for all upgrades
|
||||
* @type {Object<string, number>}
|
||||
*/
|
||||
this.upgradeImprovements = {};
|
||||
for (const key in UPGRADES) {
|
||||
this.upgradeImprovements[key] = UPGRADES[key].baseValue || 1;
|
||||
}
|
||||
|
||||
this.createNextGoal();
|
||||
|
||||
// Allow quickly switching goals in dev mode
|
||||
if (G_IS_DEV) {
|
||||
window.addEventListener("keydown", ev => {
|
||||
if (ev.key === "b") {
|
||||
// root is not guaranteed to exist within ~0.5s after loading in
|
||||
if (this.root && this.root.app && this.root.app.gameAnalytics) {
|
||||
this.onGoalCompleted();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns how much of the current shape is stored
|
||||
* @param {ShapeDefinition} definition
|
||||
* @returns {number}
|
||||
*/
|
||||
getShapesStored(definition) {
|
||||
return this.storedShapes[definition.getHash()] || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {number} amount
|
||||
*/
|
||||
takeShapeByKey(key, amount) {
|
||||
assert(this.getShapesStoredByKey(key) >= amount, "Can not afford: " + key + " x " + amount);
|
||||
assert(amount >= 0, "Amount < 0 for " + key);
|
||||
assert(Number.isInteger(amount), "Invalid amount: " + amount);
|
||||
this.storedShapes[key] = (this.storedShapes[key] || 0) - amount;
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns how much of the current shape is stored
|
||||
* @param {string} key
|
||||
* @returns {number}
|
||||
*/
|
||||
getShapesStoredByKey(key) {
|
||||
return this.storedShapes[key] || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns how much of the current goal was already delivered
|
||||
*/
|
||||
getCurrentGoalDelivered() {
|
||||
return this.getShapesStored(this.currentGoal.definition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current level of a given upgrade
|
||||
* @param {string} upgradeId
|
||||
*/
|
||||
getUpgradeLevel(upgradeId) {
|
||||
return this.upgradeLevels[upgradeId] || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given reward is already unlocked
|
||||
* @param {enumHubGoalRewards} reward
|
||||
*/
|
||||
isRewardUnlocked(reward) {
|
||||
if (G_IS_DEV && globalConfig.debug.allBuildingsUnlocked) {
|
||||
return true;
|
||||
}
|
||||
return !!this.gainedRewards[reward];
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the given definition, by either accounting it towards the
|
||||
* goal or otherwise granting some points
|
||||
* @param {ShapeDefinition} definition
|
||||
*/
|
||||
handleDefinitionDelivered(definition) {
|
||||
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 (
|
||||
this.storedShapes[targetHash] >= this.currentGoal.required ||
|
||||
(G_IS_DEV && globalConfig.debug.rewardsInstant)
|
||||
) {
|
||||
this.onGoalCompleted();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the next goal
|
||||
*/
|
||||
createNextGoal() {
|
||||
const storyIndex = this.level - 1;
|
||||
if (storyIndex < tutorialGoals.length) {
|
||||
const { shape, required, reward } = tutorialGoals[storyIndex];
|
||||
this.currentGoal = {
|
||||
/** @type {ShapeDefinition} */
|
||||
definition: this.root.shapeDefinitionMgr.getShapeFromShortKey(shape),
|
||||
required,
|
||||
reward,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentGoal = {
|
||||
/** @type {ShapeDefinition} */
|
||||
definition: this.createRandomShape(),
|
||||
required: 10000 + findNiceIntegerValue(this.level * 2000),
|
||||
reward: enumHubGoalRewards.no_reward_freeplay,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the level was completed
|
||||
*/
|
||||
onGoalCompleted() {
|
||||
const reward = this.currentGoal.reward;
|
||||
this.gainedRewards[reward] = (this.gainedRewards[reward] || 0) + 1;
|
||||
|
||||
this.root.app.gameAnalytics.handleLevelCompleted(this.level);
|
||||
++this.level;
|
||||
this.createNextGoal();
|
||||
|
||||
this.root.signals.storyGoalCompleted.dispatch(this.level - 1, reward);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether we are playing in free-play
|
||||
*/
|
||||
isFreePlay() {
|
||||
return this.level >= tutorialGoals.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a given upgrade can be unlocked
|
||||
* @param {string} upgradeId
|
||||
*/
|
||||
canUnlockUpgrade(upgradeId) {
|
||||
const handle = UPGRADES[upgradeId];
|
||||
const currentLevel = this.getUpgradeLevel(upgradeId);
|
||||
|
||||
if (currentLevel >= handle.tiers.length) {
|
||||
// Max level
|
||||
return false;
|
||||
}
|
||||
|
||||
if (G_IS_DEV && globalConfig.debug.upgradesNoCost) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const tierData = handle.tiers[currentLevel];
|
||||
|
||||
for (let i = 0; i < tierData.required.length; ++i) {
|
||||
const requirement = tierData.required[i];
|
||||
if ((this.storedShapes[requirement.shape] || 0) < requirement.amount) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of available upgrades
|
||||
* @returns {number}
|
||||
*/
|
||||
getAvailableUpgradeCount() {
|
||||
let count = 0;
|
||||
for (const upgradeId in UPGRADES) {
|
||||
if (this.canUnlockUpgrade(upgradeId)) {
|
||||
++count;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to unlock the given upgrade
|
||||
* @param {string} upgradeId
|
||||
* @returns {boolean}
|
||||
*/
|
||||
tryUnlockUpgrade(upgradeId) {
|
||||
if (!this.canUnlockUpgrade(upgradeId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const handle = UPGRADES[upgradeId];
|
||||
const currentLevel = this.getUpgradeLevel(upgradeId);
|
||||
|
||||
const tierData = handle.tiers[currentLevel];
|
||||
if (!tierData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (G_IS_DEV && globalConfig.debug.upgradesNoCost) {
|
||||
// Dont take resources
|
||||
} else {
|
||||
for (let i = 0; i < tierData.required.length; ++i) {
|
||||
const requirement = tierData.required[i];
|
||||
|
||||
// Notice: Don't have to check for hash here
|
||||
this.storedShapes[requirement.shape] -= requirement.amount;
|
||||
}
|
||||
}
|
||||
|
||||
this.upgradeLevels[upgradeId] = (this.upgradeLevels[upgradeId] || 0) + 1;
|
||||
this.upgradeImprovements[upgradeId] += tierData.improvement;
|
||||
|
||||
this.root.signals.upgradePurchased.dispatch(upgradeId);
|
||||
|
||||
this.root.app.gameAnalytics.handleUpgradeUnlocked(upgradeId, currentLevel);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {ShapeDefinition}
|
||||
*/
|
||||
createRandomShape() {
|
||||
const layerCount = clamp(this.level / 25, 2, 4);
|
||||
/** @type {Array<import("./shape_definition").ShapeLayer>} */
|
||||
let layers = [];
|
||||
|
||||
const randomColor = () => randomChoice(Object.values(enumColors));
|
||||
const randomShape = () => randomChoice(Object.values(enumSubShape));
|
||||
|
||||
let anyIsMissingTwo = false;
|
||||
|
||||
for (let i = 0; i < layerCount; ++i) {
|
||||
/** @type {import("./shape_definition").ShapeLayer} */
|
||||
const layer = [null, null, null, null];
|
||||
|
||||
for (let quad = 0; quad < 4; ++quad) {
|
||||
layer[quad] = {
|
||||
subShape: randomShape(),
|
||||
color: randomColor(),
|
||||
};
|
||||
}
|
||||
|
||||
// Sometimes shapes are missing
|
||||
if (Math.random() > 0.85) {
|
||||
layer[randomInt(0, 3)] = null;
|
||||
}
|
||||
|
||||
// Sometimes they actually are missing *two* ones!
|
||||
// Make sure at max only one layer is missing it though, otherwise we could
|
||||
// create an uncreateable shape
|
||||
if (Math.random() > 0.95 && !anyIsMissingTwo) {
|
||||
layer[randomInt(0, 3)] = null;
|
||||
anyIsMissingTwo = true;
|
||||
}
|
||||
|
||||
layers.push(layer);
|
||||
}
|
||||
|
||||
const definition = new ShapeDefinition({ layers });
|
||||
return this.root.shapeDefinitionMgr.registerOrReturnHandle(definition);
|
||||
}
|
||||
|
||||
////////////// HELPERS
|
||||
|
||||
/**
|
||||
* Belt speed
|
||||
* @returns {number} items / sec
|
||||
*/
|
||||
getBeltBaseSpeed() {
|
||||
return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Underground belt speed
|
||||
* @returns {number} items / sec
|
||||
*/
|
||||
getUndergroundBeltBaseSpeed() {
|
||||
return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Miner speed
|
||||
* @returns {number} items / sec
|
||||
*/
|
||||
getMinerBaseSpeed() {
|
||||
return globalConfig.minerSpeedItemsPerSecond * this.upgradeImprovements.miner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processor speed
|
||||
* @param {enumItemProcessorTypes} processorType
|
||||
* @returns {number} items / sec
|
||||
*/
|
||||
getProcessorBaseSpeed(processorType) {
|
||||
switch (processorType) {
|
||||
case enumItemProcessorTypes.splitterWires:
|
||||
return globalConfig.wiresSpeedItemsPerSecond * 2;
|
||||
|
||||
case enumItemProcessorTypes.trash:
|
||||
case enumItemProcessorTypes.hub:
|
||||
return 1e30;
|
||||
case enumItemProcessorTypes.splitter:
|
||||
return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt * 2;
|
||||
case enumItemProcessorTypes.filter:
|
||||
case enumItemProcessorTypes.reader:
|
||||
return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt;
|
||||
|
||||
case enumItemProcessorTypes.mixer:
|
||||
case enumItemProcessorTypes.painter:
|
||||
case enumItemProcessorTypes.painterDouble:
|
||||
case enumItemProcessorTypes.painterQuad: {
|
||||
assert(
|
||||
globalConfig.buildingSpeeds[processorType],
|
||||
"Processor type has no speed set in globalConfig.buildingSpeeds: " + processorType
|
||||
);
|
||||
return (
|
||||
globalConfig.beltSpeedItemsPerSecond *
|
||||
this.upgradeImprovements.painting *
|
||||
globalConfig.buildingSpeeds[processorType]
|
||||
);
|
||||
}
|
||||
|
||||
case enumItemProcessorTypes.cutter:
|
||||
case enumItemProcessorTypes.cutterQuad:
|
||||
case enumItemProcessorTypes.rotater:
|
||||
case enumItemProcessorTypes.rotaterCCW:
|
||||
case enumItemProcessorTypes.rotaterFL:
|
||||
case enumItemProcessorTypes.stacker: {
|
||||
assert(
|
||||
globalConfig.buildingSpeeds[processorType],
|
||||
"Processor type has no speed set in globalConfig.buildingSpeeds: " + processorType
|
||||
);
|
||||
return (
|
||||
globalConfig.beltSpeedItemsPerSecond *
|
||||
this.upgradeImprovements.processors *
|
||||
globalConfig.buildingSpeeds[processorType]
|
||||
);
|
||||
}
|
||||
default:
|
||||
assertAlways(false, "invalid processor type: " + processorType);
|
||||
}
|
||||
|
||||
return 1 / globalConfig.beltSpeedItemsPerSecond;
|
||||
}
|
||||
}
|
||||
|
@ -1,41 +1,43 @@
|
||||
import { MetaBeltBaseBuilding } from "../../buildings/belt_base";
|
||||
import { MetaCutterBuilding } from "../../buildings/cutter";
|
||||
import { MetaMinerBuilding } from "../../buildings/miner";
|
||||
import { MetaMixerBuilding } from "../../buildings/mixer";
|
||||
import { MetaPainterBuilding } from "../../buildings/painter";
|
||||
import { MetaRotaterBuilding } from "../../buildings/rotater";
|
||||
import { MetaSplitterBuilding } from "../../buildings/splitter";
|
||||
import { MetaStackerBuilding } from "../../buildings/stacker";
|
||||
import { MetaTrashBuilding } from "../../buildings/trash";
|
||||
import { MetaUndergroundBeltBuilding } from "../../buildings/underground_belt";
|
||||
import { HUDBaseToolbar } from "./base_toolbar";
|
||||
import { MetaLeverBuilding } from "../../buildings/lever";
|
||||
import { MetaFilterBuilding } from "../../buildings/filter";
|
||||
import { MetaDisplayBuilding } from "../../buildings/display";
|
||||
|
||||
const supportedBuildings = [
|
||||
MetaBeltBaseBuilding,
|
||||
MetaSplitterBuilding,
|
||||
MetaUndergroundBeltBuilding,
|
||||
MetaMinerBuilding,
|
||||
MetaCutterBuilding,
|
||||
MetaRotaterBuilding,
|
||||
MetaStackerBuilding,
|
||||
MetaMixerBuilding,
|
||||
MetaPainterBuilding,
|
||||
MetaTrashBuilding,
|
||||
MetaLeverBuilding,
|
||||
MetaFilterBuilding,
|
||||
MetaDisplayBuilding,
|
||||
];
|
||||
|
||||
export class HUDBuildingsToolbar extends HUDBaseToolbar {
|
||||
constructor(root) {
|
||||
super(root, {
|
||||
supportedBuildings,
|
||||
visibilityCondition: () =>
|
||||
!this.root.camera.getIsMapOverlayActive() && this.root.currentLayer === "regular",
|
||||
htmlElementId: "ingame_HUD_buildings_toolbar",
|
||||
});
|
||||
}
|
||||
}
|
||||
import { MetaBeltBaseBuilding } from "../../buildings/belt_base";
|
||||
import { MetaCutterBuilding } from "../../buildings/cutter";
|
||||
import { MetaMinerBuilding } from "../../buildings/miner";
|
||||
import { MetaMixerBuilding } from "../../buildings/mixer";
|
||||
import { MetaPainterBuilding } from "../../buildings/painter";
|
||||
import { MetaRotaterBuilding } from "../../buildings/rotater";
|
||||
import { MetaSplitterBuilding } from "../../buildings/splitter";
|
||||
import { MetaStackerBuilding } from "../../buildings/stacker";
|
||||
import { MetaTrashBuilding } from "../../buildings/trash";
|
||||
import { MetaUndergroundBeltBuilding } from "../../buildings/underground_belt";
|
||||
import { HUDBaseToolbar } from "./base_toolbar";
|
||||
import { MetaLeverBuilding } from "../../buildings/lever";
|
||||
import { MetaFilterBuilding } from "../../buildings/filter";
|
||||
import { MetaDisplayBuilding } from "../../buildings/display";
|
||||
import { MetaReaderBuilding } from "../../buildings/reader";
|
||||
|
||||
const supportedBuildings = [
|
||||
MetaBeltBaseBuilding,
|
||||
MetaSplitterBuilding,
|
||||
MetaUndergroundBeltBuilding,
|
||||
MetaMinerBuilding,
|
||||
MetaCutterBuilding,
|
||||
MetaRotaterBuilding,
|
||||
MetaStackerBuilding,
|
||||
MetaMixerBuilding,
|
||||
MetaPainterBuilding,
|
||||
MetaTrashBuilding,
|
||||
MetaLeverBuilding,
|
||||
MetaFilterBuilding,
|
||||
MetaDisplayBuilding,
|
||||
MetaReaderBuilding,
|
||||
];
|
||||
|
||||
export class HUDBuildingsToolbar extends HUDBaseToolbar {
|
||||
constructor(root) {
|
||||
super(root, {
|
||||
supportedBuildings,
|
||||
visibilityCondition: () =>
|
||||
!this.root.camera.getIsMapOverlayActive() && this.root.currentLayer === "regular",
|
||||
htmlElementId: "ingame_HUD_buildings_toolbar",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,40 @@
|
||||
import { GameSystemWithFilter } from "../game_system_with_filter";
|
||||
import { BeltReaderComponent } from "../components/belt_reader";
|
||||
import { globalConfig } from "../../core/config";
|
||||
import { BOOL_TRUE_SINGLETON, BOOL_FALSE_SINGLETON } from "../items/boolean_item";
|
||||
|
||||
export class BeltReaderSystem extends GameSystemWithFilter {
|
||||
constructor(root) {
|
||||
super(root, [BeltReaderComponent]);
|
||||
}
|
||||
|
||||
update() {
|
||||
const now = this.root.time.now();
|
||||
const minimumTime = now - globalConfig.readerAnalyzeIntervalSeconds;
|
||||
const minimumTimeForThroughput = now - 1;
|
||||
for (let i = 0; i < this.allEntities.length; ++i) {
|
||||
const entity = this.allEntities[i];
|
||||
|
||||
const readerComp = entity.components.BeltReader;
|
||||
const pinsComp = entity.components.WiredPins;
|
||||
|
||||
// Remove outdated items
|
||||
while (readerComp.lastItemTimes[0] < minimumTime) {
|
||||
readerComp.lastItemTimes.shift();
|
||||
}
|
||||
|
||||
pinsComp.slots[1].value = readerComp.lastItem;
|
||||
pinsComp.slots[0].value =
|
||||
(readerComp.lastItemTimes[readerComp.lastItemTimes.length - 1] || 0) >
|
||||
minimumTimeForThroughput
|
||||
? BOOL_TRUE_SINGLETON
|
||||
: BOOL_FALSE_SINGLETON;
|
||||
|
||||
if (now - readerComp.lastThroughputComputation > 0.5) {
|
||||
readerComp.lastThroughputComputation = now;
|
||||
readerComp.lastThroughput =
|
||||
readerComp.lastItemTimes.length / globalConfig.readerAnalyzeIntervalSeconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|