diff --git a/res/ui/icons/play.png b/res/ui/icons/play.png new file mode 100644 index 00000000..cd7f610c Binary files /dev/null and b/res/ui/icons/play.png differ diff --git a/src/css/states/main_menu.scss b/src/css/states/main_menu.scss index bb009163..a67cef3a 100644 --- a/src/css/states/main_menu.scss +++ b/src/css/states/main_menu.scss @@ -58,7 +58,7 @@ } .mainContainer { - @include S(margin-top, 40px); + @include S(margin-top, 10px); display: flex; align-items: center; justify-content: flex-start; @@ -81,6 +81,50 @@ transform: scale(1.02); } } + + .savegames { + @include S(max-height, 92px); + overflow-y: auto; + @include S(width, 200px); + pointer-events: all; + @include S(padding-right, 5px); + display: grid; + grid-auto-flow: row; + @include S(grid-gap, 5px); + @include S(margin-top, 10px); + .savegame { + background: #eee; + @include BorderRadius(4px); + @include S(padding, 5px); + display: grid; + grid-template-columns: 1fr auto; + grid-template-rows: auto auto; + @include S(grid-column-gap, 15px); + + .internalId { + grid-column: 1 / 2; + grid-row: 2 / 3; + @include SuperSmallText; + opacity: 0.5; + } + + .updateTime { + grid-column: 1 / 2; + grid-row: 1 / 2; + @include PlainText; + } + + button.resumeGame { + grid-column: 2 / 3; + grid-row: 1 / 3; + @include S(width, 30px); + @include S(height, 30px); + padding: 0; + align-self: center; + background: #44484a uiResource("icons/play.png") center center / 40% no-repeat; + } + } + } } .footer { diff --git a/src/css/states/preload.scss b/src/css/states/preload.scss index 25b13ead..d7cb412f 100644 --- a/src/css/states/preload.scss +++ b/src/css/states/preload.scss @@ -79,6 +79,7 @@ @include Button3D($colorRedBright); @include PlainText; @include S(padding, 5px, 8px, 4px); + color: #fff; } } } diff --git a/src/js/core/async_compression.js b/src/js/core/async_compression.js index 479ee80b..4afed1ea 100644 --- a/src/js/core/async_compression.js +++ b/src/js/core/async_compression.js @@ -84,39 +84,6 @@ class AsynCompression { }); } - /** - * Compresses regulary - * @param {string} text - */ - compressX64Async(text) { - if (text.length < 1024) { - // Ok so this is not worth it - return Promise.resolve(compressX64(text)); - } - return this.internalQueueJob("compressX64", text); - } - - /** - * Compresses with checksum - * @param {any} obj - */ - compressWithChecksum(obj) { - const stringified = JSON_stringify(obj); - return this.internalQueueJob("compressWithChecksum", stringified); - } - - /** - * Compresses with checksum - * @param {any} data The packets data - * @param {number} packetId The numeric packet id - */ - compressPacket(data, packetId) { - return this.internalQueueJob("compressPacket", { - data, - packetId, - }); - } - /** * Queues a new job * @param {string} job diff --git a/src/js/core/config.js b/src/js/core/config.js index a7e629cf..7b09443e 100644 --- a/src/js/core/config.js +++ b/src/js/core/config.js @@ -73,11 +73,11 @@ export const globalConfig = { /* dev:start */ // fastGameEnter: true, noArtificialDelays: true, - disableSavegameWrite: false, + // disableSavegameWrite: true, showEntityBounds: false, showAcceptorEjectors: false, usePlainShapeIds: true, - // disableMusic: true, + disableMusic: true, doNotRenderStatics: false, disableZoomLimits: false, showChunkBorders: false, diff --git a/src/js/core/perlin_noise.js b/src/js/core/perlin_noise.js deleted file mode 100644 index 0cce61ed..00000000 --- a/src/js/core/perlin_noise.js +++ /dev/null @@ -1,175 +0,0 @@ -import { perlinNoiseData } from "./perlin_noise_data"; -import { Math_sqrt } from "./builtins"; - -class Grad { - constructor(x, y, z) { - this.x = x; - this.y = y; - this.z = z; - } - - dot2(x, y) { - return this.x * x + this.y * y; - } - - dot3(x, y, z) { - return this.x * x + this.y * y + this.z * z; - } -} - -function fade(t) { - return t * t * t * (t * (t * 6 - 15) + 10); -} - -function lerp(a, b, t) { - return (1 - t) * a + t * b; -} - -const F2 = 0.5 * (Math_sqrt(3) - 1); -const G2 = (3 - Math_sqrt(3)) / 6; - -const F3 = 1 / 3; -const G3 = 1 / 6; - -export class PerlinNoise { - constructor(seed) { - this.perm = new Array(512); - this.gradP = new Array(512); - this.grad3 = [ - new Grad(1, 1, 0), - new Grad(-1, 1, 0), - new Grad(1, -1, 0), - new Grad(-1, -1, 0), - new Grad(1, 0, 1), - new Grad(-1, 0, 1), - new Grad(1, 0, -1), - new Grad(-1, 0, -1), - new Grad(0, 1, 1), - new Grad(0, -1, 1), - new Grad(0, 1, -1), - new Grad(0, -1, -1), - ]; - - this.seed = seed; - this.initializeFromSeed(seed); - } - - initializeFromSeed(seed) { - const P = perlinNoiseData; - - if (seed > 0 && seed < 1) { - // Scale the seed out - seed *= 65536; - } - - seed = Math.floor(seed); - if (seed < 256) { - seed |= seed << 8; - } - - for (let i = 0; i < 256; i++) { - let v; - if (i & 1) { - v = P[i] ^ (seed & 255); - } else { - v = P[i] ^ ((seed >> 8) & 255); - } - - this.perm[i] = this.perm[i + 256] = v; - this.gradP[i] = this.gradP[i + 256] = this.grad3[v % 12]; - } - } - - /** - * 2d Perlin Noise - * @param {number} x - * @param {number} y - * @returns {number} - */ - computePerlin2(x, y) { - // Find unit grid cell containing point - let X = Math.floor(x), - Y = Math.floor(y); - - // Get relative xy coordinates of point within that cell - x = x - X; - y = y - Y; - - // Wrap the integer cells at 255 (smaller integer period can be introduced here) - X = X & 255; - Y = Y & 255; - - // Calculate noise contributions from each of the four corners - let n00 = this.gradP[X + this.perm[Y]].dot2(x, y); - let n01 = this.gradP[X + this.perm[Y + 1]].dot2(x, y - 1); - let n10 = this.gradP[X + 1 + this.perm[Y]].dot2(x - 1, y); - let n11 = this.gradP[X + 1 + this.perm[Y + 1]].dot2(x - 1, y - 1); - - // Compute the fade curve value for x - let u = fade(x); - - // Interpolate the four results - return lerp(lerp(n00, n10, u), lerp(n01, n11, u), fade(y)); - } - - computeSimplex2(xin, yin) { - var n0, n1, n2; // Noise contributions from the three corners - // Skew the input space to determine which simplex cell we're in - var s = (xin + yin) * F2; // Hairy factor for 2D - var i = Math.floor(xin + s); - var j = Math.floor(yin + s); - var t = (i + j) * G2; - var x0 = xin - i + t; // The x,y distances from the cell origin, unskewed. - var y0 = yin - j + t; - // For the 2D case, the simplex shape is an equilateral triangle. - // Determine which simplex we are in. - var i1, j1; // Offsets for second (middle) corner of simplex in (i,j) coords - if (x0 > y0) { - // lower triangle, XY order: (0,0)->(1,0)->(1,1) - i1 = 1; - j1 = 0; - } else { - // upper triangle, YX order: (0,0)->(0,1)->(1,1) - i1 = 0; - j1 = 1; - } - // A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and - // a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where - // c = (3-sqrt(3))/6 - var x1 = x0 - i1 + G2; // Offsets for middle corner in (x,y) unskewed coords - var y1 = y0 - j1 + G2; - var x2 = x0 - 1 + 2 * G2; // Offsets for last corner in (x,y) unskewed coords - var y2 = y0 - 1 + 2 * G2; - // Work out the hashed gradient indices of the three simplex corners - i &= 255; - j &= 255; - var gi0 = this.gradP[i + this.perm[j]]; - var gi1 = this.gradP[i + i1 + this.perm[j + j1]]; - var gi2 = this.gradP[i + 1 + this.perm[j + 1]]; - // Calculate the contribution from the three corners - var t0 = 0.5 - x0 * x0 - y0 * y0; - if (t0 < 0) { - n0 = 0; - } else { - t0 *= t0; - n0 = t0 * t0 * gi0.dot2(x0, y0); // (x,y) of grad3 used for 2D gradient - } - var t1 = 0.5 - x1 * x1 - y1 * y1; - if (t1 < 0) { - n1 = 0; - } else { - t1 *= t1; - n1 = t1 * t1 * gi1.dot2(x1, y1); - } - var t2 = 0.5 - x2 * x2 - y2 * y2; - if (t2 < 0) { - n2 = 0; - } else { - t2 *= t2; - n2 = t2 * t2 * gi2.dot2(x2, y2); - } - // Add contributions from each corner to get the final noise value. - // The result is scaled to return values in the interval [-1,1]. - return 70 * (n0 + n1 + n2); - } -} diff --git a/src/js/core/perlin_noise_data.js b/src/js/core/perlin_noise_data.js deleted file mode 100644 index 14a25ba0..00000000 --- a/src/js/core/perlin_noise_data.js +++ /dev/null @@ -1,258 +0,0 @@ -export const perlinNoiseData = [ - 151, - 160, - 137, - 91, - 90, - 15, - 131, - 13, - 201, - 95, - 96, - 53, - 194, - 233, - 7, - 225, - 140, - 36, - 103, - 30, - 69, - 142, - 8, - 99, - 37, - 240, - 21, - 10, - 23, - 190, - 6, - 148, - 247, - 120, - 234, - 75, - 0, - 26, - 197, - 62, - 94, - 252, - 219, - 203, - 117, - 35, - 11, - 32, - 57, - 177, - 33, - 88, - 237, - 149, - 56, - 87, - 174, - 20, - 125, - 136, - 171, - 168, - 68, - 175, - 74, - 165, - 71, - 134, - 139, - 48, - 27, - 166, - 77, - 146, - 158, - 231, - 83, - 111, - 229, - 122, - 60, - 211, - 133, - 230, - 220, - 105, - 92, - 41, - 55, - 46, - 245, - 40, - 244, - 102, - 143, - 54, - 65, - 25, - 63, - 161, - 1, - 216, - 80, - 73, - 209, - 76, - 132, - 187, - 208, - 89, - 18, - 169, - 200, - 196, - 135, - 130, - 116, - 188, - 159, - 86, - 164, - 100, - 109, - 198, - 173, - 186, - 3, - 64, - 52, - 217, - 226, - 250, - 124, - 123, - 5, - 202, - 38, - 147, - 118, - 126, - 255, - 82, - 85, - 212, - 207, - 206, - 59, - 227, - 47, - 16, - 58, - 17, - 182, - 189, - 28, - 42, - 223, - 183, - 170, - 213, - 119, - 248, - 152, - 2, - 44, - 154, - 163, - 70, - 221, - 153, - 101, - 155, - 167, - 43, - 172, - 9, - 129, - 22, - 39, - 253, - 19, - 98, - 108, - 110, - 79, - 113, - 224, - 232, - 178, - 185, - 112, - 104, - 218, - 246, - 97, - 228, - 251, - 34, - 242, - 193, - 238, - 210, - 144, - 12, - 191, - 179, - 162, - 241, - 81, - 51, - 145, - 235, - 249, - 14, - 239, - 107, - 49, - 192, - 214, - 31, - 181, - 199, - 106, - 157, - 184, - 84, - 204, - 176, - 115, - 121, - 50, - 45, - 127, - 4, - 150, - 254, - 138, - 236, - 205, - 93, - 222, - 114, - 67, - 29, - 24, - 72, - 243, - 141, - 128, - 195, - 78, - 66, - 215, - 61, - 156, - 180, -]; diff --git a/src/js/core/read_write_proxy.js b/src/js/core/read_write_proxy.js index 07bf5667..a735f9ad 100644 --- a/src/js/core/read_write_proxy.js +++ b/src/js/core/read_write_proxy.js @@ -11,6 +11,7 @@ import { JSON_stringify, JSON_parse } from "./builtins"; import { ExplainedResult } from "./explained_result"; import { decompressX64, compressX64 } from ".//lzstring"; import { asyncCompressor, compressionPrefix } from "./async_compression"; +import { compressObject, decompressObject } from "../savegame/savegame_compressor"; const logger = createLogger("read_write_proxy"); @@ -89,7 +90,7 @@ export class ReadWriteProxy { logger.error("Tried to write invalid data to", this.filename, "reason:", verifyResult.reason); return Promise.reject(verifyResult.reason); } - const jsonString = JSON_stringify(this.currentData); + const jsonString = JSON_stringify(compressObject(this.currentData)); if (!this.app.pageVisible || this.app.unloaded) { logger.log("Saving file sync because in unload handler"); @@ -149,7 +150,7 @@ export class ReadWriteProxy { .then(rawData => { if (rawData == null) { // So, the file has not been found, use default data - return JSON_stringify(this.getDefaultData()); + return JSON_stringify(compressObject(this.getDefaultData())); } if (rawData.startsWith(compressionPrefix)) { @@ -198,6 +199,9 @@ export class ReadWriteProxy { } }) + // Decompress + .then(compressed => decompressObject(compressed)) + // Verify basic structure .then(contents => { const result = this.internalVerifyBasicStructure(contents); diff --git a/src/js/core/rng.js b/src/js/core/rng.js index 0653b95c..3322aafa 100644 --- a/src/js/core/rng.js +++ b/src/js/core/rng.js @@ -90,6 +90,15 @@ export class RandomNumberGenerator { return this.internalRng(); } + /** + * Random choice of an array + * @param {array} array + */ + choice(array) { + const index = this.nextIntRange(0, array.length); + return array[index]; + } + /** * @param {number} min * @param {number} max diff --git a/src/js/core/sensitive_utils.encrypt.js b/src/js/core/sensitive_utils.encrypt.js index 7434164e..710de478 100644 --- a/src/js/core/sensitive_utils.encrypt.js +++ b/src/js/core/sensitive_utils.encrypt.js @@ -17,46 +17,3 @@ export function sha1(str) { export function getNameOfProvider() { return window[decodeHashedString("DYewxghgLgliB2Q")][decodeHashedString("BYewzgLgdghgtgUyA")]; } - -export function compressWithChecksum(object) { - const stringified = JSON.stringify(object); - const checksum = Rusha.createHash() - .update(stringified + encryptKey) - .digest("hex"); - return compressX64(checksum + stringified); -} - -export function decompressWithChecksum(binary) { - let decompressed = null; - try { - decompressed = decompressX64(binary); - } catch (err) { - throw new Error("failed-to-decompress"); - } - - // Split into checksum and content - if (!decompressed || decompressed.length < 41) { - throw new Error("checksum-missing"); - } - - const checksum = decompressed.substr(0, 40); - const rawData = decompressed.substr(40); - - // Validate checksum - const computedChecksum = Rusha.createHash() - .update(rawData + encryptKey) - .digest("hex"); - if (computedChecksum !== checksum) { - throw new Error("checksum-mismatch"); - } - - // Try parsing the JSON - let data = null; - try { - data = JSON.parse(rawData); - } catch (err) { - throw new Error("failed-to-parse"); - } - - return data; -} diff --git a/src/js/core/utils.js b/src/js/core/utils.js index 0995fe7f..08011a45 100644 --- a/src/js/core/utils.js +++ b/src/js/core/utils.js @@ -825,3 +825,37 @@ export function fastRotateMultipleOf90(x, y, deg) { } } } + +/** + * Formats an amount of seconds into something like "5s ago" + * @param {number} secs Seconds + * @returns {string} + */ +export function formatSecondsToTimeAgo(secs) { + const seconds = Math_floor(secs); + const minutes = Math_floor(seconds / 60); + const hours = Math_floor(minutes / 60); + const days = Math_floor(hours / 24); + + if (seconds <= 60) { + if (seconds <= 1) { + return "one second ago"; + } + return seconds + " seconds ago"; + } else if (minutes <= 60) { + if (minutes <= 1) { + return "one minute ago"; + } + return minutes + " minutes ago"; + } else if (hours <= 60) { + if (hours <= 1) { + return "one hour ago"; + } + return hours + " hour ago"; + } else { + if (days <= 1) { + return "one day ago"; + } + return days + " days ago"; + } +} diff --git a/src/js/game/camera.js b/src/js/game/camera.js index 6bae86a5..83e8e9b3 100644 --- a/src/js/game/camera.js +++ b/src/js/game/camera.js @@ -2,21 +2,21 @@ import { Math_abs, Math_ceil, Math_floor, + Math_max, Math_min, Math_random, performanceNow, - Math_max, } from "../core/builtins"; -import { Rectangle } from "../core/rectangle"; -import { Signal, STOP_PROPAGATION } from "../core/signal"; -import { clamp, lerp } from "../core/utils"; -import { mixVector, Vector } from "../core/vector"; -import { globalConfig } from "../core/config"; -import { GameRoot } from "./root"; -import { BasicSerializableObject, types } from "../savegame/serialization"; import { clickDetectorGlobals } from "../core/click_detector"; +import { globalConfig } from "../core/config"; import { createLogger } from "../core/logging"; import { queryParamOptions } from "../core/query_parameters"; +import { Rectangle } from "../core/rectangle"; +import { Signal, STOP_PROPAGATION } from "../core/signal"; +import { clamp } from "../core/utils"; +import { mixVector, Vector } from "../core/vector"; +import { BasicSerializableObject, types } from "../savegame/serialization"; +import { GameRoot } from "./root"; const logger = createLogger("camera"); diff --git a/src/js/game/components/hub.js b/src/js/game/components/hub.js index 6df1bcf5..f9c13dc3 100644 --- a/src/js/game/components/hub.js +++ b/src/js/game/components/hub.js @@ -1,11 +1,18 @@ import { Component } from "../component"; import { ShapeDefinition } from "../shape_definition"; +import { types } from "../../savegame/serialization"; export class HubComponent extends Component { static getId() { return "Hub"; } + static getSchema() { + return { + definitionsToAnalyze: types.array(types.knownType(ShapeDefinition)), + }; + } + constructor() { super(); diff --git a/src/js/game/components/item_acceptor.js b/src/js/game/components/item_acceptor.js index bc02330c..f696c0e4 100644 --- a/src/js/game/components/item_acceptor.js +++ b/src/js/game/components/item_acceptor.js @@ -3,6 +3,7 @@ import { Vector, enumDirection, enumDirectionToAngle, enumInvertedDirections } f import { BaseItem } from "../base_item"; import { ShapeItem } from "../items/shape_item"; import { ColorItem } from "../items/color_item"; +import { types } from "../../savegame/serialization"; /** * @enum {string?} @@ -26,7 +27,13 @@ export class ItemAcceptorComponent extends Component { static getSchema() { return { - // slots: "TODO", + slots: types.array( + types.structured({ + pos: types.vector, + directions: types.array(types.enum(enumDirection)), + filter: types.nullable(types.enum(enumItemAcceptorItemFilter)), + }) + ), }; } @@ -35,7 +42,7 @@ export class ItemAcceptorComponent extends Component { * @param {object} param0 * @param {Array<{pos: Vector, directions: enumDirection[], filter?: enumItemAcceptorItemFilter}>} param0.slots The slots from which we accept items */ - constructor({ slots }) { + constructor({ slots = [] }) { super(); this.setSlots(slots); diff --git a/src/js/game/components/item_ejector.js b/src/js/game/components/item_ejector.js index 786213ce..95c677d3 100644 --- a/src/js/game/components/item_ejector.js +++ b/src/js/game/components/item_ejector.js @@ -1,7 +1,8 @@ -import { globalConfig } from "../../core/config"; import { Vector, enumDirection, enumDirectionToVector } from "../../core/vector"; import { BaseItem } from "../base_item"; import { Component } from "../component"; +import { types } from "../../savegame/serialization"; +import { gItemRegistry } from "../../core/global_registries"; /** * @typedef {{ @@ -19,7 +20,15 @@ export class ItemEjectorComponent extends Component { static getSchema() { return { - // slots: "TODO" + instantEject: types.bool, + slots: types.array( + types.structured({ + pos: types.vector, + direction: types.enum(enumDirection), + item: types.nullable(types.obj(gItemRegistry)), + progress: types.ufloat, + }) + ), }; } @@ -29,7 +38,7 @@ export class ItemEjectorComponent extends Component { * @param {Array<{pos: Vector, direction: enumDirection}>} param0.slots The slots to eject on * @param {boolean=} param0.instantEject If the ejection is instant */ - constructor({ slots, instantEject = false }) { + constructor({ slots = [], instantEject = false }) { super(); // How long items take to eject diff --git a/src/js/game/components/item_processor.js b/src/js/game/components/item_processor.js index f5257bcb..019511c4 100644 --- a/src/js/game/components/item_processor.js +++ b/src/js/game/components/item_processor.js @@ -1,6 +1,8 @@ import { BaseItem } from "../base_item"; import { Component } from "../component"; import { enumDirection, Vector } from "../../core/vector"; +import { types } from "../../savegame/serialization"; +import { gItemRegistry } from "../../core/global_registries"; /** @enum {string} */ export const enumItemProcessorTypes = { @@ -21,7 +23,37 @@ export class ItemProcessorComponent extends Component { static getSchema() { return { - // TODO + nextOutputSlot: types.uint, + type: types.enum(enumItemProcessorTypes), + inputsPerCharge: types.uint, + beltUnderlays: types.array( + types.structured({ + pos: types.vector, + direction: types.enum(enumDirection), + }) + ), + inputSlots: types.array( + types.structured({ + item: types.obj(gItemRegistry), + sourceSlot: types.uint, + }) + ), + itemsToEject: types.array( + types.structured({ + item: types.obj(gItemRegistry), + requiredSlot: types.nullable(types.uint), + preferredSlot: types.nullable(types.uint), + }) + ), + secondsUntilEject: types.ufloat, + itemConsumptionAnimations: types.array( + types.structured({ + item: types.obj(gItemRegistry), + slotIndex: types.uint, + animProgress: types.ufloat, + direction: types.enum(enumDirection), + }) + ), }; } diff --git a/src/js/game/components/miner.js b/src/js/game/components/miner.js index 09c4f437..2e5ef0df 100644 --- a/src/js/game/components/miner.js +++ b/src/js/game/components/miner.js @@ -14,9 +14,8 @@ export class MinerComponent extends Component { } /** - * @param {object} param0 */ - constructor({}) { + constructor() { super(); this.lastMiningTime = 0; } diff --git a/src/js/game/components/static_map_entity.js b/src/js/game/components/static_map_entity.js index 9b5c3687..ff45b0f1 100644 --- a/src/js/game/components/static_map_entity.js +++ b/src/js/game/components/static_map_entity.js @@ -13,7 +13,14 @@ export class StaticMapEntityComponent extends Component { } static getSchema() { - return {}; + return { + origin: types.tileVector, + tileSize: types.tileVector, + rotation: types.float, + originalRotation: types.float, + spriteKey: types.nullable(types.string), + silhouetteColor: types.nullable(types.string), + }; } /** diff --git a/src/js/game/components/underground_belt.js b/src/js/game/components/underground_belt.js index 707cd131..b9aa3922 100644 --- a/src/js/game/components/underground_belt.js +++ b/src/js/game/components/underground_belt.js @@ -1,6 +1,8 @@ import { BaseItem } from "../base_item"; import { Component } from "../component"; import { globalConfig } from "../../core/config"; +import { types } from "../../savegame/serialization"; +import { gItemRegistry } from "../../core/global_registries"; /** @enum {string} */ export const enumUndergroundBeltMode = { @@ -13,6 +15,13 @@ export class UndergroundBeltComponent extends Component { return "UndergroundBelt"; } + static getSchema() { + return { + mode: types.enum(enumUndergroundBeltMode), + pendingItems: types.array(types.pair(types.obj(gItemRegistry), types.number)), + }; + } + /** * * @param {object} param0 diff --git a/src/js/game/core.js b/src/js/game/core.js index b581b089..be8e5c6e 100644 --- a/src/js/game/core.js +++ b/src/js/game/core.js @@ -11,7 +11,6 @@ import { getDeviceDPI, resizeHighDPICanvas } from "../core/dpi_manager"; import { DrawParameters } from "../core/draw_parameters"; import { gMetaBuildingRegistry } from "../core/global_registries"; import { createLogger } from "../core/logging"; -import { PerlinNoise } from "../core/perlin_noise"; import { Vector } from "../core/vector"; import { Savegame } from "../savegame/savegame"; import { SavegameSerializer } from "../savegame/savegame_serializer"; @@ -31,6 +30,7 @@ import { ShapeDefinitionManager } from "./shape_definition_manager"; import { SoundProxy } from "./sound_proxy"; import { GameTime } from "./time/game_time"; import { ProductionAnalytics } from "./production_analytics"; +import { randomInt } from "../core/utils"; const logger = createLogger("ingame/core"); @@ -112,7 +112,6 @@ export class GameCore { root.shapeDefinitionMgr = new ShapeDefinitionManager(root); root.hubGoals = new HubGoals(root); root.productionAnalytics = new ProductionAnalytics(root); - root.mapNoiseGenerator = new PerlinNoise(Math.random()); // TODO: Save seed root.buffers = new BufferMaintainer(root); // Initialize the hud once everything is loaded @@ -136,6 +135,7 @@ export class GameCore { initNewGame() { logger.log("Initializing new game"); this.root.gameIsFresh = true; + this.root.map.seed = randomInt(0, 100000); gMetaBuildingRegistry.findByClass(MetaHubBuilding).createAndPlaceEntity({ root: this.root, diff --git a/src/js/game/entity.js b/src/js/game/entity.js index 4161f2a7..bc3ea0db 100644 --- a/src/js/game/entity.js +++ b/src/js/game/entity.js @@ -5,14 +5,13 @@ import { Component } from "./component"; /* typehints:end */ import { globalConfig } from "../core/config"; -import { Vector, enumDirectionToVector, enumDirectionToAngle } from "../core/vector"; +import { enumDirectionToVector, enumDirectionToAngle } from "../core/vector"; import { BasicSerializableObject, types } from "../savegame/serialization"; import { EntityComponentStorage } from "./entity_components"; import { Loader } from "../core/loader"; import { drawRotatedSprite } from "../core/draw_utils"; import { Math_radians } from "../core/builtins"; -// import { gFactionRegistry, gComponentRegistry } from "../core/global_registries"; -// import { EntityComponentStorage } from "./entity_components"; +import { gComponentRegistry } from "../core/global_registries"; export class Entity extends BasicSerializableObject { /** @@ -78,7 +77,7 @@ export class Entity extends BasicSerializableObject { static getSchema() { return { uid: types.uint, - // components: types.keyValueMap(types.objData(gComponentRegistry), false) + components: types.keyValueMap(types.objData(gComponentRegistry), false), }; } diff --git a/src/js/game/game_speed_registry.js b/src/js/game/game_speed_registry.js new file mode 100644 index 00000000..963e89ac --- /dev/null +++ b/src/js/game/game_speed_registry.js @@ -0,0 +1,8 @@ +import { RegularGameSpeed } from "./time/regular_game_speed"; +import { gGameSpeedRegistry } from "../core/global_registries"; + +export function initGameSpeedRegistry() { + gGameSpeedRegistry.register(RegularGameSpeed); + + // Others are disabled for now +} diff --git a/src/js/game/hub_goals.js b/src/js/game/hub_goals.js index 15a1f9d1..376cba8c 100644 --- a/src/js/game/hub_goals.js +++ b/src/js/game/hub_goals.js @@ -1,23 +1,43 @@ -import { BasicSerializableObject } from "../savegame/serialization"; -import { GameRoot } from "./root"; -import { ShapeDefinition, enumSubShape } from "./shape_definition"; -import { enumColors, enumShortcodeToColor, enumColorToShortcode } from "./colors"; -import { randomChoice, clamp, randomInt, findNiceIntegerValue } from "../core/utils"; -import { tutorialGoals, enumHubGoalRewards } from "./tutorial_goals"; -import { createLogger } from "../core/logging"; -import { globalConfig } from "../core/config"; import { Math_random } from "../core/builtins"; -import { UPGRADES } from "./upgrades"; -import { enumItemProcessorTypes } from "./components/item_processor"; +import { globalConfig } from "../core/config"; import { queryParamOptions } from "../core/query_parameters"; - -const logger = createLogger("hub_goals"); +import { clamp, findNiceIntegerValue, randomChoice, randomInt } from "../core/utils"; +import { BasicSerializableObject, types } from "../savegame/serialization"; +import { enumColors } from "./colors"; +import { enumItemProcessorTypes } from "./components/item_processor"; +import { GameRoot } from "./root"; +import { enumSubShape, ShapeDefinition } from "./shape_definition"; +import { enumHubGoalRewards, tutorialGoals } from "./tutorial_goals"; +import { UPGRADES } from "./upgrades"; export class HubGoals extends BasicSerializableObject { static getId() { return "HubGoals"; } + static getSchema() { + return { + level: types.uint, + storedShapes: types.keyValueMap(types.uint), + upgradeLevels: types.keyValueMap(types.uint), + + currentGoal: types.structured({ + definition: types.knownType(ShapeDefinition), + required: types.uint, + reward: types.nullable(types.enum(enumHubGoalRewards)), + }), + }; + } + + deserialize(data) { + const errorCode = super.deserialize(data); + if (errorCode) { + return errorCode; + } + + console.error("TODO: HubGoals deserialize() properly"); + } + /** * @param {GameRoot} root */ @@ -30,6 +50,7 @@ export class HubGoals extends BasicSerializableObject { /** * Which story rewards we already gained + * @type {Object.} */ this.gainedRewards = {}; diff --git a/src/js/game/hud/parts/unlock_notification.js b/src/js/game/hud/parts/unlock_notification.js index b13e7d46..8a8dae7a 100644 --- a/src/js/game/hud/parts/unlock_notification.js +++ b/src/js/game/hud/parts/unlock_notification.js @@ -1,20 +1,18 @@ -import { BaseHUDPart } from "../base_hud_part"; -import { makeDiv } from "../../../core/utils"; -import { DynamicDomAttach } from "../dynamic_dom_attach"; -import { gMetaBuildingRegistry } from "../../../core/global_registries"; -import { MetaBuilding } from "../../meta_building"; -import { MetaSplitterBuilding } from "../../buildings/splitter"; -import { MetaCutterBuilding } from "../../buildings/cutter"; -import { enumHubGoalRewards } from "../../tutorial_goals"; -import { MetaTrashBuilding } from "../../buildings/trash"; -import { MetaMinerBuilding } from "../../buildings/miner"; -import { MetaPainterBuilding } from "../../buildings/painter"; -import { MetaMixerBuilding } from "../../buildings/mixer"; -import { MetaRotaterBuilding } from "../../buildings/rotater"; -import { MetaStackerBuilding } from "../../buildings/stacker"; -import { MetaUndergroundBeltBuilding } from "../../buildings/underground_belt"; import { globalConfig } from "../../../core/config"; +import { gMetaBuildingRegistry } from "../../../core/global_registries"; +import { makeDiv } from "../../../core/utils"; import { SOUNDS } from "../../../platform/sound"; +import { MetaCutterBuilding } from "../../buildings/cutter"; +import { MetaMixerBuilding } from "../../buildings/mixer"; +import { MetaPainterBuilding } from "../../buildings/painter"; +import { MetaRotaterBuilding } from "../../buildings/rotater"; +import { MetaSplitterBuilding } from "../../buildings/splitter"; +import { MetaStackerBuilding } from "../../buildings/stacker"; +import { MetaTrashBuilding } from "../../buildings/trash"; +import { MetaUndergroundBeltBuilding } from "../../buildings/underground_belt"; +import { enumHubGoalRewards, enumHubGoalRewardToString } from "../../tutorial_goals"; +import { BaseHUDPart } from "../base_hud_part"; +import { DynamicDomAttach } from "../dynamic_dom_attach"; export class HUDUnlockNotification extends BaseHUDPart { initialize() { @@ -58,10 +56,14 @@ export class HUDUnlockNotification extends BaseHUDPart { this.trackClicks(this.btnClose, this.close); } + /** + * @param {number} level + * @param {enumHubGoalRewards} reward + */ showForLevel(level, reward) { this.elemTitle.innerText = "Level " + ("" + level).padStart(2, "0"); - let html = `Unlocked ${reward}!`; + let html = `Unlocked ${enumHubGoalRewardToString[reward]}!`; const addBuildingExplanation = metaBuildingClass => { const metaBuilding = gMetaBuildingRegistry.findByClass(metaBuildingClass); diff --git a/src/js/game/items/color_item.js b/src/js/game/items/color_item.js index cc618228..f2b97298 100644 --- a/src/js/game/items/color_item.js +++ b/src/js/game/items/color_item.js @@ -1,13 +1,9 @@ -import { DrawParameters } from "../../core/draw_parameters"; -import { createLogger } from "../../core/logging"; -import { extendSchema } from "../../savegame/serialization"; -import { BaseItem } from "../base_item"; -import { enumColorsToHexCode, enumColors } from "../colors"; -import { makeOffscreenBuffer } from "../../core/buffer_utils"; import { globalConfig } from "../../core/config"; -import { round1Digit } from "../../core/utils"; -import { Math_max, Math_round } from "../../core/builtins"; import { smoothenDpi } from "../../core/dpi_manager"; +import { DrawParameters } from "../../core/draw_parameters"; +import { types } from "../../savegame/serialization"; +import { BaseItem } from "../base_item"; +import { enumColors, enumColorsToHexCode } from "../colors"; /** @enum {string} */ const enumColorToMapBackground = { @@ -22,9 +18,15 @@ export class ColorItem extends BaseItem { } static getSchema() { - return extendSchema(BaseItem.getCachedSchema(), { - // TODO - }); + return types.enum(enumColors); + } + + serialize() { + return this.color; + } + + deserialize(data) { + this.color = data; } /** @@ -33,7 +35,6 @@ export class ColorItem extends BaseItem { constructor(color) { super(); this.color = color; - this.bufferGenerator = this.internalGenerateColorBuffer.bind(this); } diff --git a/src/js/game/items/shape_item.js b/src/js/game/items/shape_item.js index 9aa1b78d..3773ae84 100644 --- a/src/js/game/items/shape_item.js +++ b/src/js/game/items/shape_item.js @@ -1,10 +1,7 @@ -import { BaseItem } from "../base_item"; import { DrawParameters } from "../../core/draw_parameters"; -import { extendSchema } from "../../savegame/serialization"; +import { types } from "../../savegame/serialization"; +import { BaseItem } from "../base_item"; import { ShapeDefinition } from "../shape_definition"; -import { createLogger } from "../../core/logging"; - -const logger = createLogger("shape_item"); export class ShapeItem extends BaseItem { static getId() { @@ -12,9 +9,15 @@ export class ShapeItem extends BaseItem { } static getSchema() { - return extendSchema(BaseItem.getCachedSchema(), { - // TODO - }); + return types.string; + } + + serialize() { + return this.definition.getHash(); + } + + deserialize(data) { + this.definition = ShapeDefinition.fromShortKey(data); } /** diff --git a/src/js/game/map.js b/src/js/game/map.js index 97f6d3e6..3fd82844 100644 --- a/src/js/game/map.js +++ b/src/js/game/map.js @@ -9,17 +9,32 @@ import { Math_floor } from "../core/builtins"; import { createLogger } from "../core/logging"; import { BaseItem } from "./base_item"; import { MapChunkView } from "./map_chunk_view"; +import { randomInt } from "../core/utils"; +import { BasicSerializableObject, types } from "../savegame/serialization"; const logger = createLogger("map"); -export class BaseMap { +export class BaseMap extends BasicSerializableObject { + static getId() { + return "Map"; + } + + static getSchema() { + return { + seed: types.uint, + }; + } + /** * * @param {GameRoot} root */ constructor(root) { + super(); this.root = root; + this.seed = 0; + /** * Mapping of 'X|Y' to chunk * @type {Map} */ diff --git a/src/js/game/map_chunk.js b/src/js/game/map_chunk.js index 243238af..a2305991 100644 --- a/src/js/game/map_chunk.js +++ b/src/js/game/map_chunk.js @@ -2,16 +2,10 @@ import { GameRoot } from "./root"; /* typehints:end */ -import { Math_ceil, Math_max, Math_min, Math_random, Math_round } from "../core/builtins"; +import { Math_ceil, Math_max, Math_min, Math_round } from "../core/builtins"; import { globalConfig } from "../core/config"; import { createLogger } from "../core/logging"; -import { - clamp, - fastArrayDeleteValueIfContained, - make2DUndefinedArray, - randomChoice, - randomInt, -} from "../core/utils"; +import { clamp, fastArrayDeleteValueIfContained, make2DUndefinedArray } from "../core/utils"; import { Vector } from "../core/vector"; import { BaseItem } from "./base_item"; import { enumColors } from "./colors"; @@ -19,6 +13,7 @@ import { Entity } from "./entity"; import { ColorItem } from "./items/color_item"; import { ShapeItem } from "./items/shape_item"; import { enumSubShape } from "./shape_definition"; +import { RandomNumberGenerator } from "../core/rng"; const logger = createLogger("map_chunk"); @@ -64,17 +59,18 @@ export class MapChunk { /** * Generates a patch filled with the given item + * @param {RandomNumberGenerator} rng * @param {number} patchSize * @param {BaseItem} item * @param {number=} overrideX Override the X position of the patch * @param {number=} overrideY Override the Y position of the patch */ - internalGeneratePatch(patchSize, item, overrideX = null, overrideY = null) { + internalGeneratePatch(rng, patchSize, item, overrideX = null, overrideY = null) { const border = Math_ceil(patchSize / 2 + 3); // Find a position within the chunk which is not blocked - let patchX = randomInt(border, globalConfig.mapChunkSize - border - 1); - let patchY = randomInt(border, globalConfig.mapChunkSize - border - 1); + let patchX = rng.nextIntRange(border, globalConfig.mapChunkSize - border - 1); + let patchY = rng.nextIntRange(border, globalConfig.mapChunkSize - border - 1); if (overrideX !== null) { patchX = overrideX; @@ -89,7 +85,6 @@ export class MapChunk { // Each patch consists of multiple circles const numCircles = patchSize; - // const numCircles = 1; for (let i = 0; i <= numCircles; ++i) { // Determine circle parameters @@ -98,11 +93,11 @@ export class MapChunk { const circleOffsetRadius = (numCircles - i) / 2 + 2; // We draw an elipsis actually - const circleScaleY = 1 + (Math_random() * 2 - 1) * 0.1; - const circleScaleX = 1 + (Math_random() * 2 - 1) * 0.1; + const circleScaleX = rng.nextRange(0.9, 1.1); + const circleScaleY = rng.nextRange(0.9, 1.1); - const circleX = patchX + randomInt(-circleOffsetRadius, circleOffsetRadius); - const circleY = patchY + randomInt(-circleOffsetRadius, circleOffsetRadius); + const circleX = patchX + rng.nextIntRange(-circleOffsetRadius, circleOffsetRadius); + const circleY = patchY + rng.nextIntRange(-circleOffsetRadius, circleOffsetRadius); for (let dx = -circleRadius * circleScaleX - 2; dx <= circleRadius * circleScaleX + 2; ++dx) { for (let dy = -circleRadius * circleScaleY - 2; dy <= circleRadius * circleScaleY + 2; ++dy) { @@ -135,24 +130,26 @@ export class MapChunk { /** * Generates a color patch + * @param {RandomNumberGenerator} rng * @param {number} colorPatchSize * @param {number} distanceToOriginInChunks */ - internalGenerateColorPatch(colorPatchSize, distanceToOriginInChunks) { + internalGenerateColorPatch(rng, colorPatchSize, distanceToOriginInChunks) { // First, determine available colors let availableColors = [enumColors.red, enumColors.green]; if (distanceToOriginInChunks > 2) { availableColors.push(enumColors.blue); } - this.internalGeneratePatch(colorPatchSize, new ColorItem(randomChoice(availableColors))); + this.internalGeneratePatch(rng, colorPatchSize, new ColorItem(rng.choice(availableColors))); } /** * Generates a shape patch + * @param {RandomNumberGenerator} rng * @param {number} shapePatchSize * @param {number} distanceToOriginInChunks */ - internalGenerateShapePatch(shapePatchSize, distanceToOriginInChunks) { + internalGenerateShapePatch(rng, shapePatchSize, distanceToOriginInChunks) { /** @type {[enumSubShape, enumSubShape, enumSubShape, enumSubShape]} */ let subShapes = null; @@ -174,37 +171,38 @@ export class MapChunk { if (distanceToOriginInChunks < 7) { // Initial chunk patches always have the same shape - const subShape = this.internalGenerateRandomSubShape(weights); + const subShape = this.internalGenerateRandomSubShape(rng, weights); subShapes = [subShape, subShape, subShape, subShape]; } else if (distanceToOriginInChunks < 17) { // Later patches can also have mixed ones - const subShapeA = this.internalGenerateRandomSubShape(weights); - const subShapeB = this.internalGenerateRandomSubShape(weights); + const subShapeA = this.internalGenerateRandomSubShape(rng, weights); + const subShapeB = this.internalGenerateRandomSubShape(rng, weights); subShapes = [subShapeA, subShapeA, subShapeB, subShapeB]; } else { // Finally there is a mix of everything subShapes = [ - this.internalGenerateRandomSubShape(weights), - this.internalGenerateRandomSubShape(weights), - this.internalGenerateRandomSubShape(weights), - this.internalGenerateRandomSubShape(weights), + this.internalGenerateRandomSubShape(rng, weights), + this.internalGenerateRandomSubShape(rng, weights), + this.internalGenerateRandomSubShape(rng, weights), + this.internalGenerateRandomSubShape(rng, weights), ]; } const definition = this.root.shapeDefinitionMgr.getDefinitionFromSimpleShapes(subShapes); - this.internalGeneratePatch(shapePatchSize, new ShapeItem(definition)); + this.internalGeneratePatch(rng, shapePatchSize, new ShapeItem(definition)); } /** * Chooses a random shape with the given weights + * @param {RandomNumberGenerator} rng * @param {Object.} weights * @returns {enumSubShape} */ - internalGenerateRandomSubShape(weights) { + internalGenerateRandomSubShape(rng, weights) { // @ts-ignore const sum = Object.values(weights).reduce((a, b) => a + b, 0); - const chosenNumber = randomInt(0, sum - 1); + const chosenNumber = rng.nextIntRange(0, sum - 1); let accumulated = 0; for (const key in weights) { const weight = weights[key]; @@ -222,7 +220,9 @@ export class MapChunk { * Generates the lower layer "terrain" */ generateLowerLayer() { - if (this.generatePredefined()) { + const rng = new RandomNumberGenerator(this.x + "|" + this.y + "|" + this.root.map.seed); + + if (this.generatePredefined(rng)) { return; } @@ -231,27 +231,28 @@ export class MapChunk { // Determine how likely it is that there is a color patch const colorPatchChance = 0.9 - clamp(distanceToOriginInChunks / 25, 0, 1) * 0.5; - if (Math_random() < colorPatchChance) { + if (rng.next() < colorPatchChance) { const colorPatchSize = Math_max(2, Math_round(1 + clamp(distanceToOriginInChunks / 8, 0, 4))); - this.internalGenerateColorPatch(colorPatchSize, distanceToOriginInChunks); + this.internalGenerateColorPatch(rng, colorPatchSize, distanceToOriginInChunks); } // Determine how likely it is that there is a shape patch const shapePatchChance = 0.9 - clamp(distanceToOriginInChunks / 25, 0, 1) * 0.5; - if (Math_random() < shapePatchChance) { + if (rng.next() < shapePatchChance) { const shapePatchSize = Math_max(2, Math_round(1 + clamp(distanceToOriginInChunks / 8, 0, 4))); - this.internalGenerateShapePatch(shapePatchSize, distanceToOriginInChunks); + this.internalGenerateShapePatch(rng, shapePatchSize, distanceToOriginInChunks); } } /** * Checks if this chunk has predefined contents, and if so returns true and generates the * predefined contents + * @param {RandomNumberGenerator} rng * @returns {boolean} */ - generatePredefined() { + generatePredefined(rng) { if (this.x === 0 && this.y === 0) { - this.internalGeneratePatch(2, new ColorItem(enumColors.red), 7, 7); + this.internalGeneratePatch(rng, 2, new ColorItem(enumColors.red), 7, 7); return true; } if (this.x === -1 && this.y === 0) { @@ -261,7 +262,7 @@ export class MapChunk { enumSubShape.circle, enumSubShape.circle, ]); - this.internalGeneratePatch(2, new ShapeItem(definition), globalConfig.mapChunkSize - 9, 7); + this.internalGeneratePatch(rng, 2, new ShapeItem(definition), globalConfig.mapChunkSize - 9, 7); return true; } if (this.x === 0 && this.y === -1) { @@ -271,12 +272,12 @@ export class MapChunk { enumSubShape.rect, enumSubShape.rect, ]); - this.internalGeneratePatch(2, new ShapeItem(definition), 5, globalConfig.mapChunkSize - 7); + this.internalGeneratePatch(rng, 2, new ShapeItem(definition), 5, globalConfig.mapChunkSize - 7); return true; } if (this.x === -1 && this.y === -1) { - this.internalGeneratePatch(2, new ColorItem(enumColors.green)); + this.internalGeneratePatch(rng, 2, new ColorItem(enumColors.green)); return true; } diff --git a/src/js/game/root.js b/src/js/game/root.js index 41cd5eeb..19d32db5 100644 --- a/src/js/game/root.js +++ b/src/js/game/root.js @@ -2,7 +2,6 @@ import { Signal } from "../core/signal"; import { RandomNumberGenerator } from "../core/rng"; -// import { gFactionRegistry } from "./global_registries"; import { createLogger } from "../core/logging"; // Type hints @@ -11,12 +10,9 @@ import { GameTime } from "./time/game_time"; import { EntityManager } from "./entity_manager"; import { GameSystemManager } from "./game_system_manager"; import { GameHUD } from "./hud/hud"; -// import { GameLogic } from "./game_logic"; import { MapView } from "./map_view"; import { Camera } from "./camera"; -// import { ParticleManager } from "../particles/particle_manager"; import { InGameState } from "../states/ingame"; -// import { CanvasClickInterceptor } from "/canvas_click_interceptor"; import { AutomaticSave } from "./automatic_save"; import { Application } from "../application"; import { SoundProxy } from "./sound_proxy"; @@ -99,21 +95,12 @@ export class GameRoot { /** @type {GameTime} */ this.time = null; - /** @type {PerlinNoise} */ - this.mapNoiseGenerator = null; - /** @type {HubGoals} */ this.hubGoals = null; /** @type {BufferMaintainer} */ this.buffers = null; - // /** @type {ParticleManager} */ - // this.particleMgr = null; - - // /** @type {ParticleManager} */ - // this.uiParticleMgr = null; - /** @type {CanvasClickInterceptor} */ this.canvasClickInterceptor = null; @@ -123,9 +110,6 @@ export class GameRoot { /** @type {SoundProxy} */ this.soundProxy = null; - // /** @type {MinimapRenderer} */ - // this.minimapRenderer = null; - /** @type {ShapeDefinitionManager} */ this.shapeDefinitionMgr = null; @@ -147,7 +131,6 @@ export class GameRoot { // Game Hooks gameSaved: /** @type {TypedSignal<[]>} */ (new Signal()), // Game got saved gameRestored: /** @type {TypedSignal<[]>} */ (new Signal()), // Game got restored - gameOver: /** @type {TypedSignal<[]>} */ (new Signal()), // Game over storyGoalCompleted: /** @type {TypedSignal<[number, string]>} */ (new Signal()), upgradePurchased: /** @type {TypedSignal<[string]>} */ (new Signal()), @@ -182,20 +165,6 @@ export class GameRoot { this.reset(); } - /** - * Prepares the root for game over, this sets the right flags and - * detaches all signals so no bad stuff happens - */ - prepareGameOver() { - this.gameInitialized = false; - this.logicInitialized = false; - // for (const key in this.signals) { - // if (key !== "aboutToDestruct") { - // this.signals[key].removeAll(); - // } - // } - } - /** * Resets the whole root and removes all properties */ diff --git a/src/js/game/shape_definition.js b/src/js/game/shape_definition.js index 268d9ce0..537f60b1 100644 --- a/src/js/game/shape_definition.js +++ b/src/js/game/shape_definition.js @@ -5,7 +5,7 @@ import { smoothenDpi } from "../core/dpi_manager"; import { DrawParameters } from "../core/draw_parameters"; import { createLogger } from "../core/logging"; import { Vector } from "../core/vector"; -import { BasicSerializableObject } from "../savegame/serialization"; +import { BasicSerializableObject, types } from "../savegame/serialization"; import { enumColors, enumColorsToHexCode, enumColorToShortcode, enumShortcodeToColor } from "./colors"; const rusha = require("rusha"); @@ -74,6 +74,23 @@ export class ShapeDefinition extends BasicSerializableObject { return "ShapeDefinition"; } + static getSchema() { + return {}; + } + + deserialize(data) { + const errorCode = super.deserialize(data); + if (errorCode) { + return errorCode; + } + const definition = ShapeDefinition.fromShortKey(data); + this.layers = definition.layers; + } + + serialize() { + return this.getHash(); + } + /** * * @param {object} param0 diff --git a/src/js/game/systems/hub.js b/src/js/game/systems/hub.js index 88fb8712..55ce3544 100644 --- a/src/js/game/systems/hub.js +++ b/src/js/game/systems/hub.js @@ -3,6 +3,7 @@ import { HubComponent } from "../components/hub"; import { DrawParameters } from "../../core/draw_parameters"; import { Entity } from "../entity"; import { formatBigNumber } from "../../core/utils"; +import { enumHubGoalRewardToString } from "../tutorial_goals"; export class HubSystem extends GameSystemWithFilter { constructor(root) { @@ -77,7 +78,7 @@ export class HubSystem extends GameSystemWithFilter { context.font = "bold 11px GameFont"; context.fillStyle = "#fd0752"; context.textAlign = "center"; - context.fillText(goals.reward.toUpperCase(), pos.x, pos.y + 46); + context.fillText(enumHubGoalRewardToString[goals.reward].toUpperCase(), pos.x, pos.y + 46); // Level context.font = "bold 11px GameFont"; diff --git a/src/js/game/tutorial_goals.js b/src/js/game/tutorial_goals.js index 8a57484b..bc404873 100644 --- a/src/js/game/tutorial_goals.js +++ b/src/js/game/tutorial_goals.js @@ -4,15 +4,30 @@ import { ShapeDefinition } from "./shape_definition"; * @enum {string} */ export const enumHubGoalRewards = { - reward_cutter_and_trash: "Cutting Shapes", - reward_rotater: "Rotating", - reward_painter: "Painting", - reward_mixer: "Color Mixing", - reward_stacker: "Combiner", - reward_splitter: "Splitter/Merger", - reward_tunnel: "Tunnel", + reward_cutter_and_trash: "reward_cutter_and_trash", + reward_rotater: "reward_rotater", + reward_painter: "reward_painter", + reward_mixer: "reward_mixer", + reward_stacker: "reward_stacker", + reward_splitter: "reward_splitter", + reward_tunnel: "reward_tunnel", - no_reward: "Next level", + no_reward: "no_reward", +}; + +/** + * @enum {string} + */ +export const enumHubGoalRewardToString = { + [enumHubGoalRewards.reward_cutter_and_trash]: "Cutting Shapes", + [enumHubGoalRewards.reward_rotater]: "Rotating", + [enumHubGoalRewards.reward_painter]: "Painting", + [enumHubGoalRewards.reward_mixer]: "Color Mixing", + [enumHubGoalRewards.reward_stacker]: "Combiner", + [enumHubGoalRewards.reward_splitter]: "Splitter/Merger", + [enumHubGoalRewards.reward_tunnel]: "Tunnel", + + [enumHubGoalRewards.no_reward]: "Next level", }; export const tutorialGoals = [ diff --git a/src/js/main.js b/src/js/main.js index 5eef3a3f..af9301c9 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -9,6 +9,7 @@ import { initComponentRegistry } from "./game/component_registry"; import { initDrawUtils } from "./core/draw_utils"; import { initItemRegistry } from "./game/item_registry"; import { initMetaBuildingRegistry } from "./game/meta_building_registry"; +import { initGameSpeedRegistry } from "./game/game_speed_registry"; const logger = createLogger("main"); @@ -49,6 +50,7 @@ initDrawUtils(); initComponentRegistry(); initItemRegistry(); initMetaBuildingRegistry(); +initGameSpeedRegistry(); let app = null; diff --git a/src/js/platform/sound.js b/src/js/platform/sound.js index 792507e3..f326e0dc 100644 --- a/src/js/platform/sound.js +++ b/src/js/platform/sound.js @@ -117,11 +117,8 @@ export class SoundInterface { this.music[musicPath] = music; } - // this.musicMuted = this.app.userProfile.getMusicMuted(); - // this.soundsMuted = this.app.userProfile.getSoundsMuted(); - - this.musicMuted = false; - this.soundsMuted = false; + this.musicMuted = this.app.settings.getAllSettings().musicMuted; + this.soundsMuted = this.app.settings.getAllSettings().soundsMuted; if (G_IS_DEV && globalConfig.debug.disableMusic) { this.musicMuted = true; diff --git a/src/js/savegame/savegame.js b/src/js/savegame/savegame.js index f752c836..3c52abfc 100644 --- a/src/js/savegame/savegame.js +++ b/src/js/savegame/savegame.js @@ -11,6 +11,8 @@ import { createLogger } from "../core/logging"; import { globalConfig } from "../core/config"; import { SavegameInterface_V1000 } from "./schemas/1000"; import { getSavegameInterface } from "./savegame_interface_registry"; +import { compressObject } from "./savegame_compressor"; +import { compressX64 } from "../core/lzstring"; const logger = createLogger("savegame"); @@ -37,7 +39,7 @@ export class Savegame extends ReadWriteProxy { * @returns {number} */ static getCurrentVersion() { - return 1015; + return 1000; } /** @@ -129,7 +131,7 @@ export class Savegame extends ReadWriteProxy { * Returns if this game has a serialized game dump */ hasGameDump() { - return !!this.currentData.dump; + return !!this.currentData.dump && this.currentData.dump.entities.length > 0; } /** @@ -185,6 +187,12 @@ export class Savegame extends ReadWriteProxy { if (!dump) { return false; } + const parsed = JSON.stringify(compressObject(dump)); + const compressed = compressX64(parsed); + + console.log("Regular: ", Math.round(parsed.length / 1024.0), "KB"); + console.log("Compressed: ", Math.round(compressed.length / 1024.0), "KB"); + // let duration = performanceNow() - timer; // console.log("TOOK", duration, "ms to generate dump:", dump); diff --git a/src/js/savegame/savegame_compressor.js b/src/js/savegame/savegame_compressor.js new file mode 100644 index 00000000..3cfae5e9 --- /dev/null +++ b/src/js/savegame/savegame_compressor.js @@ -0,0 +1,134 @@ +const charmap = + "!#%&'()*+,-./:;<=>?@[]^_`{|}~¥¦§¨©ª«¬­®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿABCDEFGHIJKLMNOPQRSTUVWXYZ"; + +let compressionCache = {}; +let decompressionCache = {}; + +/** + * Compresses an integer into a tight string representation + * @param {number} i + * @returns {string} + */ +function compressInt(i) { + // Zero value breaks + i += 1; + + if (compressionCache[i]) { + return compressionCache[i]; + } + let result = ""; + do { + result += charmap[i % charmap.length]; + i = Math.floor(i / charmap.length); + } while (i > 0); + return (compressionCache[i] = result); +} + +/** + * Decompresses an integer from its tight string representation + * @param {string} s + * @returns {number} + */ +function decompressInt(s) { + if (decompressionCache[s]) { + return decompressionCache[s]; + } + s = "" + s; + let result = 0; + for (let i = s.length - 1; i >= 0; --i) { + result = result * charmap.length + charmap.indexOf(s.charAt(i)); + } + // Fixes zero value break fix from above + result -= 1; + return (decompressionCache[s] = result); +} + +// Sanity +for (let i = 0; i < 10000; ++i) { + if (decompressInt(compressInt(i)) !== i) { + throw new Error( + "Bad compression for: " + + i + + " compressed: " + + compressInt(i) + + " decompressed: " + + decompressInt(compressInt(i)) + ); + } +} + +function compressObjectInternal(obj, keys = [], values = []) { + if (Array.isArray(obj)) { + let result = []; + for (let i = 0; i < obj.length; ++i) { + result.push(compressObjectInternal(obj[i], keys, values)); + } + return result; + } else if (typeof obj === "object") { + let result = {}; + for (const key in obj) { + let index = keys.indexOf(key); + if (index < 0) { + keys.push(key); + index = keys.length - 1; + } + const value = obj[key]; + result[compressInt(index)] = compressObjectInternal(value, keys, values); + } + return result; + } else if (typeof obj === "string") { + let index = values.indexOf(obj); + if (index < 0) { + values.push(obj); + index = values.length - 1; + } + return compressInt(index); + } + return obj; +} + +export function compressObject(obj) { + if (G_IS_DEV) { + return obj; + } + const keys = []; + const values = []; + const data = compressObjectInternal(obj, keys, values); + return { + keys, + values, + data, + }; +} + +function decompressObjectInternal(obj, keys = [], values = []) { + if (Array.isArray(obj)) { + let result = []; + for (let i = 0; i < obj.length; ++i) { + result.push(decompressObjectInternal(obj[i], keys, values)); + } + return result; + } else if (typeof obj === "object") { + let result = {}; + for (const key in obj) { + const realIndex = decompressInt(key); + const value = obj[key]; + result[keys[realIndex]] = decompressObjectInternal(value, keys, values); + } + return result; + } else if (typeof obj === "string") { + const realIndex = decompressInt(obj); + return values[realIndex]; + } + return obj; +} + +export function decompressObject(obj) { + if (G_IS_DEV) { + return obj; + } + const keys = obj.keys; + const values = obj.values; + const result = decompressObjectInternal(obj.data, keys, values); + return result; +} diff --git a/src/js/savegame/savegame_interface.js b/src/js/savegame/savegame_interface.js index 66cada22..aeec062f 100644 --- a/src/js/savegame/savegame_interface.js +++ b/src/js/savegame/savegame_interface.js @@ -98,7 +98,7 @@ export class BaseSavegameInterface { //////// ANTICHEAT /////// /** - * Detects cheats in the savegmae - returns false if the game looks cheated + * Detects cheats in the savegame - returns false if the game looks cheated */ performAnticheatCheck() { // TODO diff --git a/src/js/savegame/savegame_serializer.js b/src/js/savegame/savegame_serializer.js index c1f0f500..96ff701f 100644 --- a/src/js/savegame/savegame_serializer.js +++ b/src/js/savegame/savegame_serializer.js @@ -24,7 +24,7 @@ export class SavegameSerializer { * Serializes the game root into a dump * @param {GameRoot} root * @param {boolean=} sanityChecks Whether to check for validity - * @returns {SerializedGame} + * @returns {object} */ generateDumpFromGameRoot(root, sanityChecks = true) { // Finalize particles before saving (Like granting destroy indicator rewards) @@ -32,21 +32,15 @@ export class SavegameSerializer { // root.uiParticleMgr.finalizeBeforeSave(); // Now store generic savegame payload - const data = /** @type {SerializedGame} */ ({ + const data = { camera: root.camera.serialize(), time: root.time.serialize(), + map: root.map.serialize(), entityMgr: root.entityMgr.serialize(), - entities: {}, - }); + hubGoals: root.hubGoals.serialize(), + }; - // Serialize all types of entities - const serializeEntities = component => - this.internal.serializeEntityArray(root.entityMgr.getAllWithComponent(component)); - const serializeEntitiesFixed = component => - this.internal.serializeEntityArrayFixedType(root.entityMgr.getAllWithComponent(component)); - - // data.entities.resources = serializeEntitiesFixed(RawMaterialComponent); - // data.entities.buildings = serializeEntities(BuildingComponent); + data.entities = this.internal.serializeEntityArray(root.entityMgr.entities); if (!G_IS_RELEASE) { if (sanityChecks) { @@ -58,13 +52,12 @@ export class SavegameSerializer { } } } - return data; } /** * Verifies if there are logical errors in the savegame - * @param {SerializedGame} savegame + * @param {object} savegame * @returns {ExplainedResult} */ verifyLogicalErrors(savegame) { @@ -138,12 +131,12 @@ export class SavegameSerializer { let errorReason = null; - // entities errorReason = errorReason || root.entityMgr.deserialize(savegame.entityMgr); - - // other stuff 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 || this.internal.deserializeEntityArray(root, savegame.entities); // Check for errors if (errorReason) { diff --git a/src/js/savegame/serialization.js b/src/js/savegame/serialization.js index e121ca71..7ab6f678 100644 --- a/src/js/savegame/serialization.js +++ b/src/js/savegame/serialization.js @@ -21,6 +21,7 @@ import { TypeVector, TypeClassFromMetaclass, TypeClassData, + TypeStructuredObject, } from "./serialization_data_types"; import { createLogger } from "../core/logging"; @@ -61,7 +62,7 @@ export const types = { }, /** - * @param {Array} values + * @param {Object} values */ enum(values) { return new TypeEnum(values); @@ -102,6 +103,13 @@ export const types = { return new TypeMetaClass(registry); }, + /** + * @param {Object.} descriptor + */ + structured(descriptor) { + return new TypeStructuredObject(descriptor); + }, + /** * @param {BaseDataType} a * @param {BaseDataType} b @@ -215,7 +223,7 @@ export function serializeSchema(obj, schema, mergeWith = {}) { ); } if (!schema[key]) { - assert(false, "Invalid schema: " + JSON_stringify(schema) + " / " + key); + assert(false, "Invalid schema (bad key '" + key + "'): " + JSON_stringify(schema)); } if (G_IS_DEV) { diff --git a/src/js/savegame/serialization_data_types.js b/src/js/savegame/serialization_data_types.js index 240e2313..5020e09f 100644 --- a/src/js/savegame/serialization_data_types.js +++ b/src/js/savegame/serialization_data_types.js @@ -4,7 +4,7 @@ import { BasicSerializableObject } from "./serialization"; /* typehints:end */ import { Vector } from "../core/vector"; -import { round4Digits, schemaObject } from "../core/utils"; +import { round4Digits, schemaObject, accessNestedPropertyReverse } from "../core/utils"; import { JSON_stringify } from "../core/builtins"; export const globalJsonSchemaDefs = {}; @@ -458,11 +458,11 @@ export class TypePositiveNumber extends BaseDataType { export class TypeEnum extends BaseDataType { /** - * @param {Array} availableValues + * @param {Object.} enumeration */ - constructor(availableValues = []) { + constructor(enumeration = {}) { super(); - this.availableValues = availableValues; + this.availableValues = Object.keys(enumeration); } serialize(value) { @@ -664,7 +664,7 @@ export class TypeClass extends BaseDataType { } if (!this.registry.hasId(value.$)) { - return "Invalid class id: " + value.$; + return "Invalid class id: " + value.$ + " (factory is " + this.registry.getId() + ")"; } } @@ -709,7 +709,7 @@ export class TypeClassData extends BaseDataType { * @returns {string|void} String error code or null on success */ deserialize(value, targetObject, targetKey, root) { - assert(false, "can not deserialize class data"); + assert(false, "can not deserialize class data of type " + this.registry.getId()); } verifySerializedValue(value) { @@ -785,7 +785,7 @@ export class TypeClassFromMetaclass extends BaseDataType { } if (!this.registry.hasId(value.$)) { - return "Invalid class id: " + value.$; + return "Invalid class id: " + value.$ + " (factory is " + this.registry.getId() + ")"; } } @@ -841,7 +841,7 @@ export class TypeMetaClass extends BaseDataType { } if (!this.registry.hasId(value)) { - return "Invalid class id: " + value; + return "Invalid class id: " + value + " (factory is " + this.registry.getId() + ")"; } } @@ -1100,12 +1100,11 @@ export class TypePair extends BaseDataType { deserialize(value, targetObject, targetKey, root) { const result = [undefined, undefined]; - let errorCode = this.type1.deserialize(value, result, 0, root); + let errorCode = this.type1.deserialize(value[0], result, 0, root); if (errorCode) { return errorCode; } - - errorCode = this.type2.deserialize(value, result, 1, root); + errorCode = this.type2.deserialize(value[1], result, 1, root); if (errorCode) { return errorCode; } @@ -1202,3 +1201,79 @@ export class TypeNullable extends BaseDataType { return "nullable." + this.wrapped.getCacheKey(); } } + +export class TypeStructuredObject extends BaseDataType { + /** + * @param {Object.} descriptor + */ + constructor(descriptor) { + super(); + this.descriptor = descriptor; + } + + serialize(value) { + assert(typeof value === "object", "not an object"); + let result = {}; + for (const key in this.descriptor) { + // assert(value.hasOwnProperty(key), "Serialization: Object does not have", key, "property!"); + result[key] = this.descriptor[key].serialize(value[key]); + } + return result; + } + + /** + * @see BaseDataType.deserialize + * @param {any} value + * @param {GameRoot} root + * @param {object} targetObject + * @param {string|number} targetKey + * @returns {string|void} String error code or null on success + */ + deserialize(value, targetObject, targetKey, root) { + let result = {}; + for (const key in value) { + const valueType = this.descriptor[key]; + const errorCode = valueType.deserializeWithVerify(value[key], result, key, root); + if (errorCode) { + return errorCode; + } + } + targetObject[targetKey] = result; + } + + getAsJsonSchemaUncached() { + let properties = {}; + for (const key in this.descriptor) { + properties[key] = this.descriptor[key].getAsJsonSchema(); + } + + return { + type: "object", + required: Object.keys(this.descriptor), + properties, + }; + } + + verifySerializedValue(value) { + if (typeof value !== "object") { + return "structured object is not an object"; + } + for (const key in this.descriptor) { + if (!value.hasOwnProperty(key)) { + return "structured object is missing key " + key; + } + const subError = this.descriptor[key].verifySerializedValue(value[key]); + if (subError) { + return "structured object::" + subError; + } + } + } + + getCacheKey() { + let props = []; + for (const key in this.descriptor) { + props.push(key + "=" + this.descriptor[key].getCacheKey()); + } + return "structured[" + props.join(",") + "]"; + } +} diff --git a/src/js/savegame/serializer_internal.js b/src/js/savegame/serializer_internal.js index 449f4fc0..75a7b88f 100644 --- a/src/js/savegame/serializer_internal.js +++ b/src/js/savegame/serializer_internal.js @@ -2,13 +2,9 @@ import { GameRoot } from "../game/root"; /* typehints:end */ -import { Vector } from "../core/vector"; +import { gComponentRegistry } from "../core/global_registries"; import { createLogger } from "../core/logging"; -import { gMetaBuildingRegistry } from "../core/global_registries"; import { Entity } from "../game/entity"; -import { MapResourcesSystem } from "../game/systems/map_resources"; - -const logger = createLogger("serializer_internal"); // Internal serializer methods export class SerializerInternal { @@ -19,24 +15,6 @@ export class SerializerInternal { * @param {Array} array */ serializeEntityArray(array) { - const serialized = []; - for (let i = 0; i < array.length; ++i) { - const entity = array[i]; - if (!entity.queuedForDestroy && !entity.destroyed) { - serialized.push({ - $: entity.getMetaclass().getId(), - data: entity.serialize(), - }); - } - } - return serialized; - } - - /** - * Serializes an array of entities where we know the type of - * @param {Array} array - */ - serializeEntityArrayFixedType(array) { const serialized = []; for (let i = 0; i < array.length; ++i) { const entity = array[i]; @@ -51,12 +29,11 @@ export class SerializerInternal { * * @param {GameRoot} root * @param {Array} array - * @param {function(GameRoot, { $: string, data: object }):string|void} deserializerMethod * @returns {string|void} */ - deserializeEntityArray(root, array, deserializerMethod) { + deserializeEntityArray(root, array) { for (let i = 0; i < array.length; ++i) { - const errorState = deserializerMethod.call(this, root, array[i]); + const errorState = this.deserializeEntity(root, array[i]); if (errorState) { return errorState; } @@ -67,18 +44,17 @@ export class SerializerInternal { /** * * @param {GameRoot} root - * @param {Array} array - * @param {function(GameRoot, object):string|void} deserializerMethod - * @returns {string|void} + * @param {Entity} payload */ - deserializeEntityArrayFixedType(root, array, deserializerMethod) { - for (let i = 0; i < array.length; ++i) { - const errorState = deserializerMethod.call(this, root, array[i]); - if (errorState) { - return errorState; - } + deserializeEntity(root, payload) { + const entity = new Entity(null); + this.deserializeComponents(entity, payload.components); + + root.entityMgr.registerEntity(entity, payload.uid); + + if (entity.components.StaticMapEntity) { + root.map.placeStaticEntity(entity); } - return null; } /////// COMPONENTS //// @@ -91,17 +67,10 @@ export class SerializerInternal { */ deserializeComponents(entity, data) { for (const componentId in data) { - const componentHandle = entity.components[componentId]; - if (!componentHandle) { - logger.warn( - "Loading outdated savegame, where entity had component", - componentId, - "but now no longer has" - ); - continue; - } - const componentData = data[componentId]; - const errorStatus = componentHandle.deserialize(componentData); + const componentClass = gComponentRegistry.findById(componentId); + const componentHandle = new componentClass({}); + entity.addComponent(componentHandle); + const errorStatus = componentHandle.deserialize(data[componentId]); if (errorStatus) { return errorStatus; } diff --git a/src/js/states/ingame.js b/src/js/states/ingame.js index d8b3ee00..0aa036ff 100644 --- a/src/js/states/ingame.js +++ b/src/js/states/ingame.js @@ -67,6 +67,10 @@ export class InGameState extends GameState { this.savegame; this.boundInputFilter = this.filterInput.bind(this); + + if (G_IS_DEV) { + window.performSave = this.doSave.bind(this); + } } /** @@ -96,7 +100,7 @@ export class InGameState extends GameState { onBeforeExit() { logger.log("Saving before quitting"); - return this.doSave(true, true).then(() => { + return this.doSave().then(() => { logger.log(this, "Successfully saved"); // this.stageDestroyed(); }); @@ -105,7 +109,7 @@ export class InGameState extends GameState { onAppPause() { if (this.stage === stages.s10_gameRunning) { logger.log("Saving because app got paused"); - this.doSave(true, true); + this.doSave(); } } @@ -397,14 +401,9 @@ export class InGameState extends GameState { /** * Saves the game - * @param {boolean=} syncWithServer - * @param {boolean} force */ - doSave(syncWithServer = true, force = false) { - // TODO - return; - + doSave() { if (!this.savegame || !this.savegame.isSaveable()) { return Promise.resolve(); } @@ -424,19 +423,9 @@ export class InGameState extends GameState { } // First update the game data - logger.log("Starting to save game ..."); this.savegame.updateData(this.core.root); - - let savePromise = this.savegame.writeSavegameAndMetadata(); - - if (syncWithServer) { - // Sync in parallel - // @ts-ignore - savePromise = savePromise.then(() => this.syncer.sync(this.core, this.savegame, force)); - } - - return savePromise.catch(err => { + return this.savegame.writeSavegameAndMetadata().catch(err => { logger.warn("Failed to save:", err); }); } diff --git a/src/js/states/main_menu.js b/src/js/states/main_menu.js index 78f2ae6f..0992437e 100644 --- a/src/js/states/main_menu.js +++ b/src/js/states/main_menu.js @@ -1,6 +1,7 @@ import { GameState } from "../core/game_state"; import { cachebust } from "../core/cachebust"; import { globalConfig } from "../core/config"; +import { makeDiv, formatSecondsToTimeAgo } from "../core/utils"; export class MainMenuState extends GameState { constructor() { @@ -69,6 +70,45 @@ export class MainMenuState extends GameState { } }); } + + this.renderSavegames(); + } + + renderSavegames() { + const games = this.app.savegameMgr.getSavegamesMetaData(); + if (games.length > 0) { + const parent = makeDiv(this.htmlElement.querySelector(".mainContainer"), null, ["savegames"]); + + for (let i = 0; i < games.length; ++i) { + const elem = makeDiv(parent, null, ["savegame"]); + + makeDiv(elem, null, ["internalId"], games[i].internalId.substr(0, 15)); + makeDiv( + elem, + null, + ["updateTime"], + formatSecondsToTimeAgo((new Date().getTime() - games[i].lastUpdate) / 1000.0) + ); + + const resumeBtn = document.createElement("button"); + resumeBtn.classList.add("styledButton", "resumeGame"); + elem.appendChild(resumeBtn); + + this.trackClicks(resumeBtn, () => this.resumeGame(games[i])); + } + } + } + + /** + * @param {object} game + */ + resumeGame(game) { + const savegame = this.app.savegameMgr.getSavegameById(game.internalId); + savegame.readAsync().then(() => { + this.moveToState("InGameState", { + savegame, + }); + }); } onPlayButtonClicked() { diff --git a/src/js/webworkers/compression.worker.js b/src/js/webworkers/compression.worker.js index 69e7a817..d1e661e4 100644 --- a/src/js/webworkers/compression.worker.js +++ b/src/js/webworkers/compression.worker.js @@ -31,13 +31,6 @@ function performJob(job, data) { case "compressX64": { return compressX64(data); } - case "compressWithChecksum": { - const checksum = rusha - .createHash() - .update(data + encryptKey) - .digest("hex"); - return compressX64(checksum + data); - } case "compressFile": { const checksum = sha1(data.text + salt); return data.compressionPrefix + compressX64(checksum + data.text);