diff --git a/src/css/ingame_hud/beta_overlay.scss b/src/css/ingame_hud/beta_overlay.scss index 816cddb2..20c7604a 100644 --- a/src/css/ingame_hud/beta_overlay.scss +++ b/src/css/ingame_hud/beta_overlay.scss @@ -1,7 +1,8 @@ #ingame_HUD_BetaOverlay { position: fixed; @include S(top, 10px); - @include S(right, 15px); + left: 50%; + transform: translateX(-50%); color: $colorRedBright; @include Heading; text-transform: uppercase; diff --git a/src/js/game/hub_goals.js b/src/js/game/hub_goals.js index 01b98dba..8b7be917 100644 --- a/src/js/game/hub_goals.js +++ b/src/js/game/hub_goals.js @@ -1,8 +1,10 @@ import { globalConfig } from "../core/config"; +import { RandomNumberGenerator } from "../core/rng"; 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 { enumAnalyticsDataSource } from "./production_analytics"; import { GameRoot } from "./root"; import { enumSubShape, ShapeDefinition } from "./shape_definition"; import { enumHubGoalRewards, tutorialGoals } from "./tutorial_goals"; @@ -18,12 +20,6 @@ export class HubGoals extends BasicSerializableObject { 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)), - }), }; } @@ -53,15 +49,7 @@ export class HubGoals extends BasicSerializableObject { } // 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, - }; - } + this.computeNextGoal(); } /** @@ -106,7 +94,7 @@ export class HubGoals extends BasicSerializableObject { this.upgradeImprovements[key] = 1; } - this.createNextGoal(); + this.computeNextGoal(); // Allow quickly switching goals in dev mode if (G_IS_DEV) { @@ -155,6 +143,13 @@ export class HubGoals extends BasicSerializableObject { * Returns how much of the current goal was already delivered */ getCurrentGoalDelivered() { + if (this.currentGoal.throughputOnly) { + return this.root.productionAnalytics.getCurrentShapeRate( + enumAnalyticsDataSource.delivered, + this.currentGoal.definition + ); + } + return this.getShapesStored(this.currentGoal.definition); } @@ -189,9 +184,8 @@ export class HubGoals extends BasicSerializableObject { 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 || + this.getCurrentGoalDelivered() >= this.currentGoal.required || (G_IS_DEV && globalConfig.debug.rewardsInstant) ) { this.onGoalCompleted(); @@ -201,24 +195,28 @@ export class HubGoals extends BasicSerializableObject { /** * Creates the next goal */ - createNextGoal() { + computeNextGoal() { const storyIndex = this.level - 1; if (storyIndex < tutorialGoals.length) { - const { shape, required, reward } = tutorialGoals[storyIndex]; + const { shape, required, reward, throughputOnly } = tutorialGoals[storyIndex]; this.currentGoal = { /** @type {ShapeDefinition} */ definition: this.root.shapeDefinitionMgr.getShapeFromShortKey(shape), required, reward, + throughputOnly, }; return; } + const required = 4 + (this.level - 27) * 0.25; + this.currentGoal = { /** @type {ShapeDefinition} */ - definition: this.createRandomShape(), - required: findNiceIntegerValue(1000 + Math.pow(this.level * 2000, 0.8)), + definition: this.computeFreeplayShape(this.level), + required, reward: enumHubGoalRewards.no_reward_freeplay, + throughputOnly: true, }; } @@ -231,7 +229,7 @@ export class HubGoals extends BasicSerializableObject { this.root.app.gameAnalytics.handleLevelCompleted(this.level); ++this.level; - this.createNextGoal(); + this.computeNextGoal(); this.root.signals.storyGoalCompleted.dispatch(this.level - 1, reward); } @@ -325,15 +323,85 @@ export class HubGoals extends BasicSerializableObject { } /** + * Picks random colors which are close to each other + * @param {RandomNumberGenerator} rng + */ + generateRandomColorSet(rng, allowUncolored = false) { + const colorWheel = [ + enumColors.red, + enumColors.yellow, + enumColors.green, + enumColors.cyan, + enumColors.blue, + enumColors.purple, + enumColors.red, + enumColors.yellow, + ]; + + const universalColors = [enumColors.white]; + if (allowUncolored) { + universalColors.push(enumColors.uncolored); + } + const index = rng.nextIntRangeInclusive(0, colorWheel.length - 3); + const pickedColors = colorWheel.slice(index, index + 3); + pickedColors.push(rng.choice(universalColors)); + return pickedColors; + } + + /** + * Creates a (seeded) random shape + * @param {number} level * @returns {ShapeDefinition} */ - createRandomShape() { + computeFreeplayShape(level) { const layerCount = clamp(this.level / 25, 2, 4); + /** @type {Array} */ let layers = []; - const randomColor = () => randomChoice(Object.values(enumColors)); - const randomShape = () => randomChoice(Object.values(enumSubShape)); + const rng = new RandomNumberGenerator(this.root.map.seed + "/" + level); + + const colors = this.generateRandomColorSet(rng, level > 35); + + let pickedSymmetry = null; // pairs of quadrants that must be the same + let availableShapes = [enumSubShape.rect, enumSubShape.circle, enumSubShape.star]; + if (rng.next() < 0.5) { + pickedSymmetry = [ + // radial symmetry + [0, 2], + [1, 3], + ]; + availableShapes.push(enumSubShape.windmill); // windmill looks good only in radial symmetry + } else { + const symmetries = [ + [ + // horizontal axis + [0, 3], + [1, 2], + ], + [ + // vertical axis + [0, 1], + [2, 3], + ], + [ + // diagonal axis + [0, 2], + [1], + [3], + ], + [ + // other diagonal axis + [1, 3], + [0], + [2], + ], + ]; + pickedSymmetry = rng.choice(symmetries); + } + + const randomColor = () => rng.choice(colors); + const randomShape = () => rng.choice(Object.values(enumSubShape)); let anyIsMissingTwo = false; @@ -341,23 +409,24 @@ export class HubGoals extends BasicSerializableObject { /** @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; + for (let j = 0; j < pickedSymmetry.length; ++j) { + const group = pickedSymmetry[j]; + const shape = randomShape(); + const color = randomColor(); + for (let k = 0; k < group.length; ++k) { + const quad = group[k]; + layer[quad] = { + subShape: shape, + color, + }; + } } // 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; + if (level > 75 && rng.next() > 0.95 && !anyIsMissingTwo) { + layer[rng.nextIntRange(0, 4)] = null; anyIsMissingTwo = true; } diff --git a/src/js/game/hud/hud.js b/src/js/game/hud/hud.js index e0ddfd9d..11f580b1 100644 --- a/src/js/game/hud/hud.js +++ b/src/js/game/hud/hud.js @@ -44,6 +44,7 @@ import { HUDWireInfo } from "./parts/wire_info"; import { HUDLeverToggle } from "./parts/lever_toggle"; import { HUDLayerPreview } from "./parts/layer_preview"; import { HUDMinerHighlight } from "./parts/miner_highlight"; +import { HUDBetaOverlay } from "./parts/beta_overlay"; export class GameHUD { /** @@ -75,7 +76,6 @@ export class GameHUD { pinnedShapes: new HUDPinnedShapes(this.root), notifications: new HUDNotifications(this.root), settingsMenu: new HUDSettingsMenu(this.root), - // betaOverlay: new HUDBetaOverlay(this.root), debugInfo: new HUDDebugInfo(this.root), dialogs: new HUDModalDialogs(this.root), screenshotExporter: new HUDScreenshotExporter(this.root), @@ -137,6 +137,10 @@ export class GameHUD { this.parts.sandboxController = new HUDSandboxController(this.root); } + if (!G_IS_RELEASE) { + this.parts.betaOverlay = new HUDBetaOverlay(this.root); + } + const frag = document.createDocumentFragment(); for (const key in this.parts) { this.parts[key].createElements(frag); diff --git a/src/js/game/hud/parts/beta_overlay.js b/src/js/game/hud/parts/beta_overlay.js index 517a15b7..2bff5d04 100644 --- a/src/js/game/hud/parts/beta_overlay.js +++ b/src/js/game/hud/parts/beta_overlay.js @@ -3,7 +3,7 @@ import { makeDiv } from "../../../core/utils"; export class HUDBetaOverlay extends BaseHUDPart { createElements(parent) { - this.element = makeDiv(parent, "ingame_HUD_BetaOverlay", [], "CLOSED BETA"); + this.element = makeDiv(parent, "ingame_HUD_BetaOverlay", [], "BETA VERSION"); } initialize() {} diff --git a/src/js/game/hud/parts/pinned_shapes.js b/src/js/game/hud/parts/pinned_shapes.js index c54554bf..941a679f 100644 --- a/src/js/game/hud/parts/pinned_shapes.js +++ b/src/js/game/hud/parts/pinned_shapes.js @@ -4,6 +4,8 @@ import { ShapeDefinition } from "../../shape_definition"; import { BaseHUDPart } from "../base_hud_part"; import { blueprintShape, UPGRADES } from "../../upgrades"; import { enumHubGoalRewards } from "../../tutorial_goals"; +import { enumAnalyticsDataSource } from "../../production_analytics"; +import { T } from "../../../translations"; /** * Manages the pinned shapes on the left side of the screen @@ -22,11 +24,13 @@ export class HUDPinnedShapes extends BaseHUDPart { * convenient. Also allows for cleaning up handles. * @type {Array<{ * key: string, + * definition: ShapeDefinition, * amountLabel: HTMLElement, * lastRenderedValue: string, * element: HTMLElement, * detector?: ClickDetector, - * infoDetector?: ClickDetector + * infoDetector?: ClickDetector, + * throughputOnly?: boolean * }>} */ this.handles = []; @@ -163,29 +167,40 @@ export class HUDPinnedShapes extends BaseHUDPart { this.handles = []; // Pin story goal - this.internalPinShape(currentKey, false, "goal"); + this.internalPinShape({ + key: currentKey, + canUnpin: false, + className: "goal", + throughputOnly: currentGoal.throughputOnly, + }); // Pin blueprint shape as well if (this.root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_blueprints)) { - this.internalPinShape(blueprintShape, false, "blueprint"); + this.internalPinShape({ + key: blueprintShape, + canUnpin: false, + className: "blueprint", + }); } // Pin manually pinned shapes for (let i = 0; i < this.pinnedShapes.length; ++i) { const key = this.pinnedShapes[i]; if (key !== currentKey) { - this.internalPinShape(key); + this.internalPinShape({ key }); } } } /** * Pins a new shape - * @param {string} key - * @param {boolean} canUnpin - * @param {string=} className + * @param {object} param0 + * @param {string} param0.key + * @param {boolean=} param0.canUnpin + * @param {string=} param0.className + * @param {boolean=} param0.throughputOnly */ - internalPinShape(key, canUnpin = true, className = null) { + internalPinShape({ key, canUnpin = true, className = null, throughputOnly = false }) { const definition = this.root.shapeDefinitionMgr.getShapeFromShortKey(key); const element = makeDiv(this.element, null, ["shape"]); @@ -229,11 +244,13 @@ export class HUDPinnedShapes extends BaseHUDPart { this.handles.push({ key, + definition, element, amountLabel, lastRenderedValue: "", detector, infoDetector, + throughputOnly, }); } @@ -244,8 +261,20 @@ export class HUDPinnedShapes extends BaseHUDPart { for (let i = 0; i < this.handles.length; ++i) { const handle = this.handles[i]; - const currentValue = this.root.hubGoals.getShapesStoredByKey(handle.key); - const currentValueFormatted = formatBigNumber(currentValue); + let currentValue = this.root.hubGoals.getShapesStoredByKey(handle.key); + let currentValueFormatted = formatBigNumber(currentValue); + + if (handle.throughputOnly) { + currentValue = this.root.productionAnalytics.getCurrentShapeRate( + enumAnalyticsDataSource.delivered, + handle.definition + ); + currentValueFormatted = T.ingame.statistics.shapesDisplayUnits.second.replace( + "", + String(currentValue) + ); + } + if (currentValueFormatted !== handle.lastRenderedValue) { handle.lastRenderedValue = currentValueFormatted; handle.amountLabel.innerText = currentValueFormatted; diff --git a/src/js/game/hud/parts/sandbox_controller.js b/src/js/game/hud/parts/sandbox_controller.js index c382cf84..f71b87e0 100644 --- a/src/js/game/hud/parts/sandbox_controller.js +++ b/src/js/game/hud/parts/sandbox_controller.js @@ -113,7 +113,7 @@ export class HUDSandboxController extends BaseHUDPart { modifyLevel(amount) { const hubGoals = this.root.hubGoals; hubGoals.level = Math.max(1, hubGoals.level + amount); - hubGoals.createNextGoal(); + hubGoals.computeNextGoal(); // Clear all shapes of this level hubGoals.storedShapes[hubGoals.currentGoal.definition.getHash()] = 0; diff --git a/src/js/game/hud/parts/shop.js b/src/js/game/hud/parts/shop.js index cfa78c29..6c1bdc3f 100644 --- a/src/js/game/hud/parts/shop.js +++ b/src/js/game/hud/parts/shop.js @@ -90,17 +90,15 @@ export class HUDShop extends BaseHUDPart { // Max level handle.elemDescription.innerText = T.ingame.shop.maximumLevel.replace( "", - currentTierMultiplier.toString() + formatBigNumber(currentTierMultiplier) ); continue; } // Set description handle.elemDescription.innerText = T.shopUpgrades[upgradeId].description - .replace("", currentTierMultiplier.toString()) - .replace("", (currentTierMultiplier + tierHandle.improvement).toString()) - // Backwards compatibility - .replace("", (tierHandle.improvement * 100.0).toString()); + .replace("", formatBigNumber(currentTierMultiplier)) + .replace("", formatBigNumber(currentTierMultiplier + tierHandle.improvement)); tierHandle.required.forEach(({ shape, amount }) => { const container = makeDiv(handle.elemRequirements, null, ["requirement"]); diff --git a/src/js/game/hud/parts/unlock_notification.js b/src/js/game/hud/parts/unlock_notification.js index d88e2dbb..32c42f67 100644 --- a/src/js/game/hud/parts/unlock_notification.js +++ b/src/js/game/hud/parts/unlock_notification.js @@ -4,11 +4,12 @@ import { makeDiv } from "../../../core/utils"; import { SOUNDS } from "../../../platform/sound"; import { T } from "../../../translations"; import { defaultBuildingVariant } from "../../meta_building"; -import { enumHubGoalRewards } from "../../tutorial_goals"; +import { enumHubGoalRewards, tutorialGoals } from "../../tutorial_goals"; 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 { initialize() { @@ -50,6 +51,14 @@ export class HUDUnlockNotification extends BaseHUDPart { * @param {enumHubGoalRewards} reward */ showForLevel(level, reward) { + if (level > tutorialGoals.length) { + this.root.hud.signals.notification.dispatch( + T.ingame.notifications.freeplayLevelComplete.replace("", String(level)), + enumNotificationType.success + ); + return; + } + this.root.app.inputMgr.makeSureAttachedAndOnTop(this.inputReciever); this.elemTitle.innerText = T.ingame.levelCompleteNotification.levelTitle.replace( "", diff --git a/src/js/game/systems/hub.js b/src/js/game/systems/hub.js index 2270f941..a2ae1aff 100644 --- a/src/js/game/systems/hub.js +++ b/src/js/game/systems/hub.js @@ -1,172 +1,185 @@ -import { DrawParameters } from "../../core/draw_parameters"; -import { Loader } from "../../core/loader"; -import { formatBigNumber } from "../../core/utils"; -import { T } from "../../translations"; -import { HubComponent } from "../components/hub"; -import { Entity } from "../entity"; -import { GameSystemWithFilter } from "../game_system_with_filter"; -import { globalConfig } from "../../core/config"; -import { smoothenDpi } from "../../core/dpi_manager"; -import { drawSpriteClipped } from "../../core/draw_utils"; -import { Rectangle } from "../../core/rectangle"; -import { ORIGINAL_SPRITE_SCALE } from "../../core/sprites"; - -const HUB_SIZE_TILES = 4; -const HUB_SIZE_PIXELS = HUB_SIZE_TILES * globalConfig.tileSize; - -export class HubSystem extends GameSystemWithFilter { - constructor(root) { - super(root, [HubComponent]); - - this.hubSprite = Loader.getSprite("sprites/buildings/hub.png"); - } - - /** - * @param {DrawParameters} parameters - */ - draw(parameters) { - for (let i = 0; i < this.allEntities.length; ++i) { - this.drawEntity(parameters, this.allEntities[i]); - } - } - - update() { - for (let i = 0; i < this.allEntities.length; ++i) { - // Set hub goal - const entity = this.allEntities[i]; - const pinsComp = entity.components.WiredPins; - pinsComp.slots[0].value = this.root.shapeDefinitionMgr.getShapeItemFromDefinition( - this.root.hubGoals.currentGoal.definition - ); - } - } - /** - * - * @param {HTMLCanvasElement} canvas - * @param {CanvasRenderingContext2D} context - * @param {number} w - * @param {number} h - * @param {number} dpi - */ - redrawHubBaseTexture(canvas, context, w, h, dpi) { - // This method is quite ugly, please ignore it! - - context.scale(dpi, dpi); - - const parameters = new DrawParameters({ - context, - visibleRect: new Rectangle(0, 0, w, h), - desiredAtlasScale: ORIGINAL_SPRITE_SCALE, - zoomLevel: dpi * 0.75, - root: this.root, - }); - - context.clearRect(0, 0, w, h); - - this.hubSprite.draw(context, 0, 0, w, h); - - const definition = this.root.hubGoals.currentGoal.definition; - definition.drawCentered(45, 58, parameters, 36); - - const goals = this.root.hubGoals.currentGoal; - - const textOffsetX = 70; - const textOffsetY = 61; - - // Deliver count - const delivered = this.root.hubGoals.getCurrentGoalDelivered(); - const deliveredText = "" + formatBigNumber(delivered); - - if (delivered > 9999) { - context.font = "bold 16px GameFont"; - } else if (delivered > 999) { - context.font = "bold 20px GameFont"; - } else { - context.font = "bold 25px GameFont"; - } - context.fillStyle = "#64666e"; - context.textAlign = "left"; - context.fillText(deliveredText, textOffsetX, textOffsetY); - - // Required - context.font = "13px GameFont"; - context.fillStyle = "#a4a6b0"; - context.fillText("/ " + formatBigNumber(goals.required), textOffsetX, textOffsetY + 13); - - // Reward - const rewardText = T.storyRewards[goals.reward].title.toUpperCase(); - if (rewardText.length > 12) { - context.font = "bold 8px GameFont"; - } else { - context.font = "bold 10px GameFont"; - } - context.fillStyle = "#fd0752"; - context.textAlign = "center"; - - context.fillText(rewardText, HUB_SIZE_PIXELS / 2, 105); - - // Level "8" - context.font = "bold 10px GameFont"; - context.fillStyle = "#fff"; - context.fillText("" + this.root.hubGoals.level, 27, 32); - - // "LVL" - context.textAlign = "center"; - context.fillStyle = "#fff"; - context.font = "bold 6px GameFont"; - context.fillText(T.buildings.hub.levelShortcut, 27, 22); - - // "Deliver" - context.fillStyle = "#64666e"; - context.font = "bold 10px GameFont"; - context.fillText(T.buildings.hub.deliver.toUpperCase(), HUB_SIZE_PIXELS / 2, 30); - - // "To unlock" - const unlockText = T.buildings.hub.toUnlock.toUpperCase(); - if (unlockText.length > 15) { - context.font = "bold 8px GameFont"; - } else { - context.font = "bold 10px GameFont"; - } - context.fillText(T.buildings.hub.toUnlock.toUpperCase(), HUB_SIZE_PIXELS / 2, 92); - - context.textAlign = "left"; - } - - /** - * @param {DrawParameters} parameters - * @param {Entity} entity - */ - drawEntity(parameters, entity) { - const staticComp = entity.components.StaticMapEntity; - if (!staticComp.shouldBeDrawn(parameters)) { - return; - } - - // Deliver count - const delivered = this.root.hubGoals.getCurrentGoalDelivered(); - const deliveredText = "" + formatBigNumber(delivered); - - const dpi = smoothenDpi(globalConfig.shapesSharpness * parameters.zoomLevel); - const canvas = parameters.root.buffers.getForKey({ - key: "hub", - subKey: dpi + "/" + this.root.hubGoals.level + "/" + deliveredText, - w: globalConfig.tileSize * 4, - h: globalConfig.tileSize * 4, - dpi, - redrawMethod: this.redrawHubBaseTexture.bind(this), - }); - - const extrude = 8; - drawSpriteClipped({ - parameters, - sprite: canvas, - x: staticComp.origin.x * globalConfig.tileSize - extrude, - y: staticComp.origin.y * globalConfig.tileSize - extrude, - w: HUB_SIZE_PIXELS + 2 * extrude, - h: HUB_SIZE_PIXELS + 2 * extrude, - originalW: HUB_SIZE_PIXELS * dpi, - originalH: HUB_SIZE_PIXELS * dpi, - }); - } -} +import { globalConfig } from "../../core/config"; +import { smoothenDpi } from "../../core/dpi_manager"; +import { DrawParameters } from "../../core/draw_parameters"; +import { drawSpriteClipped } from "../../core/draw_utils"; +import { Loader } from "../../core/loader"; +import { Rectangle } from "../../core/rectangle"; +import { ORIGINAL_SPRITE_SCALE } from "../../core/sprites"; +import { formatBigNumber } from "../../core/utils"; +import { T } from "../../translations"; +import { HubComponent } from "../components/hub"; +import { Entity } from "../entity"; +import { GameSystemWithFilter } from "../game_system_with_filter"; + +const HUB_SIZE_TILES = 4; +const HUB_SIZE_PIXELS = HUB_SIZE_TILES * globalConfig.tileSize; + +export class HubSystem extends GameSystemWithFilter { + constructor(root) { + super(root, [HubComponent]); + + this.hubSprite = Loader.getSprite("sprites/buildings/hub.png"); + } + + /** + * @param {DrawParameters} parameters + */ + draw(parameters) { + for (let i = 0; i < this.allEntities.length; ++i) { + this.drawEntity(parameters, this.allEntities[i]); + } + } + + update() { + for (let i = 0; i < this.allEntities.length; ++i) { + // Set hub goal + const entity = this.allEntities[i]; + const pinsComp = entity.components.WiredPins; + pinsComp.slots[0].value = this.root.shapeDefinitionMgr.getShapeItemFromDefinition( + this.root.hubGoals.currentGoal.definition + ); + } + } + /** + * + * @param {HTMLCanvasElement} canvas + * @param {CanvasRenderingContext2D} context + * @param {number} w + * @param {number} h + * @param {number} dpi + */ + redrawHubBaseTexture(canvas, context, w, h, dpi) { + // This method is quite ugly, please ignore it! + + context.scale(dpi, dpi); + + const parameters = new DrawParameters({ + context, + visibleRect: new Rectangle(0, 0, w, h), + desiredAtlasScale: ORIGINAL_SPRITE_SCALE, + zoomLevel: dpi * 0.75, + root: this.root, + }); + + context.clearRect(0, 0, w, h); + + this.hubSprite.draw(context, 0, 0, w, h); + + const definition = this.root.hubGoals.currentGoal.definition; + definition.drawCentered(45, 58, parameters, 36); + + const goals = this.root.hubGoals.currentGoal; + + const textOffsetX = 70; + const textOffsetY = 61; + + if (goals.throughputOnly) { + // Throughput + const deliveredText = T.ingame.statistics.shapesDisplayUnits.second.replace( + "", + formatBigNumber(goals.required) + ); + + context.font = "bold 12px GameFont"; + context.fillStyle = "#64666e"; + context.textAlign = "left"; + context.fillText(deliveredText, textOffsetX, textOffsetY); + } else { + // Deliver count + const delivered = this.root.hubGoals.getCurrentGoalDelivered(); + const deliveredText = "" + formatBigNumber(delivered); + + if (delivered > 9999) { + context.font = "bold 16px GameFont"; + } else if (delivered > 999) { + context.font = "bold 20px GameFont"; + } else { + context.font = "bold 25px GameFont"; + } + context.fillStyle = "#64666e"; + context.textAlign = "left"; + context.fillText(deliveredText, textOffsetX, textOffsetY); + + // Required + context.font = "13px GameFont"; + context.fillStyle = "#a4a6b0"; + context.fillText("/ " + formatBigNumber(goals.required), textOffsetX, textOffsetY + 13); + } + + // Reward + const rewardText = T.storyRewards[goals.reward].title.toUpperCase(); + if (rewardText.length > 12) { + context.font = "bold 8px GameFont"; + } else { + context.font = "bold 10px GameFont"; + } + context.fillStyle = "#fd0752"; + context.textAlign = "center"; + + context.fillText(rewardText, HUB_SIZE_PIXELS / 2, 105); + + // Level "8" + context.font = "bold 10px GameFont"; + context.fillStyle = "#fff"; + context.fillText("" + this.root.hubGoals.level, 27, 32); + + // "LVL" + context.textAlign = "center"; + context.fillStyle = "#fff"; + context.font = "bold 6px GameFont"; + context.fillText(T.buildings.hub.levelShortcut, 27, 22); + + // "Deliver" + context.fillStyle = "#64666e"; + context.font = "bold 10px GameFont"; + context.fillText(T.buildings.hub.deliver.toUpperCase(), HUB_SIZE_PIXELS / 2, 30); + + // "To unlock" + const unlockText = T.buildings.hub.toUnlock.toUpperCase(); + if (unlockText.length > 15) { + context.font = "bold 8px GameFont"; + } else { + context.font = "bold 10px GameFont"; + } + context.fillText(T.buildings.hub.toUnlock.toUpperCase(), HUB_SIZE_PIXELS / 2, 92); + + context.textAlign = "left"; + } + + /** + * @param {DrawParameters} parameters + * @param {Entity} entity + */ + drawEntity(parameters, entity) { + const staticComp = entity.components.StaticMapEntity; + if (!staticComp.shouldBeDrawn(parameters)) { + return; + } + + // Deliver count + const delivered = this.root.hubGoals.getCurrentGoalDelivered(); + const deliveredText = "" + formatBigNumber(delivered); + + const dpi = smoothenDpi(globalConfig.shapesSharpness * parameters.zoomLevel); + const canvas = parameters.root.buffers.getForKey({ + key: "hub", + subKey: dpi + "/" + this.root.hubGoals.level + "/" + deliveredText, + w: globalConfig.tileSize * 4, + h: globalConfig.tileSize * 4, + dpi, + redrawMethod: this.redrawHubBaseTexture.bind(this), + }); + + const extrude = 8; + drawSpriteClipped({ + parameters, + sprite: canvas, + x: staticComp.origin.x * globalConfig.tileSize - extrude, + y: staticComp.origin.y * globalConfig.tileSize - extrude, + w: HUB_SIZE_PIXELS + 2 * extrude, + h: HUB_SIZE_PIXELS + 2 * extrude, + originalW: HUB_SIZE_PIXELS * dpi, + originalH: HUB_SIZE_PIXELS * dpi, + }); + } +} diff --git a/src/js/game/tutorial_goals.js b/src/js/game/tutorial_goals.js index c859ca26..4e9df8eb 100644 --- a/src/js/game/tutorial_goals.js +++ b/src/js/game/tutorial_goals.js @@ -143,10 +143,10 @@ export const tutorialGoals = [ // 14 // Belt reader { - // @todo - shape: "CuCuCuCu", - required: 0, + shape: "--Cg----:--Cr----", // unused + required: 16, // Per second! reward: enumHubGoalRewards.reward_belt_reader, + throughputOnly: true, }, // 15 @@ -176,8 +176,7 @@ export const tutorialGoals = [ // 18 // Rotater (180deg) { - // @TODO - shape: "CuCuCuCu", + shape: "Sg----Sg:CgCgCgCg:--CyCy--", // unused required: 20000, reward: enumHubGoalRewards.reward_rotater_180, }, @@ -185,8 +184,7 @@ export const tutorialGoals = [ // 19 // Compact splitter { - // @TODO - shape: "CuCuCuCu", + shape: "CpRpCp--:SwSwSwSw", required: 25000, reward: enumHubGoalRewards.reward_splitter, }, @@ -202,8 +200,7 @@ export const tutorialGoals = [ // 21 // Display { - // @TODO - shape: "CuCuCuCu", + shape: "CrCrCrCr:CwCwCwCw:CrCrCrCr:CwCwCwCw", required: 25000, reward: enumHubGoalRewards.reward_display, }, @@ -211,43 +208,37 @@ export const tutorialGoals = [ // 22 // Constant signal { - // @TODO - shape: "CuCuCuCu", - required: 30000, + shape: "Cg----Cr:Cw----Cw:Sy------:Cy----Cy", + required: 25000, reward: enumHubGoalRewards.reward_constant_signal, }, // 23 // Quad Painter { - // @TODO - shape: "CuCuCuCu", - // shape: "WrRgWrRg:CwCrCwCr:SgSgSgSg", // processors t4 (two variants) - required: 35000, + shape: "CcSyCcSy:SyCcSyCc:CcSyCcSy", + required: 5000, reward: enumHubGoalRewards.reward_painter_quad, }, // 24 Logic gates { - // @TODO - shape: "CuCuCuCu", - required: 40000, + shape: "CcRcCcRc:RwCwRwCw:Sr--Sw--:CyCyCyCy", + required: 10000, reward: enumHubGoalRewards.reward_logic_gates, }, // 25 Virtual Processing { - // @TODO - shape: "CuCuCuCu", - required: 45000, + shape: "Rg--Rg--:CwRwCwRw:--Rg--Rg", + required: 10000, reward: enumHubGoalRewards.reward_virtual_processing, }, // 26 Freeplay { - // @TODO - shape: "CuCuCuCu", - required: 100000, + shape: "CbCuCbCu:Sr------:--CrSrCr:CwCwCwCw", + required: 10000, reward: enumHubGoalRewards.reward_freeplay, }, ]; diff --git a/src/js/game/upgrades.js b/src/js/game/upgrades.js index 5dfd6025..f8f4f1eb 100644 --- a/src/js/game/upgrades.js +++ b/src/js/game/upgrades.js @@ -2,10 +2,28 @@ import { findNiceIntegerValue } from "../core/utils"; import { ShapeDefinition } from "./shape_definition"; 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, 2]; +const numEndgameUpgrades = G_IS_DEV || G_IS_STANDALONE ? 20 - fixedImprovements.length - 1 : 0; + +function generateEndgameUpgrades() { + return new Array(numEndgameUpgrades).fill(null).map((_, i) => ({ + required: [ + { shape: blueprintShape, 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 @@ -41,6 +59,7 @@ export const UPGRADES = { required: [{ shape: finalGameShape, amount: 50000 }], excludePrevious: true, }, + ...generateEndgameUpgrades(), ], miner: [ @@ -63,6 +82,7 @@ export const UPGRADES = { required: [{ shape: finalGameShape, amount: 50000 }], excludePrevious: true, }, + ...generateEndgameUpgrades(), ], processors: [ @@ -85,6 +105,7 @@ export const UPGRADES = { required: [{ shape: finalGameShape, amount: 50000 }], excludePrevious: true, }, + ...generateEndgameUpgrades(), ], painting: [ @@ -107,6 +128,7 @@ export const UPGRADES = { required: [{ shape: finalGameShape, amount: 50000 }], excludePrevious: true, }, + ...generateEndgameUpgrades(), ], }; diff --git a/translations/base-en.yaml b/translations/base-en.yaml index 6d02a498..4cb6adbc 100644 --- a/translations/base-en.yaml +++ b/translations/base-en.yaml @@ -350,6 +350,7 @@ ingame: notifications: newUpgrade: A new upgrade is available! gameSaved: Your game has been saved. + freeplayLevelComplete: Level has been completed! # The "Upgrades" window shop: @@ -360,7 +361,8 @@ ingame: tier: Tier # The roman number for each tier - tierLabels: [I, II, III, IV, V, VI, VII, VIII, IX, X] + tierLabels: + [I, II, III, IV, V, VI, VII, VIII, IX, X, XI, XII, XIII, XIV, XV, XVI, XVII, XVIII, XIX, XX] maximumLevel: MAXIMUM LEVEL (Speed x) @@ -788,13 +790,13 @@ storyRewards: no_reward_freeplay: title: Next level desc: >- - Congratulations! By the way, more content is planned for the standalone! + Congratulations! reward_freeplay: title: Freeplay desc: >- You did it! You unlocked the free-play mode! This means that shapes are now randomly generated!

- Since the hub will only require low quantities from now on, I highly recommend to build a machine which automatically delivers the requested shape!

+ Since the hub will require a throughput from now on, I highly recommend to build a machine which automatically delivers the requested shape!

The HUB outputs the requested shape on the wires layer, so all you have to do is to analyze it and automatically configure your factory based on that. settings: