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

This commit is contained in:
Philippe Symons 2024-09-29 19:35:07 +02:00
parent ddafa46dfc
commit 7373337e2e
2 changed files with 391 additions and 0 deletions

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;
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;
uint8_t yOffsetWhenTheresNoFrame2;
} icon;
} PokemonPartyIconWidgetStyle;
typedef struct PokemonPartyIconWidgetData
{
/**
* 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;
uint8_t fpsWhenFocused;
uint8_t fpsWhenNotFocused;
} PokemonPartyIconWidgetData;
/**
* 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_;
};
class PokemonPartyIconWidget : public IWidget
{
public:
PokemonPartyIconWidget(PokemonPartyIconFactory& iconFactory);
~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_;
PokemonPartyIconFactory& iconFactory_;
bool focused_;
bool visible_;
bool showFirstIconFrame_;
};
#endif

View File

@ -0,0 +1,235 @@
#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(PokemonPartyIconFactory& iconFactory)
: style_({0})
, data_({0})
, iconFrame1_({0})
, iconFrame2_({0})
, bounds_({0})
, frameSwitchTimeoutInTicks_(0)
, nextFrameSwitchTime_(0)
, iconFactory_(iconFactory)
, focused_(false)
, visible_(true)
, showFirstIconFrame_(true)
{
}
PokemonPartyIconWidget::~PokemonPartyIconWidget()
{
}
void PokemonPartyIconWidget::setStyle(const PokemonPartyIconWidgetStyle& style)
{
style_ = style;
}
void PokemonPartyIconWidget::setData(const PokemonPartyIconWidgetData& data)
{
data_ = data;
iconFrame1_ = iconFactory_.getIcon(data_.generation, data_.specificGenVersion, data_.iconType, true);
iconFrame2_ = 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(absoluteBounds, style_.icon.bounds);
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_)
{
gfx.drawSurface(spriteBounds, &iconFrame1_, renderSettings);
}
else
{
if(iconFrame2_.buffer)
{
gfx.drawSurface(spriteBounds, &iconFrame2_, renderSettings);
}
else
{
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_) ? data_.fpsWhenFocused : data_.fpsWhenNotFocused;
frameSwitchTimeoutInTicks_ = calculateFrameSwitchTimeout(fps);
nextFrameSwitchTime_ = get_ticks() + frameSwitchTimeoutInTicks_;
showFirstIconFrame_ = true;
}