From 8e69ad94371dcfe6d0bc03a4b14d1e61e54dd487 Mon Sep 17 00:00:00 2001 From: Philippe Symons Date: Thu, 15 Jan 2026 22:13:46 +0100 Subject: [PATCH] Work on adding a PokeShop (incomplete, WIP, untested) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit is the start of the work to add a Pokéshop, which would let you buy starters or version exclusives for in-game money. But it's far from complete. This commit lays some of the groundwork, but can't even get compiled yet. --- include/scenes/PokeShopScene.h | 57 +++++++++++ include/widget/PokeShopMenuItemWidget.h | 110 ++++++++++++++++++++ src/scenes/PokeShopScene.cpp | 39 +++++++ src/widget/PokeShopMenuItemWidget.cpp | 129 ++++++++++++++++++++++++ 4 files changed, 335 insertions(+) create mode 100644 include/scenes/PokeShopScene.h create mode 100644 include/widget/PokeShopMenuItemWidget.h create mode 100644 src/scenes/PokeShopScene.cpp create mode 100644 src/widget/PokeShopMenuItemWidget.cpp diff --git a/include/scenes/PokeShopScene.h b/include/scenes/PokeShopScene.h new file mode 100644 index 0000000..4ec0bad --- /dev/null +++ b/include/scenes/PokeShopScene.h @@ -0,0 +1,57 @@ +#ifndef _POKESHOPSCENE_H +#define _POKESHOPSCENE_H + +#include "scenes/MenuScene.h" +#include "transferpak/TransferPakRomReader.h" +#include "transferpak/TransferPakSaveManager.h" +#include "gen1/Gen1GameReader.h" +#include "gen2/Gen2GameReader.h" +#include "widget/PokeShopMenuItemWidget.h" + +/** + * @brief This scene implementation gives you a list of pokémon you + * can buy from the PokéShop and inject into your cartridge save by selecting one + */ +class PokeShopScene : public MenuScene +{ +public: + PokeShopScene(SceneDependencies& deps, void* context); + virtual ~PokeShopScene(); + + void init() override; + void destroy() override; + + bool handleUserInput(joypad_port_t port, const joypad_inputs_t& inputs) override; + + /** + * This function will start the pokémon injection. and show a non-skippable "saving" dialog + * The actual injection will be done on the next handleUserInput() call. + * That will ensure that at least 1 render() call has been handled before we start doing the work + */ + void triggerPokemonInjection(const void* data); + + /** + * @brief The core functionality of this class: it will inject the selected pokémon into your cartridge save. + * + * @param data a pointer to the Gen1DistributionPokemon or Gen2DistributionPokemon instance you want to inject. + */ + void injectPokemon(const void* data); + + void onDialogDone() override; +protected: + void setupMenu() override; +private: + void loadShopList(); + + TransferPakRomReader romReader_; + TransferPakSaveManager saveManager_; + Gen1GameReader gen1Reader_; + Gen2GameReader gen2Reader_; + PokemonPartyIconFactory iconFactory_; + ListItemFiller customListFiller_; + DialogData diag_; + sprite_t* iconBackgroundSprite_; + const void* pokeToInject_; +}; + +#endif \ No newline at end of file diff --git a/include/widget/PokeShopMenuItemWidget.h b/include/widget/PokeShopMenuItemWidget.h new file mode 100644 index 0000000..087e78b --- /dev/null +++ b/include/widget/PokeShopMenuItemWidget.h @@ -0,0 +1,110 @@ +#ifndef _POKESHOPMENUITEMWIDGET_H +#define _POKESHOPMENUITEMWIDGET_H + +#include "widget/PokemonPartyIconWidget.h" +#include "widget/MenuItemWidget.h" + +typedef struct PokeShopMenuItemStyle +{ + /** + * width and height for the MenuItemWidget + */ + Dimensions size; + struct { + /** + * (optional) background sprite + */ + sprite_t* sprite; + /* + * RenderSettings that influence how the backgroundSprite is + * being rendered + */ + SpriteRenderSettings spriteSettings; + } background; + + struct { + PokemonPartyIconWidgetStyle style; + Rectangle bounds; + } icon; + + struct { + /** + * These are the text settings for when the MenuItemWidget is NOT focused by the user + */ + TextRenderSettings labelNotFocused; + /** + * These are the text render settings for when the MenuItemWidget is focused by the user + */ + TextRenderSettings labelFocused; + /** + * @brief the bounds for the price label + */ + Rectangle bounds; + } title; + + struct { + /** + * These are the text settings for when the MenuItemWidget is NOT focused by the user + */ + TextRenderSettings labelNotFocused; + /** + * These are the text render settings for when the MenuItemWidget is focused by the user + */ + TextRenderSettings labelFocused; + /** + * @brief the bounds for the price label + */ + Rectangle bounds; + } price; +} PokeShopMenuItemStyle; + +typedef struct PokeShopMenuItemData : public MenuItemData +{ + PokemonPartyIconWidgetData iconData; + uint32_t price; +} PokeShopMenuItemData; + +/** + * @brief This is a custom MenuItem widget to display distribution event pokemon in a vertical list menu. + */ +class PokeShopMenuItem : public IWidget +{ +public: + PokeShopMenuItem(); + virtual ~PokeShopMenuItem(); + + const PokeShopMenuItemData& getData() const; + + void setData(const PokeShopMenuItemData& data); + void setStyle(const PokeShopMenuItemStyle& style); + + bool isFocused() const override; + void setFocused(bool isFocused) override; + + bool isVisible() const override; + void setVisible(bool visible) override; + + Rectangle getBounds() const override; + void setBounds(const Rectangle& bounds) override; + + Dimensions getSize() const override; + + bool handleUserInput(const joypad_inputs_t& userInput) override; + + void render(RDPQGraphics& gfx, const Rectangle& parentBounds) override; +protected: + /** + * Executes the onConfirmAction callback (if any) + */ + bool execute(); +private: + PokemonPartyIconWidget partyIconWidget_; + PokeShopMenuItemStyle style_; + PokeShopMenuItemData data_; + char priceText_[10]; + bool focused_; + bool visible_; + bool aButtonPressed_; +}; + +#endif \ No newline at end of file diff --git a/src/scenes/PokeShopScene.cpp b/src/scenes/PokeShopScene.cpp new file mode 100644 index 0000000..d4a8218 --- /dev/null +++ b/src/scenes/PokeShopScene.cpp @@ -0,0 +1,39 @@ +#include "scenes/PokeShopScene.h" +#include "scenes/SceneManager.h" +#include "scenes/StatsScene.h" +#include "transferpak/TransferPakManager.h" + +static const Rectangle menuListBounds = {20, 20, 280, 0}; +static const Rectangle imgScrollArrowUpBounds = {.x = 154, .y = 14, .width = 11, .height = 6}; +static const Rectangle imgScrollArrowDownBounds = {.x = 154, .y = 220, .width = 11, .height = 6}; + +static void injectDistributionPokemon(void* context, const void* data) +{ + auto scene = static_cast(context); + scene->triggerPokemonInjection(data); +} + +PokeShopScene::PokeShopScene(SceneDependencies& deps, void* context) + : MenuScene(deps, context) + , romReader_(deps.tpakManager) + , saveManager_(deps.tpakManager) + , gen1Reader_(romReader_, saveManager_, static_cast(deps.specificGenVersion), static_cast(deps.localization)) + , gen2Reader_(romReader_, saveManager_, static_cast(deps.specificGenVersion), static_cast(deps.localization)) + , iconFactory_(romReader_) + , customListFiller_(menuList_) + , diag_() + , iconBackgroundSprite_(nullptr) + , pokeToInject_(nullptr) +{ +} + +PokeShopScene::~PokeShopScene() +{ +} + +void PokeShopScene::init() +{ + iconBackgroundSprite_ = sprite_load("rom://bg-party-icon.sprite"); + loadShopList(); + MenuScene::init(); +} \ No newline at end of file diff --git a/src/widget/PokeShopMenuItemWidget.cpp b/src/widget/PokeShopMenuItemWidget.cpp new file mode 100644 index 0000000..0987511 --- /dev/null +++ b/src/widget/PokeShopMenuItemWidget.cpp @@ -0,0 +1,129 @@ +#include "widget/PokeShopMenuItemWidget.h" + +PokeShopMenuItem::PokeShopMenuItem() + : partyIconWidget_() + , style_({0}) + , data_() + , priceText_({0}) + , focused_(false) + , visible_(true) + , aButtonPressed_(false) +{ +} + +PokeShopMenuItem::~PokeShopMenuItem() +{ +} + +const PokeShopMenuItemData& PokeShopMenuItem::getData() const +{ + return data_; +} + +void PokeShopMenuItem::setData(const PokeShopMenuItemData& data) +{ + data_ = data; + + snprintf(priceText_, sizeof(priceText_), "$%u", data.price); + partyIconWidget_.setData(data.iconData); +} + +void PokeShopMenuItem::setStyle(const PokeShopMenuItemStyle& style) +{ + style_ = style; + + partyIconWidget_.setBounds(style.icon.bounds); + partyIconWidget_.setStyle(style.icon.style); +} + +bool PokeShopMenuItem::isFocused() const +{ + return focused_; +} + +void PokeShopMenuItem::setFocused(bool isFocused) +{ + focused_ = isFocused; + partyIconWidget_.setFocused(isFocused); +} + +bool PokeShopMenuItem::isVisible() const +{ + return visible_; +} + +void PokeShopMenuItem::setVisible(bool visible) +{ + visible_ = visible; +} + +Rectangle PokeShopMenuItem::getBounds() const +{ + return Rectangle{.x = 0, .y = 0, .width = style_.size.width, .height = style_.size.height}; +} + +void PokeShopMenuItem::setBounds(const Rectangle& bounds) +{ + // Not relevant: the actual bounds are passed from the VerticalList widget +} + +Dimensions PokeShopMenuItem::getSize() const +{ + return style_.size; +} + +bool PokeShopMenuItem::handleUserInput(const joypad_inputs_t& userInput) +{ + // We only care about the A-button press/release for this widget + if(userInput.btn.a) + { + aButtonPressed_ = true; + return true; + } + else if(aButtonPressed_) + { + aButtonPressed_ = false; + return execute(); + } + + return false; +} + +void PokeShopMenuItem::render(RDPQGraphics& gfx, const Rectangle& parentBounds) +{ + if(!visible_) + { + return; + } + const Rectangle myBounds = {.x = parentBounds.x, .y = parentBounds.y, .width = style_.size.width, .height = style_.size.height}; + if(style_.background.sprite) + { + gfx.drawSprite(myBounds, style_.background.sprite, style_.background.spriteSettings); + } + + partyIconWidget_.render(gfx, myBounds); + + const Rectangle titleBounds = { myBounds.x + style_.title.bounds.x, + myBounds.y + style_.title.bounds.y, + style_.title.bounds.width, + style_.title.bounds.height }; + + gfx.drawText(titleBounds, data_.title, (focused_) ? style_.title.labelFocused : style_.title.labelNotFocused); + + const Rectangle priceBounds = { myBounds.x + style_.price.bounds.x, + myBounds.y + style_.price.bounds.y, + style_.price.bounds.width, + style_.price.bounds.height }; + + gfx.drawText(priceBounds, priceText_, (focused_) ? style_.price.labelFocused : style_.price.labelNotFocused); +} + +bool PokeShopMenuItem::execute() +{ + if(data_.onConfirmAction) + { + data_.onConfirmAction(data_.context, data_.itemParam); + return true; + } + return false; +} \ No newline at end of file