diff --git a/res/ui/icons/upload.png b/res/ui/icons/upload.png new file mode 100644 index 00000000..54e37749 Binary files /dev/null and b/res/ui/icons/upload.png differ diff --git a/src/css/ingame_hud/buildings_toolbar.scss b/src/css/ingame_hud/buildings_toolbar.scss index 3af5edf4..3510d47f 100644 --- a/src/css/ingame_hud/buildings_toolbar.scss +++ b/src/css/ingame_hud/buildings_toolbar.scss @@ -49,96 +49,100 @@ } .building { - display: flex; - @include S(width, 40px); - position: relative; - @include S(height, 40px); - .icon { - color: $accentColorDark; + &:not(.hidden) { display: flex; - flex-direction: column-reverse; + @include S(width, 40px); position: relative; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; - padding: 0; - margin: 0; - @include S(border-radius, $globalBorderRadius); - - background: center center / 70% no-repeat; - } - - &:not(.unlocked) { - @include S(width, 25px); + @include S(height, 40px); .icon { - opacity: 0.15; + color: $accentColorDark; + display: flex; + flex-direction: column-reverse; + position: relative; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + padding: 0; + margin: 0; + @include S(border-radius, $globalBorderRadius); + + background: center center / 70% no-repeat; } - &.editor { + + &:not(.unlocked) { + @include S(width, 25px); .icon { - pointer-events: all; - cursor: pointer; - &:hover { - background-color: rgba(22, 30, 68, 0.1); + opacity: 0.15; + } + &.editor { + .icon { + pointer-events: all; + cursor: pointer; + &:hover { + background-color: rgba(22, 30, 68, 0.1); + } + } + } + &:not(.editor) { + .icon { + background-image: uiResource("locked_building.png") !important; } } } - &:not(.editor) { + + &.unlocked { .icon { - background-image: uiResource("locked_building.png") !important; + pointer-events: all; + transition: all 50ms ease-in-out; + transition-property: background-color, transform; + cursor: pointer; + + &:hover { + background-color: rgba(30, 40, 90, 0.1); + } + + &.pressed { + transform: scale(0.9) !important; + } } - } - } + &.selected { + // transform: scale(1.05); + background-color: rgba(lighten($colorBlueBright, 9), 0.4); + @include S(border-radius, 2px); - &.unlocked { - .icon { - pointer-events: all; - transition: all 50ms ease-in-out; - transition-property: background-color, transform; - cursor: pointer; - - &:hover { - background-color: rgba(30, 40, 90, 0.1); + .keybinding { + color: #111; + } } - &.pressed { - transform: scale(0.9) !important; - } - } - &.selected { - // transform: scale(1.05); - background-color: rgba(lighten($colorBlueBright, 9), 0.4); - @include S(border-radius, 2px); + .puzzle-lock { + &.active { + & { + /* @load-async */ + background: uiResource("locked_building.png") center center / 90% no-repeat; + } - .keybinding { - color: #111; - } - } + display: grid; + grid-auto-flow: column; - .puzzle-lock { - & { - /* @load-async */ - background: uiResource("locked_building.png") center center / 90% no-repeat; - } + position: absolute; + @include S(top, -15px); + left: 50%; + transform: translateX(-50%) !important; + transition: all 0.12s ease-in-out; + transition-property: opacity, transform; - display: grid; - grid-auto-flow: column; + cursor: pointer; + pointer-events: all; - position: absolute; - @include S(top, -15px); - left: 50%; - transform: translateX(-50%) !important; - transition: all 0.12s ease-in-out; - transition-property: opacity, transform; + @include S(width, 12px); + @include S(height, 12px); - cursor: pointer; - pointer-events: all; - - @include S(width, 12px); - @include S(height, 12px); - - &:hover { - opacity: 0.5; + &:hover { + opacity: 0.5; + } + } } } } diff --git a/src/css/ingame_hud/puzzle_editor_settings.scss b/src/css/ingame_hud/puzzle_editor_settings.scss index 9d093c42..cdd7c30d 100644 --- a/src/css/ingame_hud/puzzle_editor_settings.scss +++ b/src/css/ingame_hud/puzzle_editor_settings.scss @@ -2,7 +2,7 @@ position: absolute; background: $ingameHudBg; @include S(padding, 10px); - @include S(bottom, 60px); + @include S(bottom, 70px); @include S(left, 10px); @include SuperSmallText; @@ -12,6 +12,17 @@ @include S(border-radius, $globalBorderRadius); > .section { + .disabled { + transition: opacity 0.12s ease-in-out; + opacity: 0.6; + button { + pointer-events: none; + } + } + :not(.disabled) { + transition: opacity 0.12s ease-in-out; + } + > label { text-transform: uppercase; } @@ -44,7 +55,7 @@ } } - > .buttons { + > .mainButtons { > .buttonBar { display: flex; align-items: center; @@ -57,14 +68,17 @@ } } } + } - > .buildingsButton { - display: grid; - align-items: center; - @include S(margin-top, 4px); - > button { - @include SuperSmallText; + > .testToggle { + display: grid; + align-items: center; + @include S(margin-top, 4px); + > button { + &.disabled { + pointer-events: none; } + @include SuperSmallText; } } } diff --git a/src/css/ingame_hud/puzzle_import_export.scss b/src/css/ingame_hud/puzzle_import_export.scss new file mode 100644 index 00000000..c24e4123 --- /dev/null +++ b/src/css/ingame_hud/puzzle_import_export.scss @@ -0,0 +1,44 @@ +#ingame_HUD_PuzzleImportExport { + position: absolute; + @include S(top, 35px); + left: 50%; + + transform: translateX(-50%); + backdrop-filter: blur(D(1px)); + padding: D(3px); + + > .button { + @include PlainText; + pointer-events: all; + cursor: pointer; + position: relative; + color: #333438; + transition: all 0.12s ease-in-out; + transition-property: opacity, transform; + text-transform: uppercase; + @include PlainText; + @include S(width, 20px); + @include S(height, 20px); + margin: 8px 5px; + + @include DarkThemeInvert; + + opacity: 1; + + &:hover { + opacity: 0.9 !important; + } + + &.pressed { + transform: scale(0.95) !important; + } + + &.import { + background: uiResource("icons/upload.png") center center / D(15px) no-repeat !important; + } + + &.export { + background: uiResource("icons/download.png") center center / D(15px) no-repeat !important; + } + } +} diff --git a/src/css/ingame_hud/puzzle_play_settings.scss b/src/css/ingame_hud/puzzle_play_settings.scss index b53d0829..44ee3165 100644 --- a/src/css/ingame_hud/puzzle_play_settings.scss +++ b/src/css/ingame_hud/puzzle_play_settings.scss @@ -13,7 +13,7 @@ > .section { display: grid; - @include S(grid-gap, 5px); + @include S(grid-gap, 7px); grid-auto-flow: row; > button { diff --git a/src/css/main.scss b/src/css/main.scss index 1ac4f537..58d3aec1 100644 --- a/src/css/main.scss +++ b/src/css/main.scss @@ -66,6 +66,7 @@ @import "ingame_hud/puzzle_play_metadata"; @import "ingame_hud/puzzle_complete_notification"; @import "ingame_hud/puzzle_next"; +@import "ingame_hud/puzzle_import_export"; // prettier-ignore $elements: @@ -86,6 +87,8 @@ ingame_HUD_KeybindingOverlay, ingame_HUD_PuzzleBackToMenu, ingame_HUD_PuzzleNextPuzzle, ingame_HUD_PuzzleEditorReview, +ingame_HUD_PuzzleImportExport, +ingame_HUD_PuzzleNextPuzzle, ingame_HUD_PuzzleEditorControls, ingame_HUD_PuzzleEditorTitle, ingame_HUD_PuzzleEditorSettings, @@ -138,6 +141,11 @@ body.uiHidden { #ingame_HUD_PuzzleBackToMenu, #ingame_HUD_PuzzleNextPuzzle, #ingame_HUD_PuzzleEditorReview, + #ingame_HUD_PuzzleEditorSettings, + #ingame_HUD_PuzzlePlaySettings, + #ingame_HUD_PuzzleEditorControls, + #ingame_HUD_PuzzleImportExport, + #ingame_HUD_PuzzlePlayMetadata, #ingame_HUD_Notifications, #ingame_HUD_TutorialHints, #ingame_HUD_Waypoints, diff --git a/src/css/states/puzzle_menu.scss b/src/css/states/puzzle_menu.scss index 44f5d7ce..791564cf 100644 --- a/src/css/states/puzzle_menu.scss +++ b/src/css/states/puzzle_menu.scss @@ -8,9 +8,13 @@ justify-self: start; } + button { + @include S(margin-right, 5px); + } + .createPuzzle { background-color: $colorGreenBright; - @include S(margin-left, 5px); + @include S(margin-right, 0); } } diff --git a/src/js/game/buildings/block.js b/src/js/game/buildings/block.js index d6499648..be234352 100644 --- a/src/js/game/buildings/block.js +++ b/src/js/game/buildings/block.js @@ -19,7 +19,8 @@ export class MetaBlockBuilding extends MetaBuilding { * @returns */ getIsRemovable(root) { - return root.gameMode.getIsEditor(); + const settings = root.hud.parts.puzzleEditorSettings; + return settings ? !settings.getIsTestMode() : false; } /** diff --git a/src/js/game/buildings/constant_producer.js b/src/js/game/buildings/constant_producer.js index c1c502d0..3cee377f 100644 --- a/src/js/game/buildings/constant_producer.js +++ b/src/js/game/buildings/constant_producer.js @@ -23,7 +23,8 @@ export class MetaConstantProducerBuilding extends MetaBuilding { * @returns */ getIsRemovable(root) { - return root.gameMode.getIsEditor(); + const settings = root.hud.parts.puzzleEditorSettings; + return settings ? !settings.getIsTestMode() : false; } /** diff --git a/src/js/game/buildings/goal_acceptor.js b/src/js/game/buildings/goal_acceptor.js index dde720e3..eef6ac45 100644 --- a/src/js/game/buildings/goal_acceptor.js +++ b/src/js/game/buildings/goal_acceptor.js @@ -23,7 +23,8 @@ export class MetaGoalAcceptorBuilding extends MetaBuilding { * @returns */ getIsRemovable(root) { - return root.gameMode.getIsEditor(); + const settings = root.hud.parts.puzzleEditorSettings; + return settings ? !settings.getIsTestMode() : false; } /** diff --git a/src/js/game/hud/parts/base_toolbar.js b/src/js/game/hud/parts/base_toolbar.js index 15faad66..28d205b0 100644 --- a/src/js/game/hud/parts/base_toolbar.js +++ b/src/js/game/hud/parts/base_toolbar.js @@ -33,6 +33,12 @@ export class HUDBaseToolbar extends BaseHUDPart { this.htmlElementId = htmlElementId; this.layer = layer; + this.requiredBuildings = [ + gMetaBuildingRegistry.findByClass(MetaConstantProducerBuilding), + gMetaBuildingRegistry.findByClass(MetaGoalAcceptorBuilding), + gMetaBuildingRegistry.findByClass(MetaBlockBuilding), + ]; + /** @type {Object. this.toggleBuildingLock(metaBuilding), { - clickSound: null, - }); + if (!this.inRequiredBuildings(metaBuilding)) { + const puzzleLock = makeDiv(itemContainer, null, ["puzzle-lock"]); + puzzleLock.classList.add("active"); + + this.trackClicks(puzzleLock, () => this.toggleBuildingLock(metaBuilding)); + } } this.buildingHandles[metaBuilding.id] = { @@ -149,13 +155,15 @@ export class HUDBaseToolbar extends BaseHUDPart { }); this.lastSelectedIndex = 0; actionMapper.getBinding(KEYMAPPINGS.placement.cycleBuildings).add(this.cycleBuildings, this); + + this.switchingTestMode = false; } /** * Updates the toolbar */ update() { - const visible = this.visibilityCondition(); + const visible = this.visibilityCondition() && !this.switchingTestMode; this.domAttach.update(visible); if (visible) { @@ -253,9 +261,12 @@ export class HUDBaseToolbar extends BaseHUDPart { const handle = this.buildingHandles[metaBuilding.getId()]; if (handle.puzzleLocked) { - handle.puzzleLocked = false; - handle.element.classList.toggle("unlocked", false); - this.root.soundProxy.playUiClick(); + const settings = this.root.hud.parts.puzzleEditorSettings; + if (settings && !settings.getIsTestMode()) { + handle.puzzleLocked = false; + handle.element.classList.toggle("unlocked", false); + this.root.soundProxy.playUiClick(); + } return; } @@ -274,9 +285,28 @@ export class HUDBaseToolbar extends BaseHUDPart { } /** - * @param {MetaBuilding} metaBuilding + * @param {boolean} testMode */ - toggleBuildingLock(metaBuilding) { + toggleTestMode(testMode) { + // toggle the puzzle lock buttons and the editor-only buildings + + this.element.querySelectorAll(".building > .puzzle-lock").forEach(element => { + element.classList.toggle("active", !testMode); + }); + + for (let i = 0; i < this.requiredBuildings.length; ++i) { + const metaBuilding = this.requiredBuildings[i]; + const handle = this.buildingHandles[metaBuilding.getId()]; + handle.puzzleLocked = testMode; + handle.element.classList.toggle("hidden", testMode); + } + } + + /** + * @param {MetaBuilding} metaBuilding + * @param {boolean | null} force + */ + toggleBuildingLock(metaBuilding, force = null) { if (!this.visibilityCondition()) { // Not active return; @@ -288,9 +318,12 @@ export class HUDBaseToolbar extends BaseHUDPart { } const handle = this.buildingHandles[metaBuilding.getId()]; - handle.puzzleLocked = !handle.puzzleLocked; + if (force != null) { + handle.puzzleLocked = force; + } else { + handle.puzzleLocked = !handle.puzzleLocked; + } handle.element.classList.toggle("unlocked", !handle.puzzleLocked); - this.root.soundProxy.playUiClick(); const entityManager = this.root.entityMgr; for (const entity of entityManager.getAllWithComponent(StaticMapEntityComponent)) { @@ -312,11 +345,6 @@ export class HUDBaseToolbar extends BaseHUDPart { * @param {MetaBuilding} metaBuilding */ inRequiredBuildings(metaBuilding) { - const requiredBuildings = [ - gMetaBuildingRegistry.findByClass(MetaConstantProducerBuilding), - gMetaBuildingRegistry.findByClass(MetaGoalAcceptorBuilding), - gMetaBuildingRegistry.findByClass(MetaBlockBuilding), - ]; - return requiredBuildings.includes(metaBuilding); + return this.requiredBuildings.includes(metaBuilding); } } diff --git a/src/js/game/hud/parts/blueprint_placer.js b/src/js/game/hud/parts/blueprint_placer.js index 4b2bafb2..96934585 100644 --- a/src/js/game/hud/parts/blueprint_placer.js +++ b/src/js/game/hud/parts/blueprint_placer.js @@ -44,6 +44,7 @@ export class HUDBlueprintPlacer extends BaseHUDPart { this.root.camera.movePreHandler.add(this.onMouseMove, this); this.root.hud.signals.selectedPlacementBuildingChanged.add(this.abortPlacement, this); + this.root.signals.testModeChanged.add(this.abortPlacement, this); this.root.signals.editModeChanged.add(this.onEditModeChanged, this); this.domAttach = new DynamicDomAttach(this.root, this.costDisplayParent); diff --git a/src/js/game/hud/parts/building_placer_logic.js b/src/js/game/hud/parts/building_placer_logic.js index 7ed412f6..4c8d533f 100644 --- a/src/js/game/hud/parts/building_placer_logic.js +++ b/src/js/game/hud/parts/building_placer_logic.js @@ -130,6 +130,7 @@ export class HUDBuildingPlacerLogic extends BaseHUDPart { this.root.signals.storyGoalCompleted.add(() => this.signals.variantChanged.dispatch()); this.root.signals.upgradePurchased.add(() => this.signals.variantChanged.dispatch()); this.root.signals.editModeChanged.add(this.onEditModeChanged, this); + this.root.signals.testModeChanged.add(this.abortPlacement, this); // MOUSE BINDINGS this.root.camera.downPreHandler.add(this.onMouseDown, this); @@ -384,8 +385,8 @@ export class HUDBuildingPlacerLogic extends BaseHUDPart { const buildingCode = contents.components.StaticMapEntity.code; const extracted = getBuildingDataFromCode(buildingCode); - // Disable pipetting the hub - if (extracted.metaInstance.getId() === gMetaBuildingRegistry.findByClass(MetaHubBuilding).getId()) { + // Disable pipetting a non removeable building + if (!extracted.metaInstance.getIsRemovable(this.root)) { this.currentMetaBuilding.set(null); return; } diff --git a/src/js/game/hud/parts/mass_selector.js b/src/js/game/hud/parts/mass_selector.js index b8283d55..67e68209 100644 --- a/src/js/game/hud/parts/mass_selector.js +++ b/src/js/game/hud/parts/mass_selector.js @@ -45,6 +45,7 @@ export class HUDMassSelector extends BaseHUDPart { this.root.hud.signals.selectedPlacementBuildingChanged.add(this.clearSelection, this); this.root.signals.editModeChanged.add(this.clearSelection, this); + this.root.signals.testModeChanged.add(this.clearSelection, this); } /** diff --git a/src/js/game/hud/parts/puzzle_editor_settings.js b/src/js/game/hud/parts/puzzle_editor_settings.js index bd738198..3869a7ab 100644 --- a/src/js/game/hud/parts/puzzle_editor_settings.js +++ b/src/js/game/hud/parts/puzzle_editor_settings.js @@ -1,6 +1,5 @@ import { globalConfig } from "../../../core/config"; import { gMetaBuildingRegistry } from "../../../core/global_registries"; -import { createLogger } from "../../../core/logging"; import { Rectangle } from "../../../core/rectangle"; import { makeDiv } from "../../../core/utils"; import { T } from "../../../translations"; @@ -8,11 +7,10 @@ import { MetaBlockBuilding } from "../../buildings/block"; import { MetaConstantProducerBuilding } from "../../buildings/constant_producer"; import { MetaGoalAcceptorBuilding } from "../../buildings/goal_acceptor"; import { StaticMapEntityComponent } from "../../components/static_map_entity"; +import { Entity } from "../../entity"; import { PuzzleGameMode } from "../../modes/puzzle"; import { BaseHUDPart } from "../base_hud_part"; -const logger = createLogger("puzzle-editor"); - export class HUDPuzzleEditorSettings extends BaseHUDPart { createElements(parent) { this.element = makeDiv(parent, "ingame_HUD_PuzzleEditorSettings"); @@ -27,7 +25,7 @@ export class HUDPuzzleEditorSettings extends BaseHUDPart { ` -
+
@@ -35,7 +33,7 @@ export class HUDPuzzleEditorSettings extends BaseHUDPart {
-
+
@@ -47,10 +45,10 @@ export class HUDPuzzleEditorSettings extends BaseHUDPart {
-
- -
+
+
+
` ); @@ -60,7 +58,12 @@ export class HUDPuzzleEditorSettings extends BaseHUDPart { bind(".zoneHeight .plus", () => this.modifyZone(0, 1)); bind("button.trim", this.trim); bind("button.clearItems", this.clearItems); - bind("button.resetPuzzle", this.resetPuzzle); + bind("button.testPuzzle", this.toggleTestMode); + + this.testMode = false; + + /** @type {Entity[]} */ + this.storedSolution = []; } } @@ -68,27 +71,67 @@ export class HUDPuzzleEditorSettings extends BaseHUDPart { this.root.logic.clearAllBeltsAndItems(); } - resetPuzzle() { - for (const entity of this.root.entityMgr.getAllWithComponent(StaticMapEntityComponent)) { - const staticComp = entity.components.StaticMapEntity; - const goalComp = entity.components.GoalAcceptor; + toggleTestMode() { + this.testMode = !this.testMode; - if (goalComp) { - goalComp.clear(); + this.element.querySelector(".section > label").classList.toggle("disabled", this.testMode); + this.element.querySelector(".mainButtons").classList.toggle("disabled", this.testMode); + const testButton = this.element.querySelector(".testToggle > .testPuzzle"); + testButton.textContent = this.testMode + ? T.ingame.puzzleEditorSettings.disableTestMode + : T.ingame.puzzleEditorSettings.enableTestMode; + + testButton.classList.toggle("disabled", true); + + const buildingsToolbar = this.root.hud.parts.buildingsToolbar; + buildingsToolbar.switchingTestMode = true; + this.root.signals.testModeChanged.dispatch(this.testMode); + + setTimeout(() => { + buildingsToolbar.switchingTestMode = false; + buildingsToolbar.toggleTestMode(this.testMode); + + testButton.classList.toggle("disabled", false); + }, 140); + + if (this.testMode) { + const newSolution = []; + for (const entity of this.root.entityMgr.getAllWithComponent(StaticMapEntityComponent)) { + if (this.isExcludedEntity(entity)) { + continue; + } + + newSolution.push(entity.clone()); + + this.root.map.removeStaticEntity(entity); + this.root.entityMgr.destroyEntity(entity); } - if ( - [MetaGoalAcceptorBuilding, MetaConstantProducerBuilding, MetaBlockBuilding] - .map(metaClass => gMetaBuildingRegistry.findByClass(metaClass).id) - .includes(staticComp.getMetaBuilding().id) - ) { - continue; - } + this.root.entityMgr.processDestroyList(); + this.storedSolution = newSolution; + } else if (this.storedSolution.length) { + this.root.logic.performBulkOperation(() => { + for (const entity of this.root.entityMgr.getAllWithComponent(StaticMapEntityComponent)) { + if (this.isExcludedEntity(entity)) continue; - this.root.map.removeStaticEntity(entity); - this.root.entityMgr.destroyEntity(entity); + this.root.map.removeStaticEntity(entity); + this.root.entityMgr.destroyEntity(entity); + } + this.root.entityMgr.processDestroyList(); + + for (let i = 0; i < this.storedSolution.length; ++i) { + const entity = this.storedSolution[i]; + const placedEntity = this.root.logic.tryPlaceEntity(entity); + + for (const key in entity.components) { + /** @type {import("../../../core/global_registries").Component} */ (entity.components[ + key + ]).copyAdditionalStateTo(placedEntity.components[key]); + } + } + this.storedSolution = []; + }); } - this.root.entityMgr.processDestroyList(); } trim() { @@ -228,4 +271,21 @@ export class HUDPuzzleEditorSettings extends BaseHUDPart { this.element.querySelector(".zoneWidth > .value").textContent = String(mode.zoneWidth); this.element.querySelector(".zoneHeight > .value").textContent = String(mode.zoneHeight); } + + getIsTestMode() { + return this.testMode; + } + + isExcludedEntity(entity) { + const metaBuilding = entity.components.StaticMapEntity.getMetaBuilding(); + + if ( + [MetaConstantProducerBuilding, MetaBlockBuilding, MetaGoalAcceptorBuilding] + .map(metaClass => gMetaBuildingRegistry.findByClass(metaClass).id) + .includes(metaBuilding.id) + ) { + return true; + } + return false; + } } diff --git a/src/js/game/hud/parts/puzzle_import_export.js b/src/js/game/hud/parts/puzzle_import_export.js new file mode 100644 index 00000000..2656b4f6 --- /dev/null +++ b/src/js/game/hud/parts/puzzle_import_export.js @@ -0,0 +1,132 @@ +import { gMetaBuildingRegistry } from "../../../core/global_registries"; +import { ReadWriteProxy } from "../../../core/read_write_proxy"; +import { generateFileDownload, makeDiv, startFileChoose, waitNextFrame } from "../../../core/utils"; +import { PuzzleSerializer } from "../../../savegame/puzzle_serializer"; +import { T } from "../../../translations"; +import { GoalAcceptorComponent } from "../../components/goal_acceptor"; +import { StaticMapEntityComponent } from "../../components/static_map_entity"; +import { PuzzleGameMode } from "../../modes/puzzle"; +import { BaseHUDPart } from "../base_hud_part"; + +export class HUDPuzzleImportExport extends BaseHUDPart { + constructor(root) { + super(root); + } + + createElements(parent) { + this.element = makeDiv(parent, "ingame_HUD_PuzzleImportExport"); + this.importButton = document.createElement("button"); + this.importButton.classList.add("button", "import"); + this.element.appendChild(this.importButton); + + this.exportButton = document.createElement("button"); + this.exportButton.classList.add("button", "export"); + this.element.appendChild(this.exportButton); + + this.trackClicks(this.importButton, this.importPuzzle); + + this.trackClicks(this.exportButton, () => { + const { yes } = this.root.hud.parts.dialogs.showWarning( + T.dialogs.puzzleExport.title, + T.dialogs.puzzleExport.desc, + ["no", "yes:good:enter"] + ); + yes.add(() => this.exportPuzzle()); + }); + } + + initialize() {} + + importPuzzle() { + startFileChoose(".bin").then(file => { + if (file) { + const closeLoader = this.root.hud.parts.dialogs.showLoadingDialog("Importing Puzzle"); + waitNextFrame().then(() => { + const reader = new FileReader(); + reader.addEventListener("load", event => { + const fileContents = String(event.target.result); + + /** @type {import("../../../savegame/savegame_typedefs").PuzzleGameData} */ + let gameData; + + try { + gameData = ReadWriteProxy.deserializeObject(fileContents); + } catch (err) { + closeLoader(); + this.root.hud.parts.dialogs.showWarning(T.global.error, String(err)); + return; + } + + const mode = /** @type {PuzzleGameMode} */ (this.root.gameMode); + let errorText; + try { + // set excluded buildings first so if we get an error we haven't removed buildings yet + const toolbar = this.root.hud.parts.buildingsToolbar; + const handles = toolbar.buildingHandles; + const ids = gMetaBuildingRegistry.getAllIds(); + + for (let i = 0; i < ids.length; ++i) { + const handle = handles[ids[i]]; + if (handle && !toolbar.inRequiredBuildings(handle.metaBuilding)) { + const locked = gameData.excludedBuildings.includes(ids[i]); + + toolbar.toggleBuildingLock(handle.metaBuilding, locked); + } + } + + for (const entity of this.root.entityMgr.getAllWithComponent( + StaticMapEntityComponent + )) { + this.root.map.removeStaticEntity(entity); + this.root.entityMgr.destroyEntity(entity); + } + this.root.entityMgr.processDestroyList(); + + mode.zoneWidth = gameData.bounds.w; + mode.zoneHeight = gameData.bounds.h; + this.root.hud.parts.puzzleEditorSettings.updateZoneValues(); + + errorText = new PuzzleSerializer().deserializePuzzle(this.root, gameData); + } catch (ex) { + errorText = ex.message || ex; + } + + if (errorText) { + this.root.hud.parts.dialogs.showWarning( + T.dialogs.puzzleLoadError.title, + T.dialogs.puzzleLoadError.desc + " " + errorText + ); + } else { + this.root.hud.parts.dialogs.showInfo( + T.dialogs.puzzleImport.title, + T.dialogs.puzzleImport.desc + ); + } + closeLoader(); + }); + reader.readAsText(file); + }); + } + }); + } + + exportPuzzle() { + // Make sure all acceptors have an item + for (const entity of this.root.entityMgr.getAllWithComponent(GoalAcceptorComponent)) { + const goalComp = entity.components.GoalAcceptor; + if (!goalComp.item) { + this.root.hud.parts.dialogs.showWarning( + T.puzzleMenu.validation.title, + T.puzzleMenu.validation.goalAcceptorNoItem + ); + return; + } + } + + const serialized = new PuzzleSerializer().generateDumpFromGameRoot(this.root); + + const data = ReadWriteProxy.serializeObject(serialized); + const filename = "puzzle.bin"; + generateFileDownload(filename, data); + } +} diff --git a/src/js/game/logic.js b/src/js/game/logic.js index 79104958..74caa251 100644 --- a/src/js/game/logic.js +++ b/src/js/game/logic.js @@ -116,6 +116,15 @@ export class GameLogic { rotationVariant, variant, }); + return this.tryPlaceEntity(entity); + } + + /** + * Attempts to place the given entity + * @param {Entity} entity + * @returns {Entity} + */ + tryPlaceEntity(entity) { if (this.checkCanPlaceEntity(entity)) { this.freeEntityAreaBeforeBuild(entity); this.root.map.placeStaticEntity(entity); diff --git a/src/js/game/modes/puzzle_edit.js b/src/js/game/modes/puzzle_edit.js index e3d2e40d..4d0e66c0 100644 --- a/src/js/game/modes/puzzle_edit.js +++ b/src/js/game/modes/puzzle_edit.js @@ -22,6 +22,10 @@ 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 { createLogger } from "../../core/logging"; +import { HUDPuzzleImportExport } from "../hud/parts/puzzle_import_export"; + +const logger = createLogger("puzzle-edit"); export class PuzzleEditGameMode extends PuzzleGameMode { static getId() { @@ -32,7 +36,9 @@ export class PuzzleEditGameMode extends PuzzleGameMode { return {}; } - /** @param {GameRoot} root */ + /** + * @param {GameRoot} root + */ constructor(root) { super(root); @@ -58,6 +64,7 @@ export class PuzzleEditGameMode extends PuzzleGameMode { this.additionalHudParts.puzzleEditorControls = HUDPuzzleEditorControls; this.additionalHudParts.puzzleEditorReview = HUDPuzzleEditorReview; this.additionalHudParts.puzzleEditorSettings = HUDPuzzleEditorSettings; + this.additionalHudParts.puzzleEditorDownload = HUDPuzzleImportExport; } getIsEditor() { diff --git a/src/js/game/root.js b/src/js/game/root.js index 64004e9d..6ddda395 100644 --- a/src/js/game/root.js +++ b/src/js/game/root.js @@ -192,6 +192,7 @@ export class GameRoot { // Puzzle mode puzzleComplete: /** @type {TypedSignal<[]>} */ (new Signal()), + testModeChanged: /** @type {TypedSignal<[Boolean]>} */ (new Signal()), }; // RNG's diff --git a/src/js/game/systems/goal_acceptor.js b/src/js/game/systems/goal_acceptor.js index 60d4a984..959cff16 100644 --- a/src/js/game/systems/goal_acceptor.js +++ b/src/js/game/systems/goal_acceptor.js @@ -3,6 +3,7 @@ import { DrawParameters } from "../../core/draw_parameters"; import { clamp, lerp } from "../../core/utils"; import { Vector } from "../../core/vector"; import { GoalAcceptorComponent } from "../components/goal_acceptor"; +import { enumGameModeIds } from "../game_mode"; import { GameSystemWithFilter } from "../game_system_with_filter"; import { MapChunk } from "../map_chunk"; import { GameRoot } from "../root"; @@ -42,7 +43,7 @@ export class GoalAcceptorSystem extends GameSystemWithFilter { !this.puzzleCompleted && this.root.gameInitialized && allAccepted && - !this.root.gameMode.getIsEditor() + !(this.root.gameMode.getId() == enumGameModeIds.puzzleEdit) ) { this.root.signals.puzzleComplete.dispatch(); this.puzzleCompleted = true; diff --git a/src/js/game/systems/item_processor.js b/src/js/game/systems/item_processor.js index 525c242c..a4b864f9 100644 --- a/src/js/game/systems/item_processor.js +++ b/src/js/game/systems/item_processor.js @@ -589,7 +589,8 @@ export class ItemProcessorSystem extends GameSystemWithFilter { ); } - if (this.root.gameMode.getIsEditor()) { + const settings = this.root.hud.parts.puzzleEditorSettings; + if (settings && !settings.getIsTestMode()) { // while playing in editor, assign the item goalComp.item = item; } diff --git a/src/js/states/puzzle_menu.js b/src/js/states/puzzle_menu.js index 4677f6c5..a2d462a4 100644 --- a/src/js/states/puzzle_menu.js +++ b/src/js/states/puzzle_menu.js @@ -609,6 +609,7 @@ export class PuzzleMenuState extends TextualGameState { const savegame = Savegame.createPuzzleSavegame(this.app); this.moveToState("InGameState", { gameModeId: enumGameModeIds.puzzleEdit, + gameModeParameters: {}, savegame, }); } diff --git a/translations/base-en.yaml b/translations/base-en.yaml index 3f3b1412..7e076d8f 100644 --- a/translations/base-en.yaml +++ b/translations/base-en.yaml @@ -207,6 +207,8 @@ dialogs: retry: Retry continue: Continue playOffline: Play Offline + yes: Yes + no: No importSavegameError: title: Import Error @@ -418,6 +420,16 @@ dialogs: desc: >- Are you sure you want to delete ''? This can not be undone! + puzzleImport: + title: Puzzle Imported + desc: >- + Your puzzle has been successfully imported. + + puzzleExport: + title: Export Puzzle + desc: >- + Do you want to download this puzzle? + ingame: # This is shown in the top left corner and displays useful keybindings in # every situation @@ -652,6 +664,8 @@ ingame: clearItems: Clear Items clearBuildings: Clear Buildings resetPuzzle: Reset Puzzle + enableTestMode: Enable Test Mode + disableTestMode: Disable Test Mode share: Share report: Report