From 38e4243b4d42a27806f961a6412221a202cd6838 Mon Sep 17 00:00:00 2001 From: Cheeseworks Date: Tue, 19 May 2026 21:35:55 -0500 Subject: [PATCH 01/12] first draft --- mod.json | 17 +++ src/Utils.h | 1 + src/hooks/Flicks.cpp | 62 ++++++++++ src/hooks/obstructive/Captchas.cpp | 92 +++++++++++++++ src/util/ui/Captcha.hpp | 51 ++++++++ src/util/ui/src/Captcha.cpp | 180 +++++++++++++++++++++++++++++ 6 files changed, 403 insertions(+) create mode 100644 src/hooks/Flicks.cpp create mode 100644 src/hooks/obstructive/Captchas.cpp create mode 100644 src/util/ui/Captcha.hpp create mode 100644 src/util/ui/src/Captcha.cpp diff --git a/mod.json b/mod.json index 58a615f..0c227af 100644 --- a/mod.json +++ b/mod.json @@ -320,6 +320,23 @@ "big-arrow-step": 20 } }, + "captcha-chance": { + "type": "int", + "name": "Captcha Chance", + "description": "Verify Your Captchas... - Chance of the captcha appearing in a level.", + "default": 20, + "min": 0, + "max": 100, + "control": { + "input": true, + "slider": true, + "slider-step": 1, + "arrows": true, + "arrow-step": 10, + "big-arrows": true, + "big-arrow-step": 20 + } + }, "mock-chance": { "type": "int", "name": "Mock Chance", diff --git a/src/Utils.h b/src/Utils.h index ee56018..370be83 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -9,6 +9,7 @@ #include #include +#include #include #include #include diff --git a/src/hooks/Flicks.cpp b/src/hooks/Flicks.cpp new file mode 100644 index 0000000..204a884 --- /dev/null +++ b/src/hooks/Flicks.cpp @@ -0,0 +1,62 @@ +#include + +#include + +#include + +using namespace geode::prelude; +using namespace horrible::prelude; + +#define THIS_ID "flick" + +static auto const o = Option::create(THIS_ID) + ->setName("Flicks") + ->setDescription("Every time you respawn in a level, whether it be from the beginning or a checkpoint, one of your options get toggled on or off.\nCareful! It might break some things...\nsuggested by scr33n_p4r45173") + ->setCategory(category::misc) + ->setSillyTier(SillyTier::High) + ->setCheating(true) + ->autoRegister(); + +class $modify(FlicksPlayLayer, PlayLayer) { + struct Fields final { + std::vector> options = OptionManager::get()->getOptions(); + + bool firstTime = true; + }; + + void resetLevel() { + PlayLayer::resetLevel(); + + auto f = m_fields.self(); + + if (f->firstTime) { + f->firstTime = false; + return; + }; + + if (f->options.empty()) return; + + auto opt = f->options[rng::get(f->options.size() - 1)]; + if (auto o = opt.lock()) { + if (o->getID() == THIS_ID || !platformCompat(o->getSupportedPlatforms())) return Notification::create("Whoops! Missed an option!", NotificationIcon::Warning, 0.25f)->show(); + + queueInMainThread([opt]() { + if (auto o = opt.lock()) { + o->isEnabled() ? o->disable() : o->enable(); + log::warn("Flicked option {} {} due to flicks", o->getID(), o->isEnabled() ? "ON" : "OFF"); + + sfx::play(sfx::file::bad); + Notification::create(fmt::format("Flicked {} ({}) {}", o->getName(), o->getCategory(), o->isEnabled() ? "ON" : "OFF").c_str(), NotificationIcon::Warning, 0.5f)->show(); + }; + }); + }; + }; + + bool platformCompat(std::span plats) { + for (auto const& p : plats) { + if (p & GEODE_PLATFORM_TARGET) return true; + }; + + return false; + }; +}; \ No newline at end of file diff --git a/src/hooks/obstructive/Captchas.cpp b/src/hooks/obstructive/Captchas.cpp new file mode 100644 index 0000000..44c971e --- /dev/null +++ b/src/hooks/obstructive/Captchas.cpp @@ -0,0 +1,92 @@ +#include + +#include + +#include + +using namespace geode::prelude; +using namespace horrible::prelude; + +#define THIS_ID "captcha" + +static auto const o = Option::create(THIS_ID) + ->setName("Verify Your Captchas...") + ->setDescription("Occasionally, you will be prompted to verify you're not a bot with a good ole' captcha.\nsuggested by bonieGPT") + ->setCategory(category::obstructive) + ->setSillyTier(SillyTier::High) + ->setCheating(true) + ->autoRegister(); + +class $modify(CaptchaPlayLayer, PlayLayer) { + HORRIBLE_DELEGATE_HOOKS(THIS_ID); + + struct Fields final { + uint8_t chance = options::getChance(THIS_ID); + + Ref currentCaptcha = nullptr; + }; + + void setupHasCompleted() { + PlayLayer::setupHasCompleted(); + nextCaptcha(); + }; + + void resetLevelFromStart() { + PlayLayer::resetLevelFromStart(); + cue::resetNode(m_fields->currentCaptcha); + }; + + void levelComplete() { + PlayLayer::levelComplete(); + cue::resetNode(m_fields->currentCaptcha); + }; + + void destroyPlayer(PlayerObject* player, GameObject* object) { + PlayLayer::destroyPlayer(player, object); + + if (player->m_isDead) { + cue::resetNode(m_fields->currentCaptcha); + nextCaptcha(); + }; + }; + + void nextCaptcha() { + log::trace("scheduling captcha"); + + unschedule(schedule_selector(CaptchaPlayLayer::doCaptcha)); + if (!m_hasCompletedLevel) scheduleOnce(schedule_selector(CaptchaPlayLayer::doCaptcha), rng::get(30.f, 5.f) * chanceToDelayPct(m_fields->chance)); + }; + + void doCaptcha(float) { + auto f = m_fields.self(); + + if (options::isEnabled(THIS_ID) && !f->currentCaptcha && !m_hasCompletedLevel && !m_playerDied) { + if (auto captcha = Captcha::create()) { + captcha->setCallback([this](bool success) { + cursor::hide(); + m_player1->m_playerSpeed = 1.f; + m_player2->m_playerSpeed = 1.f; + + if (!success) { + Notification::create("Knew you were a robot...", NotificationIcon::Error)->show(); + resetLevelFromStart(); + }; + + cue::resetNode(m_fields->currentCaptcha); + }); + + captcha->show(); + f->currentCaptcha = captcha; + + cursor::show(); + + m_player1->m_playerSpeed = 0.125f; + m_player2->m_playerSpeed = 0.125f; + }; + }; + + queueInMainThread([self = WeakRef(this)]() { + if (auto s = self.lock()) s->nextCaptcha(); + }); + }; +}; \ No newline at end of file diff --git a/src/util/ui/Captcha.hpp b/src/util/ui/Captcha.hpp new file mode 100644 index 0000000..2d68d54 --- /dev/null +++ b/src/util/ui/Captcha.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include + +using namespace geode::prelude; + +namespace horrible { + namespace ui { + class Captcha final : public Popup { + using Callback = Function; + + private: + class Impl; + std::unique_ptr m_impl; + + protected: + Captcha(); + ~Captcha(); + + void callAfterFeedback(float); + void setSuccess(bool v); + + void update(float dt) override; + + bool init() override; + + public: + static Captcha* create(); + + void setCallback(Callback&& cb); + }; + + class RobotVerifier final : public CCNode { + using Callback = Function; + + private: + std::string m_correctID = ""; + Callback m_callback = nullptr; + + void addNewBtn(); + + protected: + void validateBtns(); + + bool init(std::string id, Callback&& cb); + + public: + static RobotVerifier* create(std::string id, Callback&& cb); + }; + }; +}; \ No newline at end of file diff --git a/src/util/ui/src/Captcha.cpp b/src/util/ui/src/Captcha.cpp new file mode 100644 index 0000000..f26e855 --- /dev/null +++ b/src/util/ui/src/Captcha.cpp @@ -0,0 +1,180 @@ +#include "../Captcha.hpp" + +#include + +#include + +using namespace geode::prelude; +using namespace horrible::prelude; + +// id, sprite name +static constexpr auto s_buttons = std::to_array>({ + {"", ""}, +}); + +class Captcha::Impl final { +public: + std::string correctID = ""; + + RobotVerifier* verifier = nullptr; + + ProgressBar* countdown = nullptr; + + float totalTime = 10.f; + float timeRemaining = totalTime; + float timeDt = 0.f; + + bool success = false; + Callback callback = nullptr; +}; + +Captcha::Captcha() : m_impl(std::make_unique()) {}; +Captcha::~Captcha() {}; + +bool Captcha::init() { + auto const theme = mod->getSettingValue("theme"); + + if (!Popup::init({375.f, 250.f}, themes::getBackgroundSprite(theme))) return false; + + setID("captcha"_spr); + setTitle("Woah there!"); + setKeypadEnabled(false); + setKeyboardEnabled(false); + setCloseButtonSpr(CircleButtonSprite::createWithSpriteFrameName(themes::close, 0.875f, themes::getCircleBaseColor(theme))); + + popup::closeBtnID(m_closeBtn); + + m_closeBtn->setVisible(false); + m_closeBtn->setEnabled(false); + + auto label = CCLabelBMFont::create("You're playing almost too well... Are you sure you're not a robot?", "chatFont.fnt"); + label->setID("message"); + label->setScale(0.875f); + label->setAlignment(kCCTextAlignmentCenter); + label->setPosition({m_mainLayer->getScaledContentWidth() / 2.f, m_mainLayer->getScaledContentHeight() - 37.5f}); + label->setAnchorPoint(anchor::center); + + m_mainLayer->addChild(label); + + m_impl->countdown = ProgressBar::create(ProgressBarStyle::Solid); + m_impl->countdown->setID("countdown"); + m_impl->countdown->setScale(0.625f); + m_impl->countdown->setFillColor(colors::yellow); + m_impl->countdown->setAnchorPoint(anchor::center); + m_impl->countdown->setPosition({m_mainLayer->getScaledContentWidth() / 2.f, 12.5f}); + + m_impl->countdown->updateProgress(100.f); + + m_mainLayer->addChild(m_impl->countdown); + + scheduleUpdate(); + + sfx::play(sfx::file::pop); + + return true; +}; + +void Captcha::setCallback(Callback&& cb) { + m_impl->callback = std::move(cb); +}; + +void Captcha::callAfterFeedback(float) { + if (m_impl->callback) m_impl->callback(m_impl->success); + unscheduleAllSelectors(); +}; + +void Captcha::setSuccess(bool v) { + m_impl->success = v; + + auto symbol = CCSprite::createWithSpriteFrameName(m_impl->success ? "GJ_completesIcon_001.png" : "GJ_deleteIcon_001.png"); + symbol->setID("success-icon"); + symbol->setScale(0.f); + symbol->setPosition(m_mainLayer->getScaledContentSize() / 2.f); + + m_mainLayer->addChild(symbol, 9); + + symbol->runAction(CCSequence::createWithTwoActions( + CCEaseSineOut::create(CCScaleTo::create(0.0875f, 2.75f)), + CCEaseSineOut::create(CCScaleTo::create(0.125f, 2.5f)))); + + sfx::play(m_impl->success ? sfx::file::good : sfx::file::bad); + scheduleOnce(schedule_selector(Captcha::callAfterFeedback), 1.25f); +}; + +void Captcha::update(float dt) { + if (m_impl->timeRemaining <= 0.f) return unscheduleUpdate(); + m_impl->timeRemaining -= dt; + + m_impl->timeDt += dt; + if (m_impl->timeDt >= 0.5f) { + // @geode-ignore(unknown-resource) + sfx::play("counter003.ogg"); + m_impl->timeDt = 0.f; + }; + + if (m_impl->timeRemaining < 0.f) m_impl->timeRemaining = 0.f; + auto pct = (m_impl->timeRemaining / m_impl->totalTime) * 100.f; + + if (m_impl->countdown) m_impl->countdown->updateProgress(pct); + + if (m_impl->timeRemaining <= 0.f) { + setSuccess(false); + unscheduleUpdate(); + }; +}; + +Captcha* Captcha::create() { + auto ret = new Captcha(); + if (ret->init()) { + ret->autorelease(); + return ret; + }; + + delete ret; + return nullptr; +}; + +bool RobotVerifier::init(std::string id, Callback&& cb) { + m_correctID = std::move(id); + m_callback = std::move(cb); + + if (!CCNode::init()) return false; + + auto layout = RowLayout::create() + ->setGap(2.5f) + ->setGrowCrossAxis(true); + + setID("captcha-verifier"); + setAnchorPoint(anchor::center); + setLayout(layout); + + for (int i = 0; i < 9; ++i) { + auto btnData = s_buttons[rng::get(s_buttons.size() - 1)]; + + auto btn = Button::createWithSpriteFrameName( + btnData.second, + [this, id = btnData.first](auto) { + if (id == m_correctID) { + m_callback(true); + } else { + m_callback(false); + }; + }); + btn->setID(btnData.first); + + cue::rescaleToMatch(btn, 25.f); + }; + + return true; +}; + +RobotVerifier* RobotVerifier::create(std::string id, Callback&& cb) { + auto ret = new RobotVerifier(); + if (ret->init(std::move(id), std::move(cb))) { + ret->autorelease(); + return ret; + }; + + delete ret; + return nullptr; +}; \ No newline at end of file From 64e3fa00dd6a09c2456697c502c743896e11ebe7 Mon Sep 17 00:00:00 2001 From: Cheeseworks Date: Fri, 22 May 2026 14:22:19 -0500 Subject: [PATCH 02/12] finish captcha impl --- src/Utils.h | 2 + src/hooks/Flicks.cpp | 2 + src/util/ui/Captcha.hpp | 9 ++-- src/util/ui/src/Captcha.cpp | 103 ++++++++++++++++++++++++++++-------- 4 files changed, 89 insertions(+), 27 deletions(-) diff --git a/src/Utils.h b/src/Utils.h index 910f801..525385a 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -26,6 +26,8 @@ #include +#include + #include #define HIGHEST_Z cocos2d::CCScene::get()->getHighestChildZ() + 1 diff --git a/src/hooks/Flicks.cpp b/src/hooks/Flicks.cpp index 204a884..1f92278 100644 --- a/src/hooks/Flicks.cpp +++ b/src/hooks/Flicks.cpp @@ -18,6 +18,8 @@ static auto const o = Option::create(THIS_ID) ->autoRegister(); class $modify(FlicksPlayLayer, PlayLayer) { + HORRIBLE_DELEGATE_HOOKS(THIS_ID); + struct Fields final { std::vector> options = OptionManager::get()->getOptions(); diff --git a/src/util/ui/Captcha.hpp b/src/util/ui/Captcha.hpp index 2d68d54..1007929 100644 --- a/src/util/ui/Captcha.hpp +++ b/src/util/ui/Captcha.hpp @@ -2,6 +2,8 @@ #include +#include + using namespace geode::prelude; namespace horrible { @@ -10,7 +12,7 @@ namespace horrible { using Callback = Function; private: - class Impl; + struct Impl; std::unique_ptr m_impl; protected: @@ -34,14 +36,13 @@ namespace horrible { using Callback = Function; private: - std::string m_correctID = ""; + std::string m_correct = ""; Callback m_callback = nullptr; void addNewBtn(); + void validateBtns(geode::Button* called); protected: - void validateBtns(); - bool init(std::string id, Callback&& cb); public: diff --git a/src/util/ui/src/Captcha.cpp b/src/util/ui/src/Captcha.cpp index f26e855..88c2759 100644 --- a/src/util/ui/src/Captcha.cpp +++ b/src/util/ui/src/Captcha.cpp @@ -8,14 +8,18 @@ using namespace geode::prelude; using namespace horrible::prelude; // id, sprite name -static constexpr auto s_buttons = std::to_array>({ - {"", ""}, +static constexpr auto g_buttons = std::to_array>({ + {"The Yellow One", "icon_yellow.png"_spr}, + {"Furry", "icon_colonthree.png"_spr}, + {"Extreme David", "diffIcon_10_btn_001.png"}, + {"Money", "currencyOrbIcon_001.png"}, + {"Scary Demon", "GJ_rateDiffBtnMod_001.png"}, + {"Compact Lists", "GJ_smallModeIcon_001.png"}, + {"Subscribe to Breakeode", "gj_ytIcon_001.png"}, + {"Globed Death Effect", "explosionIcon_20_001.png"}, }); -class Captcha::Impl final { -public: - std::string correctID = ""; - +struct Captcha::Impl final { RobotVerifier* verifier = nullptr; ProgressBar* countdown = nullptr; @@ -67,6 +71,29 @@ bool Captcha::init() { m_mainLayer->addChild(m_impl->countdown); + auto const& btnID = g_buttons[rng::get(g_buttons.size() - 1)].first; + + m_impl->verifier = RobotVerifier::create(btnID, [this](bool success) { + setSuccess(success); + }); + m_impl->verifier->setPosition(m_mainLayer->getScaledContentSize() / 2.f); + + m_mainLayer->addChild(m_impl->verifier, 9); + + auto hint = SimpleTextArea::create("Press all the buttons with", "chatFont.fnt", 0.375f, m_mainLayer->getScaledContentWidth() - 25.f); + hint->setID("hint"); + hint->setAlignment(kCCTextAlignmentCenter); + hint->setPosition({m_impl->verifier->getPositionX(), m_impl->verifier->getPositionY() - (m_impl->verifier->getScaledContentHeight() / 2.f) - 10.f}); + + m_mainLayer->addChild(hint); + + auto hintID = SimpleTextArea::create(btnID, "bigFont.fnt", 0.5f, m_mainLayer->getScaledContentWidth() - 25.f); + hintID->setID("hint-id"); + hintID->setAlignment(kCCTextAlignmentCenter); + hintID->setPosition({m_impl->verifier->getPositionX(), hint->getPositionY() - 12.5f}); + + m_mainLayer->addChild(hintID, 1); + scheduleUpdate(); sfx::play(sfx::file::pop); @@ -86,6 +113,8 @@ void Captcha::callAfterFeedback(float) { void Captcha::setSuccess(bool v) { m_impl->success = v; + cue::resetNode(m_impl->verifier); + auto symbol = CCSprite::createWithSpriteFrameName(m_impl->success ? "GJ_completesIcon_001.png" : "GJ_deleteIcon_001.png"); symbol->setID("success-icon"); symbol->setScale(0.f); @@ -134,37 +163,65 @@ Captcha* Captcha::create() { return nullptr; }; +void RobotVerifier::addNewBtn() { + auto const& btnData = g_buttons[rng::get(g_buttons.size() - 1)]; + + auto btn = Button::createWithSpriteFrameName( + btnData.second, + [this, &id = btnData.first](auto sender) { + sfx::play("chestClick.ogg"); + + if (id == m_correct) { + validateBtns(sender); + } else { + m_callback(false); + }; + }); + btn->setID(btnData.first); + + addChild(btn); + + cue::rescaleToMatch(btn, 32.5f); +}; + +void RobotVerifier::validateBtns(Button* called) { + cue::resetNode(called); + updateLayout(); + + for (auto const& btn : getChildrenExt