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/puzzle_editor_download.scss b/src/css/ingame_hud/puzzle_import_export.scss similarity index 70% rename from src/css/ingame_hud/puzzle_editor_download.scss rename to src/css/ingame_hud/puzzle_import_export.scss index 910247f1..2c951e10 100644 --- a/src/css/ingame_hud/puzzle_editor_download.scss +++ b/src/css/ingame_hud/puzzle_import_export.scss @@ -1,4 +1,4 @@ -#ingame_HUD_PuzzleEditorDownload { +#ingame_HUD_PuzzleImportExport { position: absolute; @include S(top, 35px); left: 50%; @@ -17,12 +17,14 @@ transition-property: opacity, transform; text-transform: uppercase; @include PlainText; - @include S(width, 30px); - @include S(height, 30px); + @include S(width, 20px); + @include S(height, 20px); + margin: 8px 5px; @include DarkThemeInvert; opacity: 1; + &:hover { opacity: 0.9 !important; } @@ -31,7 +33,13 @@ transform: scale(0.95) !important; } - & { + &.import { + /* @load-async */ + background: uiResource("icons/upload.png") center center / D(15px) no-repeat; + //transform: rotate(180deg); + } + + &.export { /* @load-async */ background: uiResource("icons/download.png") center center / D(15px) no-repeat; } diff --git a/src/css/main.scss b/src/css/main.scss index c611e905..8d4624fc 100644 --- a/src/css/main.scss +++ b/src/css/main.scss @@ -65,7 +65,7 @@ @import "ingame_hud/puzzle_play_settings"; @import "ingame_hud/puzzle_play_metadata"; @import "ingame_hud/puzzle_complete_notification"; -@import "ingame_hud/puzzle_editor_download"; +@import "ingame_hud/puzzle_import_export"; // prettier-ignore $elements: @@ -85,7 +85,7 @@ ingame_HUD_GameMenu, ingame_HUD_KeybindingOverlay, ingame_HUD_PuzzleBackToMenu, ingame_HUD_PuzzleEditorReview, -ingame_HUD_PuzzleEditorDownload, +ingame_HUD_PuzzleImportExport, ingame_HUD_PuzzleEditorControls, ingame_HUD_PuzzleEditorTitle, ingame_HUD_PuzzleEditorSettings, @@ -138,7 +138,7 @@ body.uiHidden { #ingame_HUD_PuzzleEditorSettings, #ingame_HUD_PuzzlePlaySettings, #ingame_HUD_PuzzleEditorControls, - #ingame_HUD_PuzzleEditorDownload, + #ingame_HUD_PuzzleImportExport, #ingame_HUD_PuzzlePlayMetadata, #ingame_HUD_Notifications, #ingame_HUD_TutorialHints, diff --git a/src/js/game/hud/parts/base_toolbar.js b/src/js/game/hud/parts/base_toolbar.js index ea06c834..186ca2de 100644 --- a/src/js/game/hud/parts/base_toolbar.js +++ b/src/js/game/hud/parts/base_toolbar.js @@ -302,8 +302,9 @@ export class HUDBaseToolbar extends BaseHUDPart { /** * @param {MetaBuilding} metaBuilding + * @param {boolean | null} force */ - toggleBuildingLock(metaBuilding) { + toggleBuildingLock(metaBuilding, force = null) { if (!this.visibilityCondition()) { // Not active return; @@ -315,7 +316,11 @@ 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); const entityManager = this.root.entityMgr; diff --git a/src/js/game/hud/parts/puzzle_editor_download.js b/src/js/game/hud/parts/puzzle_editor_download.js deleted file mode 100644 index 99f7fbdd..00000000 --- a/src/js/game/hud/parts/puzzle_editor_download.js +++ /dev/null @@ -1,37 +0,0 @@ -import { ReadWriteProxy } from "../../../core/read_write_proxy"; -import { generateFileDownload, makeDiv } from "../../../core/utils"; -import { PuzzleSerializer } from "../../../savegame/puzzle_serializer"; -import { T } from "../../../translations"; -import { BaseHUDPart } from "../base_hud_part"; - -export class HUDPuzzleEditorDownload extends BaseHUDPart { - constructor(root) { - super(root); - } - - createElements(parent) { - this.element = makeDiv(parent, "ingame_HUD_PuzzleEditorDownload"); - this.button = document.createElement("button"); - this.button.classList.add("button"); - this.element.appendChild(this.button); - - this.trackClicks(this.button, () => { - const { ok } = this.root.hud.parts.dialogs.showWarning( - T.dialogs.puzzleDownload.title, - T.dialogs.puzzleDownload.desc, - ["cancel", "ok:good:enter"] - ); - ok.add(() => this.downloadPuzzle()); - }); - } - - initialize() {} - - downloadPuzzle() { - 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/hud/parts/puzzle_editor_settings.js b/src/js/game/hud/parts/puzzle_editor_settings.js index dcb54d3c..85b198b7 100644 --- a/src/js/game/hud/parts/puzzle_editor_settings.js +++ b/src/js/game/hud/parts/puzzle_editor_settings.js @@ -92,45 +92,50 @@ export class HUDPuzzleEditorSettings extends BaseHUDPart { testButton.classList.toggle("disabled", false); }, 140); - this.root.logic.performBulkOperation(() => { + if (this.testMode) { for (const entity of this.root.entityMgr.getAllWithComponent(StaticMapEntityComponent)) { - if (this.testMode) { - this.storedSolution.push(entity.clone()); + this.storedSolution.push(entity.clone()); - const metaBuilding = entity.components.StaticMapEntity.getMetaBuilding(); - const goalComp = entity.components.GoalAcceptor; - if (goalComp) { - goalComp.clear(); - continue; - } + const metaBuilding = entity.components.StaticMapEntity.getMetaBuilding(); + const goalComp = entity.components.GoalAcceptor; + if (goalComp) { + goalComp.clear(); + continue; + } - if ( - [MetaConstantProducerBuilding, MetaBlockBuilding] - .map(metaClass => gMetaBuildingRegistry.findByClass(metaClass).id) - .includes(metaBuilding.id) - ) { - continue; - } + if ( + [MetaConstantProducerBuilding, MetaBlockBuilding] + .map(metaClass => gMetaBuildingRegistry.findByClass(metaClass).id) + .includes(metaBuilding.id) + ) { + continue; } this.root.map.removeStaticEntity(entity); this.root.entityMgr.destroyEntity(entity); } this.root.entityMgr.processDestroyList(); - - if (!this.testMode) { - for (const entity of this.storedSolution) { - 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]); + } else if (this.storedSolution.length) { + this.root.logic.performBulkOperation(() => { + this.root.logic.performImmutableOperation(() => { + for (const entity of this.root.entityMgr.getAllWithComponent(StaticMapEntityComponent)) { + this.root.map.removeStaticEntity(entity); + this.root.entityMgr.destroyEntity(entity); } - } - this.storedSolution = []; - } - }); + this.root.entityMgr.processDestroyList(); + + for (const entity of this.storedSolution) { + 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 = []; + }); + }); + } } trim() { 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..f5f71dac --- /dev/null +++ b/src/js/game/hud/parts/puzzle_import_export.js @@ -0,0 +1,118 @@ +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 { 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; + + 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() { + 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/modes/puzzle_edit.js b/src/js/game/modes/puzzle_edit.js index aacdfd60..5569ae61 100644 --- a/src/js/game/modes/puzzle_edit.js +++ b/src/js/game/modes/puzzle_edit.js @@ -23,10 +23,7 @@ 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 { PuzzleSerializer } from "../../savegame/puzzle_serializer"; -import { T } from "../../translations"; -import { gMetaBuildingRegistry } from "../../core/global_registries"; -import { HUDPuzzleEditorDownload } from "../hud/parts/puzzle_editor_download"; +import { HUDPuzzleImportExport } from "../hud/parts/puzzle_import_export"; const logger = createLogger("puzzle-edit"); @@ -41,11 +38,8 @@ export class PuzzleEditGameMode extends PuzzleGameMode { /** * @param {GameRoot} root - * @param {object} payload - * @param {import("../../savegame/savegame_typedefs").PuzzleGameData} payload.gameData - * @param {boolean} payload.startInTestMode */ - constructor(root, { gameData = null, startInTestMode = false }) { + constructor(root) { super(root); this.hiddenBuildings = [ @@ -70,57 +64,7 @@ export class PuzzleEditGameMode extends PuzzleGameMode { this.additionalHudParts.puzzleEditorControls = HUDPuzzleEditorControls; this.additionalHudParts.puzzleEditorReview = HUDPuzzleEditorReview; this.additionalHudParts.puzzleEditorSettings = HUDPuzzleEditorSettings; - this.additionalHudParts.puzzleEditorDownload = HUDPuzzleEditorDownload; - - this.gameData = gameData; - - if (gameData) { - root.signals.postLoadHook.add(() => this.loadPuzzle(gameData), this); - } - - this.startInTestMode = startInTestMode; - } - - /** - * @param {import("../../savegame/savegame_typedefs").PuzzleGameData} puzzle - */ - loadPuzzle(puzzle) { - let errorText; - logger.log("Loading puzzle", puzzle); - - // set zone and add buildings - try { - this.zoneWidth = puzzle.bounds.w; - this.zoneHeight = puzzle.bounds.h; - errorText = new PuzzleSerializer().deserializePuzzle(this.root, puzzle); - } catch (ex) { - errorText = ex.message || ex; - } - - if (errorText) { - this.root.gameState.moveToState("PuzzleMenuState", { - error: { - title: T.dialogs.puzzleLoadError.title, - desc: T.dialogs.puzzleLoadError.desc + " " + errorText, - }, - }); - } - - const toolbar = this.root.hud.parts.buildingsToolbar; - - // lock excluded buildings - for (let i = 0; i < this.gameData.excludedBuildings.length; ++i) { - const id = this.gameData.excludedBuildings[i]; - - if (!gMetaBuildingRegistry.hasId(id)) { - continue; - } - toolbar.toggleBuildingLock(gMetaBuildingRegistry.findById(id)); - } - - if (this.startInTestMode) { - this.root.hud.parts.puzzleEditorSettings.toggleTestMode(); - } + this.additionalHudParts.puzzleEditorDownload = HUDPuzzleImportExport; } getIsEditor() { diff --git a/src/js/states/puzzle_menu.js b/src/js/states/puzzle_menu.js index 86e02c92..7e76454c 100644 --- a/src/js/states/puzzle_menu.js +++ b/src/js/states/puzzle_menu.js @@ -43,7 +43,6 @@ export class PuzzleMenuState extends TextualGameState {

${this.getStateHeaderTitle()}

-
@@ -390,7 +389,6 @@ export class PuzzleMenuState extends TextualGameState { this.trackClicks(this.htmlElement.querySelector("button.createPuzzle"), () => this.createNewPuzzle()); this.trackClicks(this.htmlElement.querySelector("button.loadPuzzle"), () => this.loadPuzzle()); - this.trackClicks(this.htmlElement.querySelector("button.importPuzzle"), () => this.importPuzzle()); } createEmptySavegame() { diff --git a/translations/base-en.yaml b/translations/base-en.yaml index 841ef3eb..4a875756 100644 --- a/translations/base-en.yaml +++ b/translations/base-en.yaml @@ -189,6 +189,8 @@ dialogs: retry: Retry continue: Continue playOffline: Play Offline + yes: Yes + no: No importSavegameError: title: Import Error @@ -400,8 +402,13 @@ dialogs: desc: >- Are you sure you want to delete ''? This can not be undone! - puzzleDownload: - title: Download Puzzle + puzzleImport: + title: Puzzle Imported + desc: >- + Your puzzle has been successfully imported. + + puzzleExport: + title: Export Puzzle desc: >- Do you want to download this puzzle?