Initial support for saving games

This commit is contained in:
tobspr 2020-05-14 21:54:11 +02:00
parent 23874c43dc
commit b01d38e55d
44 changed files with 690 additions and 777 deletions

BIN
res/ui/icons/play.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 980 B

View File

@ -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 {

View File

@ -79,6 +79,7 @@
@include Button3D($colorRedBright);
@include PlainText;
@include S(padding, 5px, 8px, 4px);
color: #fff;
}
}
}

View File

@ -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

View File

@ -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,

View File

@ -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);
}
}

View File

@ -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,
];

View File

@ -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);

View File

@ -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

View File

@ -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;
}

View File

@ -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";
}
}

View File

@ -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");

View File

@ -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();

View File

@ -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);

View File

@ -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

View File

@ -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),
})
),
};
}

View File

@ -14,9 +14,8 @@ export class MinerComponent extends Component {
}
/**
* @param {object} param0
*/
constructor({}) {
constructor() {
super();
this.lastMiningTime = 0;
}

View File

@ -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),
};
}
/**

View File

@ -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

View File

@ -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,

View File

@ -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),
};
}

View 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
}

View File

@ -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 = {};

View File

@ -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);

View File

@ -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);
}

View File

@ -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);
}
/**

View File

@ -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>} */

View File

@ -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;
}

View File

@ -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
*/

View File

@ -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

View File

@ -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";

View File

@ -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 = [

View File

@ -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;

View File

@ -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;

View File

@ -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);

View 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;
}

View File

@ -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

View File

@ -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) {

View File

@ -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) {

View File

@ -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(",") + "]";
}
}

View File

@ -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;
}

View File

@ -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);
});
}

View File

@ -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() {

View File

@ -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);