Feature/show party icons on distribution pokemon selection screen (#8)

* Create PokemonPartyIconWidget for displaying the party menu icon in the distribution event pokemon menu (untested)

* Integrate PokemonPartyIconWidget into the DistributionPokemonListScene

* Do some visual changes to the DistributionPokemonListScene

* Revert "Undo README.md changes because they were already shown in github after merging the feature branch"

This reverts commit dc8d9bb00d.

* Update docs
This commit is contained in:
Philippe Symons 2024-09-30 23:29:09 +02:00 committed by GitHub
parent ddafa46dfc
commit 5f353d6a1c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 708 additions and 28 deletions

View File

@ -17,6 +17,10 @@ I'm happy to accept pull requests if the community wants to do them.
- Teach Pikachu Surf/Fly on Gen 1 cartridges
- You don't have to use the transfer pak in controller 1. You can have it in a separate controller if you want. But the UI is still controlled with controller 1.
- Unlock Mystery Gift decorations like the Pikachu Bed and Tentacool Doll that were left inaccessible in Gold/Silver/Crystal due to bugs in Pokemon Stadium 2 (suggested by /u/MermaidRaccoon on reddit)
- Make it possible to backup your cartridge save file onto the flashcart PokeMe64 is running from.
- Make it possible to restore a save file on the N64 flashcart to an actual Pokémon gameboy cartridge. You can even restore emulator saves!
- Be able to wipe the save file from a game cartridge (mostly added as a feature in case you mess up and restore a save file from a completely different game)
- Make it easy to reset/reconfigure the Generation II game clock
# Limitations
- Right now, this rom only supports the international (English) versions of the games.
@ -34,7 +38,7 @@ To build it, set up a [build environment for libdragon](https://github.com/Drago
# Usage
WARNING: Do not insert or remove your gameboy cartridge, N64 transfer pak or controller while the Nintendo 64 is powered on. Doing so might corrupt your save file or just plainly won't work! (header validation check may fail)
- Copy PokeMe64.z64 to your Nintendo 64 flash cartridge. (such as Everdrive64, Super 64, ED64Plus, ...)
- Copy PokeMe64.z64 to your Nintendo 64 flash cartridge. (such as Everdrive64, Super 64, ED64Plus, SummerCart 64, ...)
- Have your Nintendo 64 powered off. (IMPORTANT)
- Connect your N64 transfer pak to your original (OEM) Nintendo controller. Third party controllers possibly don't work. You can verify this by testing with Pokémon Stadium 1 or 2 first.
- Insert your pokémon gameboy cartridge into your N64 transfer Pak
@ -51,7 +55,6 @@ But having it done with a Nintendo 64 feels more "real"/"official" and is easier
# Future potential improvements (ideas/roadmap)
## UI ideas
- In the pokémon list, show the mini menu sprite that you would also see in the party menu in the gameboy games
- add some background images and potentially sprites here and there.
- add some acquisition sound effects from the gameboy games
- add a skippable "trade" 3D animation sequence when you receive a distribution pokémon. The idea is to have a pokéball go into a green mario pipe on either a Nintendo 64 3D model or Nintendo 64 3D logo model. Then follow the pipe with the camera and have the pokéball drop onto a huge 3D representation of the gameboy cartridge before opening the pokéball which then triggers the stats screen.
@ -61,9 +64,8 @@ But having it done with a Nintendo 64 feels more "real"/"official" and is easier
- Have a "music" widget that shows up to name the song(s) that I end up using when it/they start(s) playing. (similar to how [Need For Speed - Most Wanted (original)](https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhWk37230YvbMHaMchN8dzQiRrO66VofThpcbvUTFMoplDbkQKBVUFcIabbNCnzZ0KpuxcAQmrXQjBlqv_bvi6v6xpjmPxs3tJ-ZI_GhOn3xe5DW7XpMbtnCKFcbBQ-l_zzbrIIV4smBpth/s1600/_mwmusic.jpg) used to show this)
## Feature ideas
- Support reproduction cartridges (in libpokemegb)
- Support reproduction cartridges
- Support other language versions (in libpokemegb)
- Make it possible to backup your cartridge save file onto the flashcart PokeMe64 is running from.
- Make it possible to display your cartridge save file as a QR code and contribute to the 3DS' [PKSM](https://github.com/FlagBrew/PKSM) project to migrate the save file easily from gameboy cartridge to 3DS.
- Make it possible to swap gameboy cartridge after using the reset button on the N64. (suggested by /u/bluemooncinco on reddit)

BIN
assets/bg-party-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -2,6 +2,7 @@
#define _DISTRIBUTIONPOKEMONLISTSCENE_H
#include "scenes/MenuScene.h"
#include "widget/DistributionPokemonMenuItemWidget.h"
#include "transferpak/TransferPakRomReader.h"
#include "transferpak/TransferPakSaveManager.h"
#include "gen1/Gen1GameReader.h"
@ -74,7 +75,10 @@ private:
TransferPakSaveManager saveManager_;
Gen1GameReader gen1Reader_;
Gen2GameReader gen2Reader_;
PokemonPartyIconFactory iconFactory_;
ListItemFiller<VerticalList, DistributionPokemonMenuItemData, DistributionPokemonMenuItem, DistributionPokemonMenuItemStyle> customListFiller_;
DialogData diag_;
sprite_t* iconBackgroundSprite_;
const void* pokeToInject_;
};

View File

@ -0,0 +1,95 @@
#ifndef _DISTRIBUTIONPOKEMONMENUITEMWIDGET_H
#define _DISTRIBUTIONPOKEMONMENUITEMWIDGET_H
#include "widget/PokemonPartyIconWidget.h"
#include "widget/MenuItemWidget.h"
typedef struct DistributionPokemonMenuItemStyle
{
/**
* 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;
/**
* These are the text settings for when the MenuItemWidget is NOT focused by the user
*/
TextRenderSettings titleNotFocused;
/**
* These are the text render settings for when the MenuItemWidget is focused by the user
*/
TextRenderSettings titleFocused;
/**
* Offset to indicate how far from the left we need to start rendering the title text
*/
uint16_t leftMargin;
/**
* Offset to indicate how far from the top we need to start rendering the title text
*/
uint16_t topMargin;
} DistributionPokemonMenuItemStyle;
typedef struct DistributionPokemonMenuItemData : public MenuItemData
{
PokemonPartyIconWidgetData iconData;
} DistributionPokemonMenuItemData;
/**
* @brief This is a custom MenuItem widget to display distribution event pokemon in a vertical list menu.
*/
class DistributionPokemonMenuItem : public IWidget
{
public:
DistributionPokemonMenuItem();
virtual ~DistributionPokemonMenuItem();
const DistributionPokemonMenuItemData& getData() const;
void setData(const DistributionPokemonMenuItemData& data);
void setStyle(const DistributionPokemonMenuItemStyle& 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_;
DistributionPokemonMenuItemStyle style_;
DistributionPokemonMenuItemData data_;
bool focused_;
bool visible_;
bool aButtonPressed_;
};
#endif

View File

@ -29,7 +29,7 @@ public:
deleteWidgets();
}
void addItems(ListDataType* dataList, size_t dataListSize, const MenuItemStyle& itemStyle)
void addItems(ListDataType* dataList, size_t dataListSize, const ListItemWidgetStyleType& itemStyle)
{
ListItemWidgetType* itemWidget;
for(size_t i = 0; i < dataListSize; ++i)

View File

@ -0,0 +1,156 @@
#ifndef _POKEMONPARTYICONWIDGET_H
#define _POKEMONPARTYICONWIDGET_H
#include "widget/IWidget.h"
#include "core/Sprite.h"
#include "gen1/Gen1Common.h"
#include "gen2/Gen2Common.h"
class IRomReader;
class ISaveManager;
/**
* This factory class provides a function to obtain a surface_t instance for a given icon.
* It acts as a form of cache to prevent decoding/allocating the same icon more than once if multiple
* instances of PokemonPartyIconWidget are created.
*/
class PokemonPartyIconFactory
{
public:
typedef struct IconMapEntry
{
surface_t frame1;
surface_t frame2;
} IconMapEntry;
PokemonPartyIconFactory(IRomReader& romReader);
~PokemonPartyIconFactory();
surface_t getIcon(uint8_t generation, uint8_t specificGenType, uint8_t iconType, bool firstFrame);
protected:
private:
// This is the map of icons. Gen2 has the most icons, that's why I'm using GEN2_ICONTYPE_MAX as the array size
IconMapEntry iconMap_[(unsigned)Gen2PokemonIconType::GEN2_ICONTYPE_MAX];
IRomReader& romReader_;
};
typedef struct PokemonPartyIconWidgetStyle
{
struct {
/**
* (optional) background sprite
*/
sprite_t* sprite;
/*
* RenderSettings that influence how the backgroundSprite is
* being rendered
*/
SpriteRenderSettings spriteSettings;
} background;
struct {
Rectangle bounds;
int8_t yOffsetWhenTheresNoFrame2;
} icon;
uint8_t fpsWhenFocused;
uint8_t fpsWhenNotFocused;
} PokemonPartyIconWidgetStyle;
typedef struct PokemonPartyIconWidgetData
{
PokemonPartyIconFactory* iconFactory;
/**
* The generation of the pokemon game (gen 1/2)
*/
uint8_t generation;
/**
* The specific variant of the game
*/
uint8_t specificGenVersion;
/**
* The pokemon icon type we need to display
*/
uint8_t iconType;
} PokemonPartyIconWidgetData;
class PokemonPartyIconWidget : public IWidget
{
public:
PokemonPartyIconWidget();
virtual ~PokemonPartyIconWidget();
void setStyle(const PokemonPartyIconWidgetStyle& style);
void setData(const PokemonPartyIconWidgetData& data);
/**
* @brief Returns whether the widget is currently focused
*/
bool isFocused() const override;
/**
* @brief Sets whether the widget is currently focused
*
*/
void setFocused(bool isFocused) override;
/**
* @brief Returns whether the widget is currently visible
*/
bool isVisible() const override;
/**
* @brief Changes the visibility of the widget
*/
void setVisible(bool visible) override;
/**
* @brief Returns the current (relative) bounds of the widget
*/
Rectangle getBounds() const override;
/**
* @brief Changes the current (relative) bounds of the widget
*/
void setBounds(const Rectangle& bounds) override;
/**
* @brief Returns the size (width/height) of the widget
*/
Dimensions getSize() const override;
/**
* @brief Handles user input
*
* For button presses, it is advised to track button release situations instead of
* button presses for executing an action. Otherwise the key press might be handled again immediately
* in the next scene/widget because the user wouldn't have had the time to actually release the key.
*/
bool handleUserInput(const joypad_inputs_t& userInput) override;
/**
* @brief Renders the widget
*
* @param gfx The graphics instance that must be used to render the widget
* @param parentBounds The bounds of the parent widget or scene. You must add the x,y offset of your own bounds
* to the parentBounds to get the absolute bounds for rendering.
*
* Getting the parentBounds as an argument of this function was done because a parent widget may be
* animated or change positions independent of the child widget. But when the parent widget moves, the child must as well!
*/
void render(RDPQGraphics& gfx, const Rectangle& parentBounds) override;
protected:
private:
void reset();
PokemonPartyIconWidgetStyle style_;
PokemonPartyIconWidgetData data_;
surface_t iconFrame1_;
surface_t iconFrame2_;
Rectangle bounds_;
uint32_t frameSwitchTimeoutInTicks_;
uint64_t nextFrameSwitchTime_;
bool focused_;
bool visible_;
bool showFirstIconFrame_;
};
#endif

@ -1 +1 @@
Subproject commit bd518d0d8719418de5ad5638fb49c43f609c03cd
Subproject commit feff0c5327a84638f7bc840c15c578e2fefc0673

View File

@ -24,7 +24,10 @@ DistributionPokemonListScene::DistributionPokemonListScene(SceneDependencies& de
, saveManager_(deps.tpakManager)
, gen1Reader_(romReader_, saveManager_, static_cast<Gen1GameType>(deps.specificGenVersion))
, gen2Reader_(romReader_, saveManager_, static_cast<Gen2GameType>(deps.specificGenVersion))
, iconFactory_(romReader_)
, customListFiller_(menuList_)
, diag_()
, iconBackgroundSprite_(nullptr)
, pokeToInject_(nullptr)
{
}
@ -35,6 +38,7 @@ DistributionPokemonListScene::~DistributionPokemonListScene()
void DistributionPokemonListScene::init()
{
iconBackgroundSprite_ = sprite_load("rom://bg-party-icon.sprite");
loadDistributionPokemonList();
MenuScene::init();
}
@ -46,6 +50,9 @@ void DistributionPokemonListScene::destroy()
delete[] context_->menuEntries;
context_->menuEntries = nullptr;
context_->numMenuEntries = 0;
sprite_free(iconBackgroundSprite_);
iconBackgroundSprite_ = nullptr;
}
bool DistributionPokemonListScene::handleUserInput(joypad_port_t port, const joypad_inputs_t& inputs)
@ -150,16 +157,11 @@ void DistributionPokemonListScene::onDialogDone()
void DistributionPokemonListScene::setupMenu()
{
const VerticalListStyle listStyle = {
.background = {
.sprite = menu9SliceSprite_,
.spriteSettings = {
.renderMode = SpriteRenderMode::NINESLICE,
.srcRect = { 6, 6, 6, 6 }
}
},
.margin = {
.top = 5
.top = 5,
.bottom = 5
},
.verticalSpacingBetweenWidgets = 1,
.autogrow = {
.enabled = true,
.maxHeight = 200
@ -173,8 +175,29 @@ void DistributionPokemonListScene::setupMenu()
cursorWidget_.setVisible(false);
const MenuItemStyle itemStyle = {
.size = {280, 16},
const DistributionPokemonMenuItemStyle itemStyle = {
.size = {280, 22},
.background = {
.sprite = menu9SliceSprite_,
.spriteSettings = {
.renderMode = SpriteRenderMode::NINESLICE,
.srcRect = { 6, 6, 6, 6 }
}
},
.icon = {
.style = {
.background = {
.sprite = iconBackgroundSprite_
},
.icon = {
.bounds = { 2, 2, 16, 16 },
.yOffsetWhenTheresNoFrame2 = -1
},
.fpsWhenFocused = 8,
.fpsWhenNotFocused = 2
},
.bounds = {0, 1, 20, 20}
},
.titleNotFocused = {
.fontId = arialId_,
.fontStyleId = fontStyleWhiteId_
@ -183,11 +206,11 @@ void DistributionPokemonListScene::setupMenu()
.fontId = arialId_,
.fontStyleId = fontStyleYellowId_
},
.leftMargin = 10,
.topMargin = 1
.leftMargin = 24,
.topMargin = 4
};
menuListFiller_.addItems(context_->menuEntries, context_->numMenuEntries, itemStyle);
customListFiller_.addItems(static_cast<DistributionPokemonMenuItemData*>(context_->menuEntries), context_->numMenuEntries, itemStyle);
const ImageWidgetStyle scrollArrowUpStyle = {
.image = {
@ -218,6 +241,7 @@ void DistributionPokemonListScene::loadDistributionPokemonList()
const Gen2DistributionPokemon** gen2List;
uint32_t listSize;
uint32_t i;
uint8_t iconType;
DistributionPokemonListSceneContext* context = convert(context_);
@ -246,27 +270,51 @@ void DistributionPokemonListScene::loadDistributionPokemonList()
{
return;
}
context->menuEntries = new MenuItemData[listSize];
context->menuEntries = new DistributionPokemonMenuItemData[listSize];
context->numMenuEntries = listSize;
if(gen1List)
{
for(i = 0; i < listSize; ++i)
{
context->menuEntries[i].title = gen1List[i]->name;
context->menuEntries[i].onConfirmAction = injectDistributionPokemon;
context->menuEntries[i].context = this;
context->menuEntries[i].itemParam = gen1List[i];
DistributionPokemonMenuItemData* menuEntry = static_cast<DistributionPokemonMenuItemData*>(context_->menuEntries) + i;
menuEntry->title = gen1List[i]->name;
menuEntry->onConfirmAction = injectDistributionPokemon;
menuEntry->context = this;
menuEntry->itemParam = gen1List[i];
menuEntry->iconData = {
.iconFactory = &iconFactory_,
.generation = deps_.generation,
.specificGenVersion = deps_.specificGenVersion,
.iconType = (uint8_t)gen1Reader_.getPokemonIconType(gen1List[i]->poke.poke_index)
};
}
}
else if(gen2List)
{
for(i = 0; i < listSize; ++i)
{
context->menuEntries[i].title = gen2List[i]->name;
context->menuEntries[i].onConfirmAction = injectDistributionPokemon;
context->menuEntries[i].context = this;
context->menuEntries[i].itemParam = gen2List[i];
DistributionPokemonMenuItemData* menuEntry = static_cast<DistributionPokemonMenuItemData*>(context_->menuEntries) + i;
if(gen2List[i]->isEgg)
{
iconType = (uint8_t)Gen2PokemonIconType::GEN2_ICONTYPE_EGG;
}
else
{
iconType = (uint8_t)gen2Reader_.getPokemonIconType(gen2List[i]->poke.poke_index);
}
menuEntry->title = gen2List[i]->name;
menuEntry->onConfirmAction = injectDistributionPokemon;
menuEntry->context = this;
menuEntry->itemParam = gen2List[i];
menuEntry->iconData = {
.iconFactory = &iconFactory_,
.generation = deps_.generation,
.specificGenVersion = deps_.specificGenVersion,
.iconType = iconType
};
}
}
}

View File

@ -0,0 +1,122 @@
#include "widget/DistributionPokemonMenuItemWidget.h"
DistributionPokemonMenuItem::DistributionPokemonMenuItem()
: partyIconWidget_()
, style_({0})
, data_()
, focused_(false)
, visible_(true)
, aButtonPressed_(false)
{
}
DistributionPokemonMenuItem::~DistributionPokemonMenuItem()
{
}
const DistributionPokemonMenuItemData& DistributionPokemonMenuItem::getData() const
{
return data_;
}
void DistributionPokemonMenuItem::setData(const DistributionPokemonMenuItemData& data)
{
data_ = data;
partyIconWidget_.setData(data.iconData);
}
void DistributionPokemonMenuItem::setStyle(const DistributionPokemonMenuItemStyle& style)
{
style_ = style;
partyIconWidget_.setBounds(style.icon.bounds);
partyIconWidget_.setStyle(style.icon.style);
}
bool DistributionPokemonMenuItem::isFocused() const
{
return focused_;
}
void DistributionPokemonMenuItem::setFocused(bool isFocused)
{
focused_ = isFocused;
partyIconWidget_.setFocused(isFocused);
}
bool DistributionPokemonMenuItem::isVisible() const
{
return visible_;
}
void DistributionPokemonMenuItem::setVisible(bool visible)
{
visible_ = visible;
}
Rectangle DistributionPokemonMenuItem::getBounds() const
{
return Rectangle{.x = 0, .y = 0, .width = style_.size.width, .height = style_.size.height};
}
void DistributionPokemonMenuItem::setBounds(const Rectangle& bounds)
{
// Not relevant: the actual bounds are passed from the VerticalList widget
}
Dimensions DistributionPokemonMenuItem::getSize() const
{
return style_.size;
}
bool DistributionPokemonMenuItem::handleUserInput(const joypad_inputs_t& userInput)
{
// only handle button release, otherwise you'll be in trouble on scene transitions:
// the user can't release the button fast enough, so the same press would get handled twice.
if(userInput.btn.a)
{
aButtonPressed_ = true;
return true;
}
else if(aButtonPressed_)
{
aButtonPressed_ = false;
return execute();
}
return false;
}
void DistributionPokemonMenuItem::render(RDPQGraphics& gfx, const Rectangle& parentBounds)
{
if(!visible_)
{
return;
}
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);
myBounds.x += style_.leftMargin;
myBounds.y += style_.topMargin;
// account for leftMargin and topMargin twice (we also apply it for the rightmargin and bottom)
myBounds.width -= style_.leftMargin;
myBounds.width -= style_.leftMargin;
myBounds.height -= style_.topMargin - style_.topMargin;
gfx.drawText(myBounds, data_.title, (focused_) ? style_.titleFocused : style_.titleNotFocused);
}
bool DistributionPokemonMenuItem::execute()
{
if(data_.onConfirmAction)
{
data_.onConfirmAction(data_.context, data_.itemParam);
return true;
}
return false;
}

View File

@ -0,0 +1,253 @@
#include "widget/PokemonPartyIconWidget.h"
#include "core/RDPQGraphics.h"
#include "gen1/Gen1GameReader.h"
#include "gen2/Gen2GameReader.h"
#include "SaveManager.h"
#include "SpriteRenderer.h"
#define DEFAULT_FPS_WHEN_FOCUSED 4
#define DEFAULT_FPS_WHEN_NOT_FOCUSED 2
static uint32_t calculateFrameSwitchTimeout(uint8_t fps)
{
return TICKS_FROM_MS(1000u / fps);
}
PokemonPartyIconFactory::PokemonPartyIconFactory(IRomReader& romReader)
: iconMap_()
, romReader_(romReader)
{
memset(iconMap_, 0, sizeof(IconMapEntry) * Gen2PokemonIconType::GEN2_ICONTYPE_MAX);
}
PokemonPartyIconFactory::~PokemonPartyIconFactory()
{
for(uint8_t i = 0; i < (uint8_t)Gen2PokemonIconType::GEN2_ICONTYPE_MAX; ++i)
{
if(iconMap_[i].frame1.buffer)
{
surface_free(&iconMap_[i].frame1);
}
if(iconMap_[i].frame2.buffer)
{
surface_free(&iconMap_[i].frame2);
}
}
}
surface_t PokemonPartyIconFactory::getIcon(uint8_t generation, uint8_t specificGenType, uint8_t iconType, bool firstFrame)
{
BufferBasedSaveManager dummy(nullptr, 0);
SpriteRenderer spriteRenderer;
surface_t result = (firstFrame) ? iconMap_[iconType].frame1 : iconMap_[iconType].frame2;
const uint8_t* outputBuffer = nullptr;
uint8_t iconWidthInPixels;
uint8_t iconHeightInPixels;
// For RGBA16, we have 2 bytes per color
const uint8_t bytesPerColor = 2;
if(result.buffer)
{
return result;
}
if(generation == 1)
{
Gen1GameReader gameReader(romReader_, dummy, (Gen1GameType)specificGenType);
iconWidthInPixels = GEN1_ICON_WIDTH_IN_TILES * 8;
iconHeightInPixels = GEN1_ICON_HEIGHT_IN_TILES * 8;
const uint8_t* spriteBuffer = gameReader.decodePokemonIcon((Gen1PokemonIconType)iconType, firstFrame);
if(!spriteBuffer)
{
return result;
}
spriteRenderer.draw(spriteBuffer, SpriteRenderer::OutputFormat::RGBA16, monochromeGBColorPalette, GEN1_ICON_WIDTH_IN_TILES, GEN1_ICON_HEIGHT_IN_TILES);
outputBuffer = spriteRenderer.removeWhiteBackground(GEN1_ICON_WIDTH_IN_TILES, GEN1_ICON_HEIGHT_IN_TILES);
}
else if(generation == 2)
{
Gen2GameReader gameReader(romReader_, dummy, (Gen2GameType)specificGenType);
iconWidthInPixels = GEN2_ICON_WIDTH_IN_TILES * 8;
iconHeightInPixels = GEN2_ICON_HEIGHT_IN_TILES * 8;
const uint8_t* spriteBuffer = gameReader.decodePokemonIcon((Gen2PokemonIconType)iconType, firstFrame);
if(!spriteBuffer)
{
return result;
}
spriteRenderer.draw(spriteBuffer, SpriteRenderer::OutputFormat::RGBA16, gen2_iconColorPalette, GEN2_ICON_WIDTH_IN_TILES, GEN2_ICON_HEIGHT_IN_TILES);
outputBuffer = spriteRenderer.removeWhiteBackground(GEN2_ICON_WIDTH_IN_TILES, GEN2_ICON_HEIGHT_IN_TILES);
}
else
{
debugf("Invalid generation %hu!\n", generation);
return result;
}
// due to alignment constraints, we should use the surface_alloc function to ensure that it is done correctly
// and respect its resulting stride field
result = surface_alloc(FMT_RGBA16, iconWidthInPixels, iconHeightInPixels);
//now copy the sprite to the surface, line by line
const uint32_t actualStride = iconWidthInPixels * bytesPerColor;
for(uint8_t i=0; i < iconHeightInPixels; ++i)
{
const uint8_t* src = outputBuffer + (i * actualStride);
uint8_t* dst = ((uint8_t*)(result.buffer)) + (i * result.stride);
// copy the source buffer to the surface line by line, but advancing with the surface stride on every new row
memcpy(dst, src, actualStride);
}
// now we have a valid surface_t. Let's store it in our map
if(firstFrame)
{
iconMap_[iconType].frame1 = result;
}
else
{
iconMap_[iconType].frame2 = result;
}
return result;
}
PokemonPartyIconWidget::PokemonPartyIconWidget()
: style_({0})
, data_({0})
, iconFrame1_({0})
, iconFrame2_({0})
, bounds_({0})
, frameSwitchTimeoutInTicks_(0)
, nextFrameSwitchTime_(0)
, focused_(false)
, visible_(true)
, showFirstIconFrame_(true)
{
}
PokemonPartyIconWidget::~PokemonPartyIconWidget()
{
}
void PokemonPartyIconWidget::setStyle(const PokemonPartyIconWidgetStyle& style)
{
style_ = style;
reset();
}
void PokemonPartyIconWidget::setData(const PokemonPartyIconWidgetData& data)
{
data_ = data;
if(!data.iconFactory)
{
debugf("ERROR: No PokemonPartyIconFactory instance was provided!\n");
return;
}
iconFrame1_ = data.iconFactory->getIcon(data_.generation, data_.specificGenVersion, data_.iconType, true);
iconFrame2_ = data.iconFactory->getIcon(data_.generation, data_.specificGenVersion, data_.iconType, false);
reset();
}
bool PokemonPartyIconWidget::isFocused() const
{
return focused_;
}
void PokemonPartyIconWidget::setFocused(bool focused)
{
focused_ = focused;
reset();
}
bool PokemonPartyIconWidget::isVisible() const
{
return visible_;
}
void PokemonPartyIconWidget::setVisible(bool visible)
{
visible_ = visible;
reset();
}
Rectangle PokemonPartyIconWidget::getBounds() const
{
return bounds_;
}
void PokemonPartyIconWidget::setBounds(const Rectangle& bounds)
{
bounds_ = bounds;
}
Dimensions PokemonPartyIconWidget::getSize() const
{
return Dimensions{bounds_.width, bounds_.height};
}
bool PokemonPartyIconWidget::handleUserInput(const joypad_inputs_t&)
{
return false;
}
void PokemonPartyIconWidget::render(RDPQGraphics& gfx, const Rectangle& parentBounds)
{
const SurfaceRenderSettings renderSettings = { 0 };
const Rectangle absoluteBounds = addOffset(bounds_, parentBounds);
const Rectangle spriteBounds = addOffset(style_.icon.bounds, absoluteBounds);
const uint64_t currentTicks = get_ticks();
if(currentTicks >= nextFrameSwitchTime_)
{
showFirstIconFrame_ = !showFirstIconFrame_;
nextFrameSwitchTime_ += frameSwitchTimeoutInTicks_;
}
if(style_.background.sprite)
{
gfx.drawSprite(absoluteBounds, style_.background.sprite, style_.background.spriteSettings);
}
if(showFirstIconFrame_)
{
if(iconFrame1_.buffer)
{
gfx.drawSurface(spriteBounds, &iconFrame1_, renderSettings);
}
}
else
{
if(iconFrame2_.buffer)
{
gfx.drawSurface(spriteBounds, &iconFrame2_, renderSettings);
}
else if(iconFrame1_.buffer)
{
const Rectangle modifiedSpriteBounds = { .x = spriteBounds.x, .y = spriteBounds.y + style_.icon.yOffsetWhenTheresNoFrame2, .width = spriteBounds.width, .height = spriteBounds.height };
gfx.drawSurface(modifiedSpriteBounds, &iconFrame1_, renderSettings);
}
}
}
void PokemonPartyIconWidget::reset()
{
const uint8_t fps = (focused_) ? style_.fpsWhenFocused : style_.fpsWhenNotFocused;
if(fps)
{
frameSwitchTimeoutInTicks_ = calculateFrameSwitchTimeout(fps);
nextFrameSwitchTime_ = get_ticks() + frameSwitchTimeoutInTicks_;
}
else
{
nextFrameSwitchTime_ = 0xFFFFFFFFFFFFFFFF;
}
showFirstIconFrame_ = true;
}