Add menu options to teach Pikachu Surf and Fly

This implements functionality to teach Pikachu Surf and/or Fly and
extends existing widgets to help provide this functionality.

When a gen1 game is inserted, the user gets the choice to teach Pikachu
these moves.

This commit also restructures some of the Data and Style structs.
This commit is contained in:
Philippe Symons 2024-07-25 11:01:20 +02:00
parent fdc6f90415
commit eaf9224b31
17 changed files with 594 additions and 167 deletions

View File

@ -1,6 +1,12 @@
#ifndef _MENUFUNCTIONS_H
#define _MENUFUNCTIONS_H
#include "Moves.h"
// these are used to pass as a pointer to the gen1PrepareToTeachPikachu
extern const Move MOVE_SURF;
extern const Move MOVE_FLY;
void printMessage(void* context, const void* param);
void activateFrameLog(void* context, const void* param);
@ -10,6 +16,8 @@ void goToGen1DistributionPokemonMenu(void* context, const void* param);
void goToGen2DistributionPokemonMenu(void* context, const void* param);
void goToGen2PCNYDistributionPokemonMenu(void* context, const void* param);
void gen1PrepareToTeachPikachu(void* context, const void* param);
void gen1TeachPikachu(void* context, const void* param);
void gen2ReceiveGSBall(void* context, const void* param);
#endif

View File

@ -38,18 +38,11 @@ public:
SceneDependencies& getDependencies();
/**
* This is a helper function to show a single message to the user.
* It should only be used for simple situations. (feedback on a function executed directly in the menu for example)
*/
void showSingleMessage(const DialogData& messageData);
virtual void showDialog(DialogData* diagData);
protected:
virtual void setupMenu();
void setupFonts() override;
void setupDialog(DialogWidgetStyle& style) override;
virtual void showDialog(DialogData* diagData);
MenuSceneContext* context_;
sprite_t* menu9SliceSprite_;
sprite_t* cursorSprite_;
@ -57,10 +50,8 @@ protected:
CursorWidget cursorWidget_;
ListItemFiller<VerticalList, MenuItemData, MenuItemWidget, MenuItemStyle> menuListFiller_;
WidgetFocusChainSegment listFocusChainSegment_;
uint8_t fontStyleYellowId_;
bool bButtonPressed_;
private:
DialogData singleMessageDialog_;
};
void deleteMenuSceneContext(void* context);

View File

@ -14,6 +14,8 @@ public:
void destroy() override;
void render(RDPQGraphics& gfx, const Rectangle& sceneBounds) override;
void advanceDialog();
protected:
virtual void setupFonts();
virtual void setupDialog(DialogWidgetStyle& style);
@ -22,6 +24,7 @@ protected:
WidgetFocusChainSegment dialogFocusChainSegment_;
uint8_t arialId_;
uint8_t fontStyleWhiteId_;
uint8_t fontStyleYellowId_;
private:
};

View File

@ -1,7 +1,8 @@
#ifndef _DIALOGWIDGET_H
#define _DIALOGWIDGET_H
#include "widget/IWidget.h"
#include "widget/VerticalList.h"
#include "widget/MenuItemWidget.h"
#include "core/Sprite.h"
#include "core/RDPQGraphics.h"
@ -13,32 +14,52 @@ typedef struct DialogData
{
char text[DIALOG_TEXT_SIZE];
// optional sprite of a character that is saying the dialog text
sprite_t* characterSprite;
SpriteRenderSettings characterSpriteSettings;
// bounds of the character sprite relative to the widget
Rectangle characterSpriteBounds;
bool characterSpriteVisible;
sprite_t* buttonSprite;
SpriteRenderSettings buttonSpriteSettings;
// bounds of the button sprite relative to the widget
Rectangle buttonSpriteBounds;
bool buttonSpriteVisible;
struct {
sprite_t* sprite;
SpriteRenderSettings spriteSettings;
// bounds of the character sprite relative to the widget
Rectangle spriteBounds;
bool spriteVisible;
} character;
// optional button sprite
struct {
sprite_t* sprite;
SpriteRenderSettings spriteSettings;
// bounds of the button sprite relative to the widget
Rectangle spriteBounds;
bool spriteVisible;
} button;
// use this struct if you want to provide dialog options
struct {
MenuItemData* items;
uint8_t number;
bool shouldDeleteWhenDone;
} options;
// The next Dialog
struct DialogData* next;
bool shouldReleaseWhenDone;
bool shouldDeleteWhenDone;
bool userAdvanceBlocked;
//TODO: dialog sound
} DialogData;
typedef struct DialogWidgetStyle
{
sprite_t* backgroundSprite;
SpriteRenderSettings backgroundSpriteSettings;
struct {
sprite_t* sprite;
SpriteRenderSettings spriteSettings;
} background;
struct {
Rectangle bounds;
MenuItemStyle style;
} dialogOptions;
TextRenderSettings textSettings;
int marginLeft;
int marginRight;
int marginTop;
int marginBottom;
struct {
int left;
int right;
int top;
int bottom;
} margin;
} DialogWidgetStyle;
/**
@ -95,6 +116,7 @@ private:
*/
bool isAdvanceAllowed() const;
VerticalList dialogOptionList_;
AnimationManager& animationManager_;
Rectangle bounds_;
DialogWidgetStyle style_;

