diff --git a/src/hooks/InputRoulette.cpp b/src/hooks/InputRoulette.cpp new file mode 100644 index 0000000..8cdc7b8 --- /dev/null +++ b/src/hooks/InputRoulette.cpp @@ -0,0 +1,106 @@ +#include + +#include + +#include +#include + +using namespace geode::prelude; +using namespace horrible::prelude; + +#define THIS_ID "input_roulette" + +static auto const o = Option::create(THIS_ID) + ->setName("Input Roulette") + ->setDescription("A meter will show on the right-hand side of the screen during gameplay indicating the safe and not-so-safe times to press your jump button. If the arrow happens to be in an unsafe area when you press jump, you're at risk. Press while yellow, you have a 50/50 chance of dying, press while red, and you're guaranteed to die. Press while green, and you're safe.\ncreated by Cheeseworks") + ->setCategory(category::misc) + ->setSillyTier(SillyTier::High) + ->autoRegister(); + +class $modify(InputRouletteGJBaseGameLayer, GJBaseGameLayer) { + HORRIBLE_DELEGATE_HOOKS(THIS_ID); + + struct Fields final { + InputMeter* inputMeter = nullptr; + }; + + HORRIBLE_SETUP_INTERFACE_FUNC { + auto f = m_fields.self(); + + if (!on) return cue::resetNode(f->inputMeter); + + if (auto pl = PlayLayer::get()) { + f->inputMeter = InputMeter::create(); + f->inputMeter->setID("input-chance-meter"_spr); + f->inputMeter->setScale(1.25f); + f->inputMeter->setZOrder(HIGHEST_Z); + + pl->m_uiLayer->addChildAtPosition(f->inputMeter, Anchor::Right, {-12.5f, 0.f}, false); + }; + }; + + bool init() { + if (!GJBaseGameLayer::init()) return false; + + queueInMainThread([self = WeakRef(this)]() { + if (auto s = self.lock()) s->HORRIBLE_SETUP_INTERFACE_FUNC_NAME(); + }); + + return true; + }; + + void handleButton(bool down, int button, bool isPlayer1) { + GJBaseGameLayer::handleButton(down, button, isPlayer1); + + if (button == 1 && down) { + if (auto pl = PlayLayer::get()) { + auto f = m_fields.self(); + + if (f->inputMeter) { // nesting hell :D + if (f->inputMeter->isDeath()) { + pl->destroyPlayer(pl->m_player1, nullptr); + } else if (f->inputMeter->isDanger()) { + rng::flip() ? pl->destroyPlayer(pl->m_player1, nullptr) : Notification::create("Careful when you jump!", NotificationIcon::Warning, 0.5f)->show(); + }; + }; + }; + }; + }; + + InputMeter* getHealthMeter() noexcept { + if (auto f = m_fields.self()) return f->inputMeter; + return nullptr; + }; +}; + +class $modify(InputRoulettePlayLayer, PlayLayer) { + HORRIBLE_DELEGATE_HOOKS(THIS_ID); + + void destroyPlayer(PlayerObject* player, GameObject* object) { + PlayLayer::destroyPlayer(player, object); + + if (m_playerDied) { + if (auto gjbgl = GJBaseGameLayer::get()) { + if (auto meter = modify_cast(gjbgl)->getHealthMeter()) meter->playPause(false); + }; + }; + }; + + void resetLevel() { + PlayLayer::resetLevel(); + + if (auto gjbgl = GJBaseGameLayer::get()) { + if (auto meter = modify_cast(gjbgl)->getHealthMeter()) meter->playPause(true); + }; + }; + + void resetLevelFromStart() { + PlayLayer::resetLevelFromStart(); + + if (auto gjbgl = GJBaseGameLayer::get()) { + if (auto meter = modify_cast(gjbgl)->getHealthMeter()) meter->playPause(true); + }; + }; +}; + +HORRIBLE_TOGGLE_MODIFY(GJBaseGameLayer, InputRouletteGJBaseGameLayer); \ No newline at end of file diff --git a/src/hooks/obstructive/RealFloating.cpp b/src/hooks/obstructive/RealFloating.cpp new file mode 100644 index 0000000..1a7ed56 --- /dev/null +++ b/src/hooks/obstructive/RealFloating.cpp @@ -0,0 +1,148 @@ +#include + +#include + +#include + +using namespace geode::prelude; +using namespace horrible::prelude; + +#define THIS_ID "da_real_floating_btn" + +static auto const o = Option::create(THIS_ID) + ->setName("da real floating button") + ->setDescription("You'll have a floating button constantly zooming around your screen during gameplay. If you press it, your character will immediately die.\ncreated by Cheeseworks") + ->setCategory(category::obstructive) + ->setSillyTier(SillyTier::Medium) + ->autoRegister(); + +static constexpr auto g_icons = std::to_array({ + themes::icons::Default, + themes::icons::Breakeode, + themes::icons::Classic, + themes::icons::ColonThree, + themes::icons::CryingSkull, + themes::icons::Durr, + themes::icons::Pride, + themes::icons::TransPride, + themes::icons::SapphireSDK, + themes::icons::SoggyCat, + themes::icons::SteamHappy, + themes::icons::TheYellowOne, +}); + +static constexpr auto g_frames = std::to_array({ + CircleBaseColor::Blue, + CircleBaseColor::Cyan, + CircleBaseColor::DarkAqua, + CircleBaseColor::DarkPurple, + CircleBaseColor::Gray, + CircleBaseColor::Green, + CircleBaseColor::Pink, + CircleBaseColor::Red, +}); + +class $modify(RealFloatingPlayLayer, PlayLayer) { + HORRIBLE_DELEGATE_HOOKS(THIS_ID); + + struct Fields final { + Button* theBtn = nullptr; + }; + + HORRIBLE_SETUP_INTERFACE_FUNC { + auto f = m_fields.self(); + + if (!on) return cue::resetNode(f->theBtn); + + nextPosition(f->theBtn); + }; + + void setupHasCompleted() { + PlayLayer::setupHasCompleted(); + HORRIBLE_SETUP_INTERFACE_FUNC_NAME(true); + }; + + void resetLevel() { + PlayLayer::resetLevel(); + nextPosition(m_fields->theBtn); + }; + + void destroyPlayer(PlayerObject* player, GameObject* object) { + PlayLayer::destroyPlayer(player, object); + if (object == m_anticheatSpike || !m_playerDied) return; + + auto f = m_fields.self(); + + if (f->theBtn) f->theBtn->stopAllActions(); + }; + + void nextPosition(CCNode* sender) { + auto f = m_fields.self(); + + auto const pos = sender ? sender->getPosition() : getRandomEdge(m_uiLayer->getScaledContentSize(), {}); + + cue::resetNode(f->theBtn); + sender = nullptr; + + f->theBtn = makeButton(false); + + f->theBtn->setPosition(pos); + + m_uiLayer->addChild(f->theBtn, HIGHEST_Z); + + sfx::play(sfx::file::click); + + f->theBtn->runAction( + CCSequence::createWithTwoActions( + CCMoveTo::create(rng::get(1.25f, 0.5f), getRandomEdge(m_uiLayer->getScaledContentSize(), f->theBtn->getPosition())), + CCCallFuncN::create(this, callfuncN_selector(RealFloatingPlayLayer::nextPosition)))); + }; + + Button* makeButton(bool autoPos = true) { + auto theBtn = Button::createWithNode( + CircleButtonSprite::createWithSpriteFrameName( + themes::getIconSprite(g_icons[rng::get(g_icons.size() - 1)]), + 0.875f, + g_frames[rng::get(g_frames.size() - 1)]), + [this](auto) { + destroyPlayer(m_player1, nullptr); + }); + theBtn->setID("press-me-btn"_spr); + theBtn->setScale(rng::get(2.5f, 1.25f)); + + if (autoPos) theBtn->setPosition(getRandomEdge(m_uiLayer->getScaledContentSize(), theBtn->getPosition())); + + return theBtn; + }; + + CCPoint getRandomEdge(CCPoint const& maxSize, CCPoint const& currentPos) const { + auto x = currentPos.x; + auto y = currentPos.y; + + if (currentPos.x <= 0.f) { + x = maxSize.x; + y = rng::get(maxSize.y); + } else if (currentPos.x >= maxSize.x) { + x = 0.f; + y = rng::get(maxSize.y); + } else if (currentPos.y <= 0.f) { + x = rng::get(maxSize.x); + y = maxSize.y; + } else if (currentPos.y >= maxSize.y) { + x = rng::get(maxSize.x); + y = 0.f; + } else { + if (rng::flip()) { + x = (rng::flip() ? 0.f : maxSize.x); + y = rng::get(maxSize.y); + } else { + x = rng::get(maxSize.x); + y = (rng::flip() ? 0.f : maxSize.y); + }; + }; + + return {x, y}; + }; +}; + +HORRIBLE_TOGGLE_MODIFY(PlayLayer, RealFloatingPlayLayer); \ No newline at end of file diff --git a/src/hooks/playerlife/LimitedJump.cpp b/src/hooks/playerlife/LimitedJump.cpp new file mode 100644 index 0000000..bf43fea --- /dev/null +++ b/src/hooks/playerlife/LimitedJump.cpp @@ -0,0 +1,95 @@ +#include + +#include + +#include +#include + +using namespace geode::prelude; +using namespace horrible::prelude; + +#define THIS_ID "limited_jump" + +static auto const o = Option::create(THIS_ID) + ->setName("Limited Jump Energy") + ->setDescription("You only have a limited amount of jump inputs you can make for a period of time, which will regenerate at a random pace over time. Run out of available inputs too quickly, and your character will die.\ncreated by Cheeseworks") + ->setCategory(category::playerlife) + ->setSillyTier(SillyTier::Medium) + ->autoRegister(); + +class $modify(LimitedJumpGJBaseGameLayer, GJBaseGameLayer) { + HORRIBLE_DELEGATE_HOOKS(THIS_ID); + + struct Fields final { + JumpHealthMeter* jumpMeter = nullptr; + }; + + HORRIBLE_SETUP_INTERFACE_FUNC { + auto f = m_fields.self(); + + if (!on) return cue::resetNode(f->jumpMeter); + + if (auto pl = PlayLayer::get()) { + f->jumpMeter = JumpHealthMeter::create([this, meter = f->jumpMeter]() { + if (auto pl = PlayLayer::get()) pl->destroyPlayer(pl->m_player1, nullptr); + }); + f->jumpMeter->setID("jump-energy-meter"_spr); + f->jumpMeter->setZOrder(HIGHEST_Z); + f->jumpMeter->setAnchorPoint({1, 0}); + + pl->m_uiLayer->addChildAtPosition(f->jumpMeter, Anchor::BottomRight, {-8.75f, 8.75f}, false); + }; + }; + + bool init() { + if (!GJBaseGameLayer::init()) return false; + + queueInMainThread([self = WeakRef(this)]() { + if (auto s = self.lock()) s->HORRIBLE_SETUP_INTERFACE_FUNC_NAME(); + }); + + return true; + }; + + void handleButton(bool down, int button, bool isPlayer1) { + GJBaseGameLayer::handleButton(down, button, isPlayer1); + if (button == 1 && down) m_fields->jumpMeter->jump(); + }; + + JumpHealthMeter* getHealthMeter() noexcept { + if (auto f = m_fields.self()) return f->jumpMeter; + return nullptr; + }; +}; + +class $modify(LimitedJumpPlayLayer, PlayLayer) { + HORRIBLE_DELEGATE_HOOKS(THIS_ID); + + void destroyPlayer(PlayerObject* player, GameObject* object) { + PlayLayer::destroyPlayer(player, object); + + if (m_playerDied) { + if (auto gjbgl = GJBaseGameLayer::get()) { + if (auto meter = modify_cast(gjbgl)->getHealthMeter()) meter->playPause(false); + }; + }; + }; + + void resetLevel() { + PlayLayer::resetLevel(); + + if (auto gjbgl = GJBaseGameLayer::get()) { + if (auto meter = modify_cast(gjbgl)->getHealthMeter()) meter->reset(); + }; + }; + + void resetLevelFromStart() { + PlayLayer::resetLevelFromStart(); + + if (auto gjbgl = GJBaseGameLayer::get()) { + if (auto meter = modify_cast(gjbgl)->getHealthMeter()) meter->reset(); + }; + }; +}; + +HORRIBLE_TOGGLE_MODIFY(GJBaseGameLayer, LimitedJumpGJBaseGameLayer); \ No newline at end of file diff --git a/src/util/ui/Include.h b/src/util/ui/Include.h index 60a93c3..d4037fd 100644 --- a/src/util/ui/Include.h +++ b/src/util/ui/Include.h @@ -1,6 +1,8 @@ #pragma once #include "Captcha.hpp" +#include "InputMeter.hpp" +#include "JumpHealthMeter.hpp" #include "LabelArea.hpp" #include "MathQuiz.hpp" #include "RandomAd.hpp" diff --git a/src/util/ui/InputMeter.hpp b/src/util/ui/InputMeter.hpp new file mode 100644 index 0000000..4dfaca5 --- /dev/null +++ b/src/util/ui/InputMeter.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +namespace horrible { + namespace ui { + class InputMeter final : public cocos2d::CCNode { + private: + struct Impl; + std::unique_ptr m_impl; + + protected: + InputMeter(); + ~InputMeter(); + + void update(float dt) override; + + bool init() override; + + public: + static InputMeter* create(); + + void playPause(bool play); + + bool isDeath() const noexcept; + bool isDanger() const noexcept; + }; + }; +}; \ No newline at end of file diff --git a/src/util/ui/JumpHealthMeter.hpp b/src/util/ui/JumpHealthMeter.hpp new file mode 100644 index 0000000..851adb3 --- /dev/null +++ b/src/util/ui/JumpHealthMeter.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include + +namespace horrible { + namespace ui { + class JumpHealthMeter final : public cocos2d::CCNode { + using Callback = geode::Function; + + private: + struct Impl; + std::unique_ptr m_impl; + + protected: + JumpHealthMeter(); + ~JumpHealthMeter(); + + void update(float dt) override; + + bool init(Callback&& cb); + + public: + static JumpHealthMeter* create(Callback&& cb); + + void jump(); + void reset(); + + void playPause(bool play); + }; + }; +}; \ No newline at end of file diff --git a/src/util/ui/src/InputMeter.cpp b/src/util/ui/src/InputMeter.cpp new file mode 100644 index 0000000..cb5508d --- /dev/null +++ b/src/util/ui/src/InputMeter.cpp @@ -0,0 +1,95 @@ +#include "../InputMeter.hpp" + +#include + +#include + +using namespace geode::prelude; +using namespace horrible::prelude; + +#define PACE_RANGES 250.f, 175.f + +struct InputMeter::Impl final { + float chance = 0.f; + float pace = rng::get(PACE_RANGES); + + FMODLevelVisualizer* meter = nullptr; + CCSprite* indicator = nullptr; + + constexpr ccColor3B const& getChanceColor(float chance) { + if (chance >= 85.f) return colors::red; + if (chance >= 60.f) return colors::yellow; + + return colors::green; + }; +}; + +InputMeter::InputMeter() : m_impl(std::make_unique()) {}; +InputMeter::~InputMeter() {}; + +bool InputMeter::init() { + if (!CCNode::init()) return false; + + setAnchorPoint(anchor::center); + setContentSize({10.f, 100.f}); + + m_impl->meter = FMODLevelVisualizer::create(); + m_impl->meter->setID("meter"); + m_impl->meter->updateVisualizer(m_impl->chance, m_impl->chance, 1.f); + + addChildAtPosition(m_impl->meter, Anchor::Center, {}, false); + + m_impl->indicator = CCSprite::createWithSpriteFrameName("edit_rightBtn_001.png"); + m_impl->indicator->setID("indicator"); + m_impl->indicator->setScale(0.875f); + m_impl->indicator->setPosition({getScaledContentWidth() * -1.f, m_impl->chance}); + m_impl->indicator->setColor(m_impl->getChanceColor(m_impl->chance)); + + addChild(m_impl->indicator, 1); + + scheduleUpdate(); + + return true; +}; + +void InputMeter::update(float dt) { + if (m_impl->meter && m_impl->indicator) { + m_impl->chance += dt * m_impl->pace; + + m_impl->meter->updateVisualizer(m_impl->chance / 100.f, m_impl->chance / 100.f, dt); + + m_impl->indicator->setPositionY(m_impl->chance); + m_impl->indicator->setColor(m_impl->getChanceColor(m_impl->chance)); + + if (m_impl->chance <= 0.f) { + m_impl->chance = 0.f; + m_impl->pace = rng::get(PACE_RANGES); + } else if (m_impl->chance >= 100.f) { + m_impl->chance = 100.f; + m_impl->pace = rng::get(PACE_RANGES) * -1.f; + }; + }; +}; + +void InputMeter::playPause(bool play) { + play ? scheduleUpdate() : unscheduleUpdate(); +}; + +bool InputMeter::isDeath() const noexcept { + return m_impl->chance >= 85.f; +}; + +bool InputMeter::isDanger() const noexcept { + return m_impl->chance <= 85.f && m_impl->chance >= 60.f; +}; + +InputMeter* InputMeter::create() { + auto ret = new InputMeter(); + if (ret->init()) { + ret->autorelease(); + return ret; + }; + + delete ret; + return nullptr; +}; \ No newline at end of file diff --git a/src/util/ui/src/JumpHealthMeter.cpp b/src/util/ui/src/JumpHealthMeter.cpp new file mode 100644 index 0000000..1679779 --- /dev/null +++ b/src/util/ui/src/JumpHealthMeter.cpp @@ -0,0 +1,113 @@ +#include "../JumpHealthMeter.hpp" + +#include + +#include + +using namespace geode::prelude; +using namespace horrible::prelude; + +#define PACE_RANGES 50.f, 25.f + +struct JumpHealthMeter::Impl final { + uint8_t jumps = rng::get(15, 7); + float pace = rng::get(PACE_RANGES); + + Label* healthLabel = nullptr; + ProgressBar* healthMeter = nullptr; + + Callback callback = nullptr; + + constexpr auto getColorForJumps(uint8_t remaining) { + if (jumps <= 6) return ""; + if (jumps <= 14) return ""; + + return ""; + }; + + void updateLabel() { + if (healthLabel) healthLabel->setRichText(fmt::format("{}{} Inputs Remaining", getColorForJumps(jumps), jumps)); + }; +}; + +JumpHealthMeter::JumpHealthMeter() : m_impl(std::make_unique()) {}; +JumpHealthMeter::~JumpHealthMeter() {}; + +bool JumpHealthMeter::init(Callback&& cb) { + m_impl->callback = std::move(cb); + + if (!CCNode::init()) return false; + + setAnchorPoint(anchor::center); + setContentSize({150.f, 27.5f}); + + m_impl->healthMeter = ProgressBar::create(); + m_impl->healthMeter->setID("meter"); + m_impl->healthMeter->setFillColor(colors::pink); + m_impl->healthMeter->setAnchorPoint(anchor::center); + m_impl->healthMeter->updateProgress(0.f); + + cue::rescaleToMatch(m_impl->healthMeter, getScaledContentWidth() - 8.75f); + + addChildAtPosition(m_impl->healthMeter, Anchor::Bottom, {0.f, 3.75f}); + + m_impl->healthLabel = Label::createRich(fmt::format("{}{} Inputs Remaining", m_impl->getColorForJumps(m_impl->jumps), m_impl->jumps), font::big); + m_impl->healthLabel->setID("label"); + m_impl->healthLabel->setScale(0.375f); + + addChildAtPosition(m_impl->healthLabel, Anchor::Top, {0.f, m_impl->healthLabel->getScaledContentHeight() * -0.625f}); + + scheduleUpdate(); + + return true; +}; + +void JumpHealthMeter::update(float dt) { + if (m_impl->healthMeter) { + auto per = m_impl->healthMeter->getProgress(); + + m_impl->healthMeter->updateProgress(per + (dt * m_impl->pace)); + + if (m_impl->healthMeter->getProgress() >= 100.f) { + if (m_impl->jumps <= 25) m_impl->jumps++; + + m_impl->updateLabel(); + m_impl->healthMeter->updateProgress(0.f); + + m_impl->pace = rng::get(PACE_RANGES); + }; + }; +}; + +void JumpHealthMeter::jump() { + if (m_impl->jumps <= 0) m_impl->callback(); + + m_impl->jumps--; + m_impl->updateLabel(); +}; + +void JumpHealthMeter::reset() { + unscheduleUpdate(); + + m_impl->jumps = rng::get(15, 7); + + if (m_impl->healthMeter) m_impl->healthMeter->updateProgress(0.f); + m_impl->updateLabel(); + + scheduleUpdate(); +}; + +void JumpHealthMeter::playPause(bool play) { + play ? scheduleUpdate() : unscheduleUpdate(); +}; + +JumpHealthMeter* JumpHealthMeter::create(Callback&& cb) { + auto ret = new JumpHealthMeter(); + if (ret->init(std::move(cb))) { + ret->autorelease(); + return ret; + }; + + delete ret; + return nullptr; +}; \ No newline at end of file