diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..b2bb2ae --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,68 @@ +name: Build Geode Mod + +on: + workflow_dispatch: + push: + branches: + - "master" + +jobs: + build: + strategy: + fail-fast: false + matrix: + config: + - name: Windows + os: windows-latest + build-config: RelWithDebInfo + + - name: macOS + os: macos-latest + + - name: Android32 + os: ubuntu-latest + target: Android32 + + - name: Android64 + os: ubuntu-latest + target: Android64 + + name: ${{ matrix.config.name }} + runs-on: ${{ matrix.config.os }} + + steps: + - uses: actions/checkout@v4 + + - name: Build the mod + uses: geode-sdk/build-geode-mod@main + with: + build-config: ${{ matrix.config.build-config || 'Release' }} + export-pdb: true + combine: true + target: ${{ matrix.config.target }} + + package: + name: Package builds + runs-on: ubuntu-latest + needs: ['build'] + + steps: + - uses: geode-sdk/build-geode-mod/combine@main + id: build + + - uses: actions/upload-artifact@v4 + with: + name: Build Output + path: ${{ steps.build.outputs.build-output }} + + release: + name: Release the mod + runs-on: ubuntu-latest + needs: ['package'] + + steps: + - uses: actions/checkout@v4 + + - uses: hiimjustin000/release-geode-mod@main + with: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8babefa --- /dev/null +++ b/.gitignore @@ -0,0 +1,61 @@ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# Macos be like +**/.DS_Store + +# Cache files for Sublime Text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# Ignore build folders +**/build +# Ignore platform specific build folders +build-*/ + +# Workspace files are user-specific +*.sublime-workspace + +# ILY vscode +**/.vscode +.idea/ + +# clangd +.cache/ + +# Visual Studio +.vs/ + +# Bruh +logo.pdn diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..adc1da3 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.21) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +if (APPLE) + set(CMAKE_OSX_ARCHITECTURES "x86_64;arm64") +endif() +set(CMAKE_CXX_VISIBILITY_PRESET hidden) + +project(BetterSafe VERSION 1.0.0) + +add_library(${PROJECT_NAME} SHARED + src/BetterSafe.cpp + src/BSCalendarPopup.cpp + src/BSHoverNode.cpp + src/main.cpp +) + +if (NOT DEFINED ENV{GEODE_SDK}) + message(FATAL_ERROR "Unable to find Geode SDK! Please define GEODE_SDK environment variable to point to Geode") +else() + message(STATUS "Found Geode: $ENV{GEODE_SDK}") +endif() + +add_subdirectory($ENV{GEODE_SDK} ${CMAKE_CURRENT_BINARY_DIR}/geode) + +setup_geode_mod(${PROJECT_NAME}) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1828c58 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 hiimjustin000 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..22af4bd --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# Better Safe +A mod that expands The Safe with a calendar. + +# Features +- Calendars for daily and weekly levels, showing what was featured on each day +- Daily and weekly level previews, showing basic information about the level on click +- Navigation buttons to go to the previous or next month +- A label showing the current month and year, which can be used to switch to a specific month + +# Credits +- [Opianisal](https://gdbrowser.com/u/25221350) - Idea for the mod +- [Every Rated Level Spreadsheet](https://docs.google.com/spreadsheets/d/1BBx9X8IYBtr7dA5cWu_smM2XBkLZzCXy7TjeEWRgag0) - Help with daily/weekly dates +- [hiimjustin000](https://gdbrowser.com/u/7466002) - Creator of the mod + +# Gallery +![Daily Calendar](./resources/daily-calendar.png)\ +![Weekly Calendar](./resources/weekly-calendar.png) + +# License +This mod is licensed under the [MIT License](./LICENSE). \ No newline at end of file diff --git a/about.md b/about.md new file mode 100644 index 0000000..6e0e6e7 --- /dev/null +++ b/about.md @@ -0,0 +1,17 @@ +# Better Safe +A mod that expands The Safe with a calendar. + +# Features +- Calendars for daily and weekly levels, showing what was featured on each day +- Daily and weekly level previews, showing basic information about the level on click +- Navigation buttons to go to the previous or next month +- A label showing the current month and year, which can be used to switch to a specific month + +# Credits +- [Opianisal](user:25221350) - Idea for the mod +- [Every Rated Level Spreadsheet](https://docs.google.com/spreadsheets/d/1BBx9X8IYBtr7dA5cWu_smM2XBkLZzCXy7TjeEWRgag0) - Help with daily/weekly dates +- [hiimjustin000](user:7466002) - Creator of the mod + +# Gallery +![Daily Calendar](hiimjustin000.better_safe/daily-calendar.png?scale=0.75)\ +![Weekly Calendar](hiimjustin000.better_safe/weekly-calendar.png?scale=0.75) \ No newline at end of file diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..d04bdd4 --- /dev/null +++ b/changelog.md @@ -0,0 +1,3 @@ +# Better Safe Changelog +## v1.0.0 (2024-09-12) +- Initial release \ No newline at end of file diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..d5fd536 Binary files /dev/null and b/logo.png differ diff --git a/mod.json b/mod.json new file mode 100644 index 0000000..1a9d5a4 --- /dev/null +++ b/mod.json @@ -0,0 +1,39 @@ +{ + "geode": "3.5.0", + "gd": { + "android": "2.206", + "win": "2.206", + "mac": "2.206" + }, + "version": "v1.0.0", + "id": "hiimjustin000.better_safe", + "name": "Better Safe", + "developer": "hiimjustin000", + "description": "A mod that expands The Safe with a calendar.", + "repository": "https://github.com/hiimjustin000/BetterSafe", + "settings": { + "sunday-first": { + "name": "Sunday First", + "description": "Start the week on Sunday instead of Monday.", + "type": "bool", + "default": false + } + }, + "resources": { + "sprites": [ + "resources/*.png" + ] + }, + "links": { + "community": "https://discord.gg/QVKmbvBXA7", + "source": "https://github.com/hiimjustin000/BetterSafe", + "homepage": "https://www.hiimjustin000.com" + }, + "tags": [ + "content", + "enhancement", + "interface", + "online", + "utility" + ] +} diff --git a/resources/daily-calendar.png b/resources/daily-calendar.png new file mode 100644 index 0000000..6c5d4fb Binary files /dev/null and b/resources/daily-calendar.png differ diff --git a/resources/weekly-calendar.png b/resources/weekly-calendar.png new file mode 100644 index 0000000..100f952 Binary files /dev/null and b/resources/weekly-calendar.png differ diff --git a/src/BSCalendarPopup.cpp b/src/BSCalendarPopup.cpp new file mode 100644 index 0000000..5fd275b --- /dev/null +++ b/src/BSCalendarPopup.cpp @@ -0,0 +1,408 @@ +#include "BSCalendarPopup.hpp" + +using namespace geode::prelude; + +BSCalendarPopup* BSCalendarPopup::create(DailyLevelPage* page, bool weekly) { + auto ret = new BSCalendarPopup(); + if (ret->initAnchored(300.0f, 280.0f, page, weekly)) { + ret->autorelease(); + return ret; + } + delete ret; + return nullptr; +} + +bool BSCalendarPopup::setup(DailyLevelPage* page, bool weekly) { + m_noElasticity = true; + m_weekly = weekly; + + auto sundayFirst = Mod::get()->getSettingValue("sunday-first"); + createWeekdayLabel("Mon", (int)sundayFirst); + createWeekdayLabel("Tue", (int)sundayFirst + 1); + createWeekdayLabel("Wed", (int)sundayFirst + 2); + createWeekdayLabel("Thu", (int)sundayFirst + 3); + createWeekdayLabel("Fri", (int)sundayFirst + 4); + createWeekdayLabel("Sat", (int)sundayFirst + 5); + createWeekdayLabel("Sun", sundayFirst ? 0 : 6); + + auto glm = GameLevelManager::sharedState(); + glm->m_levelManagerDelegate = this; + + m_prevButton = CCMenuItemExt::createSpriteExtraWithFrameName("GJ_arrow_01_001.png", 1.0f, [this](auto) { + if (m_year == m_firstYear && m_month == m_firstMonth) return; + + if (m_month == 1) { + m_year--; + m_month = 12; + } + else m_month--; + loadMonth(); + }); + m_prevButton->setPosition(-34.5f, 140.0f); + m_prevButton->setVisible(false); + m_buttonMenu->addChild(m_prevButton); + + auto nextButtonSprite = CCSprite::createWithSpriteFrameName("GJ_arrow_01_001.png"); + nextButtonSprite->setFlipX(true); + m_nextButton = CCMenuItemExt::createSpriteExtra(nextButtonSprite, [this](auto) { + if (m_year == m_currentYear && m_month == m_currentMonth) return; + if (m_month == 12) { + m_year++; + m_month = 1; + } + else m_month++; + loadMonth(); + }); + m_nextButton->setPosition(334.5f, 140.0f); + m_nextButton->setVisible(false); + m_buttonMenu->addChild(m_nextButton); + + auto firstArrow = CCSprite::createWithSpriteFrameName("GJ_arrow_02_001.png"); + auto otherFirstArrow = CCSprite::createWithSpriteFrameName("GJ_arrow_02_001.png"); + otherFirstArrow->setPosition(firstArrow->getContentSize() / 2 - CCPoint { 20.0f, 0.0f }); + firstArrow->addChild(otherFirstArrow); + firstArrow->setScale(0.4f); + m_firstButton = CCMenuItemExt::createSpriteExtra(firstArrow, [this](auto) { + m_year = m_firstYear; + m_month = m_firstMonth; + loadMonth(); + }); + m_firstButton->setPosition(-27.5f, 175.0f); + m_firstButton->setVisible(false); + m_buttonMenu->addChild(m_firstButton); + + auto lastArrow = CCSprite::createWithSpriteFrameName("GJ_arrow_02_001.png"); + lastArrow->setFlipX(true); + auto otherLastArrow = CCSprite::createWithSpriteFrameName("GJ_arrow_02_001.png"); + otherLastArrow->setPosition(lastArrow->getContentSize() / 2 + CCPoint { 20.0f, 0.0f }); + otherLastArrow->setFlipX(true); + lastArrow->addChild(otherLastArrow); + lastArrow->setScale(0.4f); + m_lastButton = CCMenuItemExt::createSpriteExtra(lastArrow, [this](auto) { + m_year = m_currentYear; + m_month = m_currentMonth; + loadMonth(); + }); + m_lastButton->setPosition(327.5f, 175.0f); + m_lastButton->setVisible(false); + m_buttonMenu->addChild(m_lastButton); + + m_calendarMenu = CCMenu::create(); + m_calendarMenu->setPosition(150.0f, 140.0f); + m_calendarMenu->setContentSize({ 300.0f, 280.0f }); + m_calendarMenu->ignoreAnchorPointForPosition(false); + m_mainLayer->addChild(m_calendarMenu); + + m_monthLabel = CCLabelBMFont::create("", "goldFont.fnt"); + m_monthLabel->setScale(0.6f); + m_monthButton = CCMenuItemExt::createSpriteExtra(m_monthLabel, [this](auto) { + BSSelectPopup::create(m_year, m_month, m_firstYear, m_firstMonth, m_currentYear, m_currentMonth, [this](int year, int month) { + m_year = year; + m_month = month; + loadMonth(); + })->show(); + }); + m_monthButton->setPosition(150.0f, 265.0f); + m_monthButton->setEnabled(false); + m_buttonMenu->addChild(m_monthButton); + + m_loadingCircle = LoadingCircle::create(); + m_loadingCircle->setParentLayer(this); + m_loadingCircle->retain(); + m_loadingCircle->show(); + + auto safeButton = CCMenuItemExt::createSpriteExtraWithFrameName("GJ_safeBtn_001.png", 1.0f, [this, page](auto sender) { + page->onTheSafe(sender); + }); + safeButton->setPosition(340.0f, 25.0f); + safeButton->setTag(91508); // my birthday + m_buttonMenu->addChild(safeButton); + + auto refreshButton = CCMenuItemExt::createSpriteExtraWithFrameName("GJ_updateBtn_001.png", 1.0f, [this, weekly](auto) { + m_loadingCircle->setVisible(true); + if (m_hoverNode) m_hoverNode->close(); + m_selected = nullptr; + m_calendarMenu->removeAllChildren(); + m_prevButton->setVisible(false); + m_nextButton->setVisible(false); + m_firstButton->setVisible(false); + m_lastButton->setVisible(false); + m_monthButton->setEnabled(false); + if (weekly) { + BetterSafe::WEEKLY_SAFE.clear(); + BetterSafe::loadWeeklySafe(std::move(m_listener), m_loadingCircle, [this] { + if (!BetterSafe::WEEKLY_SAFE.empty()) loadMonth(); + }); + } + else { + BetterSafe::DAILY_SAFE.clear(); + BetterSafe::loadDailySafe(std::move(m_listener), m_loadingCircle, [this] { + if (!BetterSafe::DAILY_SAFE.empty()) loadMonth(); + }); + } + }); + refreshButton->setPosition(340.0f, 80.0f); + m_buttonMenu->addChild(refreshButton); + + time_t now = time(0); + #ifdef GEODE_IS_WINDOWS + struct tm timeinfo; + localtime_s(&timeinfo, &now); + m_year = timeinfo.tm_year + 1900; + m_month = timeinfo.tm_mon + 1; + #else + auto timeinfo = std::localtime(&now); + m_year = timeinfo->tm_year + 1900; + m_month = timeinfo->tm_mon + 1; + #endif + if (weekly) { + BetterSafe::loadWeeklySafe(std::move(m_listener), m_loadingCircle, [this] { + if (BetterSafe::WEEKLY_SAFE.empty()) return; + + auto firstLevel = BetterSafe::WEEKLY_SAFE[0]; + auto lastLevel = BetterSafe::WEEKLY_SAFE[BetterSafe::WEEKLY_SAFE.size() - 1]; + auto firstDate = std::find_if(firstLevel.dates.begin(), firstLevel.dates.end(), [this](const SafeDate& date) { + return date.year == m_year && date.month == m_month; + }); + if (firstDate == firstLevel.dates.end()) { + auto lastDate = !firstLevel.dates.empty() ? firstLevel.dates[firstLevel.dates.size() - 1] : SafeDate { m_year, m_month, 1 }; + m_currentYear = lastDate.year; + m_currentMonth = lastDate.month; + } else { + m_currentYear = firstDate->year; + m_currentMonth = firstDate->month; + } + auto startDate = !lastLevel.dates.empty() ? lastLevel.dates[0] : SafeDate { m_year, m_month, 1 }; + m_firstYear = startDate.year; + m_firstMonth = startDate.month; + loadMonth(); + }); + } + else { + BetterSafe::loadDailySafe(std::move(m_listener), m_loadingCircle, [this] { + if (BetterSafe::DAILY_SAFE.empty()) return; + + auto firstLevel = BetterSafe::DAILY_SAFE[0]; + auto lastLevel = BetterSafe::DAILY_SAFE[BetterSafe::DAILY_SAFE.size() - 1]; + auto firstDate = !firstLevel.dates.empty() ? firstLevel.dates[0] : SafeDate { m_year, m_month, 1 }; + auto lastDate = !lastLevel.dates.empty() ? lastLevel.dates[0] : SafeDate { m_year, m_month, 1 }; + m_currentYear = firstDate.year; + m_firstYear = lastDate.year; + m_currentMonth = firstDate.month; + m_firstMonth = lastDate.month; + loadMonth(); + }); + } + + handleTouchPriority(this); + + return true; +} + +void BSCalendarPopup::createWeekdayLabel(const char* text, int idx) { + auto label = CCLabelBMFont::create(text, "bigFont.fnt"); + label->setPosition(idx * 38.0f + 36.0f, 249.0f); + label->setScale(0.5f); + m_mainLayer->addChild(label); +} + +void BSCalendarPopup::loadMonth() { + m_loadingCircle->setVisible(true); + + if (m_hoverNode) m_hoverNode->close(); + + m_calendarMenu->removeAllChildren(); + m_selected = nullptr; + m_monthLabel->setString(fmt::format("{} {}", MONTHS[m_month - 1], m_year).c_str()); + m_monthButton->updateSprite(); + m_monthButton->setEnabled(false); + + m_prevButton->setVisible(false); + m_nextButton->setVisible(false); + m_firstButton->setVisible(false); + m_lastButton->setVisible(false); + + auto levelSafe = BetterSafe::getMonth(m_year, m_month, m_weekly); + std::vector ids; + for (auto& level : levelSafe) ids.push_back(std::to_string(level.id)); + auto searchObject = GJSearchObject::create(SearchType::MapPackOnClick, string::join(ids, ",")); + auto glm = GameLevelManager::sharedState(); + if (auto storedLevels = glm->getStoredOnlineLevels(searchObject->getKey())) loadLevelsFinished(storedLevels, searchObject->getKey()); + else glm->getOnlineLevels(searchObject); +} + +void BSCalendarPopup::loadLevelsFinished(CCArray* levels, const char*) { + CC_SAFE_RELEASE(m_levels); + m_levels = levels; + m_levels->retain(); + setupMonth(); +} + +void BSCalendarPopup::setupMonth() { + m_monthButton->setEnabled(true); + m_loadingCircle->setVisible(false); + + m_prevButton->setVisible(m_year != m_firstYear || m_month != m_firstMonth); + m_firstButton->setVisible(m_year != m_firstYear || m_month != m_firstMonth); + m_nextButton->setVisible(m_year != m_currentYear || m_month != m_currentMonth); + m_lastButton->setVisible(m_year != m_currentYear || m_month != m_currentMonth); + + auto daysInMonth = m_month == 2 && m_year % 4 == 0 && (m_year % 100 != 0 || m_year % 400 == 0) ? 29 : DAYS_IN_MONTH[m_month - 1]; + #ifdef GEODE_IS_WINDOWS + struct tm timeinfo = { 0, 0, 0, 1, m_month - 1, m_year - 1900 }; + auto time = mktime(&timeinfo); + localtime_s(&timeinfo, &time); + auto firstWeekday = timeinfo.tm_wday; + #else + tm timeinfo = { 0, 0, 0, 1, m_month - 1, m_year - 1900 }; + auto time = mktime(&timeinfo); + auto firstWeekday = std::localtime(&time)->tm_wday; + #endif + if (!Mod::get()->getSettingValue("sunday-first")) firstWeekday = (firstWeekday + 6) % 7; + + auto levelSafe = BetterSafe::getMonth(m_year, m_month, m_weekly); + std::vector levels; + for (auto level : CCArrayExt(m_levels)) levels.push_back(level); + for (int i = 0; i < daysInMonth; i++) { + auto safeLevelIt = std::find_if(levelSafe.begin(), levelSafe.end(), [this, i](auto const& level) { + return std::any_of(level.dates.begin(), level.dates.end(), [this, i](auto const& date) { + return date.year == m_year && date.month == m_month && date.day == i + 1; + }); + }); + if (safeLevelIt == levelSafe.end()) continue; + auto safeLevel = *safeLevelIt; + auto gameLevelIt = std::find_if(levels.begin(), levels.end(), [safeLevel](auto level) { + return level->m_levelID == safeLevel.id; + }); + if (gameLevelIt == levels.end()) continue; + auto gameLevel = *gameLevelIt; + + auto diffIcon = CCSprite::createWithSpriteFrameName(safeLevel.difficulty == -1 ? + "diffIcon_auto_btn_001.png" : fmt::format("diffIcon_{:02d}_btn_001.png", safeLevel.difficulty).c_str()); + diffIcon->setScale(0.75f); + auto featureFrame = ""; + switch (safeLevel.feature) { + case 1: featureFrame = "GJ_featuredCoin_001.png"; break; + case 2: featureFrame = "GJ_epicCoin_001.png"; break; + case 3: featureFrame = "GJ_epicCoin2_001.png"; break; + case 4: featureFrame = "GJ_epicCoin3_001.png"; break; + } + if (safeLevel.feature > 0) { + auto featureIcon = CCSprite::createWithSpriteFrameName(featureFrame); + featureIcon->setPosition(diffIcon->getContentSize() / 2 + CCPoint { 0.0f, -5.5f }); + diffIcon->addChild(featureIcon, -2); + } + auto hoverButton = CCMenuItemExt::createSpriteExtra(diffIcon, [this, gameLevel, safeLevel](auto sender) { + if (m_hoverNode) m_hoverNode->close(); + if (sender != m_selected) { + m_hoverNode = BSHoverNode::create(safeLevel, gameLevel, [this] { + m_hoverNode = nullptr; + }); + m_hoverNode->setPosition(sender->getPosition() + CCPoint { 0.0f, sender->getContentHeight() / 2 + m_hoverNode->getContentHeight() / 2 + 5.0f }); + m_mainLayer->addChild(m_hoverNode, 200); + m_selected = sender; + } + else m_selected = nullptr; + }); + hoverButton->setPosition({ (i + firstWeekday) % 7 * 38.0f + 36.0f, 219.0f - floorf(((float)i + firstWeekday) / 7) * 38.0f }); + m_calendarMenu->addChild(hoverButton); + } +} + +BSCalendarPopup::~BSCalendarPopup() { + CC_SAFE_RELEASE(m_levels); + CC_SAFE_RELEASE(m_loadingCircle); + auto glm = GameLevelManager::sharedState(); + if (glm->m_levelManagerDelegate == this) glm->m_levelManagerDelegate = nullptr; +} + +BSSelectPopup* BSSelectPopup::create(int year, int month, int minYear, int minMonth, int maxYear, int maxMonth, MiniFunction callback) { + auto ret = new BSSelectPopup(); + if (ret->initAnchored(250.0f, 150.0f, year, month, minYear, minMonth, maxYear, maxMonth, callback)) { + ret->autorelease(); + return ret; + } + delete ret; + return nullptr; +} + +bool BSSelectPopup::setup(int year, int month, int minYear, int minMonth, int maxYear, int maxMonth, MiniFunction callback) { + setTitle("Select Date"); + + m_noElasticity = true; + m_year = year; + m_month = month; + + auto monthLabel = CCLabelBMFont::create(BSCalendarPopup::MONTHS[month - 1].c_str(), "bigFont.fnt"); + monthLabel->setScale(0.9f); + monthLabel->setPosition(125.0f, 100.0f); + m_mainLayer->addChild(monthLabel); + + auto yearLabel = CCLabelBMFont::create(std::to_string(year).c_str(), "bigFont.fnt"); + yearLabel->setScale(0.9f); + yearLabel->setPosition(125.0f, 65.0f); + m_mainLayer->addChild(yearLabel); + + auto prevMonthButton = CCMenuItemExt::createSpriteExtraWithFrameName("GJ_arrow_03_001.png", 0.8f, [this, minMonth, minYear, monthLabel, yearLabel](auto) { + if (m_year == minYear && m_month == minMonth) return; + if (m_month == 1) { + m_year--; + m_month = 12; + } + else m_month--; + monthLabel->setString(BSCalendarPopup::MONTHS[m_month - 1].c_str()); + yearLabel->setString(std::to_string(m_year).c_str()); + }); + prevMonthButton->setPosition(25.0f, 100.0f); + m_buttonMenu->addChild(prevMonthButton); + + auto nextMonthButtonSprite = CCSprite::createWithSpriteFrameName("GJ_arrow_03_001.png"); + nextMonthButtonSprite->setFlipX(true); + nextMonthButtonSprite->setScale(0.8f); + auto nextMonthButton = CCMenuItemExt::createSpriteExtra(nextMonthButtonSprite, [this, maxMonth, maxYear, monthLabel, yearLabel](auto) { + if (m_year == maxYear && m_month == maxMonth) return; + if (m_month == 12) { + m_year++; + m_month = 1; + } + else m_month++; + monthLabel->setString(BSCalendarPopup::MONTHS[m_month - 1].c_str()); + yearLabel->setString(std::to_string(m_year).c_str()); + }); + nextMonthButton->setPosition(225.0f, 100.0f); + m_buttonMenu->addChild(nextMonthButton); + + auto prevYearButton = CCMenuItemExt::createSpriteExtraWithFrameName("GJ_arrow_03_001.png", 0.8f, [this, minMonth, minYear, monthLabel, yearLabel](auto) { + if (m_year == minYear) return; + m_year--; + if (m_year == minYear && m_month < minMonth) m_month = minMonth; + monthLabel->setString(BSCalendarPopup::MONTHS[m_month - 1].c_str()); + yearLabel->setString(std::to_string(m_year).c_str()); + }); + prevYearButton->setPosition(25.0f, 65.0f); + m_buttonMenu->addChild(prevYearButton); + + auto nextYearButtonSprite = CCSprite::createWithSpriteFrameName("GJ_arrow_03_001.png"); + nextYearButtonSprite->setFlipX(true); + nextYearButtonSprite->setScale(0.8f); + auto nextYearButton = CCMenuItemExt::createSpriteExtra(nextYearButtonSprite, [this, maxMonth, maxYear, monthLabel, yearLabel](auto) { + if (m_year == maxYear) return; + m_year++; + if (m_year == maxYear && m_month > maxMonth) m_month = maxMonth; + monthLabel->setString(BSCalendarPopup::MONTHS[m_month - 1].c_str()); + yearLabel->setString(std::to_string(m_year).c_str()); + }); + nextYearButton->setPosition(225.0f, 65.0f); + m_buttonMenu->addChild(nextYearButton); + + auto confirmButton = CCMenuItemExt::createSpriteExtra(ButtonSprite::create("Confirm", "goldFont.fnt", "GJ_button_01.png", 0.8f), [this, callback](auto) { + callback(m_year, m_month); + onClose(nullptr); + }); + confirmButton->setPosition(125.0f, 25.0f); + m_buttonMenu->addChild(confirmButton); + + handleTouchPriority(this); + + return true; +} diff --git a/src/BSCalendarPopup.hpp b/src/BSCalendarPopup.hpp new file mode 100644 index 0000000..a818492 --- /dev/null +++ b/src/BSCalendarPopup.hpp @@ -0,0 +1,63 @@ +#include "BSHoverNode.hpp" + +class BSCalendarPopup : public geode::Popup, public LevelManagerDelegate { +public: + inline static std::vector DAYS_IN_MONTH = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; + inline static std::vector MONTHS = { + "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" + }; +protected: + EventListener m_listener; + bool m_weekly; + int m_year; + int m_firstYear; + int m_currentYear; + int m_month; + int m_firstMonth; + int m_currentMonth; + CCArray* m_levels; + CCMenu* m_calendarMenu; + CCLabelBMFont* m_monthLabel; + CCMenuItemSpriteExtra* m_monthButton; + BSHoverNode* m_hoverNode; + LoadingCircle* m_loadingCircle; + CCMenuItemSpriteExtra* m_selected; + CCMenuItemSpriteExtra* m_prevButton; + CCMenuItemSpriteExtra* m_nextButton; + CCMenuItemSpriteExtra* m_firstButton; + CCMenuItemSpriteExtra* m_lastButton; + + bool setup(DailyLevelPage*, bool) override; + + void createWeekdayLabel(const char* text, int idx); + void loadMonth(); + void setupMonth(); +public: + static BSCalendarPopup* create(DailyLevelPage*, bool); + + void loadLevelsFinished(CCArray* levels, const char* key, int) override { + loadLevelsFinished(levels, key); + } + void loadLevelsFinished(CCArray*, const char*) override; + void loadLevelsFailed(const char* key, int) override { + loadLevelsFailed(key); + } + void loadLevelsFailed(const char*) override { + m_loadingCircle->setVisible(false); + m_loadingCircle->fadeAndRemove(); + FLAlertLayer::create("Load Failed", "Failed to load safe levels. Please try again later.", "OK")->show(); + } + void setupPageInfo(gd::string, const char*) override {} + + ~BSCalendarPopup() override; +}; + +class BSSelectPopup : public geode::Popup> { +protected: + int m_year; + int m_month; + + bool setup(int, int, int, int, int, int, MiniFunction) override; +public: + static BSSelectPopup* create(int, int, int, int, int, int, MiniFunction); +}; diff --git a/src/BSHoverNode.cpp b/src/BSHoverNode.cpp new file mode 100644 index 0000000..8097a10 --- /dev/null +++ b/src/BSHoverNode.cpp @@ -0,0 +1,117 @@ +#include "BSHoverNode.hpp" + +BSHoverNode* BSHoverNode::create(SafeLevel const& level, GJGameLevel* gameLevel, MiniFunction callback) { + auto ret = new BSHoverNode(); + if (ret->init(level, gameLevel, callback)) { + ret->autorelease(); + return ret; + } + delete ret; + return nullptr; +} + +bool BSHoverNode::init(SafeLevel const& level, GJGameLevel* gameLevel, MiniFunction callback) { + if (!CCLayer::init()) return false; + + setAnchorPoint({ 0.5f, 0.5f }); + setContentSize({ 80.0f, 70.0f }); + ignoreAnchorPointForPosition(false); + + CCTouchDispatcher::get()->registerForcePrio(this, 2); + + m_callback = callback; + + auto bg = CCScale9Sprite::createWithSpriteFrameName("square02_001.png"); + bg->setContentSize({ 80.0f, 70.0f }); + bg->setPosition(40.0f, 35.0f); + bg->setColor({ 0, 0, 0 }); + bg->setOpacity(150); + addChild(bg); + + auto dailyLabel = CCLabelBMFont::create(fmt::format("{} #{}", level.weekly ? "Weekly" : "Daily", level.timelyID).c_str(), "goldFont.fnt"); + dailyLabel->setPosition(40.0f, 65.0f); + dailyLabel->setScale(0.3f); + addChild(dailyLabel); + + auto nameLabel = CCLabelBMFont::create(level.name.c_str(), "bigFont.fnt"); + nameLabel->setPosition(40.0f, 55.0f); + nameLabel->setScale(0.5f); + addChild(nameLabel); + + auto creatorLabel = CCLabelBMFont::create(fmt::format("by {}", level.creator).c_str(), "goldFont.fnt"); + creatorLabel->setPosition(40.0f, 43.0f); + creatorLabel->setScale(0.4f); + addChild(creatorLabel); + + setContentWidth(std::max(nameLabel->getScaledContentWidth() + 6.0f, std::max(creatorLabel->getScaledContentWidth() + 6.0f, 80.0f))); + + bg->setContentWidth(getContentWidth()); + bg->setPositionX(getContentWidth() / 2); + dailyLabel->setPositionX(getContentWidth() / 2); + nameLabel->setPositionX(getContentWidth() / 2); + creatorLabel->setPositionX(getContentWidth() / 2); + + auto starLayout = CCNode::create(); + starLayout->setPosition(getContentWidth() / 2, 30.0f); + starLayout->setContentSize({ 80.0f, 15.0f }); + starLayout->setAnchorPoint({ 0.5f, 0.5f }); + starLayout->setLayout(RowLayout::create()->setGap(1.75f)->setAutoScale(false)); + addChild(starLayout); + + auto gsm = GameStatsManager::sharedState(); + auto starsLabel = CCLabelBMFont::create(std::to_string(level.stars).c_str(), "bigFont.fnt"); + starsLabel->setScale(0.4f); + auto completedLevel = gsm->m_completedLevels->objectForKey(fmt::format("c_{}", level.id)); + starsLabel->setColor(completedLevel ? ccColor3B { 255, 255, 50 } : ccColor3B { 255, 255, 255 }); + starLayout->addChild(starsLabel); + + starLayout->addChild(CCSprite::createWithSpriteFrameName("star_small01_001.png")); + + for (int i = 1; i <= level.coins; i++) { + auto coinStr = fmt::format("{}_{}", level.id, i); + auto hasCoin = gsm->hasUserCoin(coinStr.c_str()) || gsm->hasPendingUserCoin(coinStr.c_str()); + auto coinSprite = CCSprite::createWithSpriteFrameName("usercoin_small01_001.png"); + if (level.coinsVerified) coinSprite->setColor(hasCoin ? ccColor3B { 255, 255, 255 } : ccColor3B { 165, 165, 165 }); + else coinSprite->setColor(hasCoin ? ccColor3B { 255, 175, 75 } : ccColor3B { 165, 113, 48 }); + starLayout->addChild(coinSprite); + } + + starLayout->updateLayout(); + + auto viewMenu = CCMenu::create(); + viewMenu->setPosition(getContentWidth() / 2, 12.0f); + addChild(viewMenu); + + auto viewSprite = ButtonSprite::create("View", "goldFont.fnt", "GJ_button_01.png", 0.8f); + viewSprite->setScale(0.7f); + viewMenu->addChild(CCMenuItemExt::createSpriteExtra(viewSprite, [this, gameLevel](auto) { + GameLevelManager::sharedState()->gotoLevelPage(gameLevel); + })); + + auto closeButton = CCMenuItemExt::createSpriteExtraWithFrameName("GJ_closeBtn_001.png", 0.5f, [this] (auto) { close(); }); + closeButton->setPosition(viewMenu->convertToNodeSpace(convertToWorldSpace({ 0.0f, 70.0f }))); + viewMenu->addChild(closeButton); + + setTouchEnabled(true); + handleTouchPriority(this); + + return true; +} + +void BSHoverNode::registerWithTouchDispatcher() { + CCTouchDispatcher::get()->addTargetedDelegate(this, -500, true); +} + +void BSHoverNode::keyBackClicked() { + close(); +} + +void BSHoverNode::close() { + setTouchEnabled(false); + removeFromParent(); + m_callback(); +} + +BSHoverNode::~BSHoverNode() { + CCTouchDispatcher::get()->unregisterForcePrio(this); +} diff --git a/src/BSHoverNode.hpp b/src/BSHoverNode.hpp new file mode 100644 index 0000000..2d1c31e --- /dev/null +++ b/src/BSHoverNode.hpp @@ -0,0 +1,16 @@ +#include "BetterSafe.hpp" + +class BSHoverNode : public CCLayer { +protected: + MiniFunction m_callback; + + bool init(SafeLevel const&, GJGameLevel*, MiniFunction); +public: + static BSHoverNode* create(SafeLevel const&, GJGameLevel*, MiniFunction); + + void keyBackClicked() override; + void registerWithTouchDispatcher() override; + void close(); + + ~BSHoverNode() override; +}; diff --git a/src/BetterSafe.cpp b/src/BetterSafe.cpp new file mode 100644 index 0000000..434ddab --- /dev/null +++ b/src/BetterSafe.cpp @@ -0,0 +1,111 @@ +#include "BetterSafe.hpp" + +#define DAILY_SAFE_URL "https://raw.githubusercontent.com/hiimjustin000/the-safe/master/daily.json" +#define WEEKLY_SAFE_URL "https://raw.githubusercontent.com/hiimjustin000/the-safe/master/weekly.json" + +SafeDate BetterSafe::parseDate(const std::string& date) { + auto parts = string::split(date, "-"); + return { + .year = parts.size() > 0 ? numFromString(parts[0]).value_or(1970) : 1970, + .month = parts.size() > 1 ? numFromString(parts[1]).value_or(1) : 1, + .day = parts.size() > 2 ? numFromString(parts[2]).value_or(1) : 1 + }; +} + +#define PROPERTY_OR_DEFAULT(obj, prop, isFunc, asFunc, def) (obj.contains(prop) && obj[prop].isFunc() ? obj[prop].asFunc() : def) + +void BetterSafe::loadDailySafe(EventListener&& listenerRef, LoadingCircle* circle, MiniFunction callback) { + if (DAILY_SAFE.size()) return callback(); + + auto&& listener = std::move(listenerRef); + + listener.bind([callback, circle](web::WebTask::Event* e) { + if (auto res = e->getValue()) { + if (res->ok()) { + auto str = res->string().value(); + std::string error; + auto json = matjson::parse(str, error).value_or(matjson::Array()); + for (auto& level : json.as_array()) { + std::vector dates; + dates.push_back(parseDate(PROPERTY_OR_DEFAULT(level, "date", is_string, as_string, "1970-01-01"))); + if (dates[0].year == 1970 && dates[0].month == 1 && dates[0].day == 1) dates.clear(); + DAILY_SAFE.push_back({ + .id = PROPERTY_OR_DEFAULT(level, "id", is_number, as_int, 0), + .timelyID = PROPERTY_OR_DEFAULT(level, "dailyID", is_number, as_int, 0), + .dates = dates, + .name = PROPERTY_OR_DEFAULT(level, "name", is_string, as_string, ""), + .creator = PROPERTY_OR_DEFAULT(level, "creator", is_string, as_string, ""), + .stars = PROPERTY_OR_DEFAULT(level, "stars", is_number, as_int, 0), + .difficulty = PROPERTY_OR_DEFAULT(level, "difficulty", is_number, as_int, 0), + .feature = PROPERTY_OR_DEFAULT(level, "feature", is_number, as_int, 0), + .coins = PROPERTY_OR_DEFAULT(level, "coins", is_number, as_int, 0), + .coinsVerified = PROPERTY_OR_DEFAULT(level, "coinsVerified", is_bool, as_bool, false), + .weekly = false + }); + } + callback(); + } + else { + FLAlertLayer::create(fmt::format("Load Failed ({})", res->code()).c_str(), "Failed to load the daily safe. Please try again later.", "OK")->show(); + circle->setVisible(false); + } + } + }); + + listener.setFilter(web::WebRequest().get(DAILY_SAFE_URL)); +} + +void BetterSafe::loadWeeklySafe(EventListener&& listenerRef, LoadingCircle* circle, MiniFunction callback) { + if (WEEKLY_SAFE.size()) return callback(); + + auto&& listener = std::move(listenerRef); + + listener.bind([callback, circle](web::WebTask::Event* e) { + if (auto res = e->getValue()) { + if (res->ok()) { + auto str = res->string().value(); + std::string error; + auto json = matjson::parse(str, error).value_or(matjson::Array()); + for (auto& level : json.as_array()) { + std::vector dates; + if (level.contains("dates") && level["dates"].is_array()) { + for (auto& date : level["dates"].as_array()) { + dates.push_back(parseDate(date.as_string())); + } + } + WEEKLY_SAFE.push_back({ + .id = PROPERTY_OR_DEFAULT(level, "id", is_number, as_int, 0), + .timelyID = PROPERTY_OR_DEFAULT(level, "weeklyID", is_number, as_int, 0), + .dates = dates, + .name = PROPERTY_OR_DEFAULT(level, "name", is_string, as_string, ""), + .creator = PROPERTY_OR_DEFAULT(level, "creator", is_string, as_string, ""), + .stars = PROPERTY_OR_DEFAULT(level, "stars", is_number, as_int, 0), + .difficulty = PROPERTY_OR_DEFAULT(level, "difficulty", is_number, as_int, 0), + .feature = PROPERTY_OR_DEFAULT(level, "feature", is_number, as_int, 0), + .coins = PROPERTY_OR_DEFAULT(level, "coins", is_number, as_int, 0), + .coinsVerified = PROPERTY_OR_DEFAULT(level, "coinsVerified", is_bool, as_bool, false), + .weekly = true + }); + } + callback(); + } + else { + FLAlertLayer::create(fmt::format("Load Failed ({})", res->code()).c_str(), "Failed to load the weekly safe. Please try again later.", "OK")->show(); + circle->setVisible(false); + } + } + }); + + listener.setFilter(web::WebRequest().get(WEEKLY_SAFE_URL)); +} + +std::vector BetterSafe::getMonth(int year, int month, bool weekly) { + std::vector levels; + auto& safe = weekly ? WEEKLY_SAFE : DAILY_SAFE; + for (auto& level : safe) { + for (auto& date : level.dates) { + if (date.year == year && date.month == month) levels.push_back(level); + } + } + return levels; +} diff --git a/src/BetterSafe.hpp b/src/BetterSafe.hpp new file mode 100644 index 0000000..018179c --- /dev/null +++ b/src/BetterSafe.hpp @@ -0,0 +1,35 @@ +#include +#include + +using namespace geode::prelude; + +struct SafeDate { + int year; + int month; + int day; +}; + +struct SafeLevel { + int id; + int timelyID; + std::vector dates; + std::string name; + std::string creator; + int stars; + int difficulty; + int feature; + int coins; + bool coinsVerified; + bool weekly; +}; + +class BetterSafe { +public: + inline static std::vector DAILY_SAFE; + inline static std::vector WEEKLY_SAFE; + + static SafeDate parseDate(const std::string&); + static void loadDailySafe(EventListener&&, LoadingCircle*, MiniFunction); + static void loadWeeklySafe(EventListener&&, LoadingCircle*, MiniFunction); + static std::vector getMonth(int year, int month, bool weekly); +}; diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..17f464d --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,9 @@ +#include "BSCalendarPopup.hpp" + +#include +class $modify(BSDailyLevelPage, DailyLevelPage) { + void onTheSafe(CCObject* sender) { + if (sender->getTag() == 91508) DailyLevelPage::onTheSafe(sender); + else BSCalendarPopup::create(this, m_type == GJTimedLevelType::Weekly)->show(); + } +};