diff --git a/data/collections/fairy_tales/collection.json b/data/collections/fairy_tales/collection.json new file mode 100644 index 0000000..9299075 --- /dev/null +++ b/data/collections/fairy_tales/collection.json @@ -0,0 +1,48 @@ +{ + "name": "Fairy Tales", + "description": "Classic fairy tale illustrations by Elizabeth Tyler Wolcott (1892-1952). Public domain artwork.", + "puzzles": [ + { + "image": "Cinderella (1918).jpg", + "title": "Cinderella", + "description": "Cinderella at the ball, illustrated in 1918.", + "flip_mode": false, + "rectangular_mode": false + }, + { + "image": "Little Red Riding Hood (1919).jpg", + "title": "Little Red Riding Hood", + "description": "Little Red Riding Hood meets the wolf, illustrated in 1919.", + "flip_mode": false, + "rectangular_mode": false + }, + { + "image": "The Three Bears (1918).jpg", + "title": "The Three Bears", + "description": "Goldilocks and the Three Bears, illustrated in 1918.", + "flip_mode": false, + "rectangular_mode": false + }, + { + "image": "Snow White And The Seven Dwarfs (1920).jpg", + "title": "Snow White and the Seven Dwarfs", + "description": "Snow White with the seven dwarfs, illustrated in 1920.", + "flip_mode": false, + "rectangular_mode": false + }, + { + "image": "The Three Little Pigs (1920).jpg", + "title": "The Three Little Pigs", + "description": "The Three Little Pigs and the Big Bad Wolf, illustrated in 1920.", + "flip_mode": false, + "rectangular_mode": false + }, + { + "image": "Puss In Boots (1921).jpg", + "title": "Puss in Boots", + "description": "The clever cat Puss in Boots, illustrated in 1921.", + "flip_mode": false, + "rectangular_mode": false + } + ] +} diff --git a/src/CollectionListState.cpp b/src/CollectionListState.cpp new file mode 100644 index 0000000..5208480 --- /dev/null +++ b/src/CollectionListState.cpp @@ -0,0 +1,157 @@ +/* +=========================================================================== + * Sago Multi Scrambler Puzzle +Copyright (C) 2022-2026 Poul Sander + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see http://www.gnu.org/licenses/ + +Source information and contacts persons can be found at +https://github.com/sago007/saland +=========================================================================== +*/ + +#include "CollectionListState.hpp" +#include "CollectionPlayState.hpp" +#include "sago/SagoMisc.hpp" +#include "sago_common.hpp" +#include "globals.hpp" +#include "config.hpp" +#include "SagoImGui.hpp" +#include "nlohmann/json.hpp" +#include +#include + +CollectionListState::CollectionListState() { + DiscoverCollections(); +} + +CollectionListState::~CollectionListState() { +} + +bool CollectionListState::IsActive() { + return isActive; +} + +void CollectionListState::ProcessInput(const SDL_Event& event, bool& processed) { + ImGui_ImplSDL2_ProcessEvent(&event); + if (event.type == SDL_KEYDOWN) { + if (event.key.keysym.sym == SDLK_ESCAPE) { + isActive = false; + processed = true; + } + } + if (event.type == SDL_CONTROLLERBUTTONDOWN) { + if (event.cbutton.button == SDL_CONTROLLER_BUTTON_B || event.cbutton.button == SDL_CONTROLLER_BUTTON_BACK) { + isActive = false; + processed = true; + } + } +} + +void CollectionListState::Draw(SDL_Renderer* target) { + ImGui::BeginMainMenuBar(); + if (ImGui::BeginMenu("File")) { + if (ImGui::MenuItem("Close")) { + isActive = false; + } + ImGui::EndMenu(); + } + ImGui::EndMainMenuBar(); + + float window_w = 500.0f; + float window_h = globalData.ysize - 80.0f; + ImGui::SetNextWindowPos(ImVec2(globalData.xsize / 2.0f, globalData.ysize / 2.0f), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::SetNextWindowSize(ImVec2(window_w, window_h)); + ImGui::Begin("Collections", nullptr, + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoCollapse); + + if (collections.empty()) { + ImGui::TextWrapped("No collections found. Place collection folders with a collection.json file in the data/collections/ directory."); + } else { + for (size_t i = 0; i < collections.size(); ++i) { + const CollectionInfo& col = collections[i]; + ImGui::PushID(static_cast(i)); + + std::set progress = LoadCollectionProgress(col.dirName); + std::string solvedText = std::to_string(progress.size()) + " / " + std::to_string(col.puzzleCount) + " solved"; + + ImGui::Separator(); + ImGui::Spacing(); + + if (ImGui::Button(col.displayName.c_str(), ImVec2(window_w - 30.0f, 0))) { + selectedCollection = static_cast(i); + } + + ImGui::TextWrapped("%s", col.description.c_str()); + ImGui::Text("%s", solvedText.c_str()); + ImGui::Spacing(); + + ImGui::PopID(); + } + ImGui::Separator(); + } + + ImGui::Spacing(); + if (ImGui::Button("Back", ImVec2(window_w - 30.0f, 30))) { + isActive = false; + } + + ImGui::End(); +} + +void CollectionListState::Update() { + if (selectedCollection >= 0 && selectedCollection < static_cast(collections.size())) { + std::string dirName = collections[selectedCollection].dirName; + selectedCollection = -1; + + CollectionPlayState cps(dirName); + RunGameState(cps); + globalData.mouseUp = true; + + // Re-discover in case progress changed + DiscoverCollections(); + } +} + +void CollectionListState::DiscoverCollections() { + collections.clear(); + std::vector dirs = sago::GetFileList("collections"); + + for (const std::string& dir : dirs) { + std::string jsonPath = "collections/" + dir + "/collection.json"; + if (!sago::FileExists(jsonPath.c_str())) { + continue; + } + + std::string content = sago::GetFileContent(jsonPath); + try { + nlohmann::json j = nlohmann::json::parse(content); + CollectionInfo info; + info.dirName = dir; + info.displayName = j.value("name", dir); + info.description = j.value("description", ""); + if (j.contains("puzzles") && j["puzzles"].is_array()) { + info.puzzleCount = static_cast(j["puzzles"].size()); + } + collections.push_back(info); + } catch (const std::exception& e) { + std::cerr << "Error parsing " << jsonPath << ": " << e.what() << std::endl; + } + } + + std::sort(collections.begin(), collections.end(), [](const CollectionInfo& a, const CollectionInfo& b) { + return a.displayName < b.displayName; + }); +} diff --git a/src/CollectionListState.hpp b/src/CollectionListState.hpp new file mode 100644 index 0000000..6f1cdbb --- /dev/null +++ b/src/CollectionListState.hpp @@ -0,0 +1,53 @@ +/* +=========================================================================== + * Sago Multi Scrambler Puzzle +Copyright (C) 2022-2026 Poul Sander + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see http://www.gnu.org/licenses/ + +Source information and contacts persons can be found at +https://github.com/sago007/saland +=========================================================================== +*/ + +#pragma once + +#include "sago/GameStateInterface.hpp" +#include +#include + +struct CollectionInfo { + std::string dirName; + std::string displayName; + std::string description; + int puzzleCount = 0; +}; + +class CollectionListState : public sago::GameStateInterface { +public: + CollectionListState(); + CollectionListState(const CollectionListState& orig) = delete; + virtual ~CollectionListState(); + + bool IsActive() override; + void ProcessInput(const SDL_Event& event, bool& processed) override; + void Draw(SDL_Renderer* target) override; + void Update() override; + +private: + void DiscoverCollections(); + bool isActive = true; + std::vector collections; + int selectedCollection = -1; +}; diff --git a/src/CollectionPlayState.cpp b/src/CollectionPlayState.cpp new file mode 100644 index 0000000..e02a032 --- /dev/null +++ b/src/CollectionPlayState.cpp @@ -0,0 +1,290 @@ +/* +=========================================================================== + * Sago Multi Scrambler Puzzle +Copyright (C) 2022-2026 Poul Sander + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see http://www.gnu.org/licenses/ + +Source information and contacts persons can be found at +https://github.com/sago007/saland +=========================================================================== +*/ + +#include "CollectionPlayState.hpp" +#include "PuzzleSingleImageState.hpp" +#include "sago/SagoMisc.hpp" +#include "sago_common.hpp" +#include "globals.hpp" +#include "config.hpp" +#include "SagoImGui.hpp" +#include "nlohmann/json.hpp" +#include +#include + +CollectionPlayState::CollectionPlayState(const std::string& collectionDir) + : collectionDir(collectionDir) { + LoadCollection(); + solvedPuzzles = LoadCollectionProgress(collectionDir); + int firstUnsolved = FindFirstUnsolved(); + currentPuzzleIndex = (firstUnsolved >= 0) ? firstUnsolved : 0; + LoadCurrentImage(); +} + +CollectionPlayState::~CollectionPlayState() { +} + +bool CollectionPlayState::IsActive() { + return isActive; +} + +void CollectionPlayState::ProcessInput(const SDL_Event& event, bool& processed) { + ImGui_ImplSDL2_ProcessEvent(&event); + if (event.type == SDL_KEYDOWN) { + if (event.key.keysym.sym == SDLK_ESCAPE) { + isActive = false; + processed = true; + } + } + if (event.type == SDL_CONTROLLERBUTTONDOWN) { + if (event.cbutton.button == SDL_CONTROLLER_BUTTON_B || event.cbutton.button == SDL_CONTROLLER_BUTTON_BACK) { + isActive = false; + processed = true; + } + } +} + +void CollectionPlayState::Draw(SDL_Renderer* target) { + if (puzzles.empty()) { + return; + } + + const CollectionPuzzleInfo& puzzle = puzzles[currentPuzzleIndex]; + + // Draw the image preview centered in the left portion of the screen + int preview_x = 40; + int preview_y = 60; + int preview_max_w = globalData.xsize / 2 - 60; + int preview_max_h = globalData.ysize - 120; + currentImageHolder.Draw(target, preview_x, preview_y, preview_max_w, preview_max_h); + + // Draw solved indicator using sprites + bool isSolved = solvedPuzzles.count(currentPuzzleIndex) > 0; + int icon_x = preview_x + preview_max_w - 40; + int icon_y = preview_y + 10; + const sago::SagoSprite& checkbox = globalData.spriteHolder->GetSprite("i_level_check_box"); + checkbox.Draw(target, SDL_GetTicks(), icon_x, icon_y); + if (isSolved) { + const sago::SagoSprite& check = globalData.spriteHolder->GetSprite("i_level_check"); + check.Draw(target, SDL_GetTicks(), icon_x, icon_y); + } + + // Right panel with info and controls via ImGui + float panel_x = globalData.xsize / 2.0f + 20.0f; + float panel_w = globalData.xsize / 2.0f - 40.0f; + + ImGui::SetNextWindowPos(ImVec2(panel_x, 60)); + ImGui::SetNextWindowSize(ImVec2(panel_w, globalData.ysize - 120.0f)); + ImGui::Begin("Puzzle Info", nullptr, + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoCollapse); + + ImGui::TextWrapped("%s", collectionName.c_str()); + ImGui::Separator(); + + ImGui::Spacing(); + std::string progress = "Puzzle " + std::to_string(currentPuzzleIndex + 1) + " of " + std::to_string(puzzles.size()); + ImGui::Text("%s", progress.c_str()); + + std::string solvedCount = std::to_string(solvedPuzzles.size()) + " / " + std::to_string(puzzles.size()) + " solved"; + ImGui::Text("%s", solvedCount.c_str()); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + ImGui::TextWrapped("Title: %s", puzzle.title.c_str()); + ImGui::Spacing(); + ImGui::TextWrapped("%s", puzzle.description.c_str()); + + if (isSolved) { + ImGui::Spacing(); + ImGui::TextColored(ImVec4(0.2f, 0.8f, 0.2f, 1.0f), "Solved!"); + } + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + float button_width = panel_w - 30.0f; + + if (ImGui::Button("Play", ImVec2(button_width, 40))) { + shouldPlayPuzzle = true; + } + + ImGui::Spacing(); + + bool hasPrev = currentPuzzleIndex > 0; + bool hasNext = currentPuzzleIndex < static_cast(puzzles.size()) - 1; + + if (!hasPrev) { + ImGui::BeginDisabled(); + } + if (ImGui::Button("Previous", ImVec2(button_width, 30))) { + currentPuzzleIndex--; + LoadCurrentImage(); + } + if (!hasPrev) { + ImGui::EndDisabled(); + } + + if (!hasNext) { + ImGui::BeginDisabled(); + } + if (ImGui::Button("Next", ImVec2(button_width, 30))) { + currentPuzzleIndex++; + LoadCurrentImage(); + } + if (!hasNext) { + ImGui::EndDisabled(); + } + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + if (ImGui::Button("Back to Collections", ImVec2(button_width, 30))) { + isActive = false; + } + + ImGui::End(); + + // Menu bar + ImGui::BeginMainMenuBar(); + if (ImGui::BeginMenu("File")) { + if (ImGui::MenuItem("Close")) { + isActive = false; + } + ImGui::EndMenu(); + } + ImGui::EndMainMenuBar(); +} + +void CollectionPlayState::Update() { + if (shouldPlayPuzzle) { + shouldPlayPuzzle = false; + + if (currentPuzzleIndex >= 0 && currentPuzzleIndex < static_cast(puzzles.size())) { + const CollectionPuzzleInfo& puzzle = puzzles[currentPuzzleIndex]; + std::string imagePath = GetImageFilesystemPath(puzzle.image); + + if (!imagePath.empty()) { + PuzzleSingleImageState psi; + psi.flipMode = puzzle.flip_mode; + psi.rectangularMode = puzzle.rectangular_mode; + psi.LoadPictureFromFile(imagePath, globalData.screen); + RunGameState(psi); + globalData.mouseUp = true; + + if (psi.IsSolved()) { + MarkCollectionPuzzleSolved(collectionDir, currentPuzzleIndex); + solvedPuzzles = LoadCollectionProgress(collectionDir); + + // Advance to next unsolved + int nextUnsolved = -1; + for (int i = currentPuzzleIndex + 1; i < static_cast(puzzles.size()); ++i) { + if (solvedPuzzles.count(i) == 0) { + nextUnsolved = i; + break; + } + } + if (nextUnsolved >= 0) { + currentPuzzleIndex = nextUnsolved; + } else { + // All remaining are solved; try from the beginning + int fromStart = FindFirstUnsolved(); + if (fromStart >= 0) { + currentPuzzleIndex = fromStart; + } + // else all solved, stay on current + } + LoadCurrentImage(); + } + } else { + std::cerr << "Could not resolve path for collection image: " << puzzle.image << std::endl; + } + } + } +} + +void CollectionPlayState::LoadCollection() { + std::string jsonPath = "collections/" + collectionDir + "/collection.json"; + if (!sago::FileExists(jsonPath.c_str())) { + std::cerr << "Collection file not found: " << jsonPath << std::endl; + isActive = false; + return; + } + + std::string content = sago::GetFileContent(jsonPath); + try { + nlohmann::json j = nlohmann::json::parse(content); + collectionName = j.value("name", collectionDir); + collectionDescription = j.value("description", ""); + + if (j.contains("puzzles") && j["puzzles"].is_array()) { + for (const auto& p : j["puzzles"]) { + CollectionPuzzleInfo info; + info.image = p.value("image", ""); + info.title = p.value("title", ""); + info.description = p.value("description", ""); + info.flip_mode = p.value("flip_mode", false); + info.rectangular_mode = p.value("rectangular_mode", false); + if (!info.image.empty()) { + puzzles.push_back(info); + } + } + } + } catch (const std::exception& e) { + std::cerr << "Error parsing collection.json: " << e.what() << std::endl; + isActive = false; + } +} + +void CollectionPlayState::LoadCurrentImage() { + if (currentPuzzleIndex >= 0 && currentPuzzleIndex < static_cast(puzzles.size())) { + std::string imagePath = GetImageFilesystemPath(puzzles[currentPuzzleIndex].image); + ImageHolder newHolder; + if (!imagePath.empty()) { + newHolder.LoadPictureFromFileLazy(imagePath); + } + currentImageHolder = std::move(newHolder); + } +} + +std::string CollectionPlayState::GetImageFilesystemPath(const std::string& imageFilename) const { + std::string physfsPath = "collections/" + collectionDir + "/" + imageFilename; + const char* realDir = PHYSFS_getRealDir(physfsPath.c_str()); + if (realDir) { + return std::string(realDir) + "/" + physfsPath; + } + return ""; +} + +int CollectionPlayState::FindFirstUnsolved() const { + for (int i = 0; i < static_cast(puzzles.size()); ++i) { + if (solvedPuzzles.count(i) == 0) { + return i; + } + } + return -1; +} diff --git a/src/CollectionPlayState.hpp b/src/CollectionPlayState.hpp new file mode 100644 index 0000000..f876e18 --- /dev/null +++ b/src/CollectionPlayState.hpp @@ -0,0 +1,71 @@ +/* +=========================================================================== + * Sago Multi Scrambler Puzzle +Copyright (C) 2022-2026 Poul Sander + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see http://www.gnu.org/licenses/ + +Source information and contacts persons can be found at +https://github.com/sago007/saland +=========================================================================== +*/ + +#pragma once + +#include "sago/GameStateInterface.hpp" +#include "ImageHolder.hpp" +#include "sago/SagoTextField.hpp" +#include +#include +#include + +struct CollectionPuzzleInfo { + std::string image; + std::string title; + std::string description; + bool flip_mode = false; + bool rectangular_mode = false; +}; + +class CollectionPlayState : public sago::GameStateInterface { +public: + CollectionPlayState(const std::string& collectionDir); + CollectionPlayState(const CollectionPlayState& orig) = delete; + virtual ~CollectionPlayState(); + + bool IsActive() override; + void ProcessInput(const SDL_Event& event, bool& processed) override; + void Draw(SDL_Renderer* target) override; + void Update() override; + +private: + void LoadCollection(); + void LoadCurrentImage(); + std::string GetImageFilesystemPath(const std::string& imageFilename) const; + int FindFirstUnsolved() const; + + bool isActive = true; + std::string collectionDir; + std::string collectionName; + std::string collectionDescription; + std::vector puzzles; + std::set solvedPuzzles; + int currentPuzzleIndex = 0; + bool shouldPlayPuzzle = false; + + ImageHolder currentImageHolder; + sago::SagoTextField titleField; + sago::SagoTextField descriptionField; + sago::SagoTextField progressField; +}; diff --git a/src/ImageHolder.cpp b/src/ImageHolder.cpp index 3991d5e..5bf955c 100644 --- a/src/ImageHolder.cpp +++ b/src/ImageHolder.cpp @@ -35,6 +35,21 @@ ImageHolder::ImageHolder(ImageHolder&& other) { other.pictureTex = nullptr; } +ImageHolder& ImageHolder::operator=(ImageHolder&& other) { + if (this != &other) { + if (pictureTex != nullptr) { + SDL_DestroyTexture(pictureTex); + } + pictureTex = other.pictureTex; + source_image_width = other.source_image_width; + source_image_height = other.source_image_height; + source_filename = other.source_filename; + do_lazy_load = other.do_lazy_load; + other.pictureTex = nullptr; + } + return *this; +} + ImageHolder::~ImageHolder() { if (pictureTex != nullptr) { SDL_DestroyTexture(pictureTex); diff --git a/src/ImageHolder.hpp b/src/ImageHolder.hpp index 9c8bc46..28a15fd 100644 --- a/src/ImageHolder.hpp +++ b/src/ImageHolder.hpp @@ -41,6 +41,9 @@ public: // Move constructor ImageHolder(ImageHolder&& other); + // Move assignment operator + ImageHolder& operator=(ImageHolder&& other); + // Destructor ~ImageHolder(); diff --git a/src/MainGameState.cpp b/src/MainGameState.cpp index 91b7f29..19279d1 100644 --- a/src/MainGameState.cpp +++ b/src/MainGameState.cpp @@ -29,6 +29,7 @@ https://github.com/sago007/saland #include "SagoImGui.hpp" #include "config.hpp" #include "PuzzleSingleImageState.hpp" +#include "CollectionListState.hpp" #include "sago_common.hpp" MainGameState::MainGameState() { @@ -65,6 +66,10 @@ void MainGameState::Draw(SDL_Renderer* target) { ImGui::SetNextWindowPos(ImVec2(globalData.xsize / 2.0f, globalData.ysize / 2.0f), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); ImGui::Begin("Main Menu", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove); + if (ImGui::Button("Collections", ImVec2(300, 50))) { + shouldOpenCollections = true; + } + if (ImGui::Button("Random image from favorites", ImVec2(300, 50))) { shouldLoadRandomFavorite = true; } @@ -82,6 +87,12 @@ void MainGameState::Update() { shouldLoadRandomFavorite = false; LoadRandomFavorite(); } + if (shouldOpenCollections) { + shouldOpenCollections = false; + CollectionListState cls; + RunGameState(cls); + globalData.mouseUp = true; + } } void MainGameState::LoadRandomFavorite() { diff --git a/src/MainGameState.hpp b/src/MainGameState.hpp index bf0860b..2686e67 100644 --- a/src/MainGameState.hpp +++ b/src/MainGameState.hpp @@ -40,6 +40,7 @@ public: private: bool isActive = true; bool shouldLoadRandomFavorite = false; + bool shouldOpenCollections = false; void LoadRandomFavorite(); }; diff --git a/src/PuzzleSingleImageState.hpp b/src/PuzzleSingleImageState.hpp index 5a78212..b55663c 100644 --- a/src/PuzzleSingleImageState.hpp +++ b/src/PuzzleSingleImageState.hpp @@ -41,6 +41,8 @@ public: void LoadPictureFromFile(const std::string& filename, SDL_Renderer* renderer); + bool IsSolved() const { return puzzleSolved; } + void SplitPiece(); void SplitPiece(size_t piece_number); void SplitPieceVertical(size_t piece_number); diff --git a/src/config.cpp b/src/config.cpp index 01ea4ec..b73f571 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -26,6 +26,7 @@ SOFTWARE. #include "os.hpp" #include "sago/SagoMisc.hpp" #include +#include #include std::map LoadConfigMap() { @@ -217,3 +218,33 @@ bool HasCustomPieceLayout(const std::string& pictureId) { std::string layoutPath = "piece_layouts/" + pictureId + ".layout"; return sago::FileExists(layoutPath.c_str()); } + +std::set LoadCollectionProgress(const std::string& collectionName) { + std::set solved; + std::string progressPath = "collection_progress/" + collectionName + ".txt"; + if (sago::FileExists(progressPath.c_str())) { + std::string content = sago::GetFileContent(progressPath); + std::istringstream iss(content); + int index; + while (iss >> index) { + solved.insert(index); + } + } + return solved; +} + +void MarkCollectionPuzzleSolved(const std::string& collectionName, int puzzleIndex) { + std::set solved = LoadCollectionProgress(collectionName); + solved.insert(puzzleIndex); + std::string progressPath = "collection_progress/" + collectionName + ".txt"; + std::ostringstream oss; + for (int idx : solved) { + oss << idx << "\n"; + } + sago::WriteFileContent(progressPath.c_str(), oss.str()); +} + +bool IsCollectionPuzzleSolved(const std::string& collectionName, int puzzleIndex) { + std::set solved = LoadCollectionProgress(collectionName); + return solved.count(puzzleIndex) > 0; +} diff --git a/src/config.hpp b/src/config.hpp index 706f281..9ddcdf4 100644 --- a/src/config.hpp +++ b/src/config.hpp @@ -28,6 +28,7 @@ SOFTWARE. #include #include #include +#include #include "SDL.h" /** @@ -147,4 +148,26 @@ bool SaveCustomPieceLayout(const std::string& pictureId, const std::vector LoadCollectionProgress(const std::string& collectionName); + +/** + * @brief Marks a puzzle as solved in a collection + * @param collectionName The collection directory name + * @param puzzleIndex The puzzle index to mark as solved + */ +void MarkCollectionPuzzleSolved(const std::string& collectionName, int puzzleIndex); + +/** + * @brief Checks if a puzzle in a collection has been solved + * @param collectionName The collection directory name + * @param puzzleIndex The puzzle index to check + * @return true if the puzzle has been solved + */ +bool IsCollectionPuzzleSolved(const std::string& collectionName, int puzzleIndex); + #endif // CONFIG_HPP diff --git a/src/sago_multi_scrambler_puzzle2.cpp b/src/sago_multi_scrambler_puzzle2.cpp index a3c73dd..4997372 100644 --- a/src/sago_multi_scrambler_puzzle2.cpp +++ b/src/sago_multi_scrambler_puzzle2.cpp @@ -33,6 +33,7 @@ https://github.com/sago007/saland #include "ImageSelectState.hpp" #include "sago_common.hpp" #include "MainGameState.hpp" +#include "CollectionPlayState.hpp" #include "editor/SagoTextureSelector.hpp" #include "version.h" #include "os.hpp" @@ -60,9 +61,8 @@ void runGame() { void runCollection(const std::string& collection_name) { InitGame(); - ImageSelectState iss; - iss.collection = collection_name; - RunGameState(iss); + CollectionPlayState cps(collection_name); + RunGameState(cps); UninitGame(); }