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.} */ this.gainedRewards = {}; /** * Mapping from shape hash -> amount * @type {Object} */ this.storedShapes = {}; /** * Stores the levels for all upgrades * @type {Object} */ this.upgradeLevels = {}; /** * Stores the improvements for all upgrades * @type {Object} */ 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} */ 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; } }