View File

@ -40,30 +40,34 @@ typedef struct MenuItemStyle
* width and height for the MenuItemWidget
*/
Dimensions size;
/**
* (optional) background sprite
*/
sprite_t* backgroundSprite;
/*
* RenderSettings that influence how the backgroundSprite is
* being rendered
*/
SpriteRenderSettings backgroundSpriteSettings;
struct {
/**
* (optional) background sprite
*/
sprite_t* sprite;
/*
* RenderSettings that influence how the backgroundSprite is
* being rendered
*/
SpriteRenderSettings spriteSettings;
} background;
/**
* (optional) icon sprite
*/
sprite_t* iconSprite;
struct {
/**
* (optional) icon sprite
*/
sprite_t* sprite;
/**
* RenderSettings that influence how the iconSprite is being rendered
*/
SpriteRenderSettings iconSpriteSettings;
/**
* relative bounds of the icon sprite in relation to the MenuItem widget
*/
Rectangle iconSpriteBounds;
/**
* RenderSettings that influence how the iconSprite is being rendered
*/
SpriteRenderSettings spriteSettings;
/**
* relative bounds of the icon sprite in relation to the MenuItem widget
*/
Rectangle spriteBounds;
} icon;
/**
* These are the text settings for when the MenuItemWidget is NOT focused by the user
*/

View File

