src/CollectionPlayState.cpp
browsing at commit = b8386ee3d639837026e1f03438edef6523015972
/*
===========================================================================
* 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 <iostream>
CollectionPlayState::CollectionPlayState(const std::string& collectionDir)
: collectionDir(collectionDir) {
LoadCollection();
solvedPuzzles = LoadCollectionProgress(collectionDir);
int firstUnsolved = FindFirstUnsolved();
currentPuzzleIndex = (firstUnsolved >= 0) ? firstUnsolved : 0;
LoadCurrentImage();
questionMarkField.SetHolder(globalData.dataHolder);
questionMarkField.SetFont("freeserif");
questionMarkField.SetColor({200, 200, 200, 255});
questionMarkField.SetFontSize(120);
questionMarkField.SetOutline(3, {80, 80, 80, 255});
questionMarkField.SetText("?");
}
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;
// Draw solved indicator using sprites
bool isSolved = solvedPuzzles.count(currentPuzzleIndex) > 0;
if (isSolved) {
currentImageHolder.Draw(target, preview_x, preview_y, preview_max_w, preview_max_h);
} else {
// Draw a large [?] centered in the preview area
int center_x = preview_x + preview_max_w / 2;
int center_y = preview_y + preview_max_h / 2;
questionMarkField.Draw(target, center_x, center_y,
sago::SagoTextField::Alignment::center,
sago::SagoTextField::VerticalAlignment::center);
}
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<int>(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<int>(puzzles.size())) {
const CollectionPuzzleInfo& puzzle = puzzles[currentPuzzleIndex];
std::string imagePath = GetImagePhysFSPath(puzzle.image);
if (!imagePath.empty()) {
PuzzleSingleImageState psi;
psi.flipMode = puzzle.flip_mode;
psi.rectangularMode = puzzle.rectangular_mode;
psi.LoadPictureFromPhysFS(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<int>(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<int>(puzzles.size())) {
std::string imagePath = GetImagePhysFSPath(puzzles[currentPuzzleIndex].image);
ImageHolder newHolder;
if (!imagePath.empty()) {
newHolder.LoadPictureFromPhysFSLazy(imagePath);
}
currentImageHolder = std::move(newHolder);
}
}
std::string CollectionPlayState::GetImagePhysFSPath(const std::string& imageFilename) const {
return "collections/" + collectionDir + "/" + imageFilename;
}
int CollectionPlayState::FindFirstUnsolved() const {
for (int i = 0; i < static_cast<int>(puzzles.size()); ++i) {
if (solvedPuzzles.count(i) == 0) {
return i;
}
}
return -1;
}