diff --git a/mod.json b/mod.json index 6455b83..dc22fdc 100644 --- a/mod.json +++ b/mod.json @@ -267,6 +267,40 @@ "big-arrow-step": 20 } }, + "tidal_wave-chance": { + "type": "int", + "name": "Tidal Wave Chance", + "description": "'Tidal Wave' Jumpscare! - Chance of your level switching to the level named 'Tidal Wave'.", + "default": 15, + "min": 0, + "max": 100, + "control": { + "input": true, + "slider": true, + "slider-step": 1, + "arrows": true, + "arrow-step": 10, + "big-arrows": true, + "big-arrow-step": 20 + } + }, + "troll_level-chance": { + "type": "int", + "name": "Troll Level Chance", + "description": "You're Getting Trolled - Chance of your level switching to Congregation.", + "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 + } + }, "whack_a_face-chance": { "type": "int", "name": "Whack-A-Face Chance", @@ -335,6 +369,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 765792e..edbf425 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -25,6 +26,8 @@ #include +#include + #include #define HIGHEST_Z cocos2d::CCScene::get()->getHighestChildZ() + 1 @@ -93,6 +96,7 @@ namespace horrible { namespace category { inline constexpr auto playerlife = "Player Life"; inline constexpr auto jumpscares = "Jumpscares"; + inline constexpr auto mechanics = "Mechanics"; inline constexpr auto randoms = "Randoms"; inline constexpr auto chances = "Chances"; inline constexpr auto obstructive = "Obstructive"; diff --git a/src/hooks/DoubleJump.cpp b/src/hooks/DoubleJump.cpp deleted file mode 100644 index 8dc3bd3..0000000 --- a/src/hooks/DoubleJump.cpp +++ /dev/null @@ -1,39 +0,0 @@ -#include - -#include - -#include - -using namespace geode::prelude; -using namespace horrible::prelude; - -#define THIS_ID "double_jump" - -static auto const o = Option::create(THIS_ID) - ->setName("Double-Jump") - ->setDescription("Allows your character to double-jump in a level.\ncreated by Cheeseworks") - ->setCategory(category::misc) - ->setSillyTier(SillyTier::Low) - ->setCheating(true) - ->autoRegister(); - -class $modify(DoubleJumpPlayerObject, PlayerObject) { - HORRIBLE_DELEGATE_HOOKS(THIS_ID); - - struct Fields final { - uint8_t jumps = 0; - }; - - bool pushButton(PlayerButton p0) { - auto f = m_fields.self(); - - if (p0 == PlayerButton::Jump) { - if (m_isOnGround) f->jumps = 0; - if (!m_isOnGround) f->jumps++; - }; - - m_isOnGround = f->jumps < 2; - - return PlayerObject::pushButton(p0); - }; -}; \ No newline at end of file diff --git a/src/hooks/Flicks.cpp b/src/hooks/Flicks.cpp new file mode 100644 index 0000000..1f92278 --- /dev/null +++ b/src/hooks/Flicks.cpp @@ -0,0 +1,64 @@ +#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) { + HORRIBLE_DELEGATE_HOOKS(THIS_ID); + + 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/jumpscares/ForceLevels.cpp b/src/hooks/jumpscares/ForceLevels.cpp index 53a38eb..3402a2d 100644 --- a/src/hooks/jumpscares/ForceLevels.cpp +++ b/src/hooks/jumpscares/ForceLevels.cpp @@ -13,6 +13,7 @@ using namespace horrible::prelude; #define THIS_ID_GRIEF "grief" #define THIS_ID_CONGREG "congregation" +#define THIS_ID_TIDAL "tidal_wave" static auto const oGrief = Option::create(THIS_ID_GRIEF) ->setName("Get Back on Grief") @@ -20,7 +21,6 @@ static auto const oGrief = Option::create(THIS_ID_GRIEF) ->setCategory(category::jumpscares) ->setSillyTier(SillyTier::High) ->setOnline(true) - ->setCheating(true) ->autoRegister(); static auto const oCongreg = Option::create(THIS_ID_CONGREG) @@ -29,9 +29,16 @@ static auto const oCongreg = Option::create(THIS_ID_CONGREG) ->setCategory(category::jumpscares) ->setSillyTier(SillyTier::High) ->setOnline(true) - ->setCheating(true) ->autoRegister(); +static auto const oTidal = Option::create(THIS_ID_TIDAL) + ->setName("'Tidal Wave' Jumpscare!") + ->setDescription("A chance of forcing you to play the level 'Tidal Wave' when you die in a level. The level called 'Tidal Wave'. That one.\nsuggested by liliam25") + ->setCategory(category::jumpscares) + ->setSillyTier(SillyTier::Medium) + ->setOnline(true) + ->autoRegister(); + static StringMap g_jsMap; static std::vector> g_jsHookVector; @@ -39,6 +46,7 @@ namespace js_internal { static constexpr auto getLevelInfo(std::string_view id) noexcept { if (id == THIS_ID_GRIEF) return jumpscares::level::grief; if (id == THIS_ID_CONGREG) return jumpscares::level::congregation; + if (id == THIS_ID_TIDAL) return jumpscares::level::tidal; return jumpscares::level::grief; }; @@ -68,6 +76,7 @@ namespace js_internal { $on_mod(Loaded) { js_internal::toggleOption(THIS_ID_GRIEF, options::isEnabled(THIS_ID_GRIEF)); js_internal::toggleOption(THIS_ID_CONGREG, options::isEnabled(THIS_ID_CONGREG)); + js_internal::toggleOption(THIS_ID_TIDAL, options::isEnabled(THIS_ID_TIDAL)); listenForHorribleOptionChanges( THIS_ID_GRIEF, @@ -80,6 +89,12 @@ namespace js_internal { [](HorribleOptionSave data) { js_internal::toggleOption(THIS_ID_CONGREG, data.enabled); }); + + listenForHorribleOptionChanges( + THIS_ID_TIDAL, + [](HorribleOptionSave data) { + js_internal::toggleOption(THIS_ID_TIDAL, data.enabled); + }); }; static void tryJumpscare(bool useReplay) { diff --git a/src/hooks/jumpscares/TrollLevel.cpp b/src/hooks/jumpscares/TrollLevel.cpp new file mode 100644 index 0000000..e96175c --- /dev/null +++ b/src/hooks/jumpscares/TrollLevel.cpp @@ -0,0 +1,75 @@ +#include + +#include + +#include + +using namespace geode::prelude; +using namespace horrible::prelude; + +#define THIS_ID "troll_level" + +static auto const o = Option::create(THIS_ID) + ->setName("You're Getting Trolled") + ->setDescription("A chance when loading a level, to instead load a funny troll level.\ncreated by Cheeseworks") + ->setCategory(category::jumpscares) + ->setSillyTier(SillyTier::High) + ->setOnline(true) + ->autoRegister(); + +namespace js_internal { + static constexpr auto id = 57436521; + + static void saveTrollLevel() { + jumpscares::coro::getLevel(id, [](Result result) { + if (result.isOk()) { + auto level = std::move(result).unwrap(); + + if (auto mdm = MusicDownloadManager::sharedState()) { + mdm->addMusicDownloadDelegate(jumpscares::JumpscareLevelManager::get()); + mdm->downloadSong(level->m_songID); + }; + + if (auto glm = GameLevelManager::sharedState()) glm->saveLevel(level); + if (auto jm = jumpscares::JumpscareLevelManager::get()) jm->saveLevel(level); + } else if (result.isErr()) { + log::error("Failed to get level {}: {}", id, result.unwrapErr()); + }; + }); + }; + + static void switchToTrollLevel(GJGameLevel* level, bool dontCreateObjects, bool useReplay) { + log::warn("Switching to {} level ({})", level->m_levelName, level->m_levelID.value()); + + auto scene = PlayLayer::scene(level, useReplay, dontCreateObjects); + CCDirector::sharedDirector()->replaceScene(scene); + }; +}; + +$on_mod(Loaded) { + if (options::isEnabled(THIS_ID)) js_internal::saveTrollLevel(); + + listenForHorribleOptionChanges( + THIS_ID, + [](HorribleOptionSave data) { + if (data.enabled) js_internal::saveTrollLevel(); + }); +}; + +class $modify(TrollLevelPlayLayer, PlayLayer) { + HORRIBLE_DELEGATE_HOOKS(THIS_ID); + + struct Fields final { + uint8_t chance = options::getChance(THIS_ID); + }; + + bool init(GJGameLevel* level, bool useReplay, bool dontCreateObjects) { + if (auto jm = jumpscares::JumpscareLevelManager::get()) { + if (rng::fast() <= m_fields->chance) { + if (auto lvl = jm->getLevel(js_internal::id)) return PlayLayer::init(lvl, useReplay, dontCreateObjects); + }; + }; + + return PlayLayer::init(level, useReplay, dontCreateObjects); + }; +}; \ No newline at end of file diff --git a/src/hooks/mechanics/DoubleJump.cpp b/src/hooks/mechanics/DoubleJump.cpp new file mode 100644 index 0000000..ab0c779 --- /dev/null +++ b/src/hooks/mechanics/DoubleJump.cpp @@ -0,0 +1,71 @@ +#include + +#include + +#include + +using namespace geode::prelude; +using namespace horrible::prelude; + +#define THIS_ID "double_jump" + +static auto const o = Option::create(THIS_ID) + ->setName("Double-Jump") + ->setDescription("Allows your character to double-jump in a level.\ncreated by Cheeseworks") + ->setCategory(category::mechanics) + ->setSillyTier(SillyTier::Low) + ->setCheating(true) + ->autoRegister(); + +class $modify(DoubleJumpPlayerObject, PlayerObject) { + HORRIBLE_DELEGATE_HOOKS(THIS_ID); + + struct Fields final { + uint8_t jumps = 0; + + bool onGround = true; + }; + + bool pushButton(PlayerButton p0) { + auto f = m_fields.self(); + + if (p0 == PlayerButton::Jump) { + if (onGround()) f->jumps = 0; + if (!onGround()) f->jumps++; + }; + + setOnGround(f->jumps < 2); + + return PlayerObject::pushButton(p0); + }; + + void hitGround(GameObject* object, bool notFlipped) { + if (!m_gameLayer) return PlayerObject::hitGround(object, notFlipped); + + auto f = m_fields.self(); + + auto wasOnGround = f->onGround; + PlayerObject::hitGround(object, notFlipped); + auto nowOnGround = onGround(); + + if (m_hasEverJumped) { + if (nowOnGround && !wasOnGround) { + f->jumps = 0; + setOnGround(true); + }; + + f->onGround = nowOnGround; + }; + }; + + bool onGround() noexcept { + return m_isOnGround && m_isOnGround2 && m_isOnGround3 && m_isOnGround4; + }; + + void setOnGround(bool onGround) { + m_isOnGround = onGround; + m_isOnGround2 = onGround; + m_isOnGround3 = onGround; + m_isOnGround4 = onGround; + }; +}; \ No newline at end of file diff --git a/src/hooks/InverseInput.cpp b/src/hooks/mechanics/InverseInput.cpp similarity index 95% rename from src/hooks/InverseInput.cpp rename to src/hooks/mechanics/InverseInput.cpp index c6d388e..f8fa43c 100644 --- a/src/hooks/InverseInput.cpp +++ b/src/hooks/mechanics/InverseInput.cpp @@ -12,7 +12,7 @@ using namespace horrible::prelude; static auto const o = Option::create(THIS_ID) ->setName("Inversed Inputs") ->setDescription("You jump while you're not holding the button, and don't jump while you hold the button. In platformer, horizontal movement inputs are switched with each other.\nsuggested by ItsZentry") - ->setCategory(category::misc) + ->setCategory(category::mechanics) ->setSillyTier(SillyTier::Low) ->setCheating(true) ->autoRegister(); diff --git a/src/hooks/mechanics/Leap.cpp b/src/hooks/mechanics/Leap.cpp new file mode 100644 index 0000000..d34cfd5 --- /dev/null +++ b/src/hooks/mechanics/Leap.cpp @@ -0,0 +1,186 @@ +#include + +#include + +#include + +using namespace geode::prelude; +using namespace horrible::prelude; + +#define THIS_ID "leap" + +static auto const o = Option::create(THIS_ID) + ->setName("Charged Leap") + ->setDescription("While you're a ground-based game mode, you'll be forced to charge up every jump you make. The higher the charge, the further upwards and forwards you'll be boosted.\ncreated by Cheeseworks") + ->setCategory(category::mechanics) + ->setSillyTier(SillyTier::High) + ->setCheating(true) + ->autoRegister(); + +class $modify(LeapGJBaseGameLayer, GJBaseGameLayer) { + HORRIBLE_DELEGATE_HOOKS(THIS_ID); + + struct Fields final { + float charge = 0.f; + float speed = 0.f; + + Ref chargeMeter = nullptr; + }; + + bool init() { + if (!GJBaseGameLayer::init()) return false; + + queueInMainThread([self = WeakRef(this)]() { + if (auto s = self.lock()) { + auto f = s->m_fields.self(); + + f->speed = s->m_player1->m_playerSpeed; + + if (auto pl = PlayLayer::get()) { + f->chargeMeter = ProgressBar::create(); + f->chargeMeter->setID("charge-meter"_spr); + f->chargeMeter->setFillColor(colors::red); + f->chargeMeter->setAnchorPoint(anchor::center); + f->chargeMeter->setPosition({pl->m_uiLayer->getScaledContentWidth() / 2.f, 25.f}); + f->chargeMeter->setScale(0.625f); + + pl->m_uiLayer->addChild(f->chargeMeter, HIGHEST_Z); + + f->chargeMeter->updateProgress(0.f); + f->chargeMeter->setVisible(false); + }; + }; + }); + + return true; + }; + + void handleButton(bool down, int button, bool isPlayer1) { + if (button != 1 || !isGroundMode(isPlayer1 ? m_player1 : m_player2)) return GJBaseGameLayer::handleButton(down, button, isPlayer1); + + auto f = m_fields.self(); + + if (down) { + f->charge = 0.f; + + schedule(schedule_selector(LeapGJBaseGameLayer::chargeUp), 0.125f); + if (f->chargeMeter) f->chargeMeter->setVisible(true); + } else { + unschedule(schedule_selector(LeapGJBaseGameLayer::chargeUp)); + unschedule(schedule_selector(LeapGJBaseGameLayer::decharge)); + + m_player1->m_playerSpeed = f->speed; + m_player2->m_playerSpeed = f->speed; + + if (f->chargeMeter) { + f->chargeMeter->setVisible(false); + f->chargeMeter->updateProgress(0.f); + }; + + if (f->charge <= 0.f) return; + + f->speed = m_player1->m_playerSpeed; // cba to check p2 + + auto pct = f->charge / 100.f; + + auto newSpeed = (f->speed * 3.75f) * pct; + auto boostHeight = 20.f * pct; + + m_player1->m_playerSpeed = newSpeed; + m_player2->m_playerSpeed = newSpeed; + + if (onGround(m_player1)) m_player1->boostPlayer(boostHeight); + if (onGround(m_player2)) m_player2->boostPlayer(boostHeight); + + GJBaseGameLayer::handleButton(true, button, isPlayer1); + + sfx::play(sfx::file::pop); + + schedule(schedule_selector(LeapGJBaseGameLayer::decharge)); + + f->charge = 0.f; + + GJBaseGameLayer::handleButton(false, button, isPlayer1); + }; + }; + + void chargeUp(float dt) { + auto f = m_fields.self(); + + if (m_playerDied) { + f->charge = 0.f; + + if (f->chargeMeter) { + f->chargeMeter->setVisible(false); + f->chargeMeter->updateProgress(0.f); + }; + + setOnGround(m_player1, true); + setOnGround(m_player2, true); + + handleButton(false, 1, true); + + unschedule(schedule_selector(LeapGJBaseGameLayer::chargeUp)); + }; + + f->charge += 12.5f; + + if (f->chargeMeter) { + f->chargeMeter->updateProgress(f->charge); + f->chargeMeter->setFillColor(colors::fadeColor(f->charge)); + }; + + if (f->charge >= 100.f) { + f->charge = 100.f; + unschedule(schedule_selector(LeapGJBaseGameLayer::chargeUp)); + }; + }; + + void decharge(float dt) { + auto f = m_fields.self(); + + if (m_playerDied) { + f->charge = 0.f; + + m_player1->m_playerSpeed = f->speed; + m_player2->m_playerSpeed = f->speed; + + if (f->chargeMeter) { + f->chargeMeter->setVisible(false); + f->chargeMeter->updateProgress(0.f); + }; + + setOnGround(m_player1, true); + setOnGround(m_player2, true); + + unschedule(schedule_selector(LeapGJBaseGameLayer::decharge)); + }; + + auto speedFt = dt * 1.5f; + + m_player1->m_playerSpeed -= speedFt; + m_player2->m_playerSpeed -= speedFt; + + if (m_player1->m_playerSpeed <= f->speed) m_player1->m_playerSpeed = f->speed; + if (m_player2->m_playerSpeed <= f->speed) m_player2->m_playerSpeed = f->speed; + + if ((m_player1->m_playerSpeed == f->speed) && (m_player2->m_playerSpeed == f->speed)) unschedule(schedule_selector(LeapGJBaseGameLayer::decharge)); + }; + + bool isGroundMode(PlayerObject* player) noexcept { + if (player) return player->m_isRobot || (!player->m_isShip && !player->m_isBall && !player->m_isBird && !player->m_isDart && !player->m_isRobot && !player->m_isSpider && !player->m_isSwing); + return false; + }; + + bool onGround(PlayerObject* player) noexcept { + if (player) return player->m_isOnGround && player->m_isOnGround2 && player->m_isOnGround3 && player->m_isOnGround4; + return false; + }; + + void setOnGround(PlayerObject* player, bool onGround) { + player->m_isOnGround = onGround; + player->m_isOnGround2 = onGround; + player->m_isOnGround3 = onGround; + player->m_isOnGround4 = onGround; + }; +}; \ No newline at end of file diff --git a/src/hooks/Parry.cpp b/src/hooks/mechanics/Parry.cpp similarity index 98% rename from src/hooks/Parry.cpp rename to src/hooks/mechanics/Parry.cpp index 171f776..783f042 100644 --- a/src/hooks/Parry.cpp +++ b/src/hooks/mechanics/Parry.cpp @@ -15,8 +15,8 @@ using namespace horrible::prelude; static auto const o = Option::create(THIS_ID) ->setName("Parry Obstacles") - ->setDescription("Whenever your hitbox is inside of a hazard hitbox, you will instead destroy it if you time your input right.\nsuggested by Wuffin") - ->setCategory(category::misc) + ->setDescription("Whenever your hitbox is inside of a hazard hitbox, you will instead destroy it if you time your input right.\nsuggested by Wuffin\nCOMING SOON!") + ->setCategory(category::mechanics) ->setSillyTier(SillyTier::None) ->setRequiresRestart(true) ->setSupportedPlatforms({}) diff --git a/src/hooks/mechanics/Velocity.cpp b/src/hooks/mechanics/Velocity.cpp new file mode 100644 index 0000000..0962fb2 --- /dev/null +++ b/src/hooks/mechanics/Velocity.cpp @@ -0,0 +1,114 @@ +#include + +#include + +#include + +using namespace geode::prelude; +using namespace horrible::prelude; + +#define THIS_ID "velocity" + +static auto const o = Option::create(THIS_ID) + ->setName("Powering Velocity") + ->setDescription("Your movement speed gradually decreases the longer you go without pressing your jump button. Hold it to recover your speed.\ncreated by Cheeseworks") + ->setCategory(category::mechanics) + ->setSillyTier(SillyTier::High) + ->setCheating(true) + ->autoRegister(); + +class $modify(VelocityGJBaseGameLayer, GJBaseGameLayer) { + HORRIBLE_DELEGATE_HOOKS(THIS_ID); + + struct Fields final { + float speed = 0.f; + + Ref speedMeter = nullptr; + }; + + bool init() { + if (!GJBaseGameLayer::init()) return false; + + // gjbgl doesnt init everything instantly + queueInMainThread([self = WeakRef(this)]() { + if (auto s = self.lock()) { + auto f = s->m_fields.self(); + + f->speed = s->m_player1->m_playerSpeed * 2.5f; + s->m_player1->m_playerSpeed = f->speed / 2.f; + + if (auto pl = PlayLayer::get()) { + auto speedMeterLabel = CCLabelBMFont::create("Speed", font::big); + speedMeterLabel->setScale(0.375f); + speedMeterLabel->setColor(colors::gold); + speedMeterLabel->setAnchorPoint(anchor::center); + speedMeterLabel->setPosition({pl->m_uiLayer->getScaledContentWidth() / 2.f, 37.5f}); + + pl->m_uiLayer->addChild(speedMeterLabel, HIGHEST_Z); + + f->speedMeter = ProgressBar::create(); + f->speedMeter->setID("speed-meter"_spr); + f->speedMeter->setFillColor(colors::gold); + f->speedMeter->setAnchorPoint(anchor::center); + f->speedMeter->setPosition({speedMeterLabel->getPositionX(), speedMeterLabel->getPositionY() - 12.5f}); + f->speedMeter->setScale(0.875f); + + pl->m_uiLayer->addChild(f->speedMeter, HIGHEST_Z); + + f->speedMeter->updateProgress(50.f); + }; + }; + }); + + return true; + }; + + void handleButton(bool down, int button, bool isPlayer1) { + GJBaseGameLayer::handleButton(down, button, isPlayer1); + + if (button == 1) { + unschedule(down ? schedule_selector(VelocityGJBaseGameLayer::updateSpeed) : schedule_selector(VelocityGJBaseGameLayer::increaseSpeed)); + schedule(down ? schedule_selector(VelocityGJBaseGameLayer::increaseSpeed) : schedule_selector(VelocityGJBaseGameLayer::updateSpeed)); + }; + }; + + void resetPlayer() { + GJBaseGameLayer::resetPlayer(); + + auto f = m_fields.self(); + + if (f->speed > 0.f) { + m_player1->m_playerSpeed = f->speed / 2.f; + m_player2->m_playerSpeed = f->speed / 2.f; + }; + + if (f->speedMeter) f->speedMeter->updateProgress(50.f); + }; + + void increaseSpeed(float dt) { + if (m_playerDied) return; + + auto f = m_fields.self(); + + auto speedFt = dt * 0.375f; + if (m_player1->m_playerSpeed < f->speed) m_player1->m_playerSpeed += speedFt; + if (m_player2->m_playerSpeed < f->speed) m_player2->m_playerSpeed += speedFt; + + if (f->speedMeter) f->speedMeter->updateProgress((m_player1->m_playerSpeed / f->speed) * 100.f); + }; + + void updateSpeed(float dt) { + if (m_playerDied) return; + + auto f = m_fields.self(); + + if (m_player1->m_playerSpeed > f->speed) m_player1->m_playerSpeed = f->speed; + if (m_player2->m_playerSpeed > f->speed) m_player2->m_playerSpeed = f->speed; + + auto speedFt = dt * 0.125f; + if (m_player1->m_playerSpeed >= 0.125f) m_player1->m_playerSpeed -= speedFt; + if (m_player2->m_playerSpeed >= 0.125f) m_player2->m_playerSpeed -= speedFt; + + if (f->speedMeter) f->speedMeter->updateProgress((m_player1->m_playerSpeed / f->speed) * 100.f); + }; +}; \ 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..a5c10a2 --- /dev/null +++ b/src/hooks/obstructive/Captchas.cpp @@ -0,0 +1,110 @@ +#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; + + float defSpeed = 0.f; + }; + + void setupHasCompleted() { + PlayLayer::setupHasCompleted(); + nextCaptcha(); + + m_fields->defSpeed = m_player1->m_playerSpeed; + }; + + void resetLevelFromStart() { + PlayLayer::resetLevelFromStart(); + cue::resetNode(m_fields->currentCaptcha); + }; + + void levelComplete() { + PlayLayer::levelComplete(); + cue::resetNode(m_fields->currentCaptcha); + }; + + void pauseGame(bool unfocused) { + PlayLayer::pauseGame(unfocused); + + auto f = m_fields.self(); + + if (f->currentCaptcha) { + cue::resetNode(f->currentCaptcha); + resetLevelFromStart(); + }; + }; + + 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(); + + auto f = m_fields.self(); + + m_player1->m_playerSpeed = f->defSpeed; + m_player2->m_playerSpeed = f->defSpeed; + + if (!success) { + Notification::create("Knew you were a robot...", NotificationIcon::Error)->show(); + resetLevelFromStart(); + }; + + cue::resetNode(f->currentCaptcha); + }); + + captcha->show(); + f->currentCaptcha = captcha; + + cursor::show(); + + m_player1->m_playerSpeed = 0.0125f; + m_player2->m_playerSpeed = 0.0125f; + }; + }; + + queueInMainThread([self = WeakRef(this)]() { + if (auto s = self.lock()) s->nextCaptcha(); + }); + }; +}; \ No newline at end of file diff --git a/src/util/Jumpscares.hpp b/src/util/Jumpscares.hpp index e26b500..5ac5c7e 100644 --- a/src/util/Jumpscares.hpp +++ b/src/util/Jumpscares.hpp @@ -21,6 +21,11 @@ namespace horrible { 129066879, 895761, }; + + inline constexpr LevelInfo tidal = { + 93733469, + 1138377, + }; }; void switchLevel(LevelInfo const& level, bool dontCreateObjects, bool useReplay); diff --git a/src/util/ui/Captcha.hpp b/src/util/ui/Captcha.hpp new file mode 100644 index 0000000..d73e8cb --- /dev/null +++ b/src/util/ui/Captcha.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include + +#include + +using namespace geode::prelude; + +namespace horrible { + namespace ui { + class Captcha final : public Popup { + using Callback = Function; + + private: + struct Impl; + std::unique_ptr m_impl; + + void setupVerifier(std::string btnID); + + 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_expected = ""; + Callback m_callback = nullptr; + + void addNewBtn(); + + protected: + void validateBtns(geode::Button* called); + + 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..5eaefc4 --- /dev/null +++ b/src/util/ui/src/Captcha.cpp @@ -0,0 +1,300 @@ +#include "../Captcha.hpp" + +#include + +#include + +using namespace geode::prelude; +using namespace horrible::prelude; + +// id, sprite name +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"}, + {"Cooler Vaultkeeper", "GJ_rateDiffBtnMod_001.png"}, + {"Compact Lists", "GJ_smallModeIcon_001.png"}, + {"Subscribe to Breakeode", "gj_ytIcon_001.png"}, + {"Globed Death Effect", "explosionIcon_20_001.png"}, +}); + +struct Captcha::Impl final { + std::string expected = ""; + + 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() {}; + +void Captcha::setupVerifier(std::string btnID) { + cue::resetNode(m_impl->verifier); + + m_impl->verifier = RobotVerifier::create(std::move(btnID), [this](bool success) { + setSuccess(success); + }); + m_impl->verifier->setPosition({m_mainLayer->getScaledContentWidth() / 2.f, (m_mainLayer->getScaledContentHeight() / 2.f) - 8.75f}); + + m_mainLayer->addChild(m_impl->verifier, 9); +}; + +bool Captcha::init() { + m_impl->expected = g_buttons[rng::get(g_buttons.size() - 1)].first; + + auto const theme = mod->getSettingValue("theme"); + + if (!Popup::init({400.f, 275.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); + + m_bgSprite->setZOrder(-9); + + 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.75f); + 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->setAnchorPoint(anchor::center); + m_impl->countdown->setPosition({m_mainLayer->getScaledContentWidth() / 2.f, 17.5f}); + m_impl->countdown->setFillColor(colors::fadeColor(100.f)); + + m_impl->countdown->updateProgress(100.f); + + m_mainLayer->addChild(m_impl->countdown); + + setupVerifier(m_impl->expected); + + auto bg = cue::createBackground( + { + m_impl->verifier->getScaledContentWidth() * 1.25f, + 192.5f, + }, + { + .zOrder = -1, + }); + bg->setPosition(m_impl->verifier->getPosition()); + + m_mainLayer->addChild(bg); + + auto hintID = CCLabelBMFont::create(m_impl->expected.c_str(), "bigFont.fnt"); + hintID->setID("hint-id"); + hintID->setScale(0.5f); + hintID->setAnchorPoint(anchor::center); + hintID->setAlignment(kCCTextAlignmentCenter); + hintID->limitLabelWidth(bg->getScaledContentWidth() * 0.875f, 0.5f, 0.125f); + hintID->setPosition({m_impl->verifier->getPositionX(), m_impl->verifier->getPositionY() + (m_impl->verifier->getScaledContentHeight() / 2.f) + 12.5f}); + + m_mainLayer->addChild(hintID, 1); + + auto hint = SimpleTextArea::create("Press all the buttons with", "chatFont.fnt", 0.5f, bg->getScaledContentWidth() * 0.875f); + hint->setID("hint"); + hint->setColor(to4B(colors::yellow)); + hint->setAlignment(kCCTextAlignmentCenter); + hint->setPosition({m_impl->verifier->getPositionX(), hintID->getPositionY() + 10.f}); + + m_mainLayer->addChild(hint); + + auto refreshBtn = Button::createWithNode( + ButtonSprite::create( + "Refresh", + "bigFont.fnt", + themes::getButtonSquareSprite(theme)), + [this](auto) { + setupVerifier(m_impl->expected); + }); + refreshBtn->setID("refresh-btn"); + refreshBtn->setScale(0.625f); + refreshBtn->setPosition({m_impl->verifier->getPositionX(), m_impl->verifier->getPositionY() - (m_impl->verifier->getScaledContentHeight() / 2.f) - (refreshBtn->getScaledContentHeight() * 0.825f)}); + + m_mainLayer->addChild(refreshBtn, 1); + + auto infoBtn = Button::createWithSpriteFrameName( + "GJ_infoIcon_001.png", + [this](auto) { + unscheduleUpdate(); + + createQuickPopup( + "Help", + "Press on the images that correspond to the provided captcha hint. It is purposefully obscured to make this sillier.\n\nRefresh the captcha if you can't find any button that matches the hint.", + "OK", + nullptr, // captcha popup can exit asynchronously + [self = WeakRef(this)](auto, auto) { + if (auto s = self.lock()) s->scheduleUpdate(); + }); + }); + infoBtn->setID("info-btn"); + infoBtn->setScale(0.875f); + + m_mainLayer->addChildAtPosition(infoBtn, Anchor::TopRight, {-17.5f, -17.5f}); + + 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; + + unscheduleUpdate(); + + 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); + 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) { + sfx::play(sfx::file::count); + 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); + m_impl->countdown->setFillColor(colors::fadeColor(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; +}; + +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(sfx::file::click); + + if (id == m_expected) { + validateBtns(sender); + } else { + m_callback(false); + }; + }); + btn->setID(btnData.first); + + addChild(btn); + + cue::rescaleToMatch(btn, 27.5f); +}; + +bool RobotVerifier::init(std::string id, Callback&& cb) { + m_expected = std::move(id); + m_callback = std::move(cb); + + if (!CCNode::init()) return false; + + auto layout = RowLayout::create() + ->setGap(3.75f) + ->setAutoScale(false) + ->setGrowCrossAxis(true); + + setID("captcha-verifier"); + setAnchorPoint(anchor::center); + setContentWidth(125.f); + setLayout(layout); + + for (int i = 0; i < 16; ++i) { + addNewBtn(); + }; + + updateLayout(); + + return true; +}; + +void RobotVerifier::validateBtns(Button* called) { + cue::resetNode(called); + updateLayout(); + + for (auto const& btn : getChildrenExt