From 31811f20b79faf4aa732cf8bc626cc09fb17f584 Mon Sep 17 00:00:00 2001 From: tobspr Date: Fri, 30 Apr 2021 21:09:23 +0200 Subject: [PATCH] Puzzle mode --- src/css/ingame_hud/puzzle_play_metadata.scss | 21 +++- src/css/main.scss | 3 +- src/js/game/buildings/constant_producer.js | 9 ++ src/js/game/buildings/goal_acceptor.js | 9 ++ src/js/game/hud/parts/puzzle_play_metadata.js | 17 ++- src/js/game/logic.js | 2 +- src/js/game/meta_building.js | 3 +- src/js/game/modes/puzzle_edit.js | 1 - src/js/game/modes/puzzle_play.js | 29 ++++- src/js/game/systems/goal_acceptor.js | 2 +- src/js/savegame/puzzle_serializer.js | 106 +++++++++++++++++- src/js/states/puzzle_menu.js | 8 +- translations/base-en.yaml | 5 + 13 files changed, 195 insertions(+), 20 deletions(-) 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