@ -53,39 +53,62 @@ private:
*/
typedef struct VerticalListStyle
{
/**
* @brief (optional) a background sprite to render the background of
* the widget
*/
sprite_t* backgroundSprite;
/**
* @brief (optional) render settings for rendering the background sprite (if any)
*
*/
SpriteRenderSettings backgroundSpriteSettings;
/**
* @brief left margin -> the widgets will start rendering after this x spacing offset from the left edge of the list
*/
int marginLeft;
/**
* @brief right margin -> the widgets will stop rendering x pixels from the right of this list
*/
int marginRight;
/**
* @brief top margin -> the widgets will start rendering after this y spacing offset from the top edge of the list
*
*/
int marginTop;
/**
* @brief bottom margin -> the widgets will stop rendering y pixels from the bottom edge of this list.
*
*/
int marginBottom;
struct {
/**
* @brief (optional) a background sprite to render the background of
* the widget
*/
sprite_t* sprite;
/**
* @brief (optional) render settings for rendering the background sprite (if any)
*
*/
SpriteRenderSettings spriteSettings;
} background;
struct {
/**
* @brief left margin -> the widgets will start rendering after this x spacing offset from the left edge of the list
*/
int left;
/**
* @brief right margin -> the widgets will stop rendering x pixels from the right of this list
*/
int right;
/**
* @brief top margin -> the widgets will start rendering after this y spacing offset from the top edge of the list
*/
int top;
/**
* @brief bottom margin -> the widgets will stop rendering y pixels from the bottom edge of this list.
*/
int bottom;
} margin;
/**
* @brief the amount of spacing (in pixels) between 2 list widgets (default: 0)
*/
int verticalSpacingBetweenWidgets;
/**
* @brief should grow automaticaly with each item
*/
struct {
/**
* Whether the list should grow its bounds whenever items are added
*/
bool enabled;
/**
* defines the direction the list should grow in. true for upwards, false for downwards
*/
bool shouldGrowUpWards;
/**
* Maximum height the list should grow towards
*/
uint16_t maxHeight;
} autogrow;
} VerticalListStyle;
/**
@ -117,7 +140,9 @@ public:
void addWidget(IWidget* widget);
void clearWidgets();
VerticalListStyle& getStyle();
void setStyle(const VerticalListStyle& style);
void setViewWindowStartY(uint32_t windowStartY);
bool isFocused() const override;
@ -145,6 +170,7 @@ private:
int32_t scrollWindowToFocusedWidget();
void moveWindow(int32_t yAmount);
void notifyFocusListeners(const FocusChangeStatus& status);
void autoGrowBounds();
MoveVerticalListWindowAnimation moveWindowAnimation_;
IWidgetList widgetList_;

@ -1 +1 @@
Subproject commit 747bdd4b4b27c8d1bd9c44f0d651d4c8c439b90c
Subproject commit b275d12a974bb1df92edf38e5bb32267339469ad

View File

@ -5,6 +5,16 @@ MenuItemData gen1MenuEntries[] = {
{
.title = "Event Pokémon",
.onConfirmAction = goToGen1DistributionPokemonMenu
},
{
.title = "Teach Pikachu Surf",
.onConfirmAction = gen1PrepareToTeachPikachu,
.itemParam = &MOVE_SURF
},
{
.title = "Teach Pikachu Fly",
.onConfirmAction = gen1PrepareToTeachPikachu,
.itemParam = &MOVE_FLY
}
};

View File

@ -10,20 +10,36 @@
#define POKEMON_CRYSTAL_ITEM_ID_GS_BALL 0x73
#if 0
static void goToMenu(void* context, MenuItemData* menuEntries, uint32_t numMenuEntries)
const Move MOVE_SURF = Move::SURF;
const Move MOVE_FLY = Move::FLY;
typedef struct Gen1TeachPikachuParams
{
MenuScene* scene = static_cast<MenuScene*>(context);
SceneManager& sceneManager = scene->getDependencies().sceneManager;
Move moveType;
uint8_t partyIndex;
uint8_t moveIndex;
Gen1TrainerPokemon poke;
} Gen1TeachPikachuParams;
MenuSceneContext* sceneContext = new MenuSceneContext{
.menuEntries = menuEntries,
.numMenuEntries = numMenuEntries
};
sceneManager.switchScene(SceneType::MENU, deleteMenuSceneContext, sceneContext);
}
#endif
// here are a few structs I use to pass around parameters to the gen1TeachPikachu() function
// i want to avoid dynamic allocation for this purpose.
// we need 4 of them because we will have 4 menu options, each with a different moveIndex
static Gen1TeachPikachuParams teachParamsMove1 = {
.moveType = Move::SURF, // dummy value. Will be replaced later
.moveIndex = 0
};
static Gen1TeachPikachuParams teachParamsMove2 = {
.moveType = Move::SURF, // dummy value. Will be replaced later
.moveIndex = 1
};
static Gen1TeachPikachuParams teachParamsMove3 = {
.moveType = Move::SURF, // dummy value. Will be replaced later
.moveIndex = 2
};
static Gen1TeachPikachuParams teachParamsMove4 = {
.moveType = Move::SURF, // dummy value. Will be replaced later
.moveIndex = 3
};
static void goToDistributionPokemonListMenu(void* context, DistributionPokemonListType type)
{
@ -36,6 +52,25 @@ static void goToDistributionPokemonListMenu(void* context, DistributionPokemonLi
sceneManager.switchScene(SceneType::DISTRIBUTION_POKEMON_LIST, deleteDistributionPokemonListSceneContext, sceneContext);
}
static uint8_t gen1FindPikachuInParty(Gen1Party& party)
{
const uint8_t PIKACHU_INDEX_CODE = 0x54;
uint8_t foundIndex = 0xFF;
const uint8_t numberOfPokemon = party.getNumberOfPokemon();
for(uint8_t i=0; i < numberOfPokemon; ++i)
{
const uint8_t speciesIndex = party.getSpeciesAtIndex(i);
if(speciesIndex == PIKACHU_INDEX_CODE)
{
// debugf("Found Pikachu at index %hu\r\n", i);
foundIndex = i;
break;
}
}
return foundIndex;
}
void printMessage(void* context, const void*)
{
debugf((const char*)context);
@ -73,6 +108,205 @@ void goToGen2PCNYDistributionPokemonMenu(void* context, const void* param)
goToDistributionPokemonListMenu(context, DistributionPokemonListType::GEN2_POKEMON_CENTER_NEW_YORK);
}
void gen1PrepareToTeachPikachu(void* context, const void* param)
{
MenuScene* scene = static_cast<MenuScene*>(context);
TransferPakManager& tpakManager = scene->getDependencies().tpakManager;
TransferPakRomReader romReader(tpakManager);
TransferPakSaveManager saveManager(tpakManager);
Gen1GameReader gameReader(romReader, saveManager, static_cast<Gen1GameType>(scene->getDependencies().specificGenVersion));
DialogData* msg1 = nullptr;
DialogData* msg2 = nullptr;
uint8_t foundIndex;
const Move moveType = *static_cast<const Move*>(param);
tpakManager.setRAMEnabled(true);
Gen1TrainerPokemon poke;
Gen1Party party = gameReader.getParty();
foundIndex = gen1FindPikachuInParty(party);
if(foundIndex == 0xFF)
{
tpakManager.setRAMEnabled(false);
msg1 = new DialogData{
.shouldDeleteWhenDone = true
};
setDialogDataText(*msg1, "I see you don't have Pikachu in your party. Please come back once you do!");
scene->showDialog(msg1);
return;
}
if(!party.getPokemon(foundIndex, poke, false))
{
tpakManager.setRAMEnabled(false);
debugf("%s: ERROR while retrieving pokemon from party at index %hu\r\n", __FUNCTION__, foundIndex);
return;
}
tpakManager.setRAMEnabled(false);
if(poke.index_move1 == (uint8_t)moveType || poke.index_move2 == (uint8_t)moveType || poke.index_move3 == (uint8_t)moveType || poke.index_move4 == (uint8_t)moveType)
{
msg1 = new DialogData{
.shouldDeleteWhenDone = true
};
setDialogDataText(*msg1, "Your Pikachu already knows %s!", getMoveString(moveType));
scene->showDialog(msg1);
return;
}
teachParamsMove1.moveType = teachParamsMove2.moveType = teachParamsMove3.moveType = teachParamsMove4.moveType = moveType;
teachParamsMove1.partyIndex = teachParamsMove2.partyIndex = teachParamsMove3.partyIndex = teachParamsMove4.partyIndex = foundIndex;
teachParamsMove1.poke = teachParamsMove2.poke = teachParamsMove3.poke = teachParamsMove4.poke = poke;
if(!poke.index_move1)
{
gen1TeachPikachu(context, &teachParamsMove1);
return;
}
else if(!poke.index_move2)
{
gen1TeachPikachu(context, &teachParamsMove2);
return;
}
else if(!poke.index_move3)
{
gen1TeachPikachu(context, &teachParamsMove3);
return;
}
else if(!poke.index_move4)
{
gen1TeachPikachu(context, &teachParamsMove4);
return;
}
msg2 = new DialogData {
.options = {
.items = new MenuItemData[4]{
{
.title = getMoveString(static_cast<Move>(poke.index_move1)),
.onConfirmAction = gen1TeachPikachu,
.context = context,
.itemParam = &teachParamsMove1
},
{
.title = getMoveString(static_cast<Move>(poke.index_move2)),
.onConfirmAction = gen1TeachPikachu,
.context = context,
.itemParam = &teachParamsMove2
},
{
.title = getMoveString(static_cast<Move>(poke.index_move3)),
.onConfirmAction = gen1TeachPikachu,
.context = context,
.itemParam = &teachParamsMove3
},
{
.title = getMoveString(static_cast<Move>(poke.index_move4)),
.onConfirmAction = gen1TeachPikachu,
.context = context,
.itemParam = &teachParamsMove4
}
},
.number = 4,
.shouldDeleteWhenDone = true
},
.shouldDeleteWhenDone = true
};
msg1 = new DialogData{
.next = msg2,
.shouldDeleteWhenDone = true
};
setDialogDataText(*msg1, "Pikachu is trying to learn %s. But Pikachu already knows 4 moves.", getMoveString(moveType));
setDialogDataText(*msg2, "Delete an older move to make room for %s?", getMoveString(moveType));
scene->showDialog(msg1);
}
void gen1TeachPikachu(void* context, const void* param)
{
MenuScene* scene = static_cast<MenuScene*>(context);
TransferPakManager& tpakManager = scene->getDependencies().tpakManager;
TransferPakRomReader romReader(tpakManager);
TransferPakSaveManager saveManager(tpakManager);
Gen1GameReader gameReader(romReader, saveManager, static_cast<Gen1GameType>(scene->getDependencies().specificGenVersion));
const Gen1TeachPikachuParams* params = static_cast<const Gen1TeachPikachuParams*>(param);
Gen1TrainerPokemon poke = params->poke;
uint8_t previousMove;
uint8_t newMove = static_cast<uint8_t>(params->moveType);
// debugf("%s: newMove %hu, moveIndex %hu, partyIndex %hu\r\n", __FUNCTION__, newMove, params->moveIndex, params->partyIndex);
switch(params->moveIndex)
{
case 0:
previousMove = poke.index_move1;
poke.index_move1 = newMove;
poke.pp_move1 = 0;
break;
case 1:
previousMove = poke.index_move2;
poke.index_move2 = newMove;
poke.pp_move2 = 0;
break;
case 2:
previousMove = poke.index_move3;
poke.index_move3 = newMove;
poke.pp_move3 = 0;
break;
case 3:
previousMove = poke.index_move4;
poke.index_move4 = newMove;
poke.pp_move4 = 0;
break;
default:
scene->advanceDialog();
return;
}
tpakManager.setRAMEnabled(true);
Gen1Party party = gameReader.getParty();
if(!party.setPokemon(params->partyIndex, poke))
{
tpakManager.setRAMEnabled(false);
debugf("%s: ERROR: can't update Pikachu at partyIndex %hu\r\n", __FUNCTION__, params->partyIndex);
return;
}
gameReader.updateMainChecksum();
tpakManager.finishWrites();
tpakManager.setRAMEnabled(false);
DialogData* msg1 = new DialogData{
.shouldDeleteWhenDone = true
};
if(previousMove)
{
DialogData* msg3 = new DialogData{
.shouldDeleteWhenDone = true
};
DialogData* msg2 = new DialogData{
.next = msg3,
.shouldDeleteWhenDone = true
};
msg1->next = msg2;
setDialogDataText(*msg1, "1..2..3..Poof!");
setDialogDataText(*msg2, "Pikachu forgot %s!", getMoveString(static_cast<Move>(previousMove)));
setDialogDataText(*msg3, "Pikachu learned %s!", getMoveString(params->moveType));
}
else
{
setDialogDataText(*msg1, "Pikachu learned %s!", getMoveString(params->moveType));
}
scene->showDialog(msg1);
}
void gen2ReceiveGSBall(void* context, const void* param)
{
MenuScene* scene = static_cast<MenuScene*>(context);
@ -80,7 +314,9 @@ void gen2ReceiveGSBall(void* context, const void* param)
TransferPakRomReader romReader(tpakManager);
TransferPakSaveManager saveManager(tpakManager);
Gen2GameReader gameReader(romReader, saveManager, Gen2GameType::CRYSTAL);
DialogData messageData = {0};
DialogData* messageData = new DialogData{
.shouldDeleteWhenDone = true
};
bool alreadyHasOne = false;
tpakManager.setRAMEnabled(true);
@ -106,15 +342,13 @@ void gen2ReceiveGSBall(void* context, const void* param)
if(alreadyHasOne)
{
setDialogDataText(messageData, "It appears you already have one!");
setDialogDataText(*messageData, "It appears you already have one!");
}
else
{
keyItemPocket.add(POKEMON_CRYSTAL_ITEM_ID_GS_BALL, 1);
gameReader.finishSave();
setDialogDataText(messageData, "%s obtained a GS Ball!", trainerName);
setDialogDataText(*messageData, "%s obtained a GS Ball!", trainerName);
}
scene->showSingleMessage(messageData);
tpakManager.setRAMEnabled(false);
scene->showDialog(messageData);
}

View File

@ -162,12 +162,16 @@ void DistributionPokemonListScene::onDialogDone()
void DistributionPokemonListScene::setupMenu()
{
const VerticalListStyle listStyle = {
.backgroundSprite = menu9SliceSprite_,
.backgroundSpriteSettings = {
.renderMode = SpriteRenderMode::NINESLICE,
.srcRect = { 6, 6, 6, 6 }
.background = {
.sprite = menu9SliceSprite_,
.spriteSettings = {
.renderMode = SpriteRenderMode::NINESLICE,
.srcRect = { 6, 6, 6, 6 }
}
},
.marginTop = 5,
.margin = {
.top = 5
}
};
menuList_.setStyle(listStyle);

View File

@ -149,8 +149,8 @@ void InitTransferPakScene::setupTPakDetectWidget()
void InitTransferPakScene::setupDialog(DialogWidgetStyle& style)
{
style.backgroundSprite = menu9SliceSprite_;
style.backgroundSpriteSettings = {
style.background.sprite = menu9SliceSprite_;
style.background.spriteSettings = {
.renderMode = SpriteRenderMode::NINESLICE,
.srcRect = { 6, 6, 6, 6 }
};

View File

@ -21,9 +21,7 @@ MenuScene::MenuScene(SceneDependencies& deps, void* context)
, listFocusChainSegment_(WidgetFocusChainSegment{
.current = &menuList_
})
, fontStyleYellowId_(1)
, bButtonPressed_(false)
, singleMessageDialog_({0})
{
}
@ -131,33 +129,19 @@ SceneDependencies& MenuScene::getDependencies()
return deps_;
}
void MenuScene::showSingleMessage(const DialogData& messageData)
{
singleMessageDialog_ = messageData;
showDialog(&singleMessageDialog_);
}
void MenuScene::setupFonts()
{
SceneWithDialogWidget::setupFonts();
const rdpq_fontstyle_t arialYellow = {
.color = RGBA32(0xFF, 0xFF, 0x00, 0xFF),
.outline_color = RGBA32(0, 0, 0, 0xFF)
};
deps_.fontManager.registerFontStyle(arialId_, fontStyleYellowId_, arialYellow);
}
void MenuScene::setupMenu()
{
const VerticalListStyle listStyle = {
.backgroundSprite = menu9SliceSprite_,
.backgroundSpriteSettings = {
.renderMode = SpriteRenderMode::NINESLICE,
.srcRect = { 6, 6, 6, 6 }
.background = {
.sprite = menu9SliceSprite_,
.spriteSettings = {
.renderMode = SpriteRenderMode::NINESLICE,
.srcRect = { 6, 6, 6, 6 }
}
},
.marginTop = 5
.margin = {
.top = 5
}
};
const CursorStyle cursorStyle = {
@ -201,8 +185,8 @@ void MenuScene::setupMenu()
void MenuScene::setupDialog(DialogWidgetStyle& style)
{
style.backgroundSprite = menu9SliceSprite_;
style.backgroundSpriteSettings = {
style.background.sprite = menu9SliceSprite_;
style.background.spriteSettings = {
.renderMode = SpriteRenderMode::NINESLICE,
.srcRect = { 6, 6, 6, 6 }
};

View File

@ -10,6 +10,7 @@ SceneWithDialogWidget::SceneWithDialogWidget(SceneDependencies& deps)
})
, arialId_(1)
, fontStyleWhiteId_(0)
, fontStyleYellowId_(1)
{
}
@ -20,14 +21,32 @@ SceneWithDialogWidget::~SceneWithDialogWidget()
void SceneWithDialogWidget::init()
{
DialogWidgetStyle style = {
.dialogOptions = {
.bounds = Rectangle{190, 179, 120, 0},
.style = {
.size = {140, 16},
.titleNotFocused = {
.fontId = arialId_,
.fontStyleId = fontStyleWhiteId_
},
.titleFocused = {
.fontId = arialId_,
.fontStyleId = fontStyleYellowId_
},
.leftMargin = 5,
.topMargin = 1
}
},
.textSettings = {
.fontId = arialId_,
.fontStyleId = fontStyleWhiteId_
},
.marginLeft = 10,
.marginRight = 10,
.marginTop = 10,
.marginBottom = 10
.margin = {
.left = 10,
.right = 10,
.top = 10,
.bottom = 10
}
};
setupFonts();
@ -49,15 +68,26 @@ void SceneWithDialogWidget::render(RDPQGraphics& gfx, const Rectangle& sceneBoun
dialogWidget_.render(gfx, sceneBounds);
}
void SceneWithDialogWidget::advanceDialog()
{
dialogWidget_.advanceDialog();
}
void SceneWithDialogWidget::setupFonts()
{
arialId_ = deps_.fontManager.getFont("rom://Arial.font64");
const rdpq_fontstyle_t arialWhite = {
.color = RGBA32(0xFF, 0xFF, 0xFF, 0xFF),
.outline_color = RGBA32(0, 0, 0, 0xFF)
};
const rdpq_fontstyle_t arialYellow = {
.color = RGBA32(0xFF, 0xFF, 0x00, 0xFF),
.outline_color = RGBA32(0, 0, 0, 0xFF)
};
deps_.fontManager.registerFontStyle(arialId_, fontStyleWhiteId_, arialWhite);
deps_.fontManager.registerFontStyle(arialId_, fontStyleYellowId_, arialYellow);
}
void SceneWithDialogWidget::setupDialog(DialogWidgetStyle& style)

View File

@ -18,6 +18,8 @@ TransferPakSaveManager::TransferPakSaveManager(TransferPakManager& pakManager)
: pakManager_(pakManager)
, sramOffset_(0)
{
// reset back to sram bank 0
pakManager_.switchGBSRAMBank(0);
}
TransferPakSaveManager::~TransferPakSaveManager()

View File

@ -1,10 +1,39 @@
#include "widget/DialogWidget.h"
#include "widget/ListItemFiller.h"
#include "core/RDPQGraphics.h"
#include <cstdarg>
/**
* @brief Releases either a single DialogData entry or its entire chain.
*/
static void releaseEntry(DialogData* data, bool releaseAllEntries)
{
DialogData* cur = data;
DialogData* next = nullptr;
do
{
if(cur)
{
next = cur->next;
if(cur->options.shouldDeleteWhenDone && cur->options.items)
{
delete[] cur->options.items;
cur->options.items = nullptr;
}
if(cur->shouldDeleteWhenDone)
{
delete cur;
}
}
cur = next;
} while(releaseAllEntries && cur);
}
DialogWidget::DialogWidget(AnimationManager& animationManager)
: animationManager_(animationManager)
: dialogOptionList_(animationManager)
, animationManager_(animationManager)
, bounds_({0})
, style_({0})
, data_(nullptr)
@ -14,10 +43,13 @@ DialogWidget::DialogWidget(AnimationManager& animationManager)
, visible_(true)
, btnAPressedOnPrevCheck_(false)
{
dialogOptionList_.setVisible(false);
}
DialogWidget::~DialogWidget()
{
releaseEntry(data_, true);
data_ = nullptr;
}
const DialogWidgetStyle& DialogWidget::getStyle() const
@ -28,11 +60,36 @@ const DialogWidgetStyle& DialogWidget::getStyle() const
void DialogWidget::setStyle(const DialogWidgetStyle& style)
{
style_ = style;
VerticalListStyle& listStyle = dialogOptionList_.getStyle();
listStyle.background.sprite = style.background.sprite;
listStyle.background.spriteSettings = style.background.spriteSettings;
listStyle.margin.left = style.margin.left;
listStyle.margin.right = style.margin.right;
listStyle.margin.top = style.margin.top;
listStyle.margin.bottom = style.margin.bottom;
listStyle.autogrow.enabled = true;
listStyle.autogrow.shouldGrowUpWards = true;
dialogOptionList_.setBounds(style.dialogOptions.bounds);
}
void DialogWidget::setData(DialogData* data)
{
dialogOptionList_.clearWidgets();
dialogOptionList_.setVisible(false);
dialogOptionList_.setFocused(false);
releaseEntry(data_, true);
data_ = data;
if(data_ && data->options.items)
{
ListItemFiller<VerticalList, MenuItemData, MenuItemWidget, MenuItemStyle> filler(dialogOptionList_);
filler.addItems(data->options.items, data->options.number, style_.dialogOptions.style);
dialogOptionList_.setVisible(true);
dialogOptionList_.setFocused(focused_);
}
}
void DialogWidget::appendDialogData(DialogData* data)
@ -59,6 +116,11 @@ bool DialogWidget::isFocused() const
void DialogWidget::setFocused(bool focused)
{
focused_ = focused;
if(data_ && data_->options.number)
{
dialogOptionList_.setFocused(focused);
}
}
bool DialogWidget::isVisible() const
@ -102,18 +164,33 @@ void DialogWidget::advanceDialog()
}
return;
}
const DialogData* oldEntry = data_;
DialogData* oldEntry = data_;
data_ = data_->next;
releaseEntry(oldEntry, false);
if(oldEntry->shouldReleaseWhenDone)
dialogOptionList_.clearWidgets();
dialogOptionList_.setFocused(false);
dialogOptionList_.setVisible(false);
if(data_->options.items)
{
delete oldEntry;
oldEntry = nullptr;
ListItemFiller<VerticalList, MenuItemData, MenuItemWidget, MenuItemStyle> filler(dialogOptionList_);
filler.addItems(data_->options.items, data_->options.number, style_.dialogOptions.style);
dialogOptionList_.setVisible(true);
dialogOptionList_.setFocused(focused_);
}
}
bool DialogWidget::handleUserInput(const joypad_inputs_t& userInput)
{
// if dialog options are displayed and focused, the VerticalList widget needs to handle the key events
if(dialogOptionList_.isFocused())
{
return dialogOptionList_.handleUserInput(userInput);
}
// make sure the user needs to release the button before handling the A button again
// if we don't do that, a different component might react to the same a button press
if(btnAPressedOnPrevCheck_)
@ -141,9 +218,9 @@ void DialogWidget::render(RDPQGraphics& gfx, const Rectangle& parentBounds)
const Rectangle myBounds = addOffset(bounds_, parentBounds);
// render the background first, if any.
if(style_.backgroundSprite)
if(style_.background.sprite)
{
gfx.drawSprite(myBounds, style_.backgroundSprite, style_.backgroundSpriteSettings);
gfx.drawSprite(myBounds, style_.background.sprite, style_.background.spriteSettings);
}
if(!data_)
@ -151,29 +228,31 @@ void DialogWidget::render(RDPQGraphics& gfx, const Rectangle& parentBounds)
return;
}
if(data_->characterSprite && data_->characterSpriteVisible)
if(data_->character.sprite && data_->character.spriteVisible)
{
const Rectangle absoluteCharBounds = addOffset(data_->characterSpriteBounds, myBounds);
gfx.drawSprite(absoluteCharBounds, data_->characterSprite, data_->characterSpriteSettings);
const Rectangle absoluteCharBounds = addOffset(data_->character.spriteBounds, myBounds);
gfx.drawSprite(absoluteCharBounds, data_->character.sprite, data_->character.spriteSettings);
}
if(data_->buttonSprite && data_->buttonSpriteVisible)
if(data_->button.sprite && data_->button.spriteVisible)
{
const Rectangle absoluteButtonSpriteBounds = addOffset(data_->buttonSpriteBounds, myBounds);
gfx.drawSprite(absoluteButtonSpriteBounds, data_->buttonSprite, data_->buttonSpriteSettings);
const Rectangle absoluteButtonSpriteBounds = addOffset(data_->button.spriteBounds, myBounds);
gfx.drawSprite(absoluteButtonSpriteBounds, data_->button.sprite, data_->button.spriteSettings);
}
if(data_->text[0] != '\0')
{
const Rectangle textBounds = {
.x = myBounds.x + style_.marginLeft,
.y = myBounds.y + style_.marginTop,
.width = myBounds.width - style_.marginLeft - style_.marginRight,
.height = myBounds.height - style_.marginTop - style_.marginBottom
.x = myBounds.x + style_.margin.left,
.y = myBounds.y + style_.margin.top,
.width = myBounds.width - style_.margin.left - style_.margin.right,
.height = myBounds.height - style_.margin.top - style_.margin.bottom
};
gfx.drawText(textBounds, data_->text, style_.textSettings);
}
dialogOptionList_.render(gfx, parentBounds);
}
bool DialogWidget::isAdvanceAllowed() const

View File

@ -84,15 +84,15 @@ void MenuItemWidget::render(RDPQGraphics& gfx, const Rectangle& parentBounds)
return;
}
Rectangle myBounds = {.x = parentBounds.x, .y = parentBounds.y, .width = style_.size.width, .height = style_.size.height};
if(style_.backgroundSprite)
if(style_.background.sprite)
{
gfx.drawSprite(myBounds, style_.backgroundSprite, style_.backgroundSpriteSettings);
gfx.drawSprite(myBounds, style_.background.sprite, style_.background.spriteSettings);
}
if(style_.iconSprite)
if(style_.icon.sprite)
{
const Rectangle iconSpriteBounds = addOffset(style_.iconSpriteBounds, myBounds);
gfx.drawSprite(iconSpriteBounds, style_.iconSprite, style_.iconSpriteSettings);
const Rectangle iconSpriteBounds = addOffset(style_.icon.spriteBounds, myBounds);
gfx.drawSprite(iconSpriteBounds, style_.icon.sprite, style_.icon.spriteSettings);
}
myBounds.x += style_.leftMargin;

View File

@ -137,7 +137,7 @@ bool VerticalList::focusNext()
++focusedWidgetIndex_;
changeStatus.curFocus = widgetList_[focusedWidgetIndex_];
changeStatus.focusBounds = calculateListWidgetBounds(widgetBoundsList_[focusedWidgetIndex_], windowMinY_, bounds_.x + listStyle_.marginLeft, bounds_.y + listStyle_.marginTop);
changeStatus.focusBounds = calculateListWidgetBounds(widgetBoundsList_[focusedWidgetIndex_], windowMinY_, bounds_.x + listStyle_.margin.left, bounds_.y + listStyle_.margin.top);
widgetList_[focusedWidgetIndex_]->setFocused(true);
const int32_t scrollAmountY = scrollWindowToFocusedWidget();
@ -165,7 +165,7 @@ bool VerticalList::focusPrevious()
--focusedWidgetIndex_;
changeStatus.curFocus = widgetList_[focusedWidgetIndex_];
changeStatus.focusBounds = calculateListWidgetBounds(widgetBoundsList_[focusedWidgetIndex_], windowMinY_, bounds_.x + listStyle_.marginLeft, bounds_.y + listStyle_.marginTop);
changeStatus.focusBounds = calculateListWidgetBounds(widgetBoundsList_[focusedWidgetIndex_], windowMinY_, bounds_.x + listStyle_.margin.left, bounds_.y + listStyle_.margin.top);
widgetList_[focusedWidgetIndex_]->setFocused(true);
const int32_t scrollAmountY = scrollWindowToFocusedWidget();
@ -187,7 +187,7 @@ void VerticalList::addWidget(IWidget *widget)
widget->setFocused(focused_);
FocusChangeStatus changeStatus = {
.focusBounds = calculateListWidgetBounds(widgetBoundsList_[focusedWidgetIndex_], windowMinY_, bounds_.x + listStyle_.marginLeft, bounds_.y + listStyle_.marginTop),
.focusBounds = calculateListWidgetBounds(widgetBoundsList_[focusedWidgetIndex_], windowMinY_, bounds_.x + listStyle_.margin.left, bounds_.y + listStyle_.margin.top),
.prevFocus = nullptr,
.curFocus = widget
};
@ -198,12 +198,19 @@ void VerticalList::addWidget(IWidget *widget)
const Rectangle lastWidgetBounds = widgetBoundsList_.back();
widgetBoundsList_.push_back(Rectangle{.x = 0, .y = lastWidgetBounds.y + lastWidgetBounds.height + listStyle_.verticalSpacingBetweenWidgets, .width = widgetSize.width, .height = widgetSize.height});
}
autoGrowBounds();
}
void VerticalList::clearWidgets()
{
widgetList_.clear();
widgetBoundsList_.clear();
focusedWidgetIndex_ = 0;
}
VerticalListStyle& VerticalList::getStyle()
{
return listStyle_;
}
void VerticalList::setStyle(const VerticalListStyle& style)
@ -232,7 +239,7 @@ void VerticalList::setFocused(bool isFocused)
widgetList_[focusedWidgetIndex_]->setFocused(focused_);
FocusChangeStatus changeStatus = {
.focusBounds = calculateListWidgetBounds(widgetBoundsList_[focusedWidgetIndex_], windowMinY_, bounds_.x + listStyle_.marginLeft, bounds_.y + listStyle_.marginTop)
.focusBounds = calculateListWidgetBounds(widgetBoundsList_[focusedWidgetIndex_], windowMinY_, bounds_.x + listStyle_.margin.left, bounds_.y + listStyle_.margin.top)
};
if(isFocused)
@ -318,11 +325,11 @@ void VerticalList::render(RDPQGraphics& gfx, const Rectangle& parentBounds)
return;
}
const uint32_t innerListHeight = getInnerListHeight(bounds_, listStyle_.marginTop, listStyle_.marginBottom);
const uint32_t innerListHeight = getInnerListHeight(bounds_, listStyle_.margin.top, listStyle_.margin.bottom);
uint32_t i;
const Rectangle myBounds = addOffset(bounds_, parentBounds);
const int topX = myBounds.x + listStyle_.marginLeft;
const int topY = myBounds.y + listStyle_.marginTop;
const int topX = myBounds.x + listStyle_.margin.left;
const int topY = myBounds.y + listStyle_.margin.top;
// store previous clipping rectangle to restore later
// const Rectangle prevClipRect = gfx.getClippingRectangle();
@ -330,9 +337,9 @@ void VerticalList::render(RDPQGraphics& gfx, const Rectangle& parentBounds)
// gfx.setClippingRectangle(myBounds);
// render the background first, if any.
if(listStyle_.backgroundSprite)
if(listStyle_.background.sprite)
{
gfx.drawSprite(myBounds, listStyle_.backgroundSprite, listStyle_.backgroundSpriteSettings);
gfx.drawSprite(myBounds, listStyle_.background.sprite, listStyle_.background.spriteSettings);
}
if(widgetList_.empty())
@ -416,7 +423,7 @@ int32_t VerticalList::scrollWindowToFocusedWidget()
// the reason is the use of isWidgetInsideWindow() inside the render() function to cull entries from the render window.
// We could potentially eliminate this by expanding the check to allow a partially visible entry to be rendered.
// But for my goals, this is currently not needed, so I claim YAGNI for now.
const uint32_t innerListHeight = getInnerListHeight(bounds_, listStyle_.marginTop, listStyle_.marginBottom);
const uint32_t innerListHeight = getInnerListHeight(bounds_, listStyle_.margin.top, listStyle_.margin.bottom);
const int32_t windowScrollYNeeded = getVerticalWindowScrollNeededToMakeWidgetFullyVisible(widgetBoundsList_[focusedWidgetIndex_], innerListHeight, windowMinY_);
if(windowScrollYNeeded != 0)
{
@ -436,4 +443,27 @@ void VerticalList::notifyFocusListeners(const FocusChangeStatus& status)
{
listener->focusChanged(status);
}
}
void VerticalList::autoGrowBounds()
{
if(!listStyle_.autogrow.enabled)
{
return;
}
const Rectangle& lastWidgetBounds = widgetBoundsList_.back();
int heightToConsider = lastWidgetBounds.y + lastWidgetBounds.height + listStyle_.margin.top + listStyle_.margin.bottom;
if(listStyle_.autogrow.maxHeight && listStyle_.autogrow.maxHeight < heightToConsider)
{
heightToConsider = listStyle_.autogrow.maxHeight;
}
if(listStyle_.autogrow.shouldGrowUpWards)
{
const int heightDiff = abs(heightToConsider - bounds_.height);
bounds_.y -= heightDiff;
}
bounds_.height = heightToConsider;
}