diff --git a/res_built/atlas/atlas0_hq.json b/res_built/atlas/atlas0_hq.json
index 40f5c05a..12131b37 100644
--- a/res_built/atlas/atlas0_hq.json
+++ b/res_built/atlas/atlas0_hq.json
@@ -520,7 +520,7 @@
"spriteSourceSize": {"x":1,"y":0,"w":143,"h":144},
"sourceSize": {"w":144,"h":144}
},
-"sprites/blueprints/rotater-fl.png":
+"sprites/blueprints/rotater-rotate180.png":
{
"frame": {"x":423,"y":1591,"w":142,"h":144},
"rotated": false,
@@ -928,7 +928,7 @@
"spriteSourceSize": {"x":2,"y":0,"w":141,"h":143},
"sourceSize": {"w":144,"h":144}
},
-"sprites/buildings/rotater-fl.png":
+"sprites/buildings/rotater-rotate180.png":
{
"frame": {"x":1611,"y":612,"w":141,"h":143},
"rotated": false,
@@ -1487,6 +1487,6 @@
"format": "RGBA8888",
"size": {"w":2048,"h":2048},
"scale": "0.75",
- "smartupdate": "$TexturePacker:SmartUpdate:c38df9b4e442ab7ca6aca0b780d0839e:99e8394e24df838bd0f576e1caf16c3d:908b89f5ca8ff73e331a35a3b14d0604$"
+ "smartupdate": "$TexturePacker:SmartUpdate:f21861f754636ac30e87fdbab4b77dbd:161fc7d539e5ffa693c8470dcc931f75:908b89f5ca8ff73e331a35a3b14d0604$"
}
}
diff --git a/res_built/atlas/atlas0_hq.png b/res_built/atlas/atlas0_hq.png
index 6d8d5c83..06179e7d 100644
Binary files a/res_built/atlas/atlas0_hq.png and b/res_built/atlas/atlas0_hq.png differ
diff --git a/res_built/atlas/atlas0_lq.json b/res_built/atlas/atlas0_lq.json
index 629af783..e7f663ab 100644
--- a/res_built/atlas/atlas0_lq.json
+++ b/res_built/atlas/atlas0_lq.json
@@ -520,7 +520,7 @@
"spriteSourceSize": {"x":0,"y":0,"w":48,"h":48},
"sourceSize": {"w":48,"h":48}
},
-"sprites/blueprints/rotater-fl.png":
+"sprites/blueprints/rotater-rotate180.png":
{
"frame": {"x":965,"y":165,"w":48,"h":48},
"rotated": false,
@@ -928,7 +928,7 @@
"spriteSourceSize": {"x":0,"y":0,"w":48,"h":48},
"sourceSize": {"w":48,"h":48}
},
-"sprites/buildings/rotater-fl.png":
+"sprites/buildings/rotater-rotate180.png":
{
"frame": {"x":397,"y":254,"w":48,"h":48},
"rotated": false,
@@ -1487,6 +1487,6 @@
"format": "RGBA8888",
"size": {"w":1024,"h":1024},
"scale": "0.25",
- "smartupdate": "$TexturePacker:SmartUpdate:c38df9b4e442ab7ca6aca0b780d0839e:99e8394e24df838bd0f576e1caf16c3d:908b89f5ca8ff73e331a35a3b14d0604$"
+ "smartupdate": "$TexturePacker:SmartUpdate:f21861f754636ac30e87fdbab4b77dbd:161fc7d539e5ffa693c8470dcc931f75:908b89f5ca8ff73e331a35a3b14d0604$"
}
}
diff --git a/res_built/atlas/atlas0_lq.png b/res_built/atlas/atlas0_lq.png
index 13960d9c..8115a58a 100644
Binary files a/res_built/atlas/atlas0_lq.png and b/res_built/atlas/atlas0_lq.png differ
diff --git a/res_built/atlas/atlas0_mq.json b/res_built/atlas/atlas0_mq.json
index a6951b85..5a925ebb 100644
--- a/res_built/atlas/atlas0_mq.json
+++ b/res_built/atlas/atlas0_mq.json
@@ -520,7 +520,7 @@
"spriteSourceSize": {"x":0,"y":0,"w":96,"h":96},
"sourceSize": {"w":96,"h":96}
},
-"sprites/blueprints/rotater-fl.png":
+"sprites/blueprints/rotater-rotate180.png":
{
"frame": {"x":525,"y":1100,"w":95,"h":96},
"rotated": false,
@@ -928,7 +928,7 @@
"spriteSourceSize": {"x":1,"y":0,"w":95,"h":96},
"sourceSize": {"w":96,"h":96}
},
-"sprites/buildings/rotater-fl.png":
+"sprites/buildings/rotater-rotate180.png":
{
"frame": {"x":213,"y":1234,"w":95,"h":96},
"rotated": false,
@@ -1487,6 +1487,6 @@
"format": "RGBA8888",
"size": {"w":1024,"h":2048},
"scale": "0.5",
- "smartupdate": "$TexturePacker:SmartUpdate:c38df9b4e442ab7ca6aca0b780d0839e:99e8394e24df838bd0f576e1caf16c3d:908b89f5ca8ff73e331a35a3b14d0604$"
+ "smartupdate": "$TexturePacker:SmartUpdate:f21861f754636ac30e87fdbab4b77dbd:161fc7d539e5ffa693c8470dcc931f75:908b89f5ca8ff73e331a35a3b14d0604$"
}
}
diff --git a/res_built/atlas/atlas0_mq.png b/res_built/atlas/atlas0_mq.png
index 3cd030df..1cc3412a 100644
Binary files a/res_built/atlas/atlas0_mq.png and b/res_built/atlas/atlas0_mq.png differ
diff --git a/src/js/game/building_codes.js b/src/js/game/building_codes.js
index 7e7cfd3f..0a3cbc36 100644
--- a/src/js/game/building_codes.js
+++ b/src/js/game/building_codes.js
@@ -86,7 +86,10 @@ export function getCodeFromBuildingData(metaBuilding, variant, rotationVariant)
const hash = metaBuilding.getId() + "/" + variant + "/" + rotationVariant;
const result = variantsCache.get(hash);
if (G_IS_DEV) {
- assertAlways(!!result, "Building not found by data: " + hash);
+ if (!result) {
+ console.warn("Known hashes:", Array.from(variantsCache.keys()));
+ assertAlways(false, "Building not found by data: " + hash);
+ }
}
return result;
}
diff --git a/src/js/game/buildings/belt.js b/src/js/game/buildings/belt.js
index c2194acd..562b47d5 100644
--- a/src/js/game/buildings/belt.js
+++ b/src/js/game/buildings/belt.js
@@ -16,8 +16,6 @@ export const beltOverlayMatrices = {
[enumDirection.right]: generateMatrixRotations([0, 0, 0, 0, 1, 1, 0, 1, 0]),
};
-export class MetaBeltBaseBuilding extends MetaBuilding {}
-
export class MetaBeltBuilding extends MetaBuilding {
constructor() {
super("belt");
diff --git a/src/js/game/hud/parts/entity_debugger.js b/src/js/game/hud/parts/entity_debugger.js
index e290979c..640ad4d6 100644
--- a/src/js/game/hud/parts/entity_debugger.js
+++ b/src/js/game/hud/parts/entity_debugger.js
@@ -68,6 +68,10 @@ export class HUDEntityDebugger extends BaseHUDPart {
* @param {Array} recursion
*/
propertyToHTML(name, val, indent = 0, recursion = []) {
+ if (indent > 20) {
+ return;
+ }
+
if (val !== null && typeof val === "object") {
// Array is displayed like object, with indexes
recursion.push(val);
diff --git a/src/js/game/hud/parts/waypoints.js b/src/js/game/hud/parts/waypoints.js
index abf05b1f..116cd087 100644
--- a/src/js/game/hud/parts/waypoints.js
+++ b/src/js/game/hud/parts/waypoints.js
@@ -1,627 +1,626 @@
-import { makeOffscreenBuffer } from "../../../core/buffer_utils";
-import { globalConfig, IS_DEMO } from "../../../core/config";
-import { DrawParameters } from "../../../core/draw_parameters";
-import { Loader } from "../../../core/loader";
-import { DialogWithForm } from "../../../core/modal_dialog_elements";
-import { FormElementInput } from "../../../core/modal_dialog_forms";
-import { Rectangle } from "../../../core/rectangle";
-import { STOP_PROPAGATION } from "../../../core/signal";
-import { arrayDeleteValue, lerp, makeDiv, removeAllChildren, clamp } from "../../../core/utils";
-import { Vector } from "../../../core/vector";
-import { T } from "../../../translations";
-import { enumMouseButton } from "../../camera";
-import { KEYMAPPINGS } from "../../key_action_mapper";
-import { BaseHUDPart } from "../base_hud_part";
-import { DynamicDomAttach } from "../dynamic_dom_attach";
-import { enumNotificationType } from "./notifications";
-import { ShapeDefinition } from "../../shape_definition";
-import { BaseItem } from "../../base_item";
-import { ShapeItem } from "../../items/shape_item";
-
-/** @typedef {{
- * label: string | null,
- * center: { x: number, y: number },
- * zoomLevel: number
- * }} Waypoint */
-
-/**
- * Used when a shape icon is rendered instead
- */
-const MAX_LABEL_LENGTH = 71;
-
-export class HUDWaypoints extends BaseHUDPart {
- /**
- * Creates the overview of waypoints
- * @param {HTMLElement} parent
- */
- createElements(parent) {
- // Create the helper box on the lower right when zooming out
- if (this.root.app.settings.getAllSettings().offerHints) {
- this.hintElement = makeDiv(
- parent,
- "ingame_HUD_Waypoints_Hint",
- [],
- `
- ${T.ingame.waypoints.waypoints}
- ${T.ingame.waypoints.description.replace(
- "",
- `${this.root.keyMapper
- .getBinding(KEYMAPPINGS.navigation.createMarker)
- .getKeyCodeString()}
`
- )}
- `
- );
- }
-
- // Create the waypoint list on the upper right
- this.waypointsListElement = makeDiv(parent, "ingame_HUD_Waypoints", [], "Waypoints");
- }
-
- /**
- * Serializes the waypoints
- */
- serialize() {
- return {
- waypoints: this.waypoints,
- };
- }
-
- /**
- * Deserializes the waypoints
- * @param {{waypoints: Array}} data
- */
- deserialize(data) {
- if (!data || !data.waypoints || !Array.isArray(data.waypoints)) {
- return "Invalid waypoints data";
- }
- this.waypoints = data.waypoints;
- this.rerenderWaypointList();
- }
-
- /**
- * Initializes everything
- */
- initialize() {
- // Cache the sprite for the waypoints
- this.waypointSprite = Loader.getSprite("sprites/misc/waypoint.png");
- this.directionIndicatorSprite = Loader.getSprite("sprites/misc/hub_direction_indicator.png");
-
- /** @type {Array}
- */
- this.waypoints = [
- {
- label: null,
- center: { x: 0, y: 0 },
- zoomLevel: 3,
- },
- ];
-
- // Create a buffer we can use to measure text
- this.dummyBuffer = makeOffscreenBuffer(1, 1, {
- reusable: false,
- label: "waypoints-measure-canvas",
- })[1];
-
- // Dynamically attach/detach the lower right hint in the map overview
- if (this.hintElement) {
- this.domAttach = new DynamicDomAttach(this.root, this.hintElement);
- }
-
- // Catch mouse and key events
- this.root.camera.downPreHandler.add(this.onMouseDown, this);
- this.root.keyMapper
- .getBinding(KEYMAPPINGS.navigation.createMarker)
- .add(() => this.requestSaveMarker({}));
-
- /**
- * Stores at how much opacity the markers should be rendered on the map.
- * This is interpolated over multiple frames so we have some sort of fade effect
- */
- this.currentMarkerOpacity = 1;
- this.currentCompassOpacity = 0;
-
- // Create buffer which is used to indicate the hub direction
- const [canvas, context] = makeOffscreenBuffer(48, 48, {
- smooth: true,
- reusable: false,
- label: "waypoints-compass",
- });
- this.compassBuffer = { canvas, context };
-
- /**
- * Stores a cache from a shape short key to its canvas representation
- */
- this.cachedKeyToCanvas = {};
-
- /**
- * Store cached text widths
- * @type {Object}
- */
- this.cachedTextWidths = {};
-
- // Initial render
- this.rerenderWaypointList();
- }
-
- /**
- * Returns how long a text will be rendered
- * @param {string} text
- * @returns {number}
- */
- getTextWidth(text) {
- if (this.cachedTextWidths[text]) {
- return this.cachedTextWidths[text];
- }
-
- this.dummyBuffer.font = "bold " + this.getTextScale() + "px GameFont";
- return (this.cachedTextWidths[text] = this.dummyBuffer.measureText(text).width);
- }
-
- /**
- * Returns how big the text should be rendered
- */
- getTextScale() {
- return this.getWaypointUiScale() * 12;
- }
-
- /**
- * Returns the scale for rendering waypoints
- */
- getWaypointUiScale() {
- return this.root.app.getEffectiveUiScale();
- }
-
- /**
- * Re-renders the waypoint list to account for changes
- */
- rerenderWaypointList() {
- removeAllChildren(this.waypointsListElement);
- this.cleanupClickDetectors();
-
- for (let i = 0; i < this.waypoints.length; ++i) {
- const waypoint = this.waypoints[i];
- const label = this.getWaypointLabel(waypoint);
-
- const element = makeDiv(this.waypointsListElement, null, ["waypoint"]);
-
- if (ShapeDefinition.isValidShortKey(label)) {
- const canvas = this.getWaypointCanvas(waypoint);
- /**
- * Create a clone of the cached canvas, as calling appendElement when a canvas is
- * already in the document will move the existing canvas to the new position.
- */
- const [newCanvas, context] = makeOffscreenBuffer(48, 48, {
- smooth: true,
- label: label + "-waypoint-" + i,
- });
- context.drawImage(canvas, 0, 0);
- element.appendChild(newCanvas);
- element.classList.add("shapeIcon");
- } else {
- element.innerText = label;
- }
-
- if (this.isWaypointDeletable(waypoint)) {
- const editButton = makeDiv(element, null, ["editButton"]);
- this.trackClicks(editButton, () => this.requestSaveMarker({ waypoint }));
- }
-
- if (!waypoint.label) {
- // This must be the hub label
- element.classList.add("hub");
- element.insertBefore(this.compassBuffer.canvas, element.childNodes[0]);
- }
-
- this.trackClicks(element, () => this.moveToWaypoint(waypoint), {
- targetOnly: true,
- });
- }
- }
-
- /**
- * Moves the camera to a given waypoint
- * @param {Waypoint} waypoint
- */
- moveToWaypoint(waypoint) {
- this.root.camera.setDesiredCenter(new Vector(waypoint.center.x, waypoint.center.y));
- this.root.camera.setDesiredZoom(waypoint.zoomLevel);
- }
-
- /**
- * Deletes a waypoint from the list
- * @param {Waypoint} waypoint
- */
- deleteWaypoint(waypoint) {
- arrayDeleteValue(this.waypoints, waypoint);
- this.rerenderWaypointList();
- }
-
- /**
- * Gets the canvas for a given waypoint
- * @param {Waypoint} waypoint
- * @returns {HTMLCanvasElement}
- */
- getWaypointCanvas(waypoint) {
- const key = waypoint.label;
- if (this.cachedKeyToCanvas[key]) {
- return this.cachedKeyToCanvas[key];
- }
-
- assert(ShapeDefinition.isValidShortKey(key), "Invalid short key: " + key);
- const definition = this.root.shapeDefinitionMgr.getShapeFromShortKey(key);
- const preRendered = definition.generateAsCanvas(48);
- return (this.cachedKeyToCanvas[key] = preRendered);
- }
-
- /**
- * Requests to save a marker at the current camera position. If worldPos is set,
- * uses that position instead.
- * @param {object} param0
- * @param {Vector=} param0.worldPos Override the world pos, otherwise it is the camera position
- * @param {Waypoint=} param0.waypoint Waypoint to be edited. If omitted, create new
- */
- requestSaveMarker({ worldPos = null, waypoint = null }) {
- // Construct dialog with input field
- const markerNameInput = new FormElementInput({
- id: "markerName",
- label: null,
- placeholder: "",
- defaultValue: waypoint ? waypoint.label : "",
- validator: val =>
- val.length > 0 && (val.length < MAX_LABEL_LENGTH || ShapeDefinition.isValidShortKey(val)),
- });
- const dialog = new DialogWithForm({
- app: this.root.app,
- title: waypoint ? T.dialogs.createMarker.titleEdit : T.dialogs.createMarker.title,
- desc: T.dialogs.createMarker.desc,
- formElements: [markerNameInput],
- buttons: waypoint ? ["delete:bad", "cancel", "ok:good"] : ["cancel", "ok:good"],
- });
- this.root.hud.parts.dialogs.internalShowDialog(dialog);
-
- // Edit marker
- if (waypoint) {
- dialog.buttonSignals.ok.add(() => {
- // Actually rename the waypoint
- this.renameWaypoint(waypoint, markerNameInput.getValue());
- });
- dialog.buttonSignals.delete.add(() => {
- // Actually delete the waypoint
- this.deleteWaypoint(waypoint);
- });
- } else {
- // Compute where to create the marker
- const center = worldPos || this.root.camera.center;
-
- dialog.buttonSignals.ok.add(() => {
- // Show info that you can have only N markers in the demo,
- // actually show this *after* entering the name so you want the
- // standalone even more (I'm evil :P)
- if (IS_DEMO && this.waypoints.length > 2) {
- this.root.hud.parts.dialogs.showFeatureRestrictionInfo(
- "",
- T.dialogs.markerDemoLimit.desc
- );
- return;
- }
-
- // Actually create the waypoint
- this.addWaypoint(markerNameInput.getValue(), center);
- });
- }
- }
-
- /**
- * Adds a new waypoint at the given location with the given label
- * @param {string} label
- * @param {Vector} position
- */
- addWaypoint(label, position) {
- this.waypoints.push({
- label,
- center: { x: position.x, y: position.y },
- // Make sure the zoom is *just* a bit above the zoom level where the map overview
- // starts, so you always see all buildings
- zoomLevel: Math.max(this.root.camera.zoomLevel, globalConfig.mapChunkOverviewMinZoom + 0.05),
- });
-
- this.sortWaypoints();
-
- // Show notification about creation
- this.root.hud.signals.notification.dispatch(
- T.ingame.waypoints.creationSuccessNotification,
- enumNotificationType.success
- );
-
- // Re-render the list and thus add it
- this.rerenderWaypointList();
- }
-
- /**
- * Renames a waypoint with the given label
- * @param {Waypoint} waypoint
- * @param {string} label
- */
- renameWaypoint(waypoint, label) {
- waypoint.label = label;
-
- this.sortWaypoints();
-
- // Show notification about renamed
- this.root.hud.signals.notification.dispatch(
- T.ingame.waypoints.creationSuccessNotification,
- enumNotificationType.success
- );
-
- // Re-render the list and thus add it
- this.rerenderWaypointList();
- }
-
- /**
- * Called every frame to update stuff
- */
- update() {
- if (this.domAttach) {
- this.domAttach.update(this.root.camera.getIsMapOverlayActive());
- }
- }
-
- /**
- * Sort waypoints by name
- */
- sortWaypoints() {
- this.waypoints.sort((a, b) => {
- if (!a.label) {
- return -1;
- }
- if (!b.label) {
- return 1;
- }
- return this.getWaypointLabel(a)
- .padEnd(MAX_LABEL_LENGTH, "0")
- .localeCompare(this.getWaypointLabel(b).padEnd(MAX_LABEL_LENGTH, "0"));
- });
- }
-
- /**
- * Returns the label for a given waypoint
- * @param {Waypoint} waypoint
- * @returns {string}
- */
- getWaypointLabel(waypoint) {
- return waypoint.label || T.ingame.waypoints.hub;
- }
-
- /**
- * Returns if a waypoint is deletable
- * @param {Waypoint} waypoint
- * @returns {boolean}
- */
- isWaypointDeletable(waypoint) {
- return waypoint.label !== null;
- }
-
- /**
- * Returns the screen space bounds of the given waypoint or null
- * if it couldn't be determined. Also returns wheter its a shape or not
- * @param {Waypoint} waypoint
- * @return {{
- * screenBounds: Rectangle
- * item: BaseItem|null,
- * text: string
- * }}
- */
- getWaypointScreenParams(waypoint) {
- if (!this.root.camera.getIsMapOverlayActive()) {
- return null;
- }
-
- // Find parameters
- const scale = this.getWaypointUiScale();
- const screenPos = this.root.camera.worldToScreen(new Vector(waypoint.center.x, waypoint.center.y));
-
- // Distinguish between text and item waypoints -> Figure out parameters
- const originalLabel = this.getWaypointLabel(waypoint);
- let text, item, textWidth;
-
- if (ShapeDefinition.isValidShortKey(originalLabel)) {
- // If the label is actually a key, render the shape icon
- item = this.root.shapeDefinitionMgr.getShapeItemFromShortKey(originalLabel);
- textWidth = 40;
- } else {
- // Otherwise render a regular waypoint
- text = originalLabel;
- textWidth = this.getTextWidth(text);
- }
-
- return {
- screenBounds: new Rectangle(
- screenPos.x - 7 * scale,
- screenPos.y - 12 * scale,
- 15 * scale + textWidth,
- 15 * scale
- ),
- item,
- text,
- };
- }
-
- /**
- * Finds the currently intersected waypoint on the map overview under
- * the cursor.
- *
- * @returns {Waypoint | null}
- */
- findCurrentIntersectedWaypoint() {
- const mousePos = this.root.app.mousePosition;
- if (!mousePos) {
- return;
- }
-
- for (let i = 0; i < this.waypoints.length; ++i) {
- const waypoint = this.waypoints[i];
- const params = this.getWaypointScreenParams(waypoint);
- if (params && params.screenBounds.containsPoint(mousePos.x, mousePos.y)) {
- return waypoint;
- }
- }
- }
-
- /**
- * Mouse-Down handler
- * @param {Vector} pos
- * @param {enumMouseButton} button
- */
- onMouseDown(pos, button) {
- const waypoint = this.findCurrentIntersectedWaypoint();
- if (waypoint) {
- if (button === enumMouseButton.left) {
- this.root.soundProxy.playUiClick();
- this.moveToWaypoint(waypoint);
- } else if (button === enumMouseButton.right) {
- if (this.isWaypointDeletable(waypoint)) {
- this.root.soundProxy.playUiClick();
- this.requestSaveMarker({ waypoint });
- } else {
- this.root.soundProxy.playUiError();
- }
- }
-
- return STOP_PROPAGATION;
- } else {
- // Allow right click to create a marker
- if (button === enumMouseButton.right) {
- if (this.root.camera.getIsMapOverlayActive()) {
- const worldPos = this.root.camera.screenToWorld(pos);
- this.requestSaveMarker({ worldPos });
- return STOP_PROPAGATION;
- }
- }
- }
- }
-
- /**
- * Rerenders the compass
- */
- rerenderWaypointsCompass() {
- const dims = 48;
- const indicatorSize = 30;
- const cameraPos = this.root.camera.center;
-
- const context = this.compassBuffer.context;
- context.clearRect(0, 0, dims, dims);
-
- const distanceToHub = cameraPos.length();
- const compassVisible = distanceToHub > (10 * globalConfig.tileSize) / this.root.camera.zoomLevel;
- const targetCompassAlpha = compassVisible ? 1 : 0;
-
- // Fade the compas in / out
- this.currentCompassOpacity = lerp(this.currentCompassOpacity, targetCompassAlpha, 0.08);
-
- // Render the compass
- if (this.currentCompassOpacity > 0.01) {
- context.globalAlpha = this.currentCompassOpacity;
- const angle = cameraPos.angle() + Math.radians(45) + Math.PI / 2;
- context.translate(dims / 2, dims / 2);
- context.rotate(angle);
- this.directionIndicatorSprite.drawCentered(context, 0, 0, indicatorSize);
- context.rotate(-angle);
- context.translate(-dims / 2, -dims / 2);
- context.globalAlpha = 1;
- }
-
- // Render the regualr icon
- const iconOpacity = 1 - this.currentCompassOpacity;
- if (iconOpacity > 0.01) {
- context.globalAlpha = iconOpacity;
- this.waypointSprite.drawCentered(context, dims / 2, dims / 2, dims * 0.7);
- context.globalAlpha = 1;
- }
- }
-
- /**
- * Draws the waypoints on the map
- * @param {DrawParameters} parameters
- */
- drawOverlays(parameters) {
- const mousePos = this.root.app.mousePosition;
- const desiredOpacity = this.root.camera.getIsMapOverlayActive() ? 1 : 0;
- this.currentMarkerOpacity = lerp(this.currentMarkerOpacity, desiredOpacity, 0.08);
-
- this.rerenderWaypointsCompass();
-
- // Don't render with low opacity
- if (this.currentMarkerOpacity < 0.01) {
- return;
- }
-
- // Determine rendering scale
- const scale = this.getWaypointUiScale();
-
- // Set the font size
- const textSize = this.getTextScale();
- parameters.context.font = "bold " + textSize + "px GameFont";
- parameters.context.textBaseline = "middle";
-
- // Loop over all waypoints
- for (let i = 0; i < this.waypoints.length; ++i) {
- const waypoint = this.waypoints[i];
-
- const waypointData = this.getWaypointScreenParams(waypoint);
- if (!waypointData) {
- // Not relevant
- continue;
- }
-
- if (!parameters.visibleRect.containsRect(waypointData.screenBounds)) {
- // Out of screen
- continue;
- }
-
- const bounds = waypointData.screenBounds;
- const contentPaddingX = 7 * scale;
- const isSelected = mousePos && bounds.containsPoint(mousePos.x, mousePos.y);
-
- // Render the background rectangle
- parameters.context.globalAlpha = this.currentMarkerOpacity * (isSelected ? 1 : 0.7);
- parameters.context.fillStyle = "rgba(255, 255, 255, 0.7)";
- parameters.context.fillRect(bounds.x, bounds.y, bounds.w, bounds.h);
-
- // Render the text
- if (waypointData.item) {
- const canvas = this.getWaypointCanvas(waypoint);
- const itemSize = 14 * scale;
- parameters.context.drawImage(
- canvas,
- bounds.x + contentPaddingX + 6 * scale,
- bounds.y + bounds.h / 2 - itemSize / 2,
- itemSize,
- itemSize
- );
- } else if (waypointData.text) {
- // Render the text
- parameters.context.fillStyle = "#000";
- parameters.context.textBaseline = "middle";
- parameters.context.fillText(
- waypointData.text,
- bounds.x + contentPaddingX + 6 * scale,
- bounds.y + bounds.h / 2
- );
- parameters.context.textBaseline = "alphabetic";
- } else {
- assertAlways(false, "Waypoint has no item and text");
- }
-
- // Render the small icon on the left
- this.waypointSprite.drawCentered(
- parameters.context,
- bounds.x + contentPaddingX,
- bounds.y + bounds.h / 2,
- bounds.h * 0.7
- );
- }
-
- parameters.context.textBaseline = "alphabetic";
- parameters.context.globalAlpha = 1;
- }
-}
+import { makeOffscreenBuffer } from "../../../core/buffer_utils";
+import { globalConfig, IS_DEMO } from "../../../core/config";
+import { DrawParameters } from "../../../core/draw_parameters";
+import { Loader } from "../../../core/loader";
+import { DialogWithForm } from "../../../core/modal_dialog_elements";
+import { FormElementInput } from "../../../core/modal_dialog_forms";
+import { Rectangle } from "../../../core/rectangle";
+import { STOP_PROPAGATION } from "../../../core/signal";
+import { arrayDeleteValue, lerp, makeDiv, removeAllChildren } from "../../../core/utils";
+import { Vector } from "../../../core/vector";
+import { T } from "../../../translations";
+import { BaseItem } from "../../base_item";
+import { enumMouseButton } from "../../camera";
+import { KEYMAPPINGS } from "../../key_action_mapper";
+import { ShapeDefinition } from "../../shape_definition";
+import { BaseHUDPart } from "../base_hud_part";
+import { DynamicDomAttach } from "../dynamic_dom_attach";
+import { enumNotificationType } from "./notifications";
+
+/** @typedef {{
+ * label: string | null,
+ * center: { x: number, y: number },
+ * zoomLevel: number
+ * }} Waypoint */
+
+/**
+ * Used when a shape icon is rendered instead
+ */
+const MAX_LABEL_LENGTH = 71;
+
+export class HUDWaypoints extends BaseHUDPart {
+ /**
+ * Creates the overview of waypoints
+ * @param {HTMLElement} parent
+ */
+ createElements(parent) {
+ // Create the helper box on the lower right when zooming out
+ if (this.root.app.settings.getAllSettings().offerHints) {
+ this.hintElement = makeDiv(
+ parent,
+ "ingame_HUD_Waypoints_Hint",
+ [],
+ `
+ ${T.ingame.waypoints.waypoints}
+ ${T.ingame.waypoints.description.replace(
+ "",
+ `${this.root.keyMapper
+ .getBinding(KEYMAPPINGS.navigation.createMarker)
+ .getKeyCodeString()}
`
+ )}
+ `
+ );
+ }
+
+ // Create the waypoint list on the upper right
+ this.waypointsListElement = makeDiv(parent, "ingame_HUD_Waypoints", [], "Waypoints");
+ }
+
+ /**
+ * Serializes the waypoints
+ */
+ serialize() {
+ return {
+ waypoints: this.waypoints,
+ };
+ }
+
+ /**
+ * Deserializes the waypoints
+ * @param {{waypoints: Array}} data
+ */
+ deserialize(data) {
+ if (!data || !data.waypoints || !Array.isArray(data.waypoints)) {
+ return "Invalid waypoints data";
+ }
+ this.waypoints = data.waypoints;
+ this.rerenderWaypointList();
+ }
+
+ /**
+ * Initializes everything
+ */
+ initialize() {
+ // Cache the sprite for the waypoints
+ this.waypointSprite = Loader.getSprite("sprites/misc/waypoint.png");
+ this.directionIndicatorSprite = Loader.getSprite("sprites/misc/hub_direction_indicator.png");
+
+ /** @type {Array}
+ */
+ this.waypoints = [
+ {
+ label: null,
+ center: { x: 0, y: 0 },
+ zoomLevel: 3,
+ },
+ ];
+
+ // Create a buffer we can use to measure text
+ this.dummyBuffer = makeOffscreenBuffer(1, 1, {
+ reusable: false,
+ label: "waypoints-measure-canvas",
+ })[1];
+
+ // Dynamically attach/detach the lower right hint in the map overview
+ if (this.hintElement) {
+ this.domAttach = new DynamicDomAttach(this.root, this.hintElement);
+ }
+
+ // Catch mouse and key events
+ this.root.camera.downPreHandler.add(this.onMouseDown, this);
+ this.root.keyMapper
+ .getBinding(KEYMAPPINGS.navigation.createMarker)
+ .add(() => this.requestSaveMarker({}));
+
+ /**
+ * Stores at how much opacity the markers should be rendered on the map.
+ * This is interpolated over multiple frames so we have some sort of fade effect
+ */
+ this.currentMarkerOpacity = 1;
+ this.currentCompassOpacity = 0;
+
+ // Create buffer which is used to indicate the hub direction
+ const [canvas, context] = makeOffscreenBuffer(48, 48, {
+ smooth: true,
+ reusable: false,
+ label: "waypoints-compass",
+ });
+ this.compassBuffer = { canvas, context };
+
+ /**
+ * Stores a cache from a shape short key to its canvas representation
+ */
+ this.cachedKeyToCanvas = {};
+
+ /**
+ * Store cached text widths
+ * @type {Object}
+ */
+ this.cachedTextWidths = {};
+
+ // Initial render
+ this.rerenderWaypointList();
+ }
+
+ /**
+ * Returns how long a text will be rendered
+ * @param {string} text
+ * @returns {number}
+ */
+ getTextWidth(text) {
+ if (this.cachedTextWidths[text]) {
+ return this.cachedTextWidths[text];
+ }
+
+ this.dummyBuffer.font = "bold " + this.getTextScale() + "px GameFont";
+ return (this.cachedTextWidths[text] = this.dummyBuffer.measureText(text).width);
+ }
+
+ /**
+ * Returns how big the text should be rendered
+ */
+ getTextScale() {
+ return this.getWaypointUiScale() * 12;
+ }
+
+ /**
+ * Returns the scale for rendering waypoints
+ */
+ getWaypointUiScale() {
+ return this.root.app.getEffectiveUiScale();
+ }
+
+ /**
+ * Re-renders the waypoint list to account for changes
+ */
+ rerenderWaypointList() {
+ removeAllChildren(this.waypointsListElement);
+ this.cleanupClickDetectors();
+
+ for (let i = 0; i < this.waypoints.length; ++i) {
+ const waypoint = this.waypoints[i];
+ const label = this.getWaypointLabel(waypoint);
+
+ const element = makeDiv(this.waypointsListElement, null, ["waypoint"]);
+
+ if (ShapeDefinition.isValidShortKey(label)) {
+ const canvas = this.getWaypointCanvas(waypoint);
+ /**
+ * Create a clone of the cached canvas, as calling appendElement when a canvas is
+ * already in the document will move the existing canvas to the new position.
+ */
+ const [newCanvas, context] = makeOffscreenBuffer(48, 48, {
+ smooth: true,
+ label: label + "-waypoint-" + i,
+ });
+ context.drawImage(canvas, 0, 0);
+ element.appendChild(newCanvas);
+ element.classList.add("shapeIcon");
+ } else {
+ element.innerText = label;
+ }
+
+ if (this.isWaypointDeletable(waypoint)) {
+ const editButton = makeDiv(element, null, ["editButton"]);
+ this.trackClicks(editButton, () => this.requestSaveMarker({ waypoint }));
+ }
+
+ if (!waypoint.label) {
+ // This must be the hub label
+ element.classList.add("hub");
+ element.insertBefore(this.compassBuffer.canvas, element.childNodes[0]);
+ }
+
+ this.trackClicks(element, () => this.moveToWaypoint(waypoint), {
+ targetOnly: true,
+ });
+ }
+ }
+
+ /**
+ * Moves the camera to a given waypoint
+ * @param {Waypoint} waypoint
+ */
+ moveToWaypoint(waypoint) {
+ this.root.camera.setDesiredCenter(new Vector(waypoint.center.x, waypoint.center.y));
+ this.root.camera.setDesiredZoom(waypoint.zoomLevel);
+ }
+
+ /**
+ * Deletes a waypoint from the list
+ * @param {Waypoint} waypoint
+ */
+ deleteWaypoint(waypoint) {
+ arrayDeleteValue(this.waypoints, waypoint);
+ this.rerenderWaypointList();
+ }
+
+ /**
+ * Gets the canvas for a given waypoint
+ * @param {Waypoint} waypoint
+ * @returns {HTMLCanvasElement}
+ */
+ getWaypointCanvas(waypoint) {
+ const key = waypoint.label;
+ if (this.cachedKeyToCanvas[key]) {
+ return this.cachedKeyToCanvas[key];
+ }
+
+ assert(ShapeDefinition.isValidShortKey(key), "Invalid short key: " + key);
+ const definition = this.root.shapeDefinitionMgr.getShapeFromShortKey(key);
+ const preRendered = definition.generateAsCanvas(48);
+ return (this.cachedKeyToCanvas[key] = preRendered);
+ }
+
+ /**
+ * Requests to save a marker at the current camera position. If worldPos is set,
+ * uses that position instead.
+ * @param {object} param0
+ * @param {Vector=} param0.worldPos Override the world pos, otherwise it is the camera position
+ * @param {Waypoint=} param0.waypoint Waypoint to be edited. If omitted, create new
+ */
+ requestSaveMarker({ worldPos = null, waypoint = null }) {
+ // Construct dialog with input field
+ const markerNameInput = new FormElementInput({
+ id: "markerName",
+ label: null,
+ placeholder: "",
+ defaultValue: waypoint ? waypoint.label : "",
+ validator: val =>
+ val.length > 0 && (val.length < MAX_LABEL_LENGTH || ShapeDefinition.isValidShortKey(val)),
+ });
+ const dialog = new DialogWithForm({
+ app: this.root.app,
+ title: waypoint ? T.dialogs.createMarker.titleEdit : T.dialogs.createMarker.title,
+ desc: T.dialogs.createMarker.desc,
+ formElements: [markerNameInput],
+ buttons: waypoint ? ["delete:bad", "cancel", "ok:good"] : ["cancel", "ok:good"],
+ });
+ this.root.hud.parts.dialogs.internalShowDialog(dialog);
+
+ // Edit marker
+ if (waypoint) {
+ dialog.buttonSignals.ok.add(() => {
+ // Actually rename the waypoint
+ this.renameWaypoint(waypoint, markerNameInput.getValue());
+ });
+ dialog.buttonSignals.delete.add(() => {
+ // Actually delete the waypoint
+ this.deleteWaypoint(waypoint);
+ });
+ } else {
+ // Compute where to create the marker
+ const center = worldPos || this.root.camera.center;
+
+ dialog.buttonSignals.ok.add(() => {
+ // Show info that you can have only N markers in the demo,
+ // actually show this *after* entering the name so you want the
+ // standalone even more (I'm evil :P)
+ if (IS_DEMO && this.waypoints.length > 2) {
+ this.root.hud.parts.dialogs.showFeatureRestrictionInfo(
+ "",
+ T.dialogs.markerDemoLimit.desc
+ );
+ return;
+ }
+
+ // Actually create the waypoint
+ this.addWaypoint(markerNameInput.getValue(), center);
+ });
+ }
+ }
+
+ /**
+ * Adds a new waypoint at the given location with the given label
+ * @param {string} label
+ * @param {Vector} position
+ */
+ addWaypoint(label, position) {
+ this.waypoints.push({
+ label,
+ center: { x: position.x, y: position.y },
+ // Make sure the zoom is *just* a bit above the zoom level where the map overview
+ // starts, so you always see all buildings
+ zoomLevel: Math.max(this.root.camera.zoomLevel, globalConfig.mapChunkOverviewMinZoom + 0.05),
+ });
+
+ this.sortWaypoints();
+
+ // Show notification about creation
+ this.root.hud.signals.notification.dispatch(
+ T.ingame.waypoints.creationSuccessNotification,
+ enumNotificationType.success
+ );
+
+ // Re-render the list and thus add it
+ this.rerenderWaypointList();
+ }
+
+ /**
+ * Renames a waypoint with the given label
+ * @param {Waypoint} waypoint
+ * @param {string} label
+ */
+ renameWaypoint(waypoint, label) {
+ waypoint.label = label;
+
+ this.sortWaypoints();
+
+ // Show notification about renamed
+ this.root.hud.signals.notification.dispatch(
+ T.ingame.waypoints.creationSuccessNotification,
+ enumNotificationType.success
+ );
+
+ // Re-render the list and thus add it
+ this.rerenderWaypointList();
+ }
+
+ /**
+ * Called every frame to update stuff
+ */
+ update() {
+ if (this.domAttach) {
+ this.domAttach.update(this.root.camera.getIsMapOverlayActive());
+ }
+ }
+
+ /**
+ * Sort waypoints by name
+ */
+ sortWaypoints() {
+ this.waypoints.sort((a, b) => {
+ if (!a.label) {
+ return -1;
+ }
+ if (!b.label) {
+ return 1;
+ }
+ return this.getWaypointLabel(a)
+ .padEnd(MAX_LABEL_LENGTH, "0")
+ .localeCompare(this.getWaypointLabel(b).padEnd(MAX_LABEL_LENGTH, "0"));
+ });
+ }
+
+ /**
+ * Returns the label for a given waypoint
+ * @param {Waypoint} waypoint
+ * @returns {string}
+ */
+ getWaypointLabel(waypoint) {
+ return waypoint.label || T.ingame.waypoints.hub;
+ }
+
+ /**
+ * Returns if a waypoint is deletable
+ * @param {Waypoint} waypoint
+ * @returns {boolean}
+ */
+ isWaypointDeletable(waypoint) {
+ return waypoint.label !== null;
+ }
+
+ /**
+ * Returns the screen space bounds of the given waypoint or null
+ * if it couldn't be determined. Also returns wheter its a shape or not
+ * @param {Waypoint} waypoint
+ * @return {{
+ * screenBounds: Rectangle
+ * item: BaseItem|null,
+ * text: string
+ * }}
+ */
+ getWaypointScreenParams(waypoint) {
+ if (!this.root.camera.getIsMapOverlayActive()) {
+ return null;
+ }
+
+ // Find parameters
+ const scale = this.getWaypointUiScale();
+ const screenPos = this.root.camera.worldToScreen(new Vector(waypoint.center.x, waypoint.center.y));
+
+ // Distinguish between text and item waypoints -> Figure out parameters
+ const originalLabel = this.getWaypointLabel(waypoint);
+ let text, item, textWidth;
+
+ if (ShapeDefinition.isValidShortKey(originalLabel)) {
+ // If the label is actually a key, render the shape icon
+ item = this.root.shapeDefinitionMgr.getShapeItemFromShortKey(originalLabel);
+ textWidth = 40;
+ } else {
+ // Otherwise render a regular waypoint
+ text = originalLabel;
+ textWidth = this.getTextWidth(text);
+ }
+
+ return {
+ screenBounds: new Rectangle(
+ screenPos.x - 7 * scale,
+ screenPos.y - 12 * scale,
+ 15 * scale + textWidth,
+ 15 * scale
+ ),
+ item,
+ text,
+ };
+ }
+
+ /**
+ * Finds the currently intersected waypoint on the map overview under
+ * the cursor.
+ *
+ * @returns {Waypoint | null}
+ */
+ findCurrentIntersectedWaypoint() {
+ const mousePos = this.root.app.mousePosition;
+ if (!mousePos) {
+ return;
+ }
+
+ for (let i = 0; i < this.waypoints.length; ++i) {
+ const waypoint = this.waypoints[i];
+ const params = this.getWaypointScreenParams(waypoint);
+ if (params && params.screenBounds.containsPoint(mousePos.x, mousePos.y)) {
+ return waypoint;
+ }
+ }
+ }
+
+ /**
+ * Mouse-Down handler
+ * @param {Vector} pos
+ * @param {enumMouseButton} button
+ */
+ onMouseDown(pos, button) {
+ const waypoint = this.findCurrentIntersectedWaypoint();
+ if (waypoint) {
+ if (button === enumMouseButton.left) {
+ this.root.soundProxy.playUiClick();
+ this.moveToWaypoint(waypoint);
+ } else if (button === enumMouseButton.right) {
+ if (this.isWaypointDeletable(waypoint)) {
+ this.root.soundProxy.playUiClick();
+ this.requestSaveMarker({ waypoint });
+ } else {
+ this.root.soundProxy.playUiError();
+ }
+ }
+
+ return STOP_PROPAGATION;
+ } else {
+ // Allow right click to create a marker
+ if (button === enumMouseButton.right) {
+ if (this.root.camera.getIsMapOverlayActive()) {
+ const worldPos = this.root.camera.screenToWorld(pos);
+ this.requestSaveMarker({ worldPos });
+ return STOP_PROPAGATION;
+ }
+ }
+ }
+ }
+
+ /**
+ * Rerenders the compass
+ */
+ rerenderWaypointsCompass() {
+ const dims = 48;
+ const indicatorSize = 30;
+ const cameraPos = this.root.camera.center;
+
+ const context = this.compassBuffer.context;
+ context.clearRect(0, 0, dims, dims);
+
+ const distanceToHub = cameraPos.length();
+ const compassVisible = distanceToHub > (10 * globalConfig.tileSize) / this.root.camera.zoomLevel;
+ const targetCompassAlpha = compassVisible ? 1 : 0;
+
+ // Fade the compas in / out
+ this.currentCompassOpacity = lerp(this.currentCompassOpacity, targetCompassAlpha, 0.08);
+
+ // Render the compass
+ if (this.currentCompassOpacity > 0.01) {
+ context.globalAlpha = this.currentCompassOpacity;
+ const angle = cameraPos.angle() + Math.radians(45) + Math.PI / 2;
+ context.translate(dims / 2, dims / 2);
+ context.rotate(angle);
+ this.directionIndicatorSprite.drawCentered(context, 0, 0, indicatorSize);
+ context.rotate(-angle);
+ context.translate(-dims / 2, -dims / 2);
+ context.globalAlpha = 1;
+ }
+
+ // Render the regualr icon
+ const iconOpacity = 1 - this.currentCompassOpacity;
+ if (iconOpacity > 0.01) {
+ context.globalAlpha = iconOpacity;
+ this.waypointSprite.drawCentered(context, dims / 2, dims / 2, dims * 0.7);
+ context.globalAlpha = 1;
+ }
+ }
+
+ /**
+ * Draws the waypoints on the map
+ * @param {DrawParameters} parameters
+ */
+ drawOverlays(parameters) {
+ const mousePos = this.root.app.mousePosition;
+ const desiredOpacity = this.root.camera.getIsMapOverlayActive() ? 1 : 0;
+ this.currentMarkerOpacity = lerp(this.currentMarkerOpacity, desiredOpacity, 0.08);
+
+ this.rerenderWaypointsCompass();
+
+ // Don't render with low opacity
+ if (this.currentMarkerOpacity < 0.01) {
+ return;
+ }
+
+ // Determine rendering scale
+ const scale = this.getWaypointUiScale();
+
+ // Set the font size
+ const textSize = this.getTextScale();
+ parameters.context.font = "bold " + textSize + "px GameFont";
+ parameters.context.textBaseline = "middle";
+
+ // Loop over all waypoints
+ for (let i = 0; i < this.waypoints.length; ++i) {
+ const waypoint = this.waypoints[i];
+
+ const waypointData = this.getWaypointScreenParams(waypoint);
+ if (!waypointData) {
+ // Not relevant
+ continue;
+ }
+
+ if (!parameters.visibleRect.containsRect(waypointData.screenBounds)) {
+ // Out of screen
+ continue;
+ }
+
+ const bounds = waypointData.screenBounds;
+ const contentPaddingX = 7 * scale;
+ const isSelected = mousePos && bounds.containsPoint(mousePos.x, mousePos.y);
+
+ // Render the background rectangle
+ parameters.context.globalAlpha = this.currentMarkerOpacity * (isSelected ? 1 : 0.7);
+ parameters.context.fillStyle = "rgba(255, 255, 255, 0.7)";
+ parameters.context.fillRect(bounds.x, bounds.y, bounds.w, bounds.h);
+
+ // Render the text
+ if (waypointData.item) {
+ const canvas = this.getWaypointCanvas(waypoint);
+ const itemSize = 14 * scale;
+ parameters.context.drawImage(
+ canvas,
+ bounds.x + contentPaddingX + 6 * scale,
+ bounds.y + bounds.h / 2 - itemSize / 2,
+ itemSize,
+ itemSize
+ );
+ } else if (waypointData.text) {
+ // Render the text
+ parameters.context.fillStyle = "#000";
+ parameters.context.textBaseline = "middle";
+ parameters.context.fillText(
+ waypointData.text,
+ bounds.x + contentPaddingX + 6 * scale,
+ bounds.y + bounds.h / 2
+ );
+ parameters.context.textBaseline = "alphabetic";
+ } else {
+ assertAlways(false, "Waypoint has no item and text");
+ }
+
+ // Render the small icon on the left
+ this.waypointSprite.drawCentered(
+ parameters.context,
+ bounds.x + contentPaddingX,
+ bounds.y + bounds.h / 2,
+ bounds.h * 0.7
+ );
+ }
+
+ parameters.context.textBaseline = "alphabetic";
+ parameters.context.globalAlpha = 1;
+ }
+}
diff --git a/src/js/savegame/savegame.js b/src/js/savegame/savegame.js
index 2a7102a9..0ad630f6 100644
--- a/src/js/savegame/savegame.js
+++ b/src/js/savegame/savegame.js
@@ -1,273 +1,279 @@
-import { ReadWriteProxy } from "../core/read_write_proxy";
-import { ExplainedResult } from "../core/explained_result";
-import { SavegameSerializer } from "./savegame_serializer";
-import { BaseSavegameInterface } from "./savegame_interface";
-import { createLogger } from "../core/logging";
-import { globalConfig } from "../core/config";
-import { getSavegameInterface, savegameInterfaces } from "./savegame_interface_registry";
-import { SavegameInterface_V1001 } from "./schemas/1001";
-import { SavegameInterface_V1002 } from "./schemas/1002";
-import { SavegameInterface_V1003 } from "./schemas/1003";
-import { SavegameInterface_V1004 } from "./schemas/1004";
-import { SavegameInterface_V1005 } from "./schemas/1005";
-
-const logger = createLogger("savegame");
-
-/**
- * @typedef {import("../application").Application} Application
- * @typedef {import("../game/root").GameRoot} GameRoot
- * @typedef {import("./savegame_typedefs").SavegameData} SavegameData
- * @typedef {import("./savegame_typedefs").SavegameMetadata} SavegameMetadata
- * @typedef {import("./savegame_typedefs").SavegameStats} SavegameStats
- * @typedef {import("./savegame_typedefs").SerializedGame} SerializedGame
- */
-
-export class Savegame extends ReadWriteProxy {
- /**
- *
- * @param {Application} app
- * @param {object} param0
- * @param {string} param0.internalId
- * @param {SavegameMetadata} param0.metaDataRef Handle to the meta data
- */
- constructor(app, { internalId, metaDataRef }) {
- super(app, "savegame-" + internalId + ".bin");
- this.internalId = internalId;
- this.metaDataRef = metaDataRef;
-
- /** @type {SavegameData} */
- this.currentData = this.getDefaultData();
-
- assert(
- savegameInterfaces[Savegame.getCurrentVersion()],
- "Savegame interface not defined: " + Savegame.getCurrentVersion()
- );
- }
-
- //////// RW Proxy Impl //////////
-
- /**
- * @returns {number}
- */
- static getCurrentVersion() {
- return 1005;
- }
-
- /**
- * @returns {typeof BaseSavegameInterface}
- */
- static getReaderClass() {
- return savegameInterfaces[Savegame.getCurrentVersion()];
- }
-
- /**
- * @returns {number}
- */
- getCurrentVersion() {
- return /** @type {typeof Savegame} */ (this.constructor).getCurrentVersion();
- }
-
- /**
- * Returns the savegames default data
- * @returns {SavegameData}
- */
- getDefaultData() {
- return {
- version: this.getCurrentVersion(),
- dump: null,
- stats: {},
- lastUpdate: Date.now(),
- };
- }
-
- /**
- * Migrates the savegames data
- * @param {SavegameData} data
- */
- migrate(data) {
- if (data.version < 1000) {
- return ExplainedResult.bad("Can not migrate savegame, too old");
- }
-
- if (data.version === 1000) {
- SavegameInterface_V1001.migrate1000to1001(data);
- data.version = 1001;
- }
-
- if (data.version === 1001) {
- SavegameInterface_V1002.migrate1001to1002(data);
- data.version = 1002;
- }
-
- if (data.version === 1002) {
- SavegameInterface_V1003.migrate1002to1003(data);
- data.version = 1003;
- }
-
- if (data.version === 1003) {
- SavegameInterface_V1004.migrate1003to1004(data);
- data.version = 1004;
- }
-
- if (data.version === 1004) {
- SavegameInterface_V1005.migrate1004to1005(data);
- data.version = 1005;
- }
-
- return ExplainedResult.good();
- }
-
- /**
- * Verifies the savegames data
- * @param {SavegameData} data
- */
- verify(data) {
- if (!data.dump) {
- // Well, guess that works
- return ExplainedResult.good();
- }
-
- if (!this.getDumpReaderForExternalData(data).validate()) {
- return ExplainedResult.bad("dump-reader-failed-validation");
- }
- return ExplainedResult.good();
- }
-
- //////// Subclasses interface ////////
-
- /**
- * Returns if this game can be saved on disc
- * @returns {boolean}
- */
- isSaveable() {
- return true;
- }
- /**
- * Returns the statistics of the savegame
- * @returns {SavegameStats}
- */
- getStatistics() {
- return this.currentData.stats;
- }
-
- /**
- * Returns the *real* last update of the savegame, not the one of the metadata
- * which could also be the servers one
- */
- getRealLastUpdate() {
- return this.currentData.lastUpdate;
- }
-
- /**
- * Returns if this game has a serialized game dump
- */
- hasGameDump() {
- return !!this.currentData.dump && this.currentData.dump.entities.length > 0;
- }
-
- /**
- * Returns the current game dump
- * @returns {SerializedGame}
- */
- getCurrentDump() {
- return this.currentData.dump;
- }
-
- /**
- * Returns a reader to access the data
- * @returns {BaseSavegameInterface}
- */
- getDumpReader() {
- if (!this.currentData.dump) {
- logger.warn("Getting reader on null-savegame dump");
- }
-
- const cls = /** @type {typeof Savegame} */ (this.constructor).getReaderClass();
- return new cls(this.currentData);
- }
-
- /**
- * Returns a reader to access external data
- * @returns {BaseSavegameInterface}
- */
- getDumpReaderForExternalData(data) {
- assert(data.version, "External data contains no version");
- return getSavegameInterface(data);
- }
-
- ///////// Public Interface ///////////
-
- /**
- * Updates the last update field so we can send the savegame to the server,
- * WITHOUT Saving!
- */
- setLastUpdate(time) {
- this.currentData.lastUpdate = time;
- }
-
- /**
- *
- * @param {GameRoot} root
- */
- updateData(root) {
- // Construct a new serializer
- const serializer = new SavegameSerializer();
-
- // let timer = performance.now();
- const dump = serializer.generateDumpFromGameRoot(root);
- if (!dump) {
- return false;
- }
-
- const shadowData = Object.assign({}, this.currentData);
- shadowData.dump = dump;
- shadowData.lastUpdate = new Date().getTime();
- shadowData.version = this.getCurrentVersion();
-
- const reader = this.getDumpReaderForExternalData(shadowData);
-
- // Validate (not in prod though)
- if (!G_IS_RELEASE) {
- const validationResult = reader.validate();
- if (!validationResult) {
- return false;
- }
- }
-
- // Save data
- this.currentData = shadowData;
- }
-
- /**
- * Writes the savegame as well as its metadata
- */
- writeSavegameAndMetadata() {
- return this.writeAsync().then(() => this.saveMetadata());
- }
-
- /**
- * Updates the savegames metadata
- */
- saveMetadata() {
- this.metaDataRef.lastUpdate = new Date().getTime();
- this.metaDataRef.version = this.getCurrentVersion();
- if (!this.hasGameDump()) {
- this.metaDataRef.level = 0;
- } else {
- this.metaDataRef.level = this.currentData.dump.hubGoals.level;
- }
-
- return this.app.savegameMgr.writeAsync();
- }
-
- /**
- * @see ReadWriteProxy.writeAsync
- * @returns {Promise}
- */
- writeAsync() {
- if (G_IS_DEV && globalConfig.debug.disableSavegameWrite) {
- return Promise.resolve();
- }
- return super.writeAsync();
- }
-}
+import { ReadWriteProxy } from "../core/read_write_proxy";
+import { ExplainedResult } from "../core/explained_result";
+import { SavegameSerializer } from "./savegame_serializer";
+import { BaseSavegameInterface } from "./savegame_interface";
+import { createLogger } from "../core/logging";
+import { globalConfig } from "../core/config";
+import { getSavegameInterface, savegameInterfaces } from "./savegame_interface_registry";
+import { SavegameInterface_V1001 } from "./schemas/1001";
+import { SavegameInterface_V1002 } from "./schemas/1002";
+import { SavegameInterface_V1003 } from "./schemas/1003";
+import { SavegameInterface_V1004 } from "./schemas/1004";
+import { SavegameInterface_V1005 } from "./schemas/1005";
+import { SavegameInterface_V1006 } from "./schemas/1006";
+
+const logger = createLogger("savegame");
+
+/**
+ * @typedef {import("../application").Application} Application
+ * @typedef {import("../game/root").GameRoot} GameRoot
+ * @typedef {import("./savegame_typedefs").SavegameData} SavegameData
+ * @typedef {import("./savegame_typedefs").SavegameMetadata} SavegameMetadata
+ * @typedef {import("./savegame_typedefs").SavegameStats} SavegameStats
+ * @typedef {import("./savegame_typedefs").SerializedGame} SerializedGame
+ */
+
+export class Savegame extends ReadWriteProxy {
+ /**
+ *
+ * @param {Application} app
+ * @param {object} param0
+ * @param {string} param0.internalId
+ * @param {SavegameMetadata} param0.metaDataRef Handle to the meta data
+ */
+ constructor(app, { internalId, metaDataRef }) {
+ super(app, "savegame-" + internalId + ".bin");
+ this.internalId = internalId;
+ this.metaDataRef = metaDataRef;
+
+ /** @type {SavegameData} */
+ this.currentData = this.getDefaultData();
+
+ assert(
+ savegameInterfaces[Savegame.getCurrentVersion()],
+ "Savegame interface not defined: " + Savegame.getCurrentVersion()
+ );
+ }
+
+ //////// RW Proxy Impl //////////
+
+ /**
+ * @returns {number}
+ */
+ static getCurrentVersion() {
+ return 1006;
+ }
+
+ /**
+ * @returns {typeof BaseSavegameInterface}
+ */
+ static getReaderClass() {
+ return savegameInterfaces[Savegame.getCurrentVersion()];
+ }
+
+ /**
+ * @returns {number}
+ */
+ getCurrentVersion() {
+ return /** @type {typeof Savegame} */ (this.constructor).getCurrentVersion();
+ }
+
+ /**
+ * Returns the savegames default data
+ * @returns {SavegameData}
+ */
+ getDefaultData() {
+ return {
+ version: this.getCurrentVersion(),
+ dump: null,
+ stats: {},
+ lastUpdate: Date.now(),
+ };
+ }
+
+ /**
+ * Migrates the savegames data
+ * @param {SavegameData} data
+ */
+ migrate(data) {
+ if (data.version < 1000) {
+ return ExplainedResult.bad("Can not migrate savegame, too old");
+ }
+
+ if (data.version === 1000) {
+ SavegameInterface_V1001.migrate1000to1001(data);
+ data.version = 1001;
+ }
+
+ if (data.version === 1001) {
+ SavegameInterface_V1002.migrate1001to1002(data);
+ data.version = 1002;
+ }
+
+ if (data.version === 1002) {
+ SavegameInterface_V1003.migrate1002to1003(data);
+ data.version = 1003;
+ }
+
+ if (data.version === 1003) {
+ SavegameInterface_V1004.migrate1003to1004(data);
+ data.version = 1004;
+ }
+
+ if (data.version === 1004) {
+ SavegameInterface_V1005.migrate1004to1005(data);
+ data.version = 1005;
+ }
+
+ if (data.version === 1005) {
+ SavegameInterface_V1006.migrate1005to1006(data);
+ data.version = 1006;
+ }
+
+ return ExplainedResult.good();
+ }
+
+ /**
+ * Verifies the savegames data
+ * @param {SavegameData} data
+ */
+ verify(data) {
+ if (!data.dump) {
+ // Well, guess that works
+ return ExplainedResult.good();
+ }
+
+ if (!this.getDumpReaderForExternalData(data).validate()) {
+ return ExplainedResult.bad("dump-reader-failed-validation");
+ }
+ return ExplainedResult.good();
+ }
+
+ //////// Subclasses interface ////////
+
+ /**
+ * Returns if this game can be saved on disc
+ * @returns {boolean}
+ */
+ isSaveable() {
+ return true;
+ }
+ /**
+ * Returns the statistics of the savegame
+ * @returns {SavegameStats}
+ */
+ getStatistics() {
+ return this.currentData.stats;
+ }
+
+ /**
+ * Returns the *real* last update of the savegame, not the one of the metadata
+ * which could also be the servers one
+ */
+ getRealLastUpdate() {
+ return this.currentData.lastUpdate;
+ }
+
+ /**
+ * Returns if this game has a serialized game dump
+ */
+ hasGameDump() {
+ return !!this.currentData.dump && this.currentData.dump.entities.length > 0;
+ }
+
+ /**
+ * Returns the current game dump
+ * @returns {SerializedGame}
+ */
+ getCurrentDump() {
+ return this.currentData.dump;
+ }
+
+ /**
+ * Returns a reader to access the data
+ * @returns {BaseSavegameInterface}
+ */
+ getDumpReader() {
+ if (!this.currentData.dump) {
+ logger.warn("Getting reader on null-savegame dump");
+ }
+
+ const cls = /** @type {typeof Savegame} */ (this.constructor).getReaderClass();
+ return new cls(this.currentData);
+ }
+
+ /**
+ * Returns a reader to access external data
+ * @returns {BaseSavegameInterface}
+ */
+ getDumpReaderForExternalData(data) {
+ assert(data.version, "External data contains no version");
+ return getSavegameInterface(data);
+ }
+
+ ///////// Public Interface ///////////
+
+ /**
+ * Updates the last update field so we can send the savegame to the server,
+ * WITHOUT Saving!
+ */
+ setLastUpdate(time) {
+ this.currentData.lastUpdate = time;
+ }
+
+ /**
+ *
+ * @param {GameRoot} root
+ */
+ updateData(root) {
+ // Construct a new serializer
+ const serializer = new SavegameSerializer();
+
+ // let timer = performance.now();
+ const dump = serializer.generateDumpFromGameRoot(root);
+ if (!dump) {
+ return false;
+ }
+
+ const shadowData = Object.assign({}, this.currentData);
+ shadowData.dump = dump;
+ shadowData.lastUpdate = new Date().getTime();
+ shadowData.version = this.getCurrentVersion();
+
+ const reader = this.getDumpReaderForExternalData(shadowData);
+
+ // Validate (not in prod though)
+ if (!G_IS_RELEASE) {
+ const validationResult = reader.validate();
+ if (!validationResult) {
+ return false;
+ }
+ }
+
+ // Save data
+ this.currentData = shadowData;
+ }
+
+ /**
+ * Writes the savegame as well as its metadata
+ */
+ writeSavegameAndMetadata() {
+ return this.writeAsync().then(() => this.saveMetadata());
+ }
+
+ /**
+ * Updates the savegames metadata
+ */
+ saveMetadata() {
+ this.metaDataRef.lastUpdate = new Date().getTime();
+ this.metaDataRef.version = this.getCurrentVersion();
+ if (!this.hasGameDump()) {
+ this.metaDataRef.level = 0;
+ } else {
+ this.metaDataRef.level = this.currentData.dump.hubGoals.level;
+ }
+
+ return this.app.savegameMgr.writeAsync();
+ }
+
+ /**
+ * @see ReadWriteProxy.writeAsync
+ * @returns {Promise}
+ */
+ writeAsync() {
+ if (G_IS_DEV && globalConfig.debug.disableSavegameWrite) {
+ return Promise.resolve();
+ }
+ return super.writeAsync();
+ }
+}
diff --git a/src/js/savegame/savegame_interface_registry.js b/src/js/savegame/savegame_interface_registry.js
index fb1df52f..07b5353c 100644
--- a/src/js/savegame/savegame_interface_registry.js
+++ b/src/js/savegame/savegame_interface_registry.js
@@ -1,45 +1,47 @@
-import { BaseSavegameInterface } from "./savegame_interface";
-import { SavegameInterface_V1000 } from "./schemas/1000";
-import { createLogger } from "../core/logging";
-import { SavegameInterface_V1001 } from "./schemas/1001";
-import { SavegameInterface_V1002 } from "./schemas/1002";
-import { SavegameInterface_V1003 } from "./schemas/1003";
-import { SavegameInterface_V1004 } from "./schemas/1004";
-import { SavegameInterface_V1005 } from "./schemas/1005";
-
-/** @type {Object.} */
-export const savegameInterfaces = {
- 1000: SavegameInterface_V1000,
- 1001: SavegameInterface_V1001,
- 1002: SavegameInterface_V1002,
- 1003: SavegameInterface_V1003,
- 1004: SavegameInterface_V1004,
- 1005: SavegameInterface_V1005,
-};
-
-const logger = createLogger("savegame_interface_registry");
-
-/**
- * Returns if the given savegame has any supported interface
- * @param {any} savegame
- * @returns {BaseSavegameInterface|null}
- */
-export function getSavegameInterface(savegame) {
- if (!savegame || !savegame.version) {
- logger.warn("Savegame does not contain a valid version (undefined)");
- return null;
- }
- const version = savegame.version;
- if (!Number.isInteger(version)) {
- logger.warn("Savegame does not contain a valid version (non-integer):", version);
- return null;
- }
-
- const interfaceClass = savegameInterfaces[version];
- if (!interfaceClass) {
- logger.warn("Version", version, "has no implemented interface!");
- return null;
- }
-
- return new interfaceClass(savegame);
-}
+import { BaseSavegameInterface } from "./savegame_interface";
+import { SavegameInterface_V1000 } from "./schemas/1000";
+import { createLogger } from "../core/logging";
+import { SavegameInterface_V1001 } from "./schemas/1001";
+import { SavegameInterface_V1002 } from "./schemas/1002";
+import { SavegameInterface_V1003 } from "./schemas/1003";
+import { SavegameInterface_V1004 } from "./schemas/1004";
+import { SavegameInterface_V1005 } from "./schemas/1005";
+import { SavegameInterface_V1006 } from "./schemas/1006";
+
+/** @type {Object.} */
+export const savegameInterfaces = {
+ 1000: SavegameInterface_V1000,
+ 1001: SavegameInterface_V1001,
+ 1002: SavegameInterface_V1002,
+ 1003: SavegameInterface_V1003,
+ 1004: SavegameInterface_V1004,
+ 1005: SavegameInterface_V1005,
+ 1006: SavegameInterface_V1006,
+};
+
+const logger = createLogger("savegame_interface_registry");
+
+/**
+ * Returns if the given savegame has any supported interface
+ * @param {any} savegame
+ * @returns {BaseSavegameInterface|null}
+ */
+export function getSavegameInterface(savegame) {
+ if (!savegame || !savegame.version) {
+ logger.warn("Savegame does not contain a valid version (undefined)");
+ return null;
+ }
+ const version = savegame.version;
+ if (!Number.isInteger(version)) {
+ logger.warn("Savegame does not contain a valid version (non-integer):", version);
+ return null;
+ }
+
+ const interfaceClass = savegameInterfaces[version];
+ if (!interfaceClass) {
+ logger.warn("Version", version, "has no implemented interface!");
+ return null;
+ }
+
+ return new interfaceClass(savegame);
+}
diff --git a/src/js/savegame/schemas/1006.js b/src/js/savegame/schemas/1006.js
new file mode 100644
index 00000000..90504754
--- /dev/null
+++ b/src/js/savegame/schemas/1006.js
@@ -0,0 +1,265 @@
+import { gMetaBuildingRegistry } from "../../core/global_registries.js";
+import { createLogger } from "../../core/logging.js";
+import { MetaBeltBuilding } from "../../game/buildings/belt.js";
+import { enumCutterVariants, MetaCutterBuilding } from "../../game/buildings/cutter.js";
+import { MetaHubBuilding } from "../../game/buildings/hub.js";
+import { enumMinerVariants, MetaMinerBuilding } from "../../game/buildings/miner.js";
+import { MetaMixerBuilding } from "../../game/buildings/mixer.js";
+import { enumPainterVariants, MetaPainterBuilding } from "../../game/buildings/painter.js";
+import { enumRotaterVariants, MetaRotaterBuilding } from "../../game/buildings/rotater.js";
+import { enumSplitterVariants, MetaSplitterBuilding } from "../../game/buildings/splitter.js";
+import { MetaStackerBuilding } from "../../game/buildings/stacker.js";
+import { enumTrashVariants, MetaTrashBuilding } from "../../game/buildings/trash.js";
+import {
+ enumUndergroundBeltVariants,
+ MetaUndergroundBeltBuilding,
+} from "../../game/buildings/underground_belt.js";
+import { getCodeFromBuildingData } from "../../game/building_codes.js";
+import { StaticMapEntityComponent } from "../../game/components/static_map_entity.js";
+import { Entity } from "../../game/entity.js";
+import { defaultBuildingVariant, MetaBuilding } from "../../game/meta_building.js";
+import { SavegameInterface_V1005 } from "./1005.js";
+
+const schema = require("./1006.json");
+const logger = createLogger("savegame_interface/1006");
+
+/**
+ *
+ * @param {typeof MetaBuilding} metaBuilding
+ * @param {string=} variant
+ * @param {number=} rotationVariant
+ */
+function findCode(metaBuilding, variant = defaultBuildingVariant, rotationVariant = 0) {
+ return getCodeFromBuildingData(gMetaBuildingRegistry.findByClass(metaBuilding), variant, rotationVariant);
+}
+
+export class SavegameInterface_V1006 extends SavegameInterface_V1005 {
+ getVersion() {
+ return 1006;
+ }
+
+ getSchemaUncached() {
+ return schema;
+ }
+
+ static computeSpriteMapping() {
+ return {
+ // Belt
+ "sprites/blueprints/belt_top.png": findCode(MetaBeltBuilding, defaultBuildingVariant, 0),
+ "sprites/blueprints/belt_left.png": findCode(MetaBeltBuilding, defaultBuildingVariant, 1),
+ "sprites/blueprints/belt_right.png": findCode(MetaBeltBuilding, defaultBuildingVariant, 2),
+
+ // Splitter
+ "sprites/blueprints/splitter.png": findCode(MetaSplitterBuilding),
+ "sprites/blueprints/splitter-compact.png": findCode(
+ MetaSplitterBuilding,
+ enumSplitterVariants.compact
+ ),
+ "sprites/blueprints/splitter-compact-inverse.png": findCode(
+ MetaSplitterBuilding,
+ enumSplitterVariants.compactInverse
+ ),
+
+ // Underground belt
+ "sprites/blueprints/underground_belt_entry.png": findCode(
+ MetaUndergroundBeltBuilding,
+ defaultBuildingVariant,
+ 0
+ ),
+ "sprites/blueprints/underground_belt_exit.png": findCode(
+ MetaUndergroundBeltBuilding,
+ defaultBuildingVariant,
+ 1
+ ),
+
+ "sprites/blueprints/underground_belt_entry-tier2.png": findCode(
+ MetaUndergroundBeltBuilding,
+ enumUndergroundBeltVariants.tier2,
+ 0
+ ),
+ "sprites/blueprints/underground_belt_exit-tier2.png": findCode(
+ MetaUndergroundBeltBuilding,
+ enumUndergroundBeltVariants.tier2,
+ 1
+ ),
+
+ // Miner
+ "sprites/blueprints/miner.png": findCode(MetaMinerBuilding),
+ "sprites/blueprints/miner-chainable.png": findCode(
+ MetaMinerBuilding,
+ enumMinerVariants.chainable,
+ 0
+ ),
+
+ // Cutter
+ "sprites/blueprints/cutter.png": findCode(MetaCutterBuilding),
+ "sprites/blueprints/cutter-quad.png": findCode(MetaCutterBuilding, enumCutterVariants.quad),
+
+ // Rotater
+ "sprites/blueprints/rotater.png": findCode(MetaRotaterBuilding),
+ "sprites/blueprints/rotater-ccw.png": findCode(MetaRotaterBuilding, enumRotaterVariants.ccw),
+
+ // Stacker
+ "sprites/blueprints/stacker.png": findCode(MetaStackerBuilding),
+
+ // Mixer
+ "sprites/blueprints/mixer.png": findCode(MetaMixerBuilding),
+
+ // Painter
+ "sprites/blueprints/painter.png": findCode(MetaPainterBuilding),
+ "sprites/blueprints/painter-mirrored.png": findCode(
+ MetaPainterBuilding,
+ enumPainterVariants.mirrored
+ ),
+ "sprites/blueprints/painter-double.png": findCode(
+ MetaPainterBuilding,
+ enumPainterVariants.double
+ ),
+ "sprites/blueprints/painter-quad.png": findCode(MetaPainterBuilding, enumPainterVariants.quad),
+
+ // Trash / Storage
+ "sprites/blueprints/trash.png": findCode(MetaTrashBuilding),
+ "sprites/blueprints/trash-storage.png": findCode(MetaTrashBuilding, enumTrashVariants.storage),
+ };
+ }
+
+ /**
+ * @param {import("../savegame_typedefs.js").SavegameData} data
+ */
+ static migrate1005to1006(data) {
+ logger.log("Migrating 1005 to 1006");
+ const dump = data.dump;
+ if (!dump) {
+ return true;
+ }
+
+ // Update entities
+ const entities = dump.entities;
+ for (let i = 0; i < entities.length; ++i) {
+ const entity = entities[i];
+ const components = entity.components;
+ this.migrateStaticComp1005to1006(entity);
+
+ // HUB
+ if (components.Hub) {
+ // @ts-ignore
+ components.Hub = {};
+ }
+
+ // Item Processor
+ if (components.ItemProcessor) {
+ // @ts-ignore
+ components.ItemProcessor = {
+ nextOutputSlot: 0,
+ };
+ }
+
+ // OLD: Unremovable component
+ // @ts-ignore
+ if (components.Unremovable) {
+ // @ts-ignore
+ delete components.Unremovable;
+ }
+
+ // OLD: ReplaceableMapEntity
+ // @ts-ignore
+ if (components.ReplaceableMapEntity) {
+ // @ts-ignore
+ delete components.ReplaceableMapEntity;
+ }
+
+ // ItemAcceptor
+ if (components.ItemAcceptor) {
+ // @ts-ignore
+ components.ItemAcceptor = {};
+ }
+
+ // Belt
+ if (components.Belt) {
+ // @ts-ignore
+ components.Belt = {};
+ }
+
+ // Item Ejector
+ if (components.ItemEjector) {
+ // @ts-ignore
+ components.ItemEjector = {
+ slots: [],
+ };
+ }
+
+ // UndergroundBelt
+ if (components.UndergroundBelt) {
+ // @ts-ignore
+ components.UndergroundBelt = {
+ pendingItems: [],
+ };
+ }
+
+ // Miner
+ if (components.Miner) {
+ // @ts-ignore
+ delete components.Miner.chainable;
+
+ components.Miner.lastMiningTime = 0;
+ components.Miner.itemChainBuffer = [];
+ }
+
+ // Storage
+ if (components.Storage) {
+ // @ts-ignore
+ components.Storage = {
+ storedCount: 0,
+ storedItem: null,
+ };
+ }
+ }
+ }
+
+ /**
+ *
+ * @param {Entity} entity
+ */
+ static migrateStaticComp1005to1006(entity) {
+ const spriteMapping = this.computeSpriteMapping();
+ const staticComp = entity.components.StaticMapEntity;
+
+ /** @type {StaticMapEntityComponent} */
+ const newStaticComp = {};
+ newStaticComp.origin = staticComp.origin;
+ newStaticComp.originalRotation = staticComp.originalRotation;
+ newStaticComp.rotation = staticComp.rotation;
+
+ // @ts-ignore
+ newStaticComp.code = spriteMapping[staticComp.blueprintSpriteKey];
+
+ // Hub special case
+ if (entity.components.Hub) {
+ newStaticComp.code = findCode(MetaHubBuilding);
+ }
+
+ // Belt special case
+ if (entity.components.Belt) {
+ const actualCode = {
+ top: findCode(MetaBeltBuilding, defaultBuildingVariant, 0),
+ left: findCode(MetaBeltBuilding, defaultBuildingVariant, 1),
+ right: findCode(MetaBeltBuilding, defaultBuildingVariant, 2),
+ }[entity.components.Belt.direction];
+ if (actualCode !== newStaticComp.code) {
+ if (G_IS_DEV) {
+ console.warn("Belt mismatch");
+ }
+ newStaticComp.code = actualCode;
+ }
+ }
+
+ if (!newStaticComp.code) {
+ throw new Error(
+ // @ts-ignore
+ "1006 Migration: Could not reconstruct code for " + staticComp.blueprintSpriteKey
+ );
+ }
+
+ entity.components.StaticMapEntity = newStaticComp;
+ }
+}
diff --git a/src/js/savegame/schemas/1006.json b/src/js/savegame/schemas/1006.json
new file mode 100644
index 00000000..b0916986
--- /dev/null
+++ b/src/js/savegame/schemas/1006.json
@@ -0,0 +1,5 @@
+{
+ "type": "object",
+ "required": [],
+ "additionalProperties": true
+}
diff --git a/src/js/savegame/serializer_internal.js b/src/js/savegame/serializer_internal.js
index 6e5dfbc2..fa02a437 100644
--- a/src/js/savegame/serializer_internal.js
+++ b/src/js/savegame/serializer_internal.js
@@ -1,3 +1,4 @@
+import { globalConfig } from "../core/config";
import { createLogger } from "../core/logging";
import { Vector } from "../core/vector";
import { getBuildingDataFromCode } from "../game/building_codes";
@@ -78,7 +79,9 @@ export class SerializerInternal {
deserializeComponents(root, entity, data) {
for (const componentId in data) {
if (!entity.components[componentId]) {
- logger.warn("Entity no longer has component:", componentId);
+ if (G_IS_DEV && !globalConfig.debug.disableSlowAsserts) {
+ logger.warn("Entity no longer has component:", componentId);
+ }
continue;
}