diff --git a/src/css/ingame_hud/puzzle_play_metadata.scss b/src/css/ingame_hud/puzzle_play_metadata.scss
index 4cefcb52..152b8871 100644
--- a/src/css/ingame_hud/puzzle_play_metadata.scss
+++ b/src/css/ingame_hud/puzzle_play_metadata.scss
@@ -1,4 +1,4 @@
-#ingame_HUD_PuzzleEditorMetadata {
+#ingame_HUD_PuzzlePlayMetadata {
position: absolute;
@include S(top, 70px);
@@ -17,3 +17,22 @@
}
}
}
+
+#ingame_HUD_PuzzlePlayTitle {
+ position: absolute;
+
+ @include S(top, 18px);
+ left: 50%;
+ transform: translateX(-50%);
+ text-transform: uppercase;
+ @include Heading;
+ text-align: center;
+
+ display: flex;
+ flex-direction: column;
+
+ > .name {
+ @include PlainText;
+ opacity: 0.5;
+ }
+}
diff --git a/src/css/main.scss b/src/css/main.scss
index 1c70123c..e0bb389c 100644
--- a/src/css/main.scss
+++ b/src/css/main.scss
@@ -84,7 +84,8 @@ ingame_HUD_PuzzleEditorReview,
ingame_HUD_PuzzleEditorControls,
ingame_HUD_PuzzleEditorTitle,
ingame_HUD_PuzzleEditorSettings,
-ingame_HUD_PuzzlePlayMetadata
+ingame_HUD_PuzzlePlayMetadata,
+ingame_HUD_PuzzlePlayTitle,
ingame_HUD_Notifications,
ingame_HUD_DebugInfo,
ingame_HUD_EntityDebugger,
diff --git a/src/js/game/buildings/constant_producer.js b/src/js/game/buildings/constant_producer.js
index 2725402a..1b08eeb7 100644
--- a/src/js/game/buildings/constant_producer.js
+++ b/src/js/game/buildings/constant_producer.js
@@ -17,6 +17,15 @@ export class MetaConstantProducerBuilding extends MetaBuilding {
return "#bfd630";
}
+ /**
+ *
+ * @param {import("../../savegame/savegame_serializer").GameRoot} root
+ * @returns
+ */
+ getIsRemovable(root) {
+ return root.gameMode.getIsEditor();
+ }
+
/**
* Creates the entity at the given location
* @param {Entity} entity
diff --git a/src/js/game/buildings/goal_acceptor.js b/src/js/game/buildings/goal_acceptor.js
index 2f9df765..d3b79c97 100644
--- a/src/js/game/buildings/goal_acceptor.js
+++ b/src/js/game/buildings/goal_acceptor.js
@@ -19,6 +19,15 @@ export class MetaGoalAcceptorBuilding extends MetaBuilding {
return "#ce418a";
}
+ /**
+ *
+ * @param {import("../../savegame/savegame_serializer").GameRoot} root
+ * @returns
+ */
+ getIsRemovable(root) {
+ return root.gameMode.getIsEditor();
+ }
+
/**
* Creates the entity at the given location
* @param {Entity} entity
diff --git a/src/js/game/hud/parts/puzzle_play_metadata.js b/src/js/game/hud/parts/puzzle_play_metadata.js
index a2a82648..15c4c20a 100644
--- a/src/js/game/hud/parts/puzzle_play_metadata.js
+++ b/src/js/game/hud/parts/puzzle_play_metadata.js
@@ -1,14 +1,21 @@
import { makeDiv } from "../../../core/utils";
-import { T } from "../../../translations";
import { BaseHUDPart } from "../base_hud_part";
export class HUDPuzzlePlayMetadata extends BaseHUDPart {
createElements(parent) {
- this.element = makeDiv(parent, "ingame_HUD_PuzzlePlayMetadata");
+ this.titleElement = makeDiv(parent, "ingame_HUD_PuzzlePlayTitle");
+ this.titleElement.innerText = "PUZZLE";
- this.titleElement = makeDiv(parent, "ingame_HUD_PuzzleEditorTitle");
- // this.titleElement.innerText = T.ingame.PuzzlePlayMetadata.title;
- this.titleElement.innerText = "tobspr's first puzzle";
+ this.puzzleNameElement = makeDiv(this.titleElement, null, ["name"]);
+ this.puzzleNameElement.innerText = "tobspr's first puzzle";
+
+ this.element = makeDiv(parent, "ingame_HUD_PuzzlePlayMetadata");
+ this.element.innerHTML = `
+
+
Author: tobspr
+ Plays: 12.000
+ Likes: 512
+ `;
}
initialize() {}
diff --git a/src/js/game/logic.js b/src/js/game/logic.js
index 7ec7b8ab..bdd98eca 100644
--- a/src/js/game/logic.js
+++ b/src/js/game/logic.js
@@ -167,7 +167,7 @@ export class GameLogic {
*/
canDeleteBuilding(building) {
const staticComp = building.components.StaticMapEntity;
- return staticComp.getMetaBuilding().getIsRemovable();
+ return staticComp.getMetaBuilding().getIsRemovable(this.root);
}
/**
diff --git a/src/js/game/meta_building.js b/src/js/game/meta_building.js
index 9deee272..f3df0b62 100644
--- a/src/js/game/meta_building.js
+++ b/src/js/game/meta_building.js
@@ -108,9 +108,10 @@ export class MetaBuilding {
/**
* Returns whether this building is removable
+ * @param {GameRoot} root
* @returns {boolean}
*/
- getIsRemovable() {
+ getIsRemovable(root) {
return true;
}
diff --git a/src/js/game/modes/puzzle_edit.js b/src/js/game/modes/puzzle_edit.js
index 04d4dad5..e3d2e40d 100644
--- a/src/js/game/modes/puzzle_edit.js
+++ b/src/js/game/modes/puzzle_edit.js
@@ -22,7 +22,6 @@ import { MetaTransistorBuilding } from "../buildings/transistor";
import { HUDPuzzleEditorControls } from "../hud/parts/puzzle_editor_controls";
import { HUDPuzzleEditorReview } from "../hud/parts/puzzle_editor_review";
import { HUDPuzzleEditorSettings } from "../hud/parts/puzzle_editor_settings";
-import { HUDPuzzleBackToMenu } from "../hud/parts/puzzle_back_to_menu";
export class PuzzleEditGameMode extends PuzzleGameMode {
static getId() {
diff --git a/src/js/game/modes/puzzle_play.js b/src/js/game/modes/puzzle_play.js
index b4109696..05825bef 100644
--- a/src/js/game/modes/puzzle_play.js
+++ b/src/js/game/modes/puzzle_play.js
@@ -22,6 +22,9 @@ import { MetaTransistorBuilding } from "../buildings/transistor";
import { MetaConstantProducerBuilding } from "../buildings/constant_producer";
import { MetaGoalAcceptorBuilding } from "../buildings/goal_acceptor";
import { HUDConstantSignalEdit } from "../hud/parts/constant_signal_edit";
+import { PuzzleSerializer } from "../../savegame/puzzle_serializer";
+import { T } from "../../translations";
+import { HUDPuzzlePlayMetadata } from "../hud/parts/puzzle_play_metadata";
export class PuzzlePlayGameMode extends PuzzleGameMode {
static getId() {
@@ -58,9 +61,31 @@ export class PuzzlePlayGameMode extends PuzzleGameMode {
MetaTransistorBuilding,
];
- this.additionalHudParts.constantSignalEdit = HUDConstantSignalEdit;
+ this.additionalHudParts.puzzlePlayMetadata = HUDPuzzlePlayMetadata;
+
+ root.signals.postLoadHook.add(this.loadPuzzle, this);
- console.log("playing puzzle:", puzzle);
this.puzzle = puzzle;
}
+
+ loadPuzzle() {
+ let errorText;
+
+ try {
+ errorText = new PuzzleSerializer().deserializePuzzle(this.root, this.puzzle.game);
+
+ this.zoneWidth = this.puzzle.game.bounds.w;
+ this.zoneHeight = this.puzzle.game.bounds.h;
+ } catch (ex) {
+ errorText = ex.message || ex;
+ }
+
+ if (errorText) {
+ const signals = this.root.hud.parts.dialogs.showWarning(
+ T.dialogs.puzzleLoadError.title,
+ T.dialogs.puzzleLoadError.desc + " " + errorText
+ );
+ signals.ok.add(() => this.root.gameState.moveToState("PuzzleMenuState"));
+ }
+ }
}
diff --git a/src/js/game/systems/goal_acceptor.js b/src/js/game/systems/goal_acceptor.js
index 36e48164..4972afb2 100644
--- a/src/js/game/systems/goal_acceptor.js
+++ b/src/js/game/systems/goal_acceptor.js
@@ -102,7 +102,7 @@ export class GoalAcceptorSystem extends GameSystemWithFilter {
parameters.context.lineWidth = 1;
parameters.context.strokeStyle = "#64666e";
- parameters.context.fillStyle = isValid ? "#8de255" : "#e2555f";
+ parameters.context.fillStyle = isValid ? "#8de255" : "#ff666a";
parameters.context.beginCircle(10, 11.8, 3);
parameters.context.fill();
parameters.context.stroke();
diff --git a/src/js/savegame/puzzle_serializer.js b/src/js/savegame/puzzle_serializer.js
index 0a3ceb9b..d94f1e9f 100644
--- a/src/js/savegame/puzzle_serializer.js
+++ b/src/js/savegame/puzzle_serializer.js
@@ -5,6 +5,19 @@ import { PuzzleGameMode } from "../game/modes/puzzle";
import { enumConstantSignalType } from "../game/components/constant_signal";
import { StaticMapEntityComponent } from "../game/components/static_map_entity";
import { ShapeItem } from "../game/items/shape_item";
+import { Vector } from "../core/vector";
+import { MetaConstantProducerBuilding } from "../game/buildings/constant_producer";
+import { defaultBuildingVariant } from "../game/meta_building";
+import { gMetaBuildingRegistry } from "../core/global_registries";
+import { MetaGoalAcceptorBuilding } from "../game/buildings/goal_acceptor";
+import { createLogger } from "../core/logging";
+import { BaseItem } from "../game/base_item";
+import trim from "trim";
+import { enumColors } from "../game/colors";
+import { COLOR_ITEM_SINGLETONS } from "../game/items/color_item";
+import { ShapeDefinition } from "../game/shape_definition";
+
+const logger = createLogger("puzzle-serializer");
export class PuzzleSerializer {
/**
@@ -26,10 +39,10 @@ export class PuzzleSerializer {
if (signalComp) {
assert(signalComp.type === enumConstantSignalType.wireless, "not a wireless signal");
- assert(signalComp.signal.getItemType() === "shape", "not a shape signal");
+ assert(["shape", "color"].includes(signalComp.signal.getItemType()), "not a shape signal");
buildings.push({
type: "emitter",
- item: /** @type {ShapeItem} */ (signalComp.signal).definition.getHash(),
+ item: signalComp.signal.getAsCopyableKey(),
pos: {
x: staticComp.origin.x,
y: staticComp.origin.y,
@@ -45,7 +58,7 @@ export class PuzzleSerializer {
assert(goalComp.item.getItemType() === "shape", "goal is not an item");
buildings.push({
type: "goal",
- item: /** @type {ShapeItem} */ (goalComp.item).definition.getHash(),
+ item: goalComp.item.getAsCopyableKey(),
pos: {
x: staticComp.origin.x,
y: staticComp.origin.y,
@@ -67,4 +80,91 @@ export class PuzzleSerializer {
},
};
}
+
+ /**
+ * Tries to parse a signal code
+ * @param {GameRoot} root
+ * @param {string} code
+ * @returns {BaseItem}
+ */
+ parseItemCode(root, code) {
+ if (!root || !root.shapeDefinitionMgr) {
+ // Stale reference
+ return null;
+ }
+
+ code = trim(code);
+ const codeLower = code.toLowerCase();
+
+ if (enumColors[codeLower]) {
+ return COLOR_ITEM_SINGLETONS[codeLower];
+ }
+
+ if (ShapeDefinition.isValidShortKey(code)) {
+ return root.shapeDefinitionMgr.getShapeItemFromShortKey(code);
+ }
+
+ return null;
+ }
+ /**
+ * @param {GameRoot} root
+ * @param {import("./savegame_typedefs").PuzzleGameData} puzzle
+ */
+ deserializePuzzle(root, puzzle) {
+ if (puzzle.version !== 1) {
+ return "invalid-version";
+ }
+
+ for (const building of puzzle.buildings) {
+ switch (building.type) {
+ case "emitter": {
+ const item = this.parseItemCode(root, building.item);
+ if (!item) {
+ return "bad-item:" + building.item;
+ }
+
+ const entity = root.logic.tryPlaceBuilding({
+ origin: new Vector(building.pos.x, building.pos.y),
+ building: gMetaBuildingRegistry.findByClass(MetaConstantProducerBuilding),
+ originalRotation: building.pos.r,
+ rotation: building.pos.r,
+ rotationVariant: 0,
+ variant: defaultBuildingVariant,
+ });
+ if (!entity) {
+ logger.warn("Failed to place emitter:", building);
+ return "failed-to-place-emitter";
+ }
+
+ entity.components.ConstantSignal.signal = item;
+ break;
+ }
+ case "goal": {
+ const item = this.parseItemCode(root, building.item);
+ if (!item) {
+ return "bad-item:" + building.item;
+ }
+ const entity = root.logic.tryPlaceBuilding({
+ origin: new Vector(building.pos.x, building.pos.y),
+ building: gMetaBuildingRegistry.findByClass(MetaGoalAcceptorBuilding),
+ originalRotation: building.pos.r,
+ rotation: building.pos.r,
+ rotationVariant: 0,
+ variant: defaultBuildingVariant,
+ });
+ if (!entity) {
+ logger.warn("Failed to place goal:", building);
+ return "failed-to-place-goal";
+ }
+
+ entity.components.GoalAcceptor.item = item;
+ break;
+ }
+ default: {
+ // @ts-ignore
+ return "invalid-building-type: " + building.type;
+ }
+ }
+ }
+ }
}
diff --git a/src/js/states/puzzle_menu.js b/src/js/states/puzzle_menu.js
index 7f5c6131..8f3fd5c6 100644
--- a/src/js/states/puzzle_menu.js
+++ b/src/js/states/puzzle_menu.js
@@ -204,20 +204,20 @@ export class PuzzleMenuState extends TextualGameState {
{
type: "emitter",
item: "CuCuCuCu",
- pos: { x: 0, y: 0, r: 180 },
+ pos: { x: -2, y: 2, r: 0 },
},
{
type: "emitter",
item: "red",
- pos: { x: 2, y: 0, r: 180 },
+ pos: { x: 1, y: 2, r: 0 },
},
{
type: "goal",
item: "CrCrCrCr",
- pos: { x: 0, y: 4, r: 0 },
+ pos: { x: 0, y: -3, r: 0 },
},
],
- bounds: { w: 10, h: 10 },
+ bounds: { w: 4, h: 6 },
};
const savegame = this.app.savegameMgr.createNewSavegame();
diff --git a/translations/base-en.yaml b/translations/base-en.yaml
index fe9444e7..0bdd2388 100644
--- a/translations/base-en.yaml
+++ b/translations/base-en.yaml
@@ -302,6 +302,11 @@ dialogs:
title: Resize not possible
desc: You can't make the zone any smaller, because then some buildings would be outside the zone.
+ puzzleLoadError:
+ title: Bad Puzzle
+ desc: >-
+ The puzzle failed to load:
+
ingame:
# This is shown in the top left corner and displays useful keybindings in
# every situation