Refactor pinned shapes and make them smart, closes #72

pull/130/head
tobspr 4 years ago
parent 8c01cc23d0
commit 3af6532d32

3
.gitignore vendored

@ -112,3 +112,6 @@ tmp_standalone_files
# Github Actions files # Github Actions files
.github/workflows .github/workflows
# Local config
config.local.js

@ -109,5 +109,9 @@
} }
} }
} }
&.completed {
opacity: 0.5;
}
} }
} }

@ -1,4 +1,13 @@
export const CHANGELOG = [ 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", version: "1.1.10",
date: "12.06.2020", date: "12.06.2020",

@ -377,7 +377,23 @@ export function findNiceValue(num) {
return 0; 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; const niceValue = Math_floor(num / roundAmount) * roundAmount;
if (num >= 10) { if (num >= 10) {
return Math_round(niceValue); return Math_round(niceValue);
@ -389,6 +405,8 @@ export function findNiceValue(num) {
return Math_round(niceValue * 100) / 100; return Math_round(niceValue * 100) / 100;
} }
window.fn = findNiceValue;
/** /**
* Finds a nice integer value * Finds a nice integer value
* @see findNiceValue * @see findNiceValue

@ -70,7 +70,7 @@ export class GameHUD {
this.signals = { this.signals = {
selectedPlacementBuildingChanged: /** @type {TypedSignal<[MetaBuilding|null]>} */ (new Signal()), 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()), shapeUnpinRequested: /** @type {TypedSignal<[string]>} */ (new Signal()),
notification: /** @type {TypedSignal<[string, enumNotificationType]>} */ (new Signal()), notification: /** @type {TypedSignal<[string, enumNotificationType]>} */ (new Signal()),
buildingsSelectedForCopy: /** @type {TypedSignal<[Array<number>]>} */ (new Signal()), buildingsSelectedForCopy: /** @type {TypedSignal<[Array<number>]>} */ (new Signal()),

@ -1,22 +1,54 @@
import { Math_max } from "../../../core/builtins"; import { Math_max } from "../../../core/builtins";
import { ClickDetector } from "../../../core/click_detector"; 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 { ShapeDefinition } from "../../shape_definition";
import { BaseHUDPart } from "../base_hud_part"; import { BaseHUDPart } from "../base_hud_part";
import { blueprintShape } from "../../upgrades"; import { blueprintShape, UPGRADES } from "../../upgrades";
import { enumHubGoalRewards } from "../../tutorial_goals"; import { enumHubGoalRewards } from "../../tutorial_goals";
/**
* Manages the pinned shapes on the left side of the screen
*/
export class HUDPinnedShapes extends BaseHUDPart { export class HUDPinnedShapes extends BaseHUDPart {
constructor(root) {
super(root);
/**
* Store a list of pinned shapes
* @type {Array<string>}
*/
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) { createElements(parent) {
this.element = makeDiv(parent, "ingame_HUD_PinnedShapes", []); this.element = makeDiv(parent, "ingame_HUD_PinnedShapes", []);
} }
/**
* Serializes the pinned shapes
*/
serialize() { serialize() {
return { return {
shapes: this.pinnedShapes, shapes: this.pinnedShapes,
}; };
} }
/**
* Deserializes the pinned shapes
* @param {{ shapes: Array<string>}} data
*/
deserialize(data) { deserialize(data) {
if (!data || !data.shapes || !Array.isArray(data.shapes)) { if (!data || !data.shapes || !Array.isArray(data.shapes)) {
return "Invalid pinned shapes data"; return "Invalid pinned shapes data";
@ -24,48 +56,99 @@ export class HUDPinnedShapes extends BaseHUDPart {
this.pinnedShapes = data.shapes; this.pinnedShapes = data.shapes;
} }
/**
* Initializes the hud component
*/
initialize() { initialize() {
/** @type {Array<{ key: string, goal: number }>} */ // Connect to any relevant signals
this.pinnedShapes = [];
/** @type {Array<{key: string, amountLabel: HTMLElement, lastRenderedValue: number, element: HTMLElement, detector?: ClickDetector}>} */
this.handles = [];
this.rerenderFull();
this.root.signals.storyGoalCompleted.add(this.rerenderFull, this); 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.signals.postLoadHook.add(this.rerenderFull, this);
this.root.hud.signals.shapePinRequested.add(this.pinNewShape, this); this.root.hud.signals.shapePinRequested.add(this.pinNewShape, this);
this.root.hud.signals.shapeUnpinRequested.add(this.unpinShape, this); this.root.hud.signals.shapeUnpinRequested.add(this.unpinShape, this);
// Perform initial render
this.updateShapesAfterUpgrade();
} }
/** /**
* Returns whether a given shape is pinned * Updates all shapes after an upgrade has been purchased and removes the unused ones
* @param {string} key
*/ */
isShapePinned(key) { updateShapesAfterUpgrade() {
if (!this.pinnedShapes) { for (let i = 0; i < this.pinnedShapes.length; ++i) {
return false; 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()) { if (key === this.root.hubGoals.currentGoal.definition.getHash()) {
return true; return this.root.hubGoals.currentGoal.required;
} }
if (key === blueprintShape) { if (key === blueprintShape) {
return true; return null;
} }
for (let i = 0; i < this.pinnedShapes.length; ++i) { // Check if this shape is required for any upgrade
if (this.pinnedShapes[i].key === key) { for (const upgradeId in UPGRADES) {
return true; 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() { rerenderFull() {
const currentGoal = this.root.hubGoals.currentGoal; const currentGoal = this.root.hubGoals.currentGoal;
const currentKey = currentGoal.definition.getHash(); const currentKey = currentGoal.definition.getHash();
// First, remove old ones // First, remove all old shapes
for (let i = 0; i < this.handles.length; ++i) { for (let i = 0; i < this.handles.length; ++i) {
this.handles[i].element.remove(); this.handles[i].element.remove();
const detector = this.handles[i].detector; const detector = this.handles[i].detector;
@ -75,28 +158,30 @@ export class HUDPinnedShapes extends BaseHUDPart {
} }
this.handles = []; 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)) { 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) { for (let i = 0; i < this.pinnedShapes.length; ++i) {
const key = this.pinnedShapes[i].key; const key = this.pinnedShapes[i];
if (key !== currentKey) { if (key !== currentKey) {
this.internalPinShape(key, this.pinnedShapes[i].goal); this.internalPinShape(key);
} }
} }
} }
/** /**
* Pins a shape * Pins a new shape
* @param {string} key * @param {string} key
* @param {number} goal
* @param {boolean} canUnpin * @param {boolean} canUnpin
* @param {string=} className * @param {string=} className
*/ */
internalPinShape(key, goal, canUnpin = true, className = null) { internalPinShape(key, canUnpin = true, className = null) {
const definition = this.root.shapeDefinitionMgr.getShapeFromShortKey(key); const definition = this.root.shapeDefinitionMgr.getShapeFromShortKey(key);
const element = makeDiv(this.element, null, ["shape"]); const element = makeDiv(this.element, null, ["shape"]);
@ -121,6 +206,7 @@ export class HUDPinnedShapes extends BaseHUDPart {
const amountLabel = makeDiv(element, null, ["amountLabel"], ""); const amountLabel = makeDiv(element, null, ["amountLabel"], "");
const goal = this.findGoalValueForShape(key);
if (goal) { if (goal) {
makeDiv(element, null, ["goalLabel"], "/" + formatBigNumber(goal)); makeDiv(element, null, ["goalLabel"], "/" + formatBigNumber(goal));
} }
@ -129,18 +215,24 @@ export class HUDPinnedShapes extends BaseHUDPart {
key, key,
element, element,
amountLabel, amountLabel,
lastRenderedValue: -1, lastRenderedValue: "",
}); });
} }
/**
* Updates all amount labels
*/
update() { update() {
for (let i = 0; i < this.handles.length; ++i) { for (let i = 0; i < this.handles.length; ++i) {
const handle = this.handles[i]; const handle = this.handles[i];
const currentValue = this.root.hubGoals.getShapesStoredByKey(handle.key); const currentValue = this.root.hubGoals.getShapesStoredByKey(handle.key);
if (currentValue !== handle.lastRenderedValue) { const currentValueFormatted = formatBigNumber(currentValue);
handle.lastRenderedValue = currentValue; if (currentValueFormatted !== handle.lastRenderedValue) {
handle.amountLabel.innerText = formatBigNumber(currentValue); 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 * @param {string} key
*/ */
unpinShape(key) { unpinShape(key) {
for (let i = 0; i < this.pinnedShapes.length; ++i) { arrayDeleteValue(this.pinnedShapes, key);
if (this.pinnedShapes[i].key === key) { this.rerenderFull();
this.pinnedShapes.splice(i, 1);
this.rerenderFull();
return;
}
}
} }
/** /**
* Requests to pin a new shape
* @param {ShapeDefinition} definition * @param {ShapeDefinition} definition
* @param {number} goal
*/ */
pinNewShape(definition, goal) { pinNewShape(definition) {
const key = definition.getHash(); const key = definition.getHash();
if (key === this.root.hubGoals.currentGoal.definition.getHash()) { if (key === this.root.hubGoals.currentGoal.definition.getHash()) {
// Can not pin current goal // Can not pin current goal
@ -171,18 +258,16 @@ export class HUDPinnedShapes extends BaseHUDPart {
} }
if (key === blueprintShape) { if (key === blueprintShape) {
// Can not pin the blueprint shape
return; return;
} }
for (let i = 0; i < this.pinnedShapes.length; ++i) { // Check if its already pinned
if (this.pinnedShapes[i].key === key) { if (this.pinnedShapes.indexOf(key) >= 0) {
// Already pinned return;
this.pinnedShapes[i].goal = Math_max(this.pinnedShapes[i].goal, goal);
return;
}
} }
this.pinnedShapes.push({ key, goal }); this.pinnedShapes.push(key);
this.rerenderFull(); this.rerenderFull();
} }
} }

@ -139,7 +139,7 @@ export class HUDShop extends BaseHUDPart {
pinButton.classList.add("unpinned"); pinButton.classList.add("unpinned");
pinButton.classList.remove("pinned", "alreadyPinned"); pinButton.classList.remove("pinned", "alreadyPinned");
} else { } else {
this.root.hud.signals.shapePinRequested.dispatch(shapeDef, amount); this.root.hud.signals.shapePinRequested.dispatch(shapeDef);
pinButton.classList.add("pinned"); pinButton.classList.add("pinned");
pinButton.classList.remove("unpinned"); pinButton.classList.remove("unpinned");
} }

@ -9,10 +9,10 @@ import { SavegameSerializer } from "./savegame_serializer";
import { BaseSavegameInterface } from "./savegame_interface"; import { BaseSavegameInterface } from "./savegame_interface";
import { createLogger } from "../core/logging"; import { createLogger } from "../core/logging";
import { globalConfig } from "../core/config"; import { globalConfig } from "../core/config";
import { SavegameInterface_V1000 } from "./schemas/1000";
import { getSavegameInterface, savegameInterfaces } from "./savegame_interface_registry"; import { getSavegameInterface, savegameInterfaces } from "./savegame_interface_registry";
import { SavegameInterface_V1001 } from "./schemas/1001"; import { SavegameInterface_V1001 } from "./schemas/1001";
import { SavegameInterface_V1002 } from "./schemas/1002"; import { SavegameInterface_V1002 } from "./schemas/1002";
import { SavegameInterface_V1003 } from "./schemas/1003";
const logger = createLogger("savegame"); const logger = createLogger("savegame");
@ -44,7 +44,7 @@ export class Savegame extends ReadWriteProxy {
* @returns {number} * @returns {number}
*/ */
static getCurrentVersion() { static getCurrentVersion() {
return 1002; return 1003;
} }
/** /**
@ -93,6 +93,11 @@ export class Savegame extends ReadWriteProxy {
data.version = 1002; data.version = 1002;
} }
if (data.version === 1002) {
SavegameInterface_V1003.migrate1002to1003(data);
data.version = 1003;
}
return ExplainedResult.good(); return ExplainedResult.good();
} }

@ -3,12 +3,14 @@ import { SavegameInterface_V1000 } from "./schemas/1000";
import { createLogger } from "../core/logging"; import { createLogger } from "../core/logging";
import { SavegameInterface_V1001 } from "./schemas/1001"; import { SavegameInterface_V1001 } from "./schemas/1001";
import { SavegameInterface_V1002 } from "./schemas/1002"; import { SavegameInterface_V1002 } from "./schemas/1002";
import { SavegameInterface_V1003 } from "./schemas/1003";
/** @type {Object.<number, typeof BaseSavegameInterface>} */ /** @type {Object.<number, typeof BaseSavegameInterface>} */
export const savegameInterfaces = { export const savegameInterfaces = {
1000: SavegameInterface_V1000, 1000: SavegameInterface_V1000,
1001: SavegameInterface_V1001, 1001: SavegameInterface_V1001,
1002: SavegameInterface_V1002, 1002: SavegameInterface_V1002,
1003: SavegameInterface_V1003,
}; };
const logger = createLogger("savegame_interface_registry"); const logger = createLogger("savegame_interface_registry");

@ -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: [] };
}
}

@ -0,0 +1,5 @@
{
"type": "object",
"required": [],
"additionalProperties": true
}
Loading…
Cancel
Save