From 3af6532d3278ab73af94f13a1f8e4d1801a8c1ca Mon Sep 17 00:00:00 2001 From: tobspr Date: Fri, 12 Jun 2020 18:38:13 +0200 Subject: [PATCH] Refactor pinned shapes and make them smart, closes #72 --- .gitignore | 3 + src/css/ingame_hud/pinned_shapes.scss | 4 + src/js/changelog.js | 9 + src/js/core/utils.js | 20 +- src/js/game/hud/hud.js | 2 +- src/js/game/hud/parts/pinned_shapes.js | 181 +++++++++++++----- src/js/game/hud/parts/shop.js | 2 +- src/js/savegame/savegame.js | 9 +- .../savegame/savegame_interface_registry.js | 2 + src/js/savegame/schemas/1003.js | 28 +++ src/js/savegame/schemas/1003.json | 5 + 11 files changed, 212 insertions(+), 53 deletions(-) create mode 100644 src/js/savegame/schemas/1003.js create mode 100644 src/js/savegame/schemas/1003.json diff --git a/.gitignore b/.gitignore index ef9fcb9e..566478c8 100644 --- a/.gitignore +++ b/.gitignore @@ -112,3 +112,6 @@ tmp_standalone_files # Github Actions files .github/workflows + +# Local config +config.local.js diff --git a/src/css/ingame_hud/pinned_shapes.scss b/src/css/ingame_hud/pinned_shapes.scss index c11d9b64..68cf7e16 100644 --- a/src/css/ingame_hud/pinned_shapes.scss +++ b/src/css/ingame_hud/pinned_shapes.scss @@ -109,5 +109,9 @@ } } } + + &.completed { + opacity: 0.5; + } } } diff --git a/src/js/changelog.js b/src/js/changelog.js index 1f411da1..14dce002 100644 --- a/src/js/changelog.js +++ b/src/js/changelog.js @@ -1,4 +1,13 @@ export const CHANGELOG = [ + { + version: "1.1.11", + date: "unrelease", + entries: [ + "Pinned shapes are now smart, they dynamically update their goal and also unpin when no longer required. Completed objectives are now rendered transparent.", + "Improve upgrade number rounding, so there are no goals like '37.4k', instead it will now be '35k'", + "Fix bug regarding number rounding", + ], + }, { version: "1.1.10", date: "12.06.2020", diff --git a/src/js/core/utils.js b/src/js/core/utils.js index e50b71c8..7399d375 100644 --- a/src/js/core/utils.js +++ b/src/js/core/utils.js @@ -377,7 +377,23 @@ export function findNiceValue(num) { return 0; } - const roundAmount = 0.5 * Math_pow(10, Math_floor(Math_log10(num) - 1)); + let roundAmount = 1; + if (num > 50000) { + roundAmount = 10000; + } else if (num > 20000) { + roundAmount = 5000; + } else if (num > 5000) { + roundAmount = 1000; + } else if (num > 2000) { + roundAmount = 500; + } else if (num > 1000) { + roundAmount = 100; + } else if (num > 100) { + roundAmount = 20; + } else if (num > 20) { + roundAmount = 5; + } + const niceValue = Math_floor(num / roundAmount) * roundAmount; if (num >= 10) { return Math_round(niceValue); @@ -389,6 +405,8 @@ export function findNiceValue(num) { return Math_round(niceValue * 100) / 100; } +window.fn = findNiceValue; + /** * Finds a nice integer value * @see findNiceValue diff --git a/src/js/game/hud/hud.js b/src/js/game/hud/hud.js index d6a078f4..fdc5743a 100644 --- a/src/js/game/hud/hud.js +++ b/src/js/game/hud/hud.js @@ -70,7 +70,7 @@ export class GameHUD { this.signals = { selectedPlacementBuildingChanged: /** @type {TypedSignal<[MetaBuilding|null]>} */ (new Signal()), - shapePinRequested: /** @type {TypedSignal<[ShapeDefinition, number]>} */ (new Signal()), + shapePinRequested: /** @type {TypedSignal<[ShapeDefinition]>} */ (new Signal()), shapeUnpinRequested: /** @type {TypedSignal<[string]>} */ (new Signal()), notification: /** @type {TypedSignal<[string, enumNotificationType]>} */ (new Signal()), buildingsSelectedForCopy: /** @type {TypedSignal<[Array]>} */ (new Signal()), diff --git a/src/js/game/hud/parts/pinned_shapes.js b/src/js/game/hud/parts/pinned_shapes.js index 042eba98..62afff25 100644 --- a/src/js/game/hud/parts/pinned_shapes.js +++ b/src/js/game/hud/parts/pinned_shapes.js @@ -1,22 +1,54 @@ import { Math_max } from "../../../core/builtins"; import { ClickDetector } from "../../../core/click_detector"; -import { formatBigNumber, makeDiv } from "../../../core/utils"; +import { formatBigNumber, makeDiv, arrayDelete, arrayDeleteValue } from "../../../core/utils"; import { ShapeDefinition } from "../../shape_definition"; import { BaseHUDPart } from "../base_hud_part"; -import { blueprintShape } from "../../upgrades"; +import { blueprintShape, UPGRADES } from "../../upgrades"; import { enumHubGoalRewards } from "../../tutorial_goals"; +/** + * Manages the pinned shapes on the left side of the screen + */ export class HUDPinnedShapes extends BaseHUDPart { + constructor(root) { + super(root); + /** + * Store a list of pinned shapes + * @type {Array} + */ + this.pinnedShapes = []; + + /** + * Store handles to the currently rendered elements, so we can update them more + * convenient. Also allows for cleaning up handles. + * @type {Array<{ + * key: string, + * amountLabel: HTMLElement, + * lastRenderedValue: string, + * element: HTMLElement, + * detector?: ClickDetector + * }>} + */ + this.handles = []; + } + createElements(parent) { this.element = makeDiv(parent, "ingame_HUD_PinnedShapes", []); } + /** + * Serializes the pinned shapes + */ serialize() { return { shapes: this.pinnedShapes, }; } + /** + * Deserializes the pinned shapes + * @param {{ shapes: Array}} data + */ deserialize(data) { if (!data || !data.shapes || !Array.isArray(data.shapes)) { return "Invalid pinned shapes data"; @@ -24,48 +56,99 @@ export class HUDPinnedShapes extends BaseHUDPart { this.pinnedShapes = data.shapes; } + /** + * Initializes the hud component + */ initialize() { - /** @type {Array<{ key: string, goal: number }>} */ - this.pinnedShapes = []; - - /** @type {Array<{key: string, amountLabel: HTMLElement, lastRenderedValue: number, element: HTMLElement, detector?: ClickDetector}>} */ - this.handles = []; - this.rerenderFull(); - + // Connect to any relevant signals this.root.signals.storyGoalCompleted.add(this.rerenderFull, this); + this.root.signals.upgradePurchased.add(this.updateShapesAfterUpgrade, this); this.root.signals.postLoadHook.add(this.rerenderFull, this); this.root.hud.signals.shapePinRequested.add(this.pinNewShape, this); this.root.hud.signals.shapeUnpinRequested.add(this.unpinShape, this); + + // Perform initial render + this.updateShapesAfterUpgrade(); } /** - * Returns whether a given shape is pinned - * @param {string} key + * Updates all shapes after an upgrade has been purchased and removes the unused ones */ - isShapePinned(key) { - if (!this.pinnedShapes) { - return false; + updateShapesAfterUpgrade() { + for (let i = 0; i < this.pinnedShapes.length; ++i) { + const key = this.pinnedShapes[i]; + if (key === blueprintShape) { + // Ignore blueprint shapes + continue; + } + let goal = this.findGoalValueForShape(key); + if (!goal) { + // Seems no longer relevant + this.pinnedShapes.splice(i, 1); + i -= 1; + } } + + this.rerenderFull(); + } + + /** + * Finds the current goal for the given key. If the key is the story goal, returns + * the story goal. If its the blueprint shape, no goal is returned. Otherwise + * it's searched for upgrades. + * @param {string} key + */ + findGoalValueForShape(key) { if (key === this.root.hubGoals.currentGoal.definition.getHash()) { - return true; + return this.root.hubGoals.currentGoal.required; } if (key === blueprintShape) { - return true; + return null; } - for (let i = 0; i < this.pinnedShapes.length; ++i) { - if (this.pinnedShapes[i].key === key) { - return true; + // Check if this shape is required for any upgrade + for (const upgradeId in UPGRADES) { + const { tiers } = UPGRADES[upgradeId]; + const currentTier = this.root.hubGoals.getUpgradeLevel(upgradeId); + const tierHandle = tiers[currentTier]; + + if (!tierHandle) { + // Max level + continue; + } + + for (let i = 0; i < tierHandle.required.length; ++i) { + const { shape, amount } = tierHandle.required[i]; + if (shape === key) { + return amount; + } } } - return false; + + return null; } + /** + * Returns whether a given shape is currently pinned + * @param {string} key + */ + isShapePinned(key) { + if (key === this.root.hubGoals.currentGoal.definition.getHash() || key === blueprintShape) { + // This is a "special" shape which is always pinned + return true; + } + + return this.pinnedShapes.indexOf(key) >= 0; + } + + /** + * Rerenders the whole component + */ rerenderFull() { const currentGoal = this.root.hubGoals.currentGoal; const currentKey = currentGoal.definition.getHash(); - // First, remove old ones + // First, remove all old shapes for (let i = 0; i < this.handles.length; ++i) { this.handles[i].element.remove(); const detector = this.handles[i].detector; @@ -75,28 +158,30 @@ export class HUDPinnedShapes extends BaseHUDPart { } this.handles = []; - this.internalPinShape(currentKey, currentGoal.required, false, "goal"); + // Pin story goal + this.internalPinShape(currentKey, false, "goal"); + // Pin blueprint shape as well if (this.root.hubGoals.isRewardUnlocked(enumHubGoalRewards.reward_blueprints)) { - this.internalPinShape(blueprintShape, null, false, "blueprint"); + this.internalPinShape(blueprintShape, false, "blueprint"); } + // Pin manually pinned shapes for (let i = 0; i < this.pinnedShapes.length; ++i) { - const key = this.pinnedShapes[i].key; + const key = this.pinnedShapes[i]; if (key !== currentKey) { - this.internalPinShape(key, this.pinnedShapes[i].goal); + this.internalPinShape(key); } } } /** - * Pins a shape + * Pins a new shape * @param {string} key - * @param {number} goal * @param {boolean} canUnpin * @param {string=} className */ - internalPinShape(key, goal, canUnpin = true, className = null) { + internalPinShape(key, canUnpin = true, className = null) { const definition = this.root.shapeDefinitionMgr.getShapeFromShortKey(key); const element = makeDiv(this.element, null, ["shape"]); @@ -121,6 +206,7 @@ export class HUDPinnedShapes extends BaseHUDPart { const amountLabel = makeDiv(element, null, ["amountLabel"], ""); + const goal = this.findGoalValueForShape(key); if (goal) { makeDiv(element, null, ["goalLabel"], "/" + formatBigNumber(goal)); } @@ -129,18 +215,24 @@ export class HUDPinnedShapes extends BaseHUDPart { key, element, amountLabel, - lastRenderedValue: -1, + lastRenderedValue: "", }); } + /** + * Updates all amount labels + */ update() { for (let i = 0; i < this.handles.length; ++i) { const handle = this.handles[i]; const currentValue = this.root.hubGoals.getShapesStoredByKey(handle.key); - if (currentValue !== handle.lastRenderedValue) { - handle.lastRenderedValue = currentValue; - handle.amountLabel.innerText = formatBigNumber(currentValue); + const currentValueFormatted = formatBigNumber(currentValue); + if (currentValueFormatted !== handle.lastRenderedValue) { + handle.lastRenderedValue = currentValueFormatted; + handle.amountLabel.innerText = currentValueFormatted; + const goal = this.findGoalValueForShape(handle.key); + handle.element.classList.toggle("completed", goal && currentValue > goal); } } } @@ -150,20 +242,15 @@ export class HUDPinnedShapes extends BaseHUDPart { * @param {string} key */ unpinShape(key) { - for (let i = 0; i < this.pinnedShapes.length; ++i) { - if (this.pinnedShapes[i].key === key) { - this.pinnedShapes.splice(i, 1); - this.rerenderFull(); - return; - } - } + arrayDeleteValue(this.pinnedShapes, key); + this.rerenderFull(); } /** + * Requests to pin a new shape * @param {ShapeDefinition} definition - * @param {number} goal */ - pinNewShape(definition, goal) { + pinNewShape(definition) { const key = definition.getHash(); if (key === this.root.hubGoals.currentGoal.definition.getHash()) { // Can not pin current goal @@ -171,18 +258,16 @@ export class HUDPinnedShapes extends BaseHUDPart { } if (key === blueprintShape) { + // Can not pin the blueprint shape return; } - for (let i = 0; i < this.pinnedShapes.length; ++i) { - if (this.pinnedShapes[i].key === key) { - // Already pinned - this.pinnedShapes[i].goal = Math_max(this.pinnedShapes[i].goal, goal); - return; - } + // Check if its already pinned + if (this.pinnedShapes.indexOf(key) >= 0) { + return; } - this.pinnedShapes.push({ key, goal }); + this.pinnedShapes.push(key); this.rerenderFull(); } } diff --git a/src/js/game/hud/parts/shop.js b/src/js/game/hud/parts/shop.js index 94da6331..912fb3f1 100644 --- a/src/js/game/hud/parts/shop.js +++ b/src/js/game/hud/parts/shop.js @@ -139,7 +139,7 @@ export class HUDShop extends BaseHUDPart { pinButton.classList.add("unpinned"); pinButton.classList.remove("pinned", "alreadyPinned"); } else { - this.root.hud.signals.shapePinRequested.dispatch(shapeDef, amount); + this.root.hud.signals.shapePinRequested.dispatch(shapeDef); pinButton.classList.add("pinned"); pinButton.classList.remove("unpinned"); } diff --git a/src/js/savegame/savegame.js b/src/js/savegame/savegame.js index 7d59a056..b67a8d0d 100644 --- a/src/js/savegame/savegame.js +++ b/src/js/savegame/savegame.js @@ -9,10 +9,10 @@ import { SavegameSerializer } from "./savegame_serializer"; import { BaseSavegameInterface } from "./savegame_interface"; import { createLogger } from "../core/logging"; import { globalConfig } from "../core/config"; -import { SavegameInterface_V1000 } from "./schemas/1000"; import { getSavegameInterface, savegameInterfaces } from "./savegame_interface_registry"; import { SavegameInterface_V1001 } from "./schemas/1001"; import { SavegameInterface_V1002 } from "./schemas/1002"; +import { SavegameInterface_V1003 } from "./schemas/1003"; const logger = createLogger("savegame"); @@ -44,7 +44,7 @@ export class Savegame extends ReadWriteProxy { * @returns {number} */ static getCurrentVersion() { - return 1002; + return 1003; } /** @@ -93,6 +93,11 @@ export class Savegame extends ReadWriteProxy { data.version = 1002; } + if (data.version === 1002) { + SavegameInterface_V1003.migrate1002to1003(data); + data.version = 1003; + } + return ExplainedResult.good(); } diff --git a/src/js/savegame/savegame_interface_registry.js b/src/js/savegame/savegame_interface_registry.js index 7c6db250..8c28fcc9 100644 --- a/src/js/savegame/savegame_interface_registry.js +++ b/src/js/savegame/savegame_interface_registry.js @@ -3,12 +3,14 @@ import { SavegameInterface_V1000 } from "./schemas/1000"; import { createLogger } from "../core/logging"; import { SavegameInterface_V1001 } from "./schemas/1001"; import { SavegameInterface_V1002 } from "./schemas/1002"; +import { SavegameInterface_V1003 } from "./schemas/1003"; /** @type {Object.} */ export const savegameInterfaces = { 1000: SavegameInterface_V1000, 1001: SavegameInterface_V1001, 1002: SavegameInterface_V1002, + 1003: SavegameInterface_V1003, }; const logger = createLogger("savegame_interface_registry"); diff --git a/src/js/savegame/schemas/1003.js b/src/js/savegame/schemas/1003.js new file mode 100644 index 00000000..4a4a3ee3 --- /dev/null +++ b/src/js/savegame/schemas/1003.js @@ -0,0 +1,28 @@ +import { createLogger } from "../../core/logging.js"; +import { SavegameInterface_V1002 } from "./1002.js"; + +const schema = require("./1003.json"); +const logger = createLogger("savegame_interface/1003"); + +export class SavegameInterface_V1003 extends SavegameInterface_V1002 { + getVersion() { + return 1003; + } + + getSchemaUncached() { + return schema; + } + + /** + * @param {import("../savegame_typedefs.js").SavegameData} data + */ + static migrate1002to1003(data) { + logger.log("Migrating 1002 to 1003"); + const dump = data.dump; + if (!dump) { + return true; + } + + dump.pinnedShapes = { shapes: [] }; + } +} diff --git a/src/js/savegame/schemas/1003.json b/src/js/savegame/schemas/1003.json new file mode 100644 index 00000000..6682f615 --- /dev/null +++ b/src/js/savegame/schemas/1003.json @@ -0,0 +1,5 @@ +{ + "type": "object", + "required": [], + "additionalProperties": true +}