Initial support for saving games
This commit is contained in:
parent
23874c43dc
commit
b01d38e55d
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),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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