diff --git a/src/js/game/blueprint.js b/src/js/game/blueprint.js index c26fb12f..63989393 100644 --- a/src/js/game/blueprint.js +++ b/src/js/game/blueprint.js @@ -1,13 +1,9 @@ import { globalConfig } from "../core/config"; import { DrawParameters } from "../core/draw_parameters"; -import { createLogger } from "../core/logging"; import { findNiceIntegerValue } from "../core/utils"; import { Vector } from "../core/vector"; import { Entity } from "./entity"; import { GameRoot } from "./root"; -import { blueprintShape } from "./upgrades"; - -const logger = createLogger("blueprint"); export class Blueprint { /** @@ -142,7 +138,7 @@ export class Blueprint { * @param {GameRoot} root */ canAfford(root) { - return root.hubGoals.getShapesStoredByKey(blueprintShape) >= this.getCost(); + return root.hubGoals.getShapesStoredByKey(root.gameMode.getBlueprintShapeKey()) >= this.getCost(); } /** diff --git a/src/js/game/core.js b/src/js/game/core.js index 306643f9..2df8989f 100644 --- a/src/js/game/core.js +++ b/src/js/game/core.js @@ -31,6 +31,7 @@ import { KeyActionMapper } from "./key_action_mapper"; import { GameLogic } from "./logic"; import { MapView } from "./map_view"; import { defaultBuildingVariant } from "./meta_building"; +import { RegularGameMode } from "./modes/regular"; import { ProductionAnalytics } from "./production_analytics"; import { GameRoot } from "./root"; import { ShapeDefinitionManager } from "./shape_definition_manager"; @@ -101,6 +102,9 @@ export class GameCore { // Needs to come first root.dynamicTickrate = new DynamicTickrate(root); + // Init game mode + root.gameMode = new RegularGameMode(root); + // Init classes root.camera = new Camera(root); root.map = new MapView(root); diff --git a/src/js/game/game_mode.js b/src/js/game/game_mode.js new file mode 100644 index 00000000..15403eb5 --- /dev/null +++ b/src/js/game/game_mode.js @@ -0,0 +1,71 @@ +/* typehints:start */ +import { enumHubGoalRewards } from "./tutorial_goals"; +/* typehints:end */ + +import { GameRoot } from "./root"; + +/** @typedef {{ + * shape: string, + * amount: number + * }} UpgradeRequirement */ + +/** @typedef {{ + * required: Array + * improvement?: number, + * excludePrevious?: boolean + * }} TierRequirement */ + +/** @typedef {Array} UpgradeTiers */ + +/** @typedef {{ + * shape: string, + * required: number, + * reward: enumHubGoalRewards, + * throughputOnly?: boolean + * }} LevelDefinition */ + +export class GameMode { + /** + * + * @param {GameRoot} root + */ + constructor(root) { + this.root = root; + } + + /** + * Should return all available upgrades + * @returns {Object} + */ + getUpgrades() { + abstract; + return null; + } + + /** + * Returns the blueprint shape key + * @returns {string} + */ + getBlueprintShapeKey() { + abstract; + return null; + } + + /** + * Returns the goals for all levels including their reward + * @returns {Array} + */ + getLevelDefinitions() { + abstract; + return null; + } + + /** + * Should return whether free play is available or if the game stops + * after the predefined levels + * @returns {boolean} + */ + getIsFreeplayAvailable() { + return true; + } +} diff --git a/src/js/game/hub_goals.js b/src/js/game/hub_goals.js index e01ab868..75501960 100644 --- a/src/js/game/hub_goals.js +++ b/src/js/game/hub_goals.js @@ -1,14 +1,13 @@ import { globalConfig, IS_DEMO } from "../core/config"; import { RandomNumberGenerator } from "../core/rng"; -import { clamp, findNiceIntegerValue, randomChoice, randomInt } from "../core/utils"; +import { clamp } from "../core/utils"; import { BasicSerializableObject, types } from "../savegame/serialization"; import { enumColors } from "./colors"; import { enumItemProcessorTypes } from "./components/item_processor"; import { enumAnalyticsDataSource } from "./production_analytics"; import { GameRoot } from "./root"; import { enumSubShape, ShapeDefinition } from "./shape_definition"; -import { enumHubGoalRewards, tutorialGoals } from "./tutorial_goals"; -import { UPGRADES } from "./upgrades"; +import { enumHubGoalRewards } from "./tutorial_goals"; export class HubGoals extends BasicSerializableObject { static getId() { @@ -23,27 +22,36 @@ export class HubGoals extends BasicSerializableObject { }; } - deserialize(data) { + /** + * + * @param {*} data + * @param {GameRoot} root + */ + deserialize(data, root) { const errorCode = super.deserialize(data); if (errorCode) { return errorCode; } - if (IS_DEMO) { - this.level = Math.min(this.level, tutorialGoals.length); + const levels = root.gameMode.getLevelDefinitions(); + + // If freeplay is not available, clamp the level + if (!root.gameMode.getIsFreeplayAvailable()) { + this.level = Math.min(this.level, levels.length); } // Compute gained rewards for (let i = 0; i < this.level - 1; ++i) { - if (i < tutorialGoals.length) { - const reward = tutorialGoals[i].reward; + if (i < levels.length) { + const reward = levels[i].reward; this.gainedRewards[reward] = (this.gainedRewards[reward] || 0) + 1; } } // Compute upgrade improvements - for (const upgradeId in UPGRADES) { - const tiers = UPGRADES[upgradeId]; + const upgrades = this.root.gameMode.getUpgrades(); + for (const upgradeId in upgrades) { + const tiers = upgrades[upgradeId]; const level = this.upgradeLevels[upgradeId] || 0; let totalImprovement = 1; for (let i = 0; i < level; ++i) { @@ -84,17 +92,16 @@ export class HubGoals extends BasicSerializableObject { */ this.upgradeLevels = {}; - // Reset levels - for (const key in UPGRADES) { - this.upgradeLevels[key] = 0; - } - /** * Stores the improvements for all upgrades * @type {Object} */ this.upgradeImprovements = {}; - for (const key in UPGRADES) { + + // Reset levels first + const upgrades = this.root.gameMode.getUpgrades(); + for (const key in upgrades) { + this.upgradeLevels[key] = 0; this.upgradeImprovements[key] = 1; } @@ -120,7 +127,10 @@ export class HubGoals extends BasicSerializableObject { * @returns {boolean} */ isEndOfDemoReached() { - return IS_DEMO && this.level >= tutorialGoals.length; + return ( + !this.root.gameMode.getIsFreeplayAvailable() && + this.level >= this.root.gameMode.getLevelDefinitions().length + ); } /** @@ -215,8 +225,9 @@ export class HubGoals extends BasicSerializableObject { */ computeNextGoal() { const storyIndex = this.level - 1; - if (storyIndex < tutorialGoals.length) { - const { shape, required, reward, throughputOnly } = tutorialGoals[storyIndex]; + const levels = this.root.gameMode.getLevelDefinitions(); + if (storyIndex < levels.length) { + const { shape, required, reward, throughputOnly } = levels[storyIndex]; this.currentGoal = { /** @type {ShapeDefinition} */ definition: this.root.shapeDefinitionMgr.getShapeFromShortKey(shape), @@ -254,7 +265,7 @@ export class HubGoals extends BasicSerializableObject { * Returns whether we are playing in free-play */ isFreePlay() { - return this.level >= tutorialGoals.length; + return this.level >= this.root.gameMode.getLevelDefinitions().length; } /** @@ -262,7 +273,7 @@ export class HubGoals extends BasicSerializableObject { * @param {string} upgradeId */ canUnlockUpgrade(upgradeId) { - const tiers = UPGRADES[upgradeId]; + const tiers = this.root.gameMode.getUpgrades()[upgradeId]; const currentLevel = this.getUpgradeLevel(upgradeId); if (currentLevel >= tiers.length) { @@ -296,7 +307,7 @@ export class HubGoals extends BasicSerializableObject { */ getAvailableUpgradeCount() { let count = 0; - for (const upgradeId in UPGRADES) { + for (const upgradeId in this.root.gameMode.getUpgrades()) { if (this.canUnlockUpgrade(upgradeId)) { ++count; } @@ -314,7 +325,7 @@ export class HubGoals extends BasicSerializableObject { return false; } - const upgradeTiers = UPGRADES[upgradeId]; + const upgradeTiers = this.root.gameMode.getUpgrades()[upgradeId]; const currentLevel = this.getUpgradeLevel(upgradeId); const tierData = upgradeTiers[currentLevel]; diff --git a/src/js/game/hud/parts/blueprint_placer.js b/src/js/game/hud/parts/blueprint_placer.js index 47bf1363..e1040c3b 100644 --- a/src/js/game/hud/parts/blueprint_placer.js +++ b/src/js/game/hud/parts/blueprint_placer.js @@ -1,202 +1,203 @@ -import { DrawParameters } from "../../../core/draw_parameters"; -import { STOP_PROPAGATION } from "../../../core/signal"; -import { TrackedState } from "../../../core/tracked_state"; -import { makeDiv } from "../../../core/utils"; -import { Vector } from "../../../core/vector"; -import { T } from "../../../translations"; -import { enumMouseButton } from "../../camera"; -import { KEYMAPPINGS } from "../../key_action_mapper"; -import { blueprintShape } from "../../upgrades"; -import { BaseHUDPart } from "../base_hud_part"; -import { DynamicDomAttach } from "../dynamic_dom_attach"; -import { Blueprint } from "../../blueprint"; -import { SOUNDS } from "../../../platform/sound"; - -export class HUDBlueprintPlacer extends BaseHUDPart { - createElements(parent) { - const blueprintCostShape = this.root.shapeDefinitionMgr.getShapeFromShortKey(blueprintShape); - const blueprintCostShapeCanvas = blueprintCostShape.generateAsCanvas(80); - - this.costDisplayParent = makeDiv(parent, "ingame_HUD_BlueprintPlacer", [], ``); - - makeDiv(this.costDisplayParent, null, ["label"], T.ingame.blueprintPlacer.cost); - const costContainer = makeDiv(this.costDisplayParent, null, ["costContainer"], ""); - this.costDisplayText = makeDiv(costContainer, null, ["costText"], ""); - costContainer.appendChild(blueprintCostShapeCanvas); - } - - initialize() { - this.root.hud.signals.buildingsSelectedForCopy.add(this.createBlueprintFromBuildings, this); - - /** @type {TypedTrackedState} */ - this.currentBlueprint = new TrackedState(this.onBlueprintChanged, this); - /** @type {Blueprint?} */ - this.lastBlueprintUsed = null; - - const keyActionMapper = this.root.keyMapper; - keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.abortPlacement, this); - keyActionMapper.getBinding(KEYMAPPINGS.placement.pipette).add(this.abortPlacement, this); - keyActionMapper.getBinding(KEYMAPPINGS.placement.rotateWhilePlacing).add(this.rotateBlueprint, this); - keyActionMapper.getBinding(KEYMAPPINGS.massSelect.pasteLastBlueprint).add(this.pasteBlueprint, this); - - this.root.camera.downPreHandler.add(this.onMouseDown, this); - this.root.camera.movePreHandler.add(this.onMouseMove, this); - - this.root.hud.signals.selectedPlacementBuildingChanged.add(this.abortPlacement, this); - this.root.signals.editModeChanged.add(this.onEditModeChanged, this); - - this.domAttach = new DynamicDomAttach(this.root, this.costDisplayParent); - this.trackedCanAfford = new TrackedState(this.onCanAffordChanged, this); - } - - abortPlacement() { - if (this.currentBlueprint.get()) { - this.currentBlueprint.set(null); - - return STOP_PROPAGATION; - } - } - - /** - * Called when the layer was changed - * @param {Layer} layer - */ - onEditModeChanged(layer) { - // Check if the layer of the blueprint differs and thus we have to deselect it - const blueprint = this.currentBlueprint.get(); - if (blueprint) { - if (blueprint.layer !== layer) { - this.currentBlueprint.set(null); - } - } - } - - /** - * Called when the blueprint is now affordable or not - * @param {boolean} canAfford - */ - onCanAffordChanged(canAfford) { - this.costDisplayParent.classList.toggle("canAfford", canAfford); - } - - update() { - const currentBlueprint = this.currentBlueprint.get(); - this.domAttach.update(currentBlueprint && currentBlueprint.getCost() > 0); - this.trackedCanAfford.set(currentBlueprint && currentBlueprint.canAfford(this.root)); - } - - /** - * Called when the blueprint was changed - * @param {Blueprint} blueprint - */ - onBlueprintChanged(blueprint) { - if (blueprint) { - this.lastBlueprintUsed = blueprint; - this.costDisplayText.innerText = "" + blueprint.getCost(); - } - } - - /** - * mouse down pre handler - * @param {Vector} pos - * @param {enumMouseButton} button - */ - onMouseDown(pos, button) { - if (button === enumMouseButton.right) { - if (this.currentBlueprint.get()) { - this.abortPlacement(); - return STOP_PROPAGATION; - } - } - - const blueprint = this.currentBlueprint.get(); - if (!blueprint) { - return; - } - - if (!blueprint.canAfford(this.root)) { - this.root.soundProxy.playUiError(); - return; - } - - const worldPos = this.root.camera.screenToWorld(pos); - const tile = worldPos.toTileSpace(); - if (blueprint.tryPlace(this.root, tile)) { - const cost = blueprint.getCost(); - this.root.hubGoals.takeShapeByKey(blueprintShape, cost); - this.root.soundProxy.playUi(SOUNDS.placeBuilding); - } - } - - /** - * Mose move handler - */ - onMouseMove() { - // Prevent movement while blueprint is selected - if (this.currentBlueprint.get()) { - return STOP_PROPAGATION; - } - } - - /** - * Called when an array of bulidings was selected - * @param {Array} uids - */ - createBlueprintFromBuildings(uids) { - if (uids.length === 0) { - return; - } - this.currentBlueprint.set(Blueprint.fromUids(this.root, uids)); - } - - /** - * Attempts to rotate the current blueprint - */ - rotateBlueprint() { - if (this.currentBlueprint.get()) { - if (this.root.keyMapper.getBinding(KEYMAPPINGS.placement.rotateInverseModifier).pressed) { - this.currentBlueprint.get().rotateCcw(); - } else { - this.currentBlueprint.get().rotateCw(); - } - } - } - - /** - * Attempts to paste the last blueprint - */ - pasteBlueprint() { - if (this.lastBlueprintUsed !== null) { - if (this.lastBlueprintUsed.layer !== this.root.currentLayer) { - // Not compatible - this.root.soundProxy.playUiError(); - return; - } - - this.root.hud.signals.pasteBlueprintRequested.dispatch(); - this.currentBlueprint.set(this.lastBlueprintUsed); - } else { - this.root.soundProxy.playUiError(); - } - } - - /** - * - * @param {DrawParameters} parameters - */ - draw(parameters) { - const blueprint = this.currentBlueprint.get(); - if (!blueprint) { - return; - } - const mousePosition = this.root.app.mousePosition; - if (!mousePosition) { - // Not on screen - return; - } - - const worldPos = this.root.camera.screenToWorld(mousePosition); - const tile = worldPos.toTileSpace(); - blueprint.draw(parameters, tile); - } -} +import { DrawParameters } from "../../../core/draw_parameters"; +import { STOP_PROPAGATION } from "../../../core/signal"; +import { TrackedState } from "../../../core/tracked_state"; +import { makeDiv } from "../../../core/utils"; +import { Vector } from "../../../core/vector"; +import { SOUNDS } from "../../../platform/sound"; +import { T } from "../../../translations"; +import { Blueprint } from "../../blueprint"; +import { enumMouseButton } from "../../camera"; +import { KEYMAPPINGS } from "../../key_action_mapper"; +import { BaseHUDPart } from "../base_hud_part"; +import { DynamicDomAttach } from "../dynamic_dom_attach"; + +export class HUDBlueprintPlacer extends BaseHUDPart { + createElements(parent) { + const blueprintCostShape = this.root.shapeDefinitionMgr.getShapeFromShortKey( + this.root.gameMode.getBlueprintShapeKey() + ); + const blueprintCostShapeCanvas = blueprintCostShape.generateAsCanvas(80); + + this.costDisplayParent = makeDiv(parent, "ingame_HUD_BlueprintPlacer", [], ``); + + makeDiv(this.costDisplayParent, null, ["label"], T.ingame.blueprintPlacer.cost); + const costContainer = makeDiv(this.costDisplayParent, null, ["costContainer"], ""); + this.costDisplayText = makeDiv(costContainer, null, ["costText"], ""); + costContainer.appendChild(blueprintCostShapeCanvas); + } + + initialize() { + this.root.hud.signals.buildingsSelectedForCopy.add(this.createBlueprintFromBuildings, this); + + /** @type {TypedTrackedState} */ + this.currentBlueprint = new TrackedState(this.onBlueprintChanged, this); + /** @type {Blueprint?} */ + this.lastBlueprintUsed = null; + + const keyActionMapper = this.root.keyMapper; + keyActionMapper.getBinding(KEYMAPPINGS.general.back).add(this.abortPlacement, this); + keyActionMapper.getBinding(KEYMAPPINGS.placement.pipette).add(this.abortPlacement, this); + keyActionMapper.getBinding(KEYMAPPINGS.placement.rotateWhilePlacing).add(this.rotateBlueprint, this); + keyActionMapper.getBinding(KEYMAPPINGS.massSelect.pasteLastBlueprint).add(this.pasteBlueprint, this); + + this.root.camera.downPreHandler.add(this.onMouseDown, this); + this.root.camera.movePreHandler.add(this.onMouseMove, this); + + this.root.hud.signals.selectedPlacementBuildingChanged.add(this.abortPlacement, this); + this.root.signals.editModeChanged.add(this.onEditModeChanged, this); + + this.domAttach = new DynamicDomAttach(this.root, this.costDisplayParent); + this.trackedCanAfford = new TrackedState(this.onCanAffordChanged, this); + } + + abortPlacement() { + if (this.currentBlueprint.get()) { + this.currentBlueprint.set(null); + + return STOP_PROPAGATION; + } + } + + /** + * Called when the layer was changed + * @param {Layer} layer + */ + onEditModeChanged(layer) { + // Check if the layer of the blueprint differs and thus we have to deselect it + const blueprint = this.currentBlueprint.get(); + if (blueprint) { + if (blueprint.layer !== layer) { + this.currentBlueprint.set(null); + } + } + } + + /** + * Called when the blueprint is now affordable or not + * @param {boolean} canAfford + */ + onCanAffordChanged(canAfford) { + this.costDisplayParent.classList.toggle("canAfford", canAfford); + } + + update() { + const currentBlueprint = this.currentBlueprint.get(); + this.domAttach.update(currentBlueprint && currentBlueprint.getCost() > 0); + this.trackedCanAfford.set(currentBlueprint && currentBlueprint.canAfford(this.root)); + } + + /** + * Called when the blueprint was changed + * @param {Blueprint} blueprint + */ + onBlueprintChanged(blueprint) { + if (blueprint) { + this.lastBlueprintUsed = blueprint; + this.costDisplayText.innerText = "" + blueprint.getCost(); + } + } + + /** + * mouse down pre handler + * @param {Vector} pos + * @param {enumMouseButton} button + */ + onMouseDown(pos, button) { + if (button === enumMouseButton.right) { + if (this.currentBlueprint.get()) { + this.abortPlacement(); + return STOP_PROPAGATION; + } + } + + const blueprint = this.currentBlueprint.get(); + if (!blueprint) { + return; + } + + if (!blueprint.canAfford(this.root)) { + this.root.soundProxy.playUiError(); + return; + } + + const worldPos = this.root.camera.screenToWorld(pos); + const tile = worldPos.toTileSpace(); + if (blueprint.tryPlace(this.root, tile)) { + const cost = blueprint.getCost(); + this.root.hubGoals.takeShapeByKey(this.root.gameMode.getBlueprintShapeKey(), cost); + this.root.soundProxy.playUi(SOUNDS.placeBuilding); + } + } + + /** + * Mose move handler + */ + onMouseMove() { + // Prevent movement while blueprint is selected + if (this.currentBlueprint.get()) { + return STOP_PROPAGATION; + } + } + + /** + * Called when an array of bulidings was selected + * @param {Array} uids + */ + createBlueprintFromBuildings(uids) { + if (uids.length === 0) { + return; + } + this.currentBlueprint.set(Blueprint.fromUids(this.root, uids)); + } + + /** + * Attempts to rotate the current blueprint + */ + rotateBlueprint() { + if (this.currentBlueprint.get()) { + if (this.root.keyMapper.getBinding(KEYMAPPINGS.placement.rotateInverseModifier).pressed) { + this.currentBlueprint.get().rotateCcw(); + } else { + this.currentBlueprint.get().rotateCw(); + } + } + } + + /** + * Attempts to paste the last blueprint + */ + pasteBlueprint() { + if (this.lastBlueprintUsed !== null) { + if (this.lastBlueprintUsed.layer !== this.root.currentLayer) { + // Not compatible + this.root.soundProxy.playUiError(); + return; + } + + this.root.hud.signals.pasteBlueprintRequested.dispatch(); + this.currentBlueprint.set(this.lastBlueprintUsed); + } else { + this.root.soundProxy.playUiError(); + } + } + + /** + * + * @param {DrawParameters} parameters + */ + draw(parameters) { + const blueprint = this.currentBlueprint.get(); + if (!blueprint) { + return; + } + const mousePosition = this.root.app.mousePosition; + if (!mousePosition) { + // Not on screen + return; + } + + const worldPos = this.root.camera.screenToWorld(mousePosition); + const tile = worldPos.toTileSpace(); + blueprint.draw(parameters, tile); + } +} diff --git a/src/js/game/hud/parts/sandbox_controller.js b/src/js/game/hud/parts/sandbox_controller.js index f71b87e0..592487ee 100644 --- a/src/js/game/hud/parts/sandbox_controller.js +++ b/src/js/game/hud/parts/sandbox_controller.js @@ -1,9 +1,7 @@ -import { BaseHUDPart } from "../base_hud_part"; import { makeDiv } from "../../../core/utils"; +import { BaseHUDPart } from "../base_hud_part"; import { DynamicDomAttach } from "../dynamic_dom_attach"; -import { blueprintShape, UPGRADES } from "../../upgrades"; import { enumNotificationType } from "./notifications"; -import { tutorialGoals } from "../../tutorial_goals"; export class HUDSandboxController extends BaseHUDPart { createElements(parent) { @@ -75,10 +73,11 @@ export class HUDSandboxController extends BaseHUDPart { } giveBlueprints() { - if (!this.root.hubGoals.storedShapes[blueprintShape]) { - this.root.hubGoals.storedShapes[blueprintShape] = 0; + const shape = this.root.gameMode.getBlueprintShapeKey(); + if (!this.root.hubGoals.storedShapes[shape]) { + this.root.hubGoals.storedShapes[shape] = 0; } - this.root.hubGoals.storedShapes[blueprintShape] += 1e9; + this.root.hubGoals.storedShapes[shape] += 1e9; } maxOutAll() { @@ -89,7 +88,7 @@ export class HUDSandboxController extends BaseHUDPart { } modifyUpgrade(id, amount) { - const upgradeTiers = UPGRADES[id]; + const upgradeTiers = this.root.gameMode.getUpgrades()[id]; const maxLevel = upgradeTiers.length; this.root.hubGoals.upgradeLevels[id] = Math.max( @@ -122,9 +121,10 @@ export class HUDSandboxController extends BaseHUDPart { // Compute gained rewards hubGoals.gainedRewards = {}; + const levels = this.root.gameMode.getLevelDefinitions(); for (let i = 0; i < hubGoals.level - 1; ++i) { - if (i < tutorialGoals.length) { - const reward = tutorialGoals[i].reward; + if (i < levels.length) { + const reward = levels[i].reward; hubGoals.gainedRewards[reward] = (hubGoals.gainedRewards[reward] || 0) + 1; } } diff --git a/src/js/game/hud/parts/shop.js b/src/js/game/hud/parts/shop.js index 4a25d16e..c543200f 100644 --- a/src/js/game/hud/parts/shop.js +++ b/src/js/game/hud/parts/shop.js @@ -3,7 +3,6 @@ import { InputReceiver } from "../../../core/input_receiver"; import { formatBigNumber, makeDiv } from "../../../core/utils"; import { T } from "../../../translations"; import { KeyActionMapper, KEYMAPPINGS } from "../../key_action_mapper"; -import { UPGRADES } from "../../upgrades"; import { BaseHUDPart } from "../base_hud_part"; import { DynamicDomAttach } from "../dynamic_dom_attach"; @@ -21,7 +20,7 @@ export class HUDShop extends BaseHUDPart { this.upgradeToElements = {}; // Upgrades - for (const upgradeId in UPGRADES) { + for (const upgradeId in this.root.gameMode.getUpgrades()) { const handle = {}; handle.requireIndexToElement = []; @@ -59,7 +58,7 @@ export class HUDShop extends BaseHUDPart { rerenderFull() { for (const upgradeId in this.upgradeToElements) { const handle = this.upgradeToElements[upgradeId]; - const upgradeTiers = UPGRADES[upgradeId]; + const upgradeTiers = this.root.gameMode.getUpgrades()[upgradeId]; const currentTier = this.root.hubGoals.getUpgradeLevel(upgradeId); const currentTierMultiplier = this.root.hubGoals.upgradeImprovements[upgradeId]; diff --git a/src/js/game/hud/parts/unlock_notification.js b/src/js/game/hud/parts/unlock_notification.js index 5fea2892..5fa0e3e2 100644 --- a/src/js/game/hud/parts/unlock_notification.js +++ b/src/js/game/hud/parts/unlock_notification.js @@ -1,14 +1,14 @@ import { globalConfig } from "../../../core/config"; import { gMetaBuildingRegistry } from "../../../core/global_registries"; +import { InputReceiver } from "../../../core/input_receiver"; import { makeDiv } from "../../../core/utils"; import { SOUNDS } from "../../../platform/sound"; import { T } from "../../../translations"; import { defaultBuildingVariant } from "../../meta_building"; -import { enumHubGoalRewards, tutorialGoals } from "../../tutorial_goals"; +import { enumHubGoalRewards } from "../../tutorial_goals"; +import { enumHubGoalRewardsToContentUnlocked } from "../../tutorial_goals_mappings"; import { BaseHUDPart } from "../base_hud_part"; import { DynamicDomAttach } from "../dynamic_dom_attach"; -import { enumHubGoalRewardsToContentUnlocked } from "../../tutorial_goals_mappings"; -import { InputReceiver } from "../../../core/input_receiver"; import { enumNotificationType } from "./notifications"; export class HUDUnlockNotification extends BaseHUDPart { @@ -53,7 +53,9 @@ export class HUDUnlockNotification extends BaseHUDPart { showForLevel(level, reward) { this.root.soundProxy.playUi(SOUNDS.levelComplete); - if (level > tutorialGoals.length) { + const levels = this.root.gameMode.getLevelDefinitions(); + // Don't use getIsFreeplay() because we want the freeplay level up to show + if (level > levels.length) { this.root.hud.signals.notification.dispatch( T.ingame.notifications.freeplayLevelComplete.replace("", String(level)), enumNotificationType.success diff --git a/src/js/game/modes/regular.js b/src/js/game/modes/regular.js new file mode 100644 index 00000000..396d81c9 --- /dev/null +++ b/src/js/game/modes/regular.js @@ -0,0 +1,445 @@ +import { IS_DEMO } from "../../core/config"; +import { findNiceIntegerValue } from "../../core/utils"; +import { GameMode } from "../game_mode"; +import { ShapeDefinition } from "../shape_definition"; +import { enumHubGoalRewards } from "../tutorial_goals"; + +const rocketShape = "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw"; +const finalGameShape = "RuCw--Cw:----Ru--"; +const preparementShape = "CpRpCp--:SwSwSwSw"; +const blueprintShape = "CbCbCbRb:CwCwCwCw"; + +const fixedImprovements = [0.5, 0.5, 1, 1, 2, 1, 1]; + +const numEndgameUpgrades = !IS_DEMO ? 20 - fixedImprovements.length - 1 : 0; + +function generateEndgameUpgrades() { + return new Array(numEndgameUpgrades).fill(null).map((_, i) => ({ + required: [ + { shape: preparementShape, amount: 30000 + i * 10000 }, + { shape: finalGameShape, amount: 20000 + i * 5000 }, + { shape: rocketShape, amount: 20000 + i * 5000 }, + ], + excludePrevious: true, + })); +} + +for (let i = 0; i < numEndgameUpgrades; ++i) { + fixedImprovements.push(0.1); +} + +/** @type {Object} */ +const cachedUpgrades = { + belt: [ + { + required: [{ shape: "CuCuCuCu", amount: 60 }], + }, + { + required: [{ shape: "--CuCu--", amount: 500 }], + }, + { + required: [{ shape: "CpCpCpCp", amount: 1000 }], + }, + { + required: [{ shape: "SrSrSrSr:CyCyCyCy", amount: 6000 }], + }, + { + required: [{ shape: "SrSrSrSr:CyCyCyCy:SwSwSwSw", amount: 25000 }], + }, + { + required: [{ shape: preparementShape, amount: 25000 }], + excludePrevious: true, + }, + { + required: [ + { shape: preparementShape, amount: 25000 }, + { shape: finalGameShape, amount: 50000 }, + ], + excludePrevious: true, + }, + ...generateEndgameUpgrades(), + ], + + miner: [ + { + required: [{ shape: "RuRuRuRu", amount: 300 }], + }, + { + required: [{ shape: "Cu------", amount: 800 }], + }, + { + required: [{ shape: "ScScScSc", amount: 3500 }], + }, + { + required: [{ shape: "CwCwCwCw:WbWbWbWb", amount: 23000 }], + }, + { + required: [{ shape: "CbRbRbCb:CwCwCwCw:WbWbWbWb", amount: 50000 }], + }, + { + required: [{ shape: preparementShape, amount: 25000 }], + excludePrevious: true, + }, + { + required: [ + { shape: preparementShape, amount: 25000 }, + { shape: finalGameShape, amount: 50000 }, + ], + excludePrevious: true, + }, + ...generateEndgameUpgrades(), + ], + + processors: [ + { + required: [{ shape: "SuSuSuSu", amount: 500 }], + }, + { + required: [{ shape: "RuRu----", amount: 600 }], + }, + { + required: [{ shape: "CgScScCg", amount: 3500 }], + }, + { + required: [{ shape: "CwCrCwCr:SgSgSgSg", amount: 25000 }], + }, + { + required: [{ shape: "WrRgWrRg:CwCrCwCr:SgSgSgSg", amount: 50000 }], + }, + { + required: [{ shape: preparementShape, amount: 25000 }], + excludePrevious: true, + }, + { + required: [ + { shape: preparementShape, amount: 25000 }, + { shape: finalGameShape, amount: 50000 }, + ], + excludePrevious: true, + }, + ...generateEndgameUpgrades(), + ], + + painting: [ + { + required: [{ shape: "RbRb----", amount: 600 }], + }, + { + required: [{ shape: "WrWrWrWr", amount: 3800 }], + }, + { + required: [{ shape: "RpRpRpRp:CwCwCwCw", amount: 6500 }], + }, + { + required: [{ shape: "WpWpWpWp:CwCwCwCw:WpWpWpWp", amount: 25000 }], + }, + { + required: [{ shape: "WpWpWpWp:CwCwCwCw:WpWpWpWp:CwCwCwCw", amount: 50000 }], + }, + { + required: [{ shape: preparementShape, amount: 25000 }], + excludePrevious: true, + }, + { + required: [ + { shape: preparementShape, amount: 25000 }, + { shape: finalGameShape, amount: 50000 }, + ], + excludePrevious: true, + }, + ...generateEndgameUpgrades(), + ], +}; + +// Tiers need % of the previous tier as requirement too +const tierGrowth = 2.5; + +// Automatically generate tier levels +for (const upgradeId in cachedUpgrades) { + const upgradeTiers = cachedUpgrades[upgradeId]; + + let currentTierRequirements = []; + for (let i = 0; i < upgradeTiers.length; ++i) { + const tierHandle = upgradeTiers[i]; + tierHandle.improvement = fixedImprovements[i]; + const originalRequired = tierHandle.required.slice(); + + for (let k = currentTierRequirements.length - 1; k >= 0; --k) { + const oldTierRequirement = currentTierRequirements[k]; + if (!tierHandle.excludePrevious) { + tierHandle.required.unshift({ + shape: oldTierRequirement.shape, + amount: oldTierRequirement.amount, + }); + } + } + currentTierRequirements.push( + ...originalRequired.map(req => ({ + amount: req.amount, + shape: req.shape, + })) + ); + currentTierRequirements.forEach(tier => { + tier.amount = findNiceIntegerValue(tier.amount * tierGrowth); + }); + } +} + +// VALIDATE +if (G_IS_DEV) { + for (const upgradeId in cachedUpgrades) { + cachedUpgrades[upgradeId].forEach(tier => { + tier.required.forEach(({ shape }) => { + try { + ShapeDefinition.fromShortKey(shape); + } catch (ex) { + throw new Error("Invalid upgrade goal: '" + ex + "' for shape" + shape); + } + }); + }); + } +} + +const levelDefinitions = [ + // 1 + // Circle + { + shape: "CuCuCuCu", // belts t1 + required: 30, + reward: enumHubGoalRewards.reward_cutter_and_trash, + }, + + // 2 + // Cutter + { + shape: "----CuCu", // + required: 40, + reward: enumHubGoalRewards.no_reward, + }, + + // 3 + // Rectangle + { + shape: "RuRuRuRu", // miners t1 + required: 70, + reward: enumHubGoalRewards.reward_balancer, + }, + + // 4 + { + shape: "RuRu----", // processors t2 + required: 70, + reward: enumHubGoalRewards.reward_rotater, + }, + + // 5 + // Rotater + { + shape: "Cu----Cu", // belts t2 + required: 170, + reward: enumHubGoalRewards.reward_tunnel, + }, + + // 6 + { + shape: "Cu------", // miners t2 + required: 270, + reward: enumHubGoalRewards.reward_painter, + }, + + // 7 + // Painter + { + shape: "CrCrCrCr", // unused + required: 300, + reward: enumHubGoalRewards.reward_rotater_ccw, + }, + + // 8 + { + shape: "RbRb----", // painter t2 + required: 480, + reward: enumHubGoalRewards.reward_mixer, + }, + + // 9 + // Mixing (purple) + { + shape: "CpCpCpCp", // belts t3 + required: 600, + reward: enumHubGoalRewards.reward_merger, + }, + + // 10 + // STACKER: Star shape + cyan + { + shape: "ScScScSc", // miners t3 + required: 800, + reward: enumHubGoalRewards.reward_stacker, + }, + + // 11 + // Chainable miner + { + shape: "CgScScCg", // processors t3 + required: 1000, + reward: enumHubGoalRewards.reward_miner_chainable, + }, + + // 12 + // Blueprints + { + shape: "CbCbCbRb:CwCwCwCw", + required: 1000, + reward: enumHubGoalRewards.reward_blueprints, + }, + + // 13 + // Tunnel Tier 2 + { + shape: "RpRpRpRp:CwCwCwCw", // painting t3 + required: 3800, + reward: enumHubGoalRewards.reward_underground_belt_tier_2, + }, + + // DEMO STOPS HERE + ...(IS_DEMO + ? [ + { + shape: "RpRpRpRp:CwCwCwCw", + required: 0, + reward: enumHubGoalRewards.reward_demo_end, + }, + ] + : [ + // 14 + // Belt reader + { + shape: "--Cg----:--Cr----", // unused + required: 16, // Per second! + reward: enumHubGoalRewards.reward_belt_reader, + throughputOnly: true, + }, + + // 15 + // Storage + { + shape: "SrSrSrSr:CyCyCyCy", // unused + required: 10000, + reward: enumHubGoalRewards.reward_storage, + }, + + // 16 + // Quad Cutter + { + shape: "SrSrSrSr:CyCyCyCy:SwSwSwSw", // belts t4 (two variants) + required: 6000, + reward: enumHubGoalRewards.reward_cutter_quad, + }, + + // 17 + // Double painter + { + shape: "CbRbRbCb:CwCwCwCw:WbWbWbWb", // miner t4 (two variants) + required: 20000, + reward: enumHubGoalRewards.reward_painter_double, + }, + + // 18 + // Rotater (180deg) + { + shape: "Sg----Sg:CgCgCgCg:--CyCy--", // unused + required: 20000, + reward: enumHubGoalRewards.reward_rotater_180, + }, + + // 19 + // Compact splitter + { + shape: "CpRpCp--:SwSwSwSw", + required: 25000, + reward: enumHubGoalRewards.reward_splitter, + }, + + // 20 + // WIRES + { + shape: finalGameShape, + required: 25000, + reward: enumHubGoalRewards.reward_wires_painter_and_levers, + }, + + // 21 + // Filter + { + shape: "CrCwCrCw:CwCrCwCr:CrCwCrCw:CwCrCwCr", + required: 25000, + reward: enumHubGoalRewards.reward_filter, + }, + + // 22 + // Constant signal + { + shape: "Cg----Cr:Cw----Cw:Sy------:Cy----Cy", + required: 25000, + reward: enumHubGoalRewards.reward_constant_signal, + }, + + // 23 + // Display + { + shape: "CcSyCcSy:SyCcSyCc:CcSyCcSy", + required: 25000, + reward: enumHubGoalRewards.reward_display, + }, + + // 24 Logic gates + { + shape: "CcRcCcRc:RwCwRwCw:Sr--Sw--:CyCyCyCy", + required: 25000, + reward: enumHubGoalRewards.reward_logic_gates, + }, + + // 25 Virtual Processing + { + shape: "Rg--Rg--:CwRwCwRw:--Rg--Rg", + required: 25000, + reward: enumHubGoalRewards.reward_virtual_processing, + }, + + // 26 Freeplay + { + shape: "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw", + required: 50000, + reward: enumHubGoalRewards.reward_freeplay, + }, + ]), +]; + +if (G_IS_DEV) { + levelDefinitions.forEach(({ shape }) => { + try { + ShapeDefinition.fromShortKey(shape); + } catch (ex) { + throw new Error("Invalid tutorial goal: '" + ex + "' for shape" + shape); + } + }); +} + +export class RegularGameMode extends GameMode { + constructor(root) { + super(root); + } + + getUpgrades() { + return cachedUpgrades; + } + + getBlueprintShapeKey() { + return blueprintShape; + } + + getLevelDefinitions() { + return levelDefinitions; + } +} diff --git a/src/js/game/root.js b/src/js/game/root.js index dd224dd8..6f1e7c36 100644 --- a/src/js/game/root.js +++ b/src/js/game/root.js @@ -1,221 +1,225 @@ -/* eslint-disable no-unused-vars */ -import { Signal } from "../core/signal"; -import { RandomNumberGenerator } from "../core/rng"; -import { createLogger } from "../core/logging"; - -// Type hints -/* typehints:start */ -import { GameTime } from "./time/game_time"; -import { EntityManager } from "./entity_manager"; -import { GameSystemManager } from "./game_system_manager"; -import { GameHUD } from "./hud/hud"; -import { MapView } from "./map_view"; -import { Camera } from "./camera"; -import { InGameState } from "../states/ingame"; -import { AutomaticSave } from "./automatic_save"; -import { Application } from "../application"; -import { SoundProxy } from "./sound_proxy"; -import { Savegame } from "../savegame/savegame"; -import { GameLogic } from "./logic"; -import { ShapeDefinitionManager } from "./shape_definition_manager"; -import { HubGoals } from "./hub_goals"; -import { BufferMaintainer } from "../core/buffer_maintainer"; -import { ProductionAnalytics } from "./production_analytics"; -import { Entity } from "./entity"; -import { ShapeDefinition } from "./shape_definition"; -import { BaseItem } from "./base_item"; -import { DynamicTickrate } from "./dynamic_tickrate"; -import { KeyActionMapper } from "./key_action_mapper"; -import { Vector } from "../core/vector"; -/* typehints:end */ - -const logger = createLogger("game/root"); - -/** @type {Array} */ -export const layers = ["regular", "wires"]; - -/** - * The game root is basically the whole game state at a given point, - * combining all important classes. We don't have globals, but this - * class is passed to almost all game classes. - */ -export class GameRoot { - /** - * Constructs a new game root - * @param {Application} app - */ - constructor(app) { - this.app = app; - - /** @type {Savegame} */ - this.savegame = null; - - /** @type {InGameState} */ - this.gameState = null; - - /** @type {KeyActionMapper} */ - this.keyMapper = null; - - // Store game dimensions - this.gameWidth = 500; - this.gameHeight = 500; - - // Stores whether the current session is a fresh game (true), or was continued (false) - /** @type {boolean} */ - this.gameIsFresh = true; - - // Stores whether the logic is already initialized - /** @type {boolean} */ - this.logicInitialized = false; - - // Stores whether the game is already initialized, that is, all systems etc have been created - /** @type {boolean} */ - this.gameInitialized = false; - - /** - * Whether a bulk operation is running - */ - this.bulkOperationRunning = false; - - //////// Other properties /////// - - /** @type {Camera} */ - this.camera = null; - - /** @type {HTMLCanvasElement} */ - this.canvas = null; - - /** @type {CanvasRenderingContext2D} */ - this.context = null; - - /** @type {MapView} */ - this.map = null; - - /** @type {GameLogic} */ - this.logic = null; - - /** @type {EntityManager} */ - this.entityMgr = null; - - /** @type {GameHUD} */ - this.hud = null; - - /** @type {GameSystemManager} */ - this.systemMgr = null; - - /** @type {GameTime} */ - this.time = null; - - /** @type {HubGoals} */ - this.hubGoals = null; - - /** @type {BufferMaintainer} */ - this.buffers = null; - - /** @type {AutomaticSave} */ - this.automaticSave = null; - - /** @type {SoundProxy} */ - this.soundProxy = null; - - /** @type {ShapeDefinitionManager} */ - this.shapeDefinitionMgr = null; - - /** @type {ProductionAnalytics} */ - this.productionAnalytics = null; - - /** @type {DynamicTickrate} */ - this.dynamicTickrate = null; - - /** @type {Layer} */ - this.currentLayer = "regular"; - - this.signals = { - // Entities - entityManuallyPlaced: /** @type {TypedSignal<[Entity]>} */ (new Signal()), - entityAdded: /** @type {TypedSignal<[Entity]>} */ (new Signal()), - entityChanged: /** @type {TypedSignal<[Entity]>} */ (new Signal()), - entityGotNewComponent: /** @type {TypedSignal<[Entity]>} */ (new Signal()), - entityComponentRemoved: /** @type {TypedSignal<[Entity]>} */ (new Signal()), - entityQueuedForDestroy: /** @type {TypedSignal<[Entity]>} */ (new Signal()), - entityDestroyed: /** @type {TypedSignal<[Entity]>} */ (new Signal()), - - // Global - resized: /** @type {TypedSignal<[number, number]>} */ (new Signal()), - readyToRender: /** @type {TypedSignal<[]>} */ (new Signal()), - aboutToDestruct: /** @type {TypedSignal<[]>} */ new Signal(), - - // Game Hooks - gameSaved: /** @type {TypedSignal<[]>} */ (new Signal()), // Game got saved - gameRestored: /** @type {TypedSignal<[]>} */ (new Signal()), // Game got restored - - gameFrameStarted: /** @type {TypedSignal<[]>} */ (new Signal()), // New frame - - storyGoalCompleted: /** @type {TypedSignal<[number, string]>} */ (new Signal()), - upgradePurchased: /** @type {TypedSignal<[string]>} */ (new Signal()), - - // Called right after game is initialized - postLoadHook: /** @type {TypedSignal<[]>} */ (new Signal()), - - shapeDelivered: /** @type {TypedSignal<[ShapeDefinition]>} */ (new Signal()), - itemProduced: /** @type {TypedSignal<[BaseItem]>} */ (new Signal()), - - bulkOperationFinished: /** @type {TypedSignal<[]>} */ (new Signal()), - - editModeChanged: /** @type {TypedSignal<[Layer]>} */ (new Signal()), - - // Called to check if an entity can be placed, second parameter is an additional offset. - // Use to introduce additional placement checks - prePlacementCheck: /** @type {TypedSignal<[Entity, Vector]>} */ (new Signal()), - - // Called before actually placing an entity, use to perform additional logic - // for freeing space before actually placing. - freeEntityAreaBeforeBuild: /** @type {TypedSignal<[Entity]>} */ (new Signal()), - }; - - // RNG's - /** @type {Object.>} */ - this.rngs = {}; - - // Work queue - this.queue = { - requireRedraw: false, - }; - } - - /** - * Destructs the game root - */ - destruct() { - logger.log("destructing root"); - this.signals.aboutToDestruct.dispatch(); - - this.reset(); - } - - /** - * Resets the whole root and removes all properties - */ - reset() { - if (this.signals) { - // Destruct all signals - for (let i = 0; i < this.signals.length; ++i) { - this.signals[i].removeAll(); - } - } - - if (this.hud) { - this.hud.cleanup(); - } - if (this.camera) { - this.camera.cleanup(); - } - - // Finally free all properties - for (let prop in this) { - if (this.hasOwnProperty(prop)) { - delete this[prop]; - } - } - } -} +/* eslint-disable no-unused-vars */ +import { Signal } from "../core/signal"; +import { RandomNumberGenerator } from "../core/rng"; +import { createLogger } from "../core/logging"; + +// Type hints +/* typehints:start */ +import { GameTime } from "./time/game_time"; +import { EntityManager } from "./entity_manager"; +import { GameSystemManager } from "./game_system_manager"; +import { GameHUD } from "./hud/hud"; +import { MapView } from "./map_view"; +import { Camera } from "./camera"; +import { InGameState } from "../states/ingame"; +import { AutomaticSave } from "./automatic_save"; +import { Application } from "../application"; +import { SoundProxy } from "./sound_proxy"; +import { Savegame } from "../savegame/savegame"; +import { GameLogic } from "./logic"; +import { ShapeDefinitionManager } from "./shape_definition_manager"; +import { HubGoals } from "./hub_goals"; +import { BufferMaintainer } from "../core/buffer_maintainer"; +import { ProductionAnalytics } from "./production_analytics"; +import { Entity } from "./entity"; +import { ShapeDefinition } from "./shape_definition"; +import { BaseItem } from "./base_item"; +import { DynamicTickrate } from "./dynamic_tickrate"; +import { KeyActionMapper } from "./key_action_mapper"; +import { Vector } from "../core/vector"; +import { GameMode } from "./game_mode"; +/* typehints:end */ + +const logger = createLogger("game/root"); + +/** @type {Array} */ +export const layers = ["regular", "wires"]; + +/** + * The game root is basically the whole game state at a given point, + * combining all important classes. We don't have globals, but this + * class is passed to almost all game classes. + */ +export class GameRoot { + /** + * Constructs a new game root + * @param {Application} app + */ + constructor(app) { + this.app = app; + + /** @type {Savegame} */ + this.savegame = null; + + /** @type {InGameState} */ + this.gameState = null; + + /** @type {KeyActionMapper} */ + this.keyMapper = null; + + // Store game dimensions + this.gameWidth = 500; + this.gameHeight = 500; + + // Stores whether the current session is a fresh game (true), or was continued (false) + /** @type {boolean} */ + this.gameIsFresh = true; + + // Stores whether the logic is already initialized + /** @type {boolean} */ + this.logicInitialized = false; + + // Stores whether the game is already initialized, that is, all systems etc have been created + /** @type {boolean} */ + this.gameInitialized = false; + + /** + * Whether a bulk operation is running + */ + this.bulkOperationRunning = false; + + //////// Other properties /////// + + /** @type {Camera} */ + this.camera = null; + + /** @type {HTMLCanvasElement} */ + this.canvas = null; + + /** @type {CanvasRenderingContext2D} */ + this.context = null; + + /** @type {MapView} */ + this.map = null; + + /** @type {GameLogic} */ + this.logic = null; + + /** @type {EntityManager} */ + this.entityMgr = null; + + /** @type {GameHUD} */ + this.hud = null; + + /** @type {GameSystemManager} */ + this.systemMgr = null; + + /** @type {GameTime} */ + this.time = null; + + /** @type {HubGoals} */ + this.hubGoals = null; + + /** @type {BufferMaintainer} */ + this.buffers = null; + + /** @type {AutomaticSave} */ + this.automaticSave = null; + + /** @type {SoundProxy} */ + this.soundProxy = null; + + /** @type {ShapeDefinitionManager} */ + this.shapeDefinitionMgr = null; + + /** @type {ProductionAnalytics} */ + this.productionAnalytics = null; + + /** @type {DynamicTickrate} */ + this.dynamicTickrate = null; + + /** @type {Layer} */ + this.currentLayer = "regular"; + + /** @type {GameMode} */ + this.gameMode = null; + + this.signals = { + // Entities + entityManuallyPlaced: /** @type {TypedSignal<[Entity]>} */ (new Signal()), + entityAdded: /** @type {TypedSignal<[Entity]>} */ (new Signal()), + entityChanged: /** @type {TypedSignal<[Entity]>} */ (new Signal()), + entityGotNewComponent: /** @type {TypedSignal<[Entity]>} */ (new Signal()), + entityComponentRemoved: /** @type {TypedSignal<[Entity]>} */ (new Signal()), + entityQueuedForDestroy: /** @type {TypedSignal<[Entity]>} */ (new Signal()), + entityDestroyed: /** @type {TypedSignal<[Entity]>} */ (new Signal()), + + // Global + resized: /** @type {TypedSignal<[number, number]>} */ (new Signal()), + readyToRender: /** @type {TypedSignal<[]>} */ (new Signal()), + aboutToDestruct: /** @type {TypedSignal<[]>} */ new Signal(), + + // Game Hooks + gameSaved: /** @type {TypedSignal<[]>} */ (new Signal()), // Game got saved + gameRestored: /** @type {TypedSignal<[]>} */ (new Signal()), // Game got restored + + gameFrameStarted: /** @type {TypedSignal<[]>} */ (new Signal()), // New frame + + storyGoalCompleted: /** @type {TypedSignal<[number, string]>} */ (new Signal()), + upgradePurchased: /** @type {TypedSignal<[string]>} */ (new Signal()), + + // Called right after game is initialized + postLoadHook: /** @type {TypedSignal<[]>} */ (new Signal()), + + shapeDelivered: /** @type {TypedSignal<[ShapeDefinition]>} */ (new Signal()), + itemProduced: /** @type {TypedSignal<[BaseItem]>} */ (new Signal()), + + bulkOperationFinished: /** @type {TypedSignal<[]>} */ (new Signal()), + + editModeChanged: /** @type {TypedSignal<[Layer]>} */ (new Signal()), + + // Called to check if an entity can be placed, second parameter is an additional offset. + // Use to introduce additional placement checks + prePlacementCheck: /** @type {TypedSignal<[Entity, Vector]>} */ (new Signal()), + + // Called before actually placing an entity, use to perform additional logic + // for freeing space before actually placing. + freeEntityAreaBeforeBuild: /** @type {TypedSignal<[Entity]>} */ (new Signal()), + }; + + // RNG's + /** @type {Object.>} */ + this.rngs = {}; + + // Work queue + this.queue = { + requireRedraw: false, + }; + } + + /** + * Destructs the game root + */ + destruct() { + logger.log("destructing root"); + this.signals.aboutToDestruct.dispatch(); + + this.reset(); + } + + /** + * Resets the whole root and removes all properties + */ + reset() { + if (this.signals) { + // Destruct all signals + for (let i = 0; i < this.signals.length; ++i) { + this.signals[i].removeAll(); + } + } + + if (this.hud) { + this.hud.cleanup(); + } + if (this.camera) { + this.camera.cleanup(); + } + + // Finally free all properties + for (let prop in this) { + if (this.hasOwnProperty(prop)) { + delete this[prop]; + } + } + } +} diff --git a/src/js/game/systems/constant_signal.js b/src/js/game/systems/constant_signal.js index 0b2f38da..aaf31a19 100644 --- a/src/js/game/systems/constant_signal.js +++ b/src/js/game/systems/constant_signal.js @@ -12,7 +12,6 @@ import { GameSystemWithFilter } from "../game_system_with_filter"; import { BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON } from "../items/boolean_item"; import { COLOR_ITEM_SINGLETONS } from "../items/color_item"; import { ShapeDefinition } from "../shape_definition"; -import { blueprintShape } from "../upgrades"; export class ConstantSignalSystem extends GameSystemWithFilter { constructor(root) { @@ -61,7 +60,9 @@ export class ConstantSignalSystem extends GameSystemWithFilter { this.root.shapeDefinitionMgr.getShapeItemFromDefinition( this.root.hubGoals.currentGoal.definition ), - this.root.shapeDefinitionMgr.getShapeItemFromShortKey(blueprintShape), + this.root.shapeDefinitionMgr.getShapeItemFromShortKey( + this.root.gameMode.getBlueprintShapeKey() + ), ...this.root.hud.parts.pinnedShapes.pinnedShapes.map(key => this.root.shapeDefinitionMgr.getShapeItemFromShortKey(key) ), diff --git a/src/js/game/tutorial_goals.js b/src/js/game/tutorial_goals.js index f7b56ffe..84634b0a 100644 --- a/src/js/game/tutorial_goals.js +++ b/src/js/game/tutorial_goals.js @@ -1,7 +1,3 @@ -import { IS_DEMO } from "../core/config"; -import { ShapeDefinition } from "./shape_definition"; -import { finalGameShape } from "./upgrades"; - /** * Don't forget to also update tutorial_goals_mappings.js as well as the translations! * @enum {string} @@ -40,229 +36,3 @@ export const enumHubGoalRewards = { no_reward: "no_reward", no_reward_freeplay: "no_reward_freeplay", }; - -export const tutorialGoals = [ - // 1 - // Circle - { - shape: "CuCuCuCu", // belts t1 - required: 30, - reward: enumHubGoalRewards.reward_cutter_and_trash, - }, - - // 2 - // Cutter - { - shape: "----CuCu", // - required: 40, - reward: enumHubGoalRewards.no_reward, - }, - - // 3 - // Rectangle - { - shape: "RuRuRuRu", // miners t1 - required: 70, - reward: enumHubGoalRewards.reward_balancer, - }, - - // 4 - { - shape: "RuRu----", // processors t2 - required: 70, - reward: enumHubGoalRewards.reward_rotater, - }, - - // 5 - // Rotater - { - shape: "Cu----Cu", // belts t2 - required: 170, - reward: enumHubGoalRewards.reward_tunnel, - }, - - // 6 - { - shape: "Cu------", // miners t2 - required: 270, - reward: enumHubGoalRewards.reward_painter, - }, - - // 7 - // Painter - { - shape: "CrCrCrCr", // unused - required: 300, - reward: enumHubGoalRewards.reward_rotater_ccw, - }, - - // 8 - { - shape: "RbRb----", // painter t2 - required: 480, - reward: enumHubGoalRewards.reward_mixer, - }, - - // 9 - // Mixing (purple) - { - shape: "CpCpCpCp", // belts t3 - required: 600, - reward: enumHubGoalRewards.reward_merger, - }, - - // 10 - // STACKER: Star shape + cyan - { - shape: "ScScScSc", // miners t3 - required: 800, - reward: enumHubGoalRewards.reward_stacker, - }, - - // 11 - // Chainable miner - { - shape: "CgScScCg", // processors t3 - required: 1000, - reward: enumHubGoalRewards.reward_miner_chainable, - }, - - // 12 - // Blueprints - { - shape: "CbCbCbRb:CwCwCwCw", - required: 1000, - reward: enumHubGoalRewards.reward_blueprints, - }, - - // 13 - // Tunnel Tier 2 - { - shape: "RpRpRpRp:CwCwCwCw", // painting t3 - required: 3800, - reward: enumHubGoalRewards.reward_underground_belt_tier_2, - }, - - // DEMO STOPS HERE - ...(IS_DEMO - ? [ - { - shape: "RpRpRpRp:CwCwCwCw", - required: 0, - reward: enumHubGoalRewards.reward_demo_end, - }, - ] - : [ - // 14 - // Belt reader - { - shape: "--Cg----:--Cr----", // unused - required: 16, // Per second! - reward: enumHubGoalRewards.reward_belt_reader, - throughputOnly: true, - }, - - // 15 - // Storage - { - shape: "SrSrSrSr:CyCyCyCy", // unused - required: 10000, - reward: enumHubGoalRewards.reward_storage, - }, - - // 16 - // Quad Cutter - { - shape: "SrSrSrSr:CyCyCyCy:SwSwSwSw", // belts t4 (two variants) - required: 6000, - reward: enumHubGoalRewards.reward_cutter_quad, - }, - - // 17 - // Double painter - { - shape: "CbRbRbCb:CwCwCwCw:WbWbWbWb", // miner t4 (two variants) - required: 20000, - reward: enumHubGoalRewards.reward_painter_double, - }, - - // 18 - // Rotater (180deg) - { - shape: "Sg----Sg:CgCgCgCg:--CyCy--", // unused - required: 20000, - reward: enumHubGoalRewards.reward_rotater_180, - }, - - // 19 - // Compact splitter - { - shape: "CpRpCp--:SwSwSwSw", - required: 25000, - reward: enumHubGoalRewards.reward_splitter, - }, - - // 20 - // WIRES - { - shape: finalGameShape, - required: 25000, - reward: enumHubGoalRewards.reward_wires_painter_and_levers, - }, - - // 21 - // Filter - { - shape: "CrCwCrCw:CwCrCwCr:CrCwCrCw:CwCrCwCr", - required: 25000, - reward: enumHubGoalRewards.reward_filter, - }, - - // 22 - // Constant signal - { - shape: "Cg----Cr:Cw----Cw:Sy------:Cy----Cy", - required: 25000, - reward: enumHubGoalRewards.reward_constant_signal, - }, - - // 23 - // Display - { - shape: "CcSyCcSy:SyCcSyCc:CcSyCcSy", - required: 25000, - reward: enumHubGoalRewards.reward_display, - }, - - // 24 Logic gates - { - shape: "CcRcCcRc:RwCwRwCw:Sr--Sw--:CyCyCyCy", - required: 25000, - reward: enumHubGoalRewards.reward_logic_gates, - }, - - // 25 Virtual Processing - { - shape: "Rg--Rg--:CwRwCwRw:--Rg--Rg", - required: 25000, - reward: enumHubGoalRewards.reward_virtual_processing, - }, - - // 26 Freeplay - { - shape: "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw", - required: 50000, - reward: enumHubGoalRewards.reward_freeplay, - }, - ]), -]; - -if (G_IS_DEV) { - tutorialGoals.forEach(({ shape }) => { - try { - ShapeDefinition.fromShortKey(shape); - } catch (ex) { - throw new Error("Invalid tutorial goal: '" + ex + "' for shape" + shape); - } - }); -} diff --git a/src/js/game/upgrades.js b/src/js/game/upgrades.js deleted file mode 100644 index db8997a1..00000000 --- a/src/js/game/upgrades.js +++ /dev/null @@ -1,212 +0,0 @@ -import { IS_DEMO } from "../core/config"; -import { findNiceIntegerValue } from "../core/utils"; -import { ShapeDefinition } from "./shape_definition"; - -export const preparementShape = "CpRpCp--:SwSwSwSw"; -export const finalGameShape = "RuCw--Cw:----Ru--"; -export const rocketShape = "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw"; -export const blueprintShape = "CbCbCbRb:CwCwCwCw"; - -const fixedImprovements = [0.5, 0.5, 1, 1, 2, 1, 1]; - -const numEndgameUpgrades = !IS_DEMO ? 20 - fixedImprovements.length - 1 : 0; - -function generateEndgameUpgrades() { - return new Array(numEndgameUpgrades).fill(null).map((_, i) => ({ - required: [ - { shape: preparementShape, amount: 30000 + i * 10000 }, - { shape: finalGameShape, amount: 20000 + i * 5000 }, - { shape: rocketShape, amount: 20000 + i * 5000 }, - ], - excludePrevious: true, - })); -} - -for (let i = 0; i < numEndgameUpgrades; ++i) { - fixedImprovements.push(0.1); -} - -/** @typedef {{ - * shape: string, - * amount: number - * }} UpgradeRequirement */ - -/** @typedef {{ - * required: Array - * improvement?: number, - * excludePrevious?: boolean - * }} TierRequirement */ - -/** @typedef {Array} UpgradeTiers */ - -/** @type {Object} */ -export const UPGRADES = { - belt: [ - { - required: [{ shape: "CuCuCuCu", amount: 60 }], - }, - { - required: [{ shape: "--CuCu--", amount: 500 }], - }, - { - required: [{ shape: "CpCpCpCp", amount: 1000 }], - }, - { - required: [{ shape: "SrSrSrSr:CyCyCyCy", amount: 6000 }], - }, - { - required: [{ shape: "SrSrSrSr:CyCyCyCy:SwSwSwSw", amount: 25000 }], - }, - { - required: [{ shape: preparementShape, amount: 25000 }], - excludePrevious: true, - }, - { - required: [ - { shape: preparementShape, amount: 25000 }, - { shape: finalGameShape, amount: 50000 }, - ], - excludePrevious: true, - }, - ...generateEndgameUpgrades(), - ], - - miner: [ - { - required: [{ shape: "RuRuRuRu", amount: 300 }], - }, - { - required: [{ shape: "Cu------", amount: 800 }], - }, - { - required: [{ shape: "ScScScSc", amount: 3500 }], - }, - { - required: [{ shape: "CwCwCwCw:WbWbWbWb", amount: 23000 }], - }, - { - required: [{ shape: "CbRbRbCb:CwCwCwCw:WbWbWbWb", amount: 50000 }], - }, - { - required: [{ shape: preparementShape, amount: 25000 }], - excludePrevious: true, - }, - { - required: [ - { shape: preparementShape, amount: 25000 }, - { shape: finalGameShape, amount: 50000 }, - ], - excludePrevious: true, - }, - ...generateEndgameUpgrades(), - ], - - processors: [ - { - required: [{ shape: "SuSuSuSu", amount: 500 }], - }, - { - required: [{ shape: "RuRu----", amount: 600 }], - }, - { - required: [{ shape: "CgScScCg", amount: 3500 }], - }, - { - required: [{ shape: "CwCrCwCr:SgSgSgSg", amount: 25000 }], - }, - { - required: [{ shape: "WrRgWrRg:CwCrCwCr:SgSgSgSg", amount: 50000 }], - }, - { - required: [{ shape: preparementShape, amount: 25000 }], - excludePrevious: true, - }, - { - required: [ - { shape: preparementShape, amount: 25000 }, - { shape: finalGameShape, amount: 50000 }, - ], - excludePrevious: true, - }, - ...generateEndgameUpgrades(), - ], - - painting: [ - { - required: [{ shape: "RbRb----", amount: 600 }], - }, - { - required: [{ shape: "WrWrWrWr", amount: 3800 }], - }, - { - required: [{ shape: "RpRpRpRp:CwCwCwCw", amount: 6500 }], - }, - { - required: [{ shape: "WpWpWpWp:CwCwCwCw:WpWpWpWp", amount: 25000 }], - }, - { - required: [{ shape: "WpWpWpWp:CwCwCwCw:WpWpWpWp:CwCwCwCw", amount: 50000 }], - }, - { - required: [{ shape: preparementShape, amount: 25000 }], - excludePrevious: true, - }, - { - required: [ - { shape: preparementShape, amount: 25000 }, - { shape: finalGameShape, amount: 50000 }, - ], - excludePrevious: true, - }, - ...generateEndgameUpgrades(), - ], -}; - -// Tiers need % of the previous tier as requirement too -const tierGrowth = 2.5; - -// Automatically generate tier levels -for (const upgradeId in UPGRADES) { - const upgradeTiers = UPGRADES[upgradeId]; - - let currentTierRequirements = []; - for (let i = 0; i < upgradeTiers.length; ++i) { - const tierHandle = upgradeTiers[i]; - tierHandle.improvement = fixedImprovements[i]; - const originalRequired = tierHandle.required.slice(); - - for (let k = currentTierRequirements.length - 1; k >= 0; --k) { - const oldTierRequirement = currentTierRequirements[k]; - if (!tierHandle.excludePrevious) { - tierHandle.required.unshift({ - shape: oldTierRequirement.shape, - amount: oldTierRequirement.amount, - }); - } - } - currentTierRequirements.push( - ...originalRequired.map(req => ({ - amount: req.amount, - shape: req.shape, - })) - ); - currentTierRequirements.forEach(tier => { - tier.amount = findNiceIntegerValue(tier.amount * tierGrowth); - }); - } -} - -// VALIDATE -if (G_IS_DEV) { - for (const upgradeId in UPGRADES) { - UPGRADES[upgradeId].forEach(tier => { - tier.required.forEach(({ shape }) => { - try { - ShapeDefinition.fromShortKey(shape); - } catch (ex) { - throw new Error("Invalid upgrade goal: '" + ex + "' for shape" + shape); - } - }); - }); - } -} diff --git a/src/js/platform/browser/game_analytics.js b/src/js/platform/browser/game_analytics.js index ea23509b..a3947be6 100644 --- a/src/js/platform/browser/game_analytics.js +++ b/src/js/platform/browser/game_analytics.js @@ -1,14 +1,12 @@ import { globalConfig } from "../../core/config"; import { createLogger } from "../../core/logging"; +import { queryParamOptions } from "../../core/query_parameters"; +import { BeltComponent } from "../../game/components/belt"; +import { StaticMapEntityComponent } from "../../game/components/static_map_entity"; import { GameRoot } from "../../game/root"; import { InGameState } from "../../states/ingame"; import { GameAnalyticsInterface } from "../game_analytics"; import { FILE_NOT_FOUND } from "../storage"; -import { blueprintShape, UPGRADES } from "../../game/upgrades"; -import { tutorialGoals } from "../../game/tutorial_goals"; -import { BeltComponent } from "../../game/components/belt"; -import { StaticMapEntityComponent } from "../../game/components/static_map_entity"; -import { queryParamOptions } from "../../core/query_parameters"; const logger = createLogger("game_analytics"); @@ -190,23 +188,26 @@ export class ShapezGameAnalytics extends GameAnalyticsInterface { /** * Returns true if the shape is interesting + * @param {GameRoot} root * @param {string} key */ - isInterestingShape(key) { - if (key === blueprintShape) { + isInterestingShape(root, key) { + if (key === root.gameMode.getBlueprintShapeKey()) { return true; } // Check if its a story goal - for (let i = 0; i < tutorialGoals.length; ++i) { - if (key === tutorialGoals[i].shape) { + const levels = root.gameMode.getLevelDefinitions(); + for (let i = 0; i < levels.length; ++i) { + if (key === levels[i].shape) { return true; } } // Check if its required to unlock an upgrade - for (const upgradeKey in UPGRADES) { - const upgradeTiers = UPGRADES[upgradeKey]; + const upgrades = root.gameMode.getUpgrades(); + for (const upgradeKey in upgrades) { + const upgradeTiers = upgrades[upgradeKey]; for (let i = 0; i < upgradeTiers.length; ++i) { const tier = upgradeTiers[i]; const required = tier.required; @@ -226,7 +227,9 @@ export class ShapezGameAnalytics extends GameAnalyticsInterface { * @param {GameRoot} root */ generateGameDump(root) { - const shapeIds = Object.keys(root.hubGoals.storedShapes).filter(this.isInterestingShape.bind(this)); + const shapeIds = Object.keys(root.hubGoals.storedShapes).filter(key => + this.isInterestingShape(root, key) + ); let shapes = {}; for (let i = 0; i < shapeIds.length; ++i) { shapes[shapeIds[i]] = root.hubGoals.storedShapes[shapeIds[i]]; diff --git a/src/js/savegame/savegame_serializer.js b/src/js/savegame/savegame_serializer.js index 552bc35c..c1247225 100644 --- a/src/js/savegame/savegame_serializer.js +++ b/src/js/savegame/savegame_serializer.js @@ -130,7 +130,7 @@ export class SavegameSerializer { errorReason = errorReason || root.time.deserialize(savegame.time); errorReason = errorReason || root.camera.deserialize(savegame.camera); errorReason = errorReason || root.map.deserialize(savegame.map); - errorReason = errorReason || root.hubGoals.deserialize(savegame.hubGoals); + errorReason = errorReason || root.hubGoals.deserialize(savegame.hubGoals, root); errorReason = errorReason || root.hud.parts.pinnedShapes.deserialize(savegame.pinnedShapes); errorReason = errorReason || root.hud.parts.waypoints.deserialize(savegame.waypoints); errorReason = errorReason || this.internal.deserializeEntityArray(root, savegame.entities); diff --git a/src/js/savegame/schemas/1006.js b/src/js/savegame/schemas/1006.js index 29f2c64f..d1c0b48f 100644 --- a/src/js/savegame/schemas/1006.js +++ b/src/js/savegame/schemas/1006.js @@ -19,7 +19,6 @@ import { getCodeFromBuildingData } from "../../game/building_codes.js"; import { StaticMapEntityComponent } from "../../game/components/static_map_entity.js"; import { Entity } from "../../game/entity.js"; import { defaultBuildingVariant, MetaBuilding } from "../../game/meta_building.js"; -import { finalGameShape } from "../../game/upgrades.js"; import { SavegameInterface_V1005 } from "./1005.js"; const schema = require("./1006.json"); @@ -152,7 +151,8 @@ export class SavegameInterface_V1006 extends SavegameInterface_V1005 { stored[shapeKey] = rebalance(stored[shapeKey]); } - stored[finalGameShape] = 0; + // Reset final game shape + stored["RuCw--Cw:----Ru--"] = 0; // Reduce goals if (dump.hubGoals.currentGoal) {