mirror of
				https://github.com/tobspr/shapez.io.git
				synced 2025-06-13 13:04:03 +00:00 
			
		
		
		
	Initial support for saving games
This commit is contained in:
		
							parent
							
								
									70af65b829
								
							
						
					
					
						commit
						926e78da2d
					
				
							
								
								
									
										
											BIN
										
									
								
								res/ui/icons/play.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								res/ui/icons/play.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 980 B | 
| @ -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 { | ||||
|  | ||||
| @ -79,6 +79,7 @@ | ||||
|                     @include Button3D($colorRedBright); | ||||
|                     @include PlainText; | ||||
|                     @include S(padding, 5px, 8px, 4px); | ||||
|                     color: #fff; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -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); | ||||
|     } | ||||
| } | ||||
| @ -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, | ||||
| ]; | ||||
| @ -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); | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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; | ||||
| } | ||||
|  | ||||
| @ -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"; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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"); | ||||
| 
 | ||||
|  | ||||
| @ -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(); | ||||
| 
 | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
| @ -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
 | ||||
|  | ||||
| @ -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), | ||||
|                 }) | ||||
|             ), | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -14,9 +14,8 @@ export class MinerComponent extends Component { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @param {object} param0 | ||||
|      */ | ||||
|     constructor({}) { | ||||
|     constructor() { | ||||
|         super(); | ||||
|         this.lastMiningTime = 0; | ||||
|     } | ||||
|  | ||||
| @ -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), | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -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), | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										8
									
								
								src/js/game/game_speed_registry.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/js/game/game_speed_registry.js
									
									
									
									
									
										Normal file
									
								
							| @ -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
 | ||||
| } | ||||
| @ -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.<string, number>} | ||||
|          */ | ||||
|         this.gainedRewards = {}; | ||||
| 
 | ||||
|  | ||||
| @ -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 = `<span class='reward'>Unlocked ${reward}!</span>`; | ||||
|         let html = `<span class='reward'>Unlocked ${enumHubGoalRewardToString[reward]}!</span>`; | ||||
| 
 | ||||
|         const addBuildingExplanation = metaBuildingClass => { | ||||
|             const metaBuilding = gMetaBuildingRegistry.findByClass(metaBuildingClass); | ||||
|  | ||||
| @ -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); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -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); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -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<string, MapChunkView>} */ | ||||
|  | ||||
| @ -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.<enumSubShape, number>} 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; | ||||
|         } | ||||
| 
 | ||||
|  | ||||
| @ -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 | ||||
|      */ | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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"; | ||||
|  | ||||
| @ -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 = [ | ||||
|  | ||||
| @ -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; | ||||
| 
 | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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);
 | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										134
									
								
								src/js/savegame/savegame_compressor.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								src/js/savegame/savegame_compressor.js
									
									
									
									
									
										Normal file
									
								
							| @ -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; | ||||
| } | ||||
| @ -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
 | ||||
|  | ||||
| @ -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) { | ||||
|  | ||||
| @ -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<string>} values | ||||
|      * @param {Object<string, any>} values | ||||
|      */ | ||||
|     enum(values) { | ||||
|         return new TypeEnum(values); | ||||
| @ -102,6 +103,13 @@ export const types = { | ||||
|         return new TypeMetaClass(registry); | ||||
|     }, | ||||
| 
 | ||||
|     /** | ||||
|      * @param {Object.<string, BaseDataType>} 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) { | ||||
|  | ||||
| @ -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<string>} availableValues | ||||
|      * @param {Object.<string, any>} 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.<string, BaseDataType>} 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(",") + "]"; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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<Entity>} 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<Entity>} 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<any>} 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<any>} 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; | ||||
|             } | ||||
|  | ||||
| @ -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); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
| @ -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() { | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 tobspr
						tobspr