2020-05-09 14:45:23 +00:00
|
|
|
import { Math_random } from "../core/builtins";
|
2020-05-14 19:54:11 +00:00
|
|
|
import { globalConfig } from "../core/config";
|
2020-05-13 08:41:00 +00:00
|
|
|
import { queryParamOptions } from "../core/query_parameters";
|
2020-05-14 19:54:11 +00:00
|
|
|
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";
|
2020-05-28 16:07:57 +00:00
|
|
|
import { UPGRADES, blueprintShape } from "./upgrades";
|
2020-05-09 14:45:23 +00:00
|
|
|
|
|
|
|
export class HubGoals extends BasicSerializableObject {
|
|
|
|
static getId() {
|
|
|
|
return "HubGoals";
|
|
|
|
}
|
|
|
|
|
2020-05-14 19:54:11 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-05-14 20:20:19 +00:00
|
|
|
// Compute gained rewards
|
2020-05-18 17:47:40 +00:00
|
|
|
for (let i = 0; i < this.level - 1; ++i) {
|
2020-05-14 20:20:19 +00:00
|
|
|
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;
|
|
|
|
}
|
2020-05-14 19:54:11 +00:00
|
|
|
}
|
|
|
|
|
2020-05-09 14:45:23 +00:00
|
|
|
/**
|
|
|
|
* @param {GameRoot} root
|
|
|
|
*/
|
|
|
|
constructor(root) {
|
|
|
|
super();
|
|
|
|
|
|
|
|
this.root = root;
|
|
|
|
|
|
|
|
this.level = 1;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Which story rewards we already gained
|
2020-05-14 19:54:11 +00:00
|
|
|
* @type {Object.<string, number>}
|
2020-05-09 14:45:23 +00:00
|
|
|
*/
|
|
|
|
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();
|
|
|
|
|
2020-06-03 14:16:41 +00:00
|
|
|
// Allow quickly switching goals in dev mode
|
2020-05-09 14:45:23 +00:00
|
|
|
if (G_IS_DEV) {
|
2020-06-03 14:16:41 +00:00
|
|
|
if (G_IS_DEV) {
|
|
|
|
window.addEventListener("keydown", ev => {
|
|
|
|
if (ev.key === "b") {
|
|
|
|
this.onGoalCompleted();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2020-05-09 14:45:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns how much of the current shape is stored
|
|
|
|
* @param {ShapeDefinition} definition
|
|
|
|
* @returns {number}
|
|
|
|
*/
|
|
|
|
getShapesStored(definition) {
|
|
|
|
return this.storedShapes[definition.getHash()] || 0;
|
|
|
|
}
|
2020-05-28 16:07:57 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {string} key
|
|
|
|
* @param {number} amount
|
|
|
|
*/
|
|
|
|
takeShapeByKey(key, amount) {
|
|
|
|
assert(this.getShapesStoredByKey(key) >= amount, "Can not afford: " + key + " x " + amount);
|
2020-05-30 15:50:29 +00:00
|
|
|
assert(amount >= 0, "Amount < 0 for " + key);
|
2020-05-28 16:07:57 +00:00
|
|
|
assert(Number.isInteger(amount), "Invalid amount: " + amount);
|
2020-05-30 15:50:29 +00:00
|
|
|
this.storedShapes[key] = (this.storedShapes[key] || 0) - amount;
|
2020-05-28 16:07:57 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-05-14 11:29:42 +00:00
|
|
|
/**
|
|
|
|
* Returns how much of the current shape is stored
|
|
|
|
* @param {string} key
|
|
|
|
* @returns {number}
|
|
|
|
*/
|
|
|
|
getShapesStoredByKey(key) {
|
|
|
|
return this.storedShapes[key] || 0;
|
|
|
|
}
|
2020-05-09 14:45:23 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
|
2020-05-13 16:04:51 +00:00
|
|
|
this.root.signals.shapeDelivered.dispatch(definition);
|
|
|
|
|
2020-05-09 14:45:23 +00:00
|
|
|
// 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} */
|
2020-05-13 16:04:51 +00:00
|
|
|
definition: this.root.shapeDefinitionMgr.getShapeFromShortKey(shape),
|
2020-05-09 14:45:23 +00:00
|
|
|
required,
|
|
|
|
reward,
|
|
|
|
};
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.currentGoal = {
|
|
|
|
/** @type {ShapeDefinition} */
|
|
|
|
definition: this.createRandomShape(),
|
|
|
|
required: 1000 + findNiceIntegerValue(this.level * 47.5),
|
2020-05-23 08:35:30 +00:00
|
|
|
reward: enumHubGoalRewards.no_reward_freeplay,
|
2020-05-09 14:45:23 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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();
|
2020-05-14 11:29:42 +00:00
|
|
|
|
|
|
|
this.root.signals.storyGoalCompleted.dispatch(this.level - 1, reward);
|
2020-05-09 14:45:23 +00:00
|
|
|
}
|
|
|
|
|
2020-05-23 08:35:30 +00:00
|
|
|
/**
|
|
|
|
* Returns whether we are playing in free-play
|
|
|
|
*/
|
|
|
|
isFreePlay() {
|
|
|
|
return this.level >= tutorialGoals.length;
|
|
|
|
}
|
|
|
|
|
2020-05-09 14:45:23 +00:00
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
|
|
|
|
2020-05-14 10:13:33 +00:00
|
|
|
/**
|
|
|
|
* Returns the number of available upgrades
|
|
|
|
* @returns {number}
|
|
|
|
*/
|
|
|
|
getAvailableUpgradeCount() {
|
|
|
|
let count = 0;
|
|
|
|
for (const upgradeId in UPGRADES) {
|
|
|
|
if (this.canUnlockUpgrade(upgradeId)) {
|
|
|
|
++count;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return count;
|
|
|
|
}
|
|
|
|
|
2020-05-09 14:45:23 +00:00
|
|
|
/**
|
|
|
|
* Tries to unlock the given upgrade
|
|
|
|
* @param {string} upgradeId
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
|
|
|
tryUnlockUgprade(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 / 50, 2, 4);
|
|
|
|
/** @type {Array<import("./shape_definition").ShapeLayer>} */
|
|
|
|
let layers = [];
|
|
|
|
|
|
|
|
// @ts-ignore
|
|
|
|
const randomColor = () => randomChoice(Object.values(enumColors));
|
|
|
|
// @ts-ignore
|
|
|
|
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.trash:
|
2020-05-09 15:12:40 +00:00
|
|
|
case enumItemProcessorTypes.hub:
|
2020-05-09 14:45:23 +00:00
|
|
|
return 1e30;
|
|
|
|
case enumItemProcessorTypes.splitter:
|
2020-05-13 16:04:51 +00:00
|
|
|
return globalConfig.beltSpeedItemsPerSecond * this.upgradeImprovements.belt * 2;
|
2020-05-22 07:27:20 +00:00
|
|
|
|
|
|
|
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]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-05-09 14:45:23 +00:00
|
|
|
case enumItemProcessorTypes.cutter:
|
2020-05-16 22:21:33 +00:00
|
|
|
case enumItemProcessorTypes.cutterQuad:
|
2020-05-09 14:45:23 +00:00
|
|
|
case enumItemProcessorTypes.rotater:
|
2020-05-16 21:13:45 +00:00
|
|
|
case enumItemProcessorTypes.rotaterCCW:
|
2020-05-22 07:27:20 +00:00
|
|
|
case enumItemProcessorTypes.stacker: {
|
2020-05-16 20:45:40 +00:00
|
|
|
assert(
|
|
|
|
globalConfig.buildingSpeeds[processorType],
|
|
|
|
"Processor type has no speed set in globalConfig.buildingSpeeds: " + processorType
|
|
|
|
);
|
2020-05-09 14:45:23 +00:00
|
|
|
return (
|
2020-05-13 16:04:51 +00:00
|
|
|
globalConfig.beltSpeedItemsPerSecond *
|
2020-05-09 15:22:27 +00:00
|
|
|
this.upgradeImprovements.processors *
|
2020-05-09 14:45:23 +00:00
|
|
|
globalConfig.buildingSpeeds[processorType]
|
|
|
|
);
|
2020-05-22 07:27:20 +00:00
|
|
|
}
|
2020-05-09 14:45:23 +00:00
|
|
|
default:
|
2020-05-09 15:12:40 +00:00
|
|
|
assertAlways(false, "invalid processor type: " + processorType);
|
2020-05-09 14:45:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return 1 / globalConfig.beltSpeedItemsPerSecond;
|
|
|
|
}
|
|
|
|
}
|