mirror of
https://github.com/tobspr/shapez.io.git
synced 2026-03-02 03:39:21 +00:00
Reworked waypoints
This commit is contained in:
@@ -1,4 +1,12 @@
|
||||
export const CHANGELOG = [
|
||||
{
|
||||
version: "1.1.14",
|
||||
date: "unreleased",
|
||||
entries: [
|
||||
"There is now an indicator (compass) to the HUB for the HUB Marker!",
|
||||
"You can now include shape short keys in markers to render shape icons instead of text!",
|
||||
],
|
||||
},
|
||||
{
|
||||
version: "1.1.13",
|
||||
date: "15.06.2020",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { makeOffscreenBuffer } from "../../../core/buffer_utils";
|
||||
import { Math_max } from "../../../core/builtins";
|
||||
import { Math_max, Math_PI, Math_radians } from "../../../core/builtins";
|
||||
import { globalConfig, IS_DEMO } from "../../../core/config";
|
||||
import { DrawParameters } from "../../../core/draw_parameters";
|
||||
import { Loader } from "../../../core/loader";
|
||||
@@ -7,7 +7,7 @@ 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 { arrayDeleteValue, lerp, makeDiv, removeAllChildren, clamp } from "../../../core/utils";
|
||||
import { Vector } from "../../../core/vector";
|
||||
import { T } from "../../../translations";
|
||||
import { enumMouseButton } from "../../camera";
|
||||
@@ -15,16 +15,26 @@ 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";
|
||||
|
||||
/** @typedef {{
|
||||
* label: string,
|
||||
* label: string | null,
|
||||
* center: { x: number, y: number },
|
||||
* zoomLevel: number,
|
||||
* deletable: boolean
|
||||
* zoomLevel: number
|
||||
* }} Waypoint */
|
||||
|
||||
/**
|
||||
* Used when a shape icon is rendered instead
|
||||
*/
|
||||
const SHAPE_LABEL_PLACEHOLDER = " ";
|
||||
|
||||
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,
|
||||
@@ -42,17 +52,23 @@ export class HUDWaypoints extends BaseHUDPart {
|
||||
);
|
||||
}
|
||||
|
||||
this.waypointSprite = Loader.getSprite("sprites/misc/waypoint.png");
|
||||
|
||||
// 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<Waypoint>}} data
|
||||
*/
|
||||
deserialize(data) {
|
||||
if (!data || !data.waypoints || !Array.isArray(data.waypoints)) {
|
||||
return "Invalid waypoints data";
|
||||
@@ -61,21 +77,97 @@ export class HUDWaypoints extends BaseHUDPart {
|
||||
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<Waypoint>}
|
||||
*/
|
||||
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.requestCreateMarker, this);
|
||||
|
||||
/**
|
||||
* 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 = {};
|
||||
|
||||
// Initial render
|
||||
this.rerenderWaypointList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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"]);
|
||||
element.innerText = waypoint.label;
|
||||
|
||||
if (waypoint.deletable) {
|
||||
if (ShapeDefinition.isValidShortKey(label)) {
|
||||
const canvas = this.getWaypointCanvas(waypoint);
|
||||
element.appendChild(canvas);
|
||||
element.classList.add("shapeIcon");
|
||||
} else {
|
||||
element.innerText = label;
|
||||
}
|
||||
|
||||
if (this.isWaypointDeletable(waypoint)) {
|
||||
const deleteButton = makeDiv(element, null, ["deleteButton"]);
|
||||
this.trackClicks(deleteButton, () => this.deleteWaypoint(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,
|
||||
});
|
||||
@@ -83,6 +175,7 @@ export class HUDWaypoints extends BaseHUDPart {
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the camera to a given waypoint
|
||||
* @param {Waypoint} waypoint
|
||||
*/
|
||||
moveToWaypoint(waypoint) {
|
||||
@@ -91,6 +184,7 @@ export class HUDWaypoints extends BaseHUDPart {
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a waypoint from the list
|
||||
* @param {Waypoint} waypoint
|
||||
*/
|
||||
deleteWaypoint(waypoint) {
|
||||
@@ -98,86 +192,131 @@ export class HUDWaypoints extends BaseHUDPart {
|
||||
this.rerenderWaypointList();
|
||||
}
|
||||
|
||||
initialize() {
|
||||
/** @type {Array<Waypoint>}
|
||||
*/
|
||||
this.waypoints = [
|
||||
{
|
||||
label: T.ingame.waypoints.hub,
|
||||
center: { x: 0, y: 0 },
|
||||
zoomLevel: 3,
|
||||
deletable: false,
|
||||
},
|
||||
];
|
||||
|
||||
this.dummyBuffer = makeOffscreenBuffer(1, 1, {
|
||||
reusable: false,
|
||||
label: "waypoints-measure-canvas",
|
||||
})[1];
|
||||
|
||||
this.root.camera.downPreHandler.add(this.onMouseDown, this);
|
||||
|
||||
if (this.hintElement) {
|
||||
this.domAttach = new DynamicDomAttach(this.root, this.hintElement);
|
||||
/**
|
||||
* 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];
|
||||
}
|
||||
|
||||
this.root.keyMapper
|
||||
.getBinding(KEYMAPPINGS.navigation.createMarker)
|
||||
.add(this.requestCreateMarker, this);
|
||||
|
||||
this.currentMarkerOpacity = 1;
|
||||
this.rerenderWaypointList();
|
||||
assert(ShapeDefinition.isValidShortKey(key), "Invalid short key: " + key);
|
||||
const definition = ShapeDefinition.fromShortKey(key);
|
||||
const preRendered = definition.generateAsCanvas(48);
|
||||
return (this.cachedKeyToCanvas[key] = preRendered);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests to create a marker at the current camera position. If worldPos is set,
|
||||
* uses that position instead.
|
||||
* @param {Vector=} worldPos Override the world pos, otherwise it is the camera position
|
||||
*/
|
||||
requestCreateMarker(worldPos = null) {
|
||||
// Construct dialog with input field
|
||||
const markerNameInput = new FormElementInput({
|
||||
id: "markerName",
|
||||
label: null,
|
||||
placeholder: "",
|
||||
validator: val => val.length > 0 && val.length < 15,
|
||||
validator: val => val.length > 0 && (val.length < 15 || ShapeDefinition.isValidShortKey(val)),
|
||||
});
|
||||
|
||||
const dialog = new DialogWithForm({
|
||||
app: this.root.app,
|
||||
title: T.dialogs.createMarker.title,
|
||||
desc: T.dialogs.createMarker.desc,
|
||||
formElements: [markerNameInput],
|
||||
});
|
||||
|
||||
this.root.hud.parts.dialogs.internalShowDialog(dialog);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
this.waypoints.push({
|
||||
label: markerNameInput.getValue(),
|
||||
center: { x: center.x, y: center.y },
|
||||
zoomLevel: Math_max(this.root.camera.zoomLevel, globalConfig.mapChunkOverviewMinZoom + 0.05),
|
||||
deletable: true,
|
||||
});
|
||||
this.waypoints.sort((a, b) => a.label.padStart(20, "0").localeCompare(b.label.padStart(20, "0")));
|
||||
this.root.hud.signals.notification.dispatch(
|
||||
T.ingame.waypoints.creationSuccessNotification,
|
||||
enumNotificationType.success
|
||||
);
|
||||
this.rerenderWaypointList();
|
||||
// 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),
|
||||
});
|
||||
|
||||
// Sort waypoints by name
|
||||
this.waypoints.sort((a, b) => {
|
||||
if (!a.label) {
|
||||
return -1;
|
||||
}
|
||||
if (!b.label) {
|
||||
return 1;
|
||||
}
|
||||
return this.getWaypointLabel(a)
|
||||
.padEnd(20, "0")
|
||||
.localeCompare(this.getWaypointLabel(b).padEnd(20, "0"));
|
||||
});
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called every frame to update stuff
|
||||
*/
|
||||
update() {
|
||||
if (this.domAttach) {
|
||||
this.domAttach.update(this.root.camera.getIsMapOverlayActive());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the currently intersected waypoint on the map overview under
|
||||
* the cursor.
|
||||
*
|
||||
* @returns {Waypoint | null}
|
||||
*/
|
||||
findCurrentIntersectedWaypoint() {
|
||||
const mousePos = this.root.app.mousePosition;
|
||||
if (!mousePos) {
|
||||
@@ -197,10 +336,18 @@ export class HUDWaypoints extends BaseHUDPart {
|
||||
const screenPos = this.root.camera.worldToScreen(
|
||||
new Vector(waypoint.center.x, waypoint.center.y)
|
||||
);
|
||||
|
||||
let label = this.getWaypointLabel(waypoint);
|
||||
|
||||
// Special case for icons
|
||||
if (ShapeDefinition.isValidShortKey(label)) {
|
||||
label = SHAPE_LABEL_PLACEHOLDER;
|
||||
}
|
||||
|
||||
const intersectionRect = new Rectangle(
|
||||
screenPos.x - 7 * scale,
|
||||
screenPos.y - 12 * scale,
|
||||
15 * scale + this.dummyBuffer.measureText(waypoint.label).width,
|
||||
15 * scale + this.dummyBuffer.measureText(label).width,
|
||||
15 * scale
|
||||
);
|
||||
if (intersectionRect.containsPoint(mousePos.x, mousePos.y)) {
|
||||
@@ -210,7 +357,7 @@ export class HUDWaypoints extends BaseHUDPart {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Mouse-Down handler
|
||||
* @param {Vector} pos
|
||||
* @param {enumMouseButton} button
|
||||
*/
|
||||
@@ -221,7 +368,7 @@ export class HUDWaypoints extends BaseHUDPart {
|
||||
this.root.soundProxy.playUiClick();
|
||||
this.moveToWaypoint(waypoint);
|
||||
} else if (button === enumMouseButton.right) {
|
||||
if (waypoint.deletable) {
|
||||
if (this.isWaypointDeletable(waypoint)) {
|
||||
this.root.soundProxy.playUiClick();
|
||||
this.deleteWaypoint(waypoint);
|
||||
} else {
|
||||
@@ -243,50 +390,111 @@ export class HUDWaypoints extends BaseHUDPart {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Rerenders the compass
|
||||
*/
|
||||
rerenderWaypointsCompass() {
|
||||
const context = this.compassBuffer.context;
|
||||
const dims = 48;
|
||||
context.clearRect(0, 0, dims, dims);
|
||||
const indicatorSize = 30;
|
||||
|
||||
const cameraPos = this.root.camera.center;
|
||||
|
||||
const distanceToHub = cameraPos.length();
|
||||
const compassVisible = distanceToHub > (10 * globalConfig.tileSize) / this.root.camera.zoomLevel;
|
||||
const targetCompassAlpha = compassVisible ? 1 : 0;
|
||||
this.currentCompassOpacity = lerp(this.currentCompassOpacity, targetCompassAlpha, 0.08);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const iconOpacity = 1 - this.currentCompassOpacity;
|
||||
if (iconOpacity > 0.01) {
|
||||
// Draw icon
|
||||
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
|
||||
*/
|
||||
draw(parameters) {
|
||||
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;
|
||||
}
|
||||
|
||||
// Find waypoint below cursor
|
||||
const selected = this.findCurrentIntersectedWaypoint();
|
||||
|
||||
// Determine rendering scale
|
||||
const scale = (1 / this.root.camera.zoomLevel) * this.root.app.getEffectiveUiScale();
|
||||
|
||||
// Render all of 'em
|
||||
for (let i = 0; i < this.waypoints.length; ++i) {
|
||||
const waypoint = this.waypoints[i];
|
||||
|
||||
const pos = waypoint.center;
|
||||
|
||||
parameters.context.globalAlpha = this.currentMarkerOpacity * (selected === waypoint ? 1 : 0.7);
|
||||
|
||||
const yOffset = -5 * scale;
|
||||
const originalLabel = this.getWaypointLabel(waypoint);
|
||||
let renderLabel = originalLabel;
|
||||
let isShapeIcon = false;
|
||||
|
||||
if (ShapeDefinition.isValidShortKey(originalLabel)) {
|
||||
renderLabel = SHAPE_LABEL_PLACEHOLDER;
|
||||
isShapeIcon = true;
|
||||
}
|
||||
|
||||
// Render the background rectangle
|
||||
parameters.context.font = "bold " + 12 * scale + "px GameFont";
|
||||
|
||||
parameters.context.fillStyle = "rgba(255, 255, 255, 0.7)";
|
||||
parameters.context.fillRect(
|
||||
pos.x - 7 * scale,
|
||||
pos.y - 12 * scale,
|
||||
15 * scale + this.dummyBuffer.measureText(waypoint.label).width / this.root.camera.zoomLevel,
|
||||
15 * scale + this.dummyBuffer.measureText(renderLabel).width / this.root.camera.zoomLevel,
|
||||
15 * scale
|
||||
);
|
||||
|
||||
parameters.context.fillStyle = "#000";
|
||||
parameters.context.textAlign = "left";
|
||||
parameters.context.textBaseline = "middle";
|
||||
parameters.context.fillText(waypoint.label, pos.x + 6 * scale, pos.y + 0.5 * scale + yOffset);
|
||||
|
||||
parameters.context.textBaseline = "alphabetic";
|
||||
parameters.context.textAlign = "left";
|
||||
// Render the text
|
||||
if (isShapeIcon) {
|
||||
const canvas = this.getWaypointCanvas(waypoint);
|
||||
parameters.context.drawImage(
|
||||
canvas,
|
||||
pos.x + 6 * scale,
|
||||
pos.y - 11.5 * scale,
|
||||
14 * scale,
|
||||
14 * scale
|
||||
);
|
||||
} else {
|
||||
// Render the text
|
||||
parameters.context.fillStyle = "#000";
|
||||
parameters.context.textBaseline = "middle";
|
||||
parameters.context.fillText(renderLabel, pos.x + 6 * scale, pos.y + 0.5 * scale + yOffset);
|
||||
parameters.context.textBaseline = "alphabetic";
|
||||
}
|
||||
|
||||
// Render the small icon on the left
|
||||
this.waypointSprite.drawCentered(parameters.context, pos.x, pos.y + yOffset, 10 * scale);
|
||||
}
|
||||
|
||||
parameters.context.globalAlpha = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,12 @@ export function createSimpleShape(layers) {
|
||||
return layers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache which shapes are valid short keys and which not
|
||||
* @type {Map<string, boolean>}
|
||||
*/
|
||||
const SHORT_KEY_CACHE = new Map();
|
||||
|
||||
export class ShapeDefinition extends BasicSerializableObject {
|
||||
static getId() {
|
||||
return "ShapeDefinition";
|
||||
@@ -114,6 +120,8 @@ export class ShapeDefinition extends BasicSerializableObject {
|
||||
|
||||
/**
|
||||
* Generates the definition from the given short key
|
||||
* @param {string} key
|
||||
* @returns {ShapeDefinition}
|
||||
*/
|
||||
static fromShortKey(key) {
|
||||
const sourceLayers = key.split(":");
|
||||
@@ -147,6 +155,81 @@ export class ShapeDefinition extends BasicSerializableObject {
|
||||
return definition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given string is a valid short key
|
||||
* @param {string} key
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static isValidShortKey(key) {
|
||||
if (SHORT_KEY_CACHE.has(key)) {
|
||||
return SHORT_KEY_CACHE.get(key);
|
||||
}
|
||||
|
||||
const result = ShapeDefinition.isValidShortKeyInternal(key);
|
||||
SHORT_KEY_CACHE.set(key, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL
|
||||
* Checks if a given string is a valid short key
|
||||
* @param {string} key
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static isValidShortKeyInternal(key) {
|
||||
const sourceLayers = key.split(":");
|
||||
let layers = [];
|
||||
for (let i = 0; i < sourceLayers.length; ++i) {
|
||||
const text = sourceLayers[i];
|
||||
if (text.length !== 8) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @type {ShapeLayer} */
|
||||
const quads = [null, null, null, null];
|
||||
let anyFilled = false;
|
||||
for (let quad = 0; quad < 4; ++quad) {
|
||||
const shapeText = text[quad * 2 + 0];
|
||||
const colorText = text[quad * 2 + 1];
|
||||
const subShape = enumShortcodeToSubShape[shapeText];
|
||||
const color = enumShortcodeToColor[colorText];
|
||||
|
||||
// Valid shape
|
||||
if (subShape) {
|
||||
if (!color) {
|
||||
// Invalid color
|
||||
return false;
|
||||
}
|
||||
quads[quad] = {
|
||||
subShape,
|
||||
color,
|
||||
};
|
||||
anyFilled = true;
|
||||
} else if (shapeText === "-") {
|
||||
// Make sure color is empty then, too
|
||||
if (colorText !== "-") {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// Invalid shape key
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!anyFilled) {
|
||||
// Empty layer
|
||||
return false;
|
||||
}
|
||||
layers.push(quads);
|
||||
}
|
||||
|
||||
if (layers.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to clone the shape definition
|
||||
* @returns {Array<ShapeLayer>}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { getSavegameInterface, savegameInterfaces } from "./savegame_interface_r
|
||||
import { SavegameInterface_V1001 } from "./schemas/1001";
|
||||
import { SavegameInterface_V1002 } from "./schemas/1002";
|
||||
import { SavegameInterface_V1003 } from "./schemas/1003";
|
||||
import { SavegameInterface_V1004 } from "./schemas/1004";
|
||||
|
||||
const logger = createLogger("savegame");
|
||||
|
||||
@@ -44,7 +45,7 @@ export class Savegame extends ReadWriteProxy {
|
||||
* @returns {number}
|
||||
*/
|
||||
static getCurrentVersion() {
|
||||
return 1003;
|
||||
return 1004;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,6 +99,11 @@ export class Savegame extends ReadWriteProxy {
|
||||
data.version = 1003;
|
||||
}
|
||||
|
||||
if (data.version === 1003) {
|
||||
SavegameInterface_V1004.migrate1003to1004(data);
|
||||
data.version = 1004;
|
||||
}
|
||||
|
||||
return ExplainedResult.good();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ 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";
|
||||
|
||||
/** @type {Object.<number, typeof BaseSavegameInterface>} */
|
||||
export const savegameInterfaces = {
|
||||
@@ -11,6 +12,7 @@ export const savegameInterfaces = {
|
||||
1001: SavegameInterface_V1001,
|
||||
1002: SavegameInterface_V1002,
|
||||
1003: SavegameInterface_V1003,
|
||||
1004: SavegameInterface_V1004,
|
||||
};
|
||||
|
||||
const logger = createLogger("savegame_interface_registry");
|
||||
|
||||
36
src/js/savegame/schemas/1004.js
Normal file
36
src/js/savegame/schemas/1004.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { createLogger } from "../../core/logging.js";
|
||||
import { SavegameInterface_V1003 } from "./1003.js";
|
||||
|
||||
const schema = require("./1004.json");
|
||||
const logger = createLogger("savegame_interface/1004");
|
||||
|
||||
export class SavegameInterface_V1004 extends SavegameInterface_V1003 {
|
||||
getVersion() {
|
||||
return 1004;
|
||||
}
|
||||
|
||||
getSchemaUncached() {
|
||||
return schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("../savegame_typedefs.js").SavegameData} data
|
||||
*/
|
||||
static migrate1003to1004(data) {
|
||||
logger.log("Migrating 1003 to 1004");
|
||||
const dump = data.dump;
|
||||
if (!dump) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// The hub simply has an empty label
|
||||
const waypointData = dump.waypoints.waypoints;
|
||||
for (let i = 0; i < waypointData.length; ++i) {
|
||||
const waypoint = waypointData[i];
|
||||
if (!waypoint.deletable) {
|
||||
waypoint.label = null;
|
||||
}
|
||||
delete waypoint.deletable;
|
||||
}
|
||||
}
|
||||
}
|
||||
5
src/js/savegame/schemas/1004.json
Normal file
5
src/js/savegame/schemas/1004.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": [],
|
||||
"additionalProperties": true
|
||||
}
|
||||
Reference in New Issue
Block a user