Various changes

- Add "Pikachu Bed" and "Tentacool Doll" decoration unlock options for
  gen 2
- bugfix for unsafe behaviour
- attempt to make the RESET button work for cartridge switching. It
  failed though. (not reliable). Therefore I just added a warning
  message instead.
- Work on new ScrollWidget for Credits/About screen (Work In Progress)
This commit is contained in:
Philippe Symons 2024-07-31 22:29:21 +02:00
parent 6ce9c1ec23
commit f7a687b147
25 changed files with 676 additions and 46 deletions

View File

@ -16,6 +16,8 @@ public:
void init();
void run();
void onResetInterrupt();
protected:
private:
RDPQGraphics graphics_;

View File

@ -1,6 +1,21 @@
#ifndef _CORE_COMMON_H
#define _CORE_COMMON_H
typedef struct Point
{
int x;
int y;
} Point;
typedef struct FloatPoint
{
float x;
float y;
} FloatPoint;
typedef Point Vector;
typedef FloatPoint FloatVector;
typedef struct Dimensions
{
int width;
@ -27,6 +42,10 @@ bool isZeroSizeRectangle(const Rectangle& rect);
*/
Rectangle addOffset(const Rectangle& a, const Rectangle& b);
bool isPointInsideRectangle(const Rectangle& a, const Point& p);
bool doRectanglesOverlap(const Rectangle& a, const Rectangle& b);
/**
* @brief Extract a Dimensions struct from a Rectangle that only contains the width and height
*/

View File

@ -3,12 +3,17 @@
#include "Moves.h"
#include <cstdint>
// 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);
extern const uint16_t GEN2_EVENTFLAG_DECORATION_PIKACHU_BED;
extern const uint16_t GEN2_EVENTFLAG_DECORATION_TENTACOOL_DOLL;
void activateFrameLog(void* context, const void* param);
void advanceDialog(void* context, const void* param);
void goToTestScene(void* context, const void* param);
@ -19,5 +24,7 @@ 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);
void gen2SetEventFlag(void* context, const void* param);
#endif

View File

@ -49,6 +49,8 @@ typedef struct WidgetFocusChainSegment
class AbstractUIScene : public IScene
{
public:
static const uint16_t MINIMUM_TIME_BETWEEN_INPUT_EVENTS;
AbstractUIScene(SceneDependencies& deps);
virtual ~AbstractUIScene();

View File

@ -13,6 +13,7 @@ typedef struct MenuSceneContext
{
MenuItemData* menuEntries;
uint32_t numMenuEntries;
bool bButtonMeansUserWantsToSwitchCartridge;
} MenuSceneContext;
/**
@ -38,7 +39,7 @@ public:
SceneDependencies& getDependencies();
virtual void showDialog(DialogData* diagData);
void showDialog(DialogData* diagData) override;
protected:
virtual void setupMenu();
void setupDialog(DialogWidgetStyle& style) override;

View File

@ -41,7 +41,7 @@ public:
* WARNING: If you specify a sceneContext, you MUST also specify a deleteContextFunc callback function pointer.
* This is needed because you can't call delete() on a void*. And we need to keep the context around in the sceneHistory for as long as it needs to
*/
void switchScene(SceneType sceneType, void (*deleteContextFunc)(void*) = nullptr, void* sceneContext = nullptr);
void switchScene(SceneType sceneType, void (*deleteContextFunc)(void*) = nullptr, void* sceneContext = nullptr, bool deleteHistory = false);
/**
* @brief Switch back to the previous scene in the history stack

View File

@ -16,6 +16,7 @@ public:
void render(RDPQGraphics& gfx, const Rectangle& sceneBounds) override;
void advanceDialog();
virtual void showDialog(DialogData* diagData);
protected:
virtual void setupFonts();
virtual void setupDialog(DialogWidgetStyle& style);

View File

@ -42,6 +42,7 @@ public:
void setPort(joypad_port_t port);
bool hasTransferPak();
bool isPoweredOn() const;
bool setPower(bool on);
uint8_t getStatus();
@ -92,7 +93,7 @@ public:
protected:
private:
joypad_port_t port_;
bool wasPoweredAtLeastOnce_;
bool isPoweredOn_;
uint8_t currentSRAMBank_;
uint16_t readBufferBankOffset_;
uint16_t writeBufferSRAMBankOffset_;

View File

@ -3,8 +3,7 @@
#include "widget/VerticalList.h"
#include "widget/MenuItemWidget.h"
#include "core/Sprite.h"
#include "core/RDPQGraphics.h"
#include "widget/ListItemFiller.h"
#define DIALOG_TEXT_SIZE 512
@ -117,6 +116,7 @@ private:
bool isAdvanceAllowed() const;
VerticalList dialogOptionList_;
ListItemFiller<VerticalList, MenuItemData, MenuItemWidget, MenuItemStyle> dialogOptionListFiller_;
AnimationManager& animationManager_;
Rectangle bounds_;
DialogWidgetStyle style_;

View File

@ -26,11 +26,7 @@ public:
~ListItemFiller()
{
for(ListItemWidgetType* item : widgets_)
{
delete item;
}
widgets_.clear();
deleteWidgets();
}
void addItems(ListDataType* dataList, size_t dataListSize, const MenuItemStyle& itemStyle)
@ -44,6 +40,15 @@ public:
list_.addWidget(itemWidget);
}
}
void deleteWidgets()
{
for(ListItemWidgetType* item : widgets_)
{
delete item;
}
widgets_.clear();
}
protected:
private:
ListType& list_;

View File

@ -115,7 +115,7 @@ protected:
/**
* Executes the onConfirmAction callback (if any)
*/
void execute();
bool execute();
private:
MenuItemData data_;
MenuItemStyle style_;

View File

@ -0,0 +1,115 @@
#ifndef _SCROLLWIDGET_H
#define _SCROLLWIDGET_H
#include "widget/IWidget.h"
#include "animations/IAnimation.h"
#include <vector>
typedef std::vector<IWidget*> WidgetList;
class ScrollWidget;
class AnimationManager;
typedef struct ScrollWidgetStyle
{
// it defines the amount of pixels we should scroll when the analog stick or dpad is used.
uint8_t scrollStep;
} ScrollWidgetStyle;
/**
* @brief This Animation implementation is used internal in ScrollWidget
* to move the "view window"'s x and y-coordinates.
*
* It's specific to the ScrollWidget
*/
class MoveScrollWidgetWindowAnimation : public AbstractAnimation
{
public:
MoveScrollWidgetWindowAnimation(ScrollWidget* list);
virtual ~MoveScrollWidgetWindowAnimation();
AnimationDistanceTimeFunctionType getDistanceTimeFunctionType() const override;
uint32_t getDurationInMs() const override;
void start(const Point& startPoint, const Point& endPoint);
protected:
void apply(float pos) override;
private:
ScrollWidget* list_;
Point windowStartPoint_;
Point windowEndPoint_;
};
/**
* @brief This is a widget that contains other widgets and can scroll through it.
* It differs from VerticalList in that ScrollWidget manages "unstructured" widgets instead of just a list in a certain direction.
*
* But this scroll widget does not allow you to select a widget and/or handle events on it. Every widget added will remain unfocused and won't handle keys
* display only.
*
*/
class ScrollWidget : public IWidget
{
public:
ScrollWidget(AnimationManager& animManager);
virtual ~ScrollWidget();
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;
void setStyle(const ScrollWidgetStyle& style);
void addWidget(IWidget* widget);
void removeWidget(IWidget* widget);
bool handleUserInput(const joypad_inputs_t& userInput) override;
void render(RDPQGraphics& gfx, const Rectangle& parentBounds) override;
/**
* @brief Sets the view window start xy coords.
* This is used by MoveScrollWidgetWindowAnimation
*/
void setWindowStart(const Point& windowStart);
/**
* @brief Indicates the percentage at which the current window position is at horizontally
* in relation to the entire window
*/
float getWindowProgressX() const;
/**
* @brief Indicates the percentage at which the current window position is at vertically
* in relation to the entire window
*/
float getWindowProgressY() const;
protected:
private:
/**
* @brief This function will grow the maximum dimensions of the window
* based on the specified IWidget
*/
void growWindow(IWidget* widget);
/**
* @brief Recalculates the entire width and height of the view window
* This is necessary when removing a widget
*/
void recalculateWindowSize();
MoveScrollWidgetWindowAnimation windowAnimation_;
WidgetList widgets_;
ScrollWidgetStyle style_;
AnimationManager& animManager_;
Rectangle bounds_;
Rectangle windowBounds_;
bool visible_;
bool focused_;
};
#endif

View File

@ -1,6 +1,16 @@
#include "core/Application.h"
#include "scenes/IScene.h"
static Application* appInstance = nullptr;
static void resetInterruptHandler()
{
if(!appInstance)
{
return;
}
appInstance->onResetInterrupt();
}
Application::Application()
: graphics_()
, animationManager_()
@ -9,15 +19,21 @@ Application::Application()
, sceneManager_(graphics_, animationManager_, fontManager_, tpakManager_)
, sceneBounds_({0})
{
appInstance = this;
}
Application::~Application()
{
tpakManager_.setPower(false);
unregister_RESET_handler(resetInterruptHandler);
graphics_.destroy();
display_close();
timer_close();
joypad_close();
appInstance = nullptr;
}
void Application::init()
@ -37,6 +53,8 @@ void Application::init()
//display_init(RESOLUTION_320x240, DEPTH_16_BPP, 3, GAMMA_NONE, ANTIALIAS_RESAMPLE_FETCH_ALWAYS);
sceneManager_.switchScene(SceneType::INIT_TRANSFERPAK);
register_RESET_handler(resetInterruptHandler);
}
void Application::run()
@ -57,3 +75,8 @@ void Application::run()
graphics_.finishAndShowFrame();
}
}
void Application::onResetInterrupt()
{
tpakManager_.setPower(false);
}

View File

@ -10,6 +10,26 @@ Rectangle addOffset(const Rectangle &a, const Rectangle &b)
return Rectangle{.x = a.x + b.x, .y = a.y + b.y, .width = a.width, .height = a.height};
}
bool isPointInsideRectangle(const Rectangle& a, const Point& p)
{
const bool isInsideHorizontally = (p.x >= a.x) && (p.x < (a.x + a.width));
const bool isInsideVertically = (p.y >= a.y) && (p.y < (a.y + a.width));
return (isInsideHorizontally && isInsideVertically);
}
bool doRectanglesOverlap(const Rectangle& a, const Rectangle& b)
{
if (a.x < (b.x + b.width) && (a.x + a.width) > b.x &&
a.y + a.height > b.y && a.y < (b.y + b.height) )
{
return true;
}
else
{
return false;
}
}
Dimensions getDimensions(const Rectangle &r)
{
return Dimensions{.width = r.width, .height = r.height};

View File

@ -28,6 +28,16 @@ MenuItemData gen2MenuEntries[] = {
{
.title = "PCNY Pokémon",
.onConfirmAction = goToGen2PCNYDistributionPokemonMenu,
},
{
.title = "Unlock Pikachu Bed",
.onConfirmAction = gen2SetEventFlag,
.itemParam = &GEN2_EVENTFLAG_DECORATION_PIKACHU_BED
},
{
.title = "Unlock Tentacool Doll",
.onConfirmAction = gen2SetEventFlag,
.itemParam = &GEN2_EVENTFLAG_DECORATION_TENTACOOL_DOLL
}
};
@ -45,6 +55,16 @@ MenuItemData gen2CrystalMenuEntries[] = {
{
.title = "Unlock GS Ball",
.onConfirmAction = gen2ReceiveGSBall
},
{
.title = "Unlock Pikachu Bed",
.onConfirmAction = gen2SetEventFlag,
.itemParam = &GEN2_EVENTFLAG_DECORATION_PIKACHU_BED
},
{
.title = "Unlock Tentacool Doll",
.onConfirmAction = gen2SetEventFlag,
.itemParam = &GEN2_EVENTFLAG_DECORATION_TENTACOOL_DOLL
}
};

View File

@ -13,6 +13,10 @@
const Move MOVE_SURF = Move::SURF;
const Move MOVE_FLY = Move::FLY;
// based on https://github.com/kwsch/PKHeX/blob/master/PKHeX.Core/Resources/text/script/gen2/flags_c_en.txt
const uint16_t GEN2_EVENTFLAG_DECORATION_PIKACHU_BED = 679;
const uint16_t GEN2_EVENTFLAG_DECORATION_TENTACOOL_DOLL = 715;
typedef struct Gen1TeachPikachuParams
{
Move moveType;
@ -71,9 +75,17 @@ static uint8_t gen1FindPikachuInParty(Gen1Party& party)
return foundIndex;
}
void printMessage(void* context, const void*)
static const char* convertGen2EventFlagToString(uint16_t eventFlagIndex)
{
debugf((const char*)context);
switch(eventFlagIndex)
{
case GEN2_EVENTFLAG_DECORATION_PIKACHU_BED:
return "Pikachu Bed";
case GEN2_EVENTFLAG_DECORATION_TENTACOOL_DOLL:
return "Tentacool doll";
default:
return "UNKNOWN";
}
}
void activateFrameLog(void* context, const void*)
@ -84,6 +96,12 @@ void activateFrameLog(void* context, const void*)
gfx.triggerDebugFrame();
}
void advanceDialog(void* context, const void*)
{
MenuScene* scene = static_cast<MenuScene*>(context);
scene->advanceDialog();
}
void goToTestScene(void* context, const void* param)
{
MenuScene* scene = static_cast<MenuScene*>(context);
@ -330,5 +348,37 @@ void gen2ReceiveGSBall(void* context, const void* param)
setDialogDataText(*messageData, "GS Ball event unlocked! Please go to the Golden Rod Pokémon Center and try to leave!", trainerName);
scene->showDialog(messageData);
}
void gen2SetEventFlag(void* context, const void* param)
{
MenuScene* scene = static_cast<MenuScene*>(context);
TransferPakManager& tpakManager = scene->getDependencies().tpakManager;
TransferPakRomReader romReader(tpakManager);
TransferPakSaveManager saveManager(tpakManager);
Gen2GameReader gameReader(romReader, saveManager, static_cast<Gen2GameType>(scene->getDependencies().specificGenVersion));
DialogData* messageData = new DialogData{
.shouldDeleteWhenDone = true
};
const uint16_t eventFlagIndex = *static_cast<const uint16_t*>(param);
tpakManager.setRAMEnabled(true);
const char* trainerName = gameReader.getTrainerName();
if(gameReader.getEventFlag(eventFlagIndex))
{
setDialogDataText(*messageData, "%s already has %s!", trainerName, convertGen2EventFlagToString(eventFlagIndex));
}
else
{
gameReader.setEventFlag(eventFlagIndex, true);
gameReader.finishSave();
tpakManager.finishWrites();
setDialogDataText(*messageData, "%s has unlocked %s!", trainerName, convertGen2EventFlagToString(eventFlagIndex));
}
tpakManager.setRAMEnabled(false);
scene->showDialog(messageData);
}

View File

@ -2,7 +2,7 @@
#include "core/DragonUtils.h"
#include "widget/IWidget.h"
static uint16_t minimumTimeBetweenInputEventsInMs = 150;
const uint16_t AbstractUIScene::MINIMUM_TIME_BETWEEN_INPUT_EVENTS = 150;
AbstractUIScene::AbstractUIScene(SceneDependencies& deps)
: deps_(deps)
@ -23,7 +23,7 @@ void AbstractUIScene::processUserInput()
}
const uint64_t now = get_ticks();
if(TICKS_TO_MS(now - lastInputHandleTime_) < minimumTimeBetweenInputEventsInMs)
if(TICKS_TO_MS(now - lastInputHandleTime_) < MINIMUM_TIME_BETWEEN_INPUT_EVENTS)
{
// not enough time has passed since last handled input event. Ignore
return;

View File

@ -77,7 +77,8 @@ void InitTransferPakScene::onDialogDone()
{
menuContext = new MenuSceneContext({
.menuEntries = gen1MenuEntries,
.numMenuEntries = static_cast<uint32_t>(gen1MenuEntriesSize / sizeof(gen1MenuEntries[0]))
.numMenuEntries = static_cast<uint32_t>(gen1MenuEntriesSize / sizeof(gen1MenuEntries[0])),
.bButtonMeansUserWantsToSwitchCartridge = true
});
}
else if(gen2Type != Gen2GameType::INVALID)
@ -86,14 +87,16 @@ void InitTransferPakScene::onDialogDone()
{
menuContext = new MenuSceneContext({
.menuEntries = gen2CrystalMenuEntries,
.numMenuEntries = static_cast<uint32_t>(gen2CrystalMenuEntriesSize / sizeof(gen2CrystalMenuEntries[0]))
.numMenuEntries = static_cast<uint32_t>(gen2CrystalMenuEntriesSize / sizeof(gen2CrystalMenuEntries[0])),
.bButtonMeansUserWantsToSwitchCartridge = true
});
}
else
{
menuContext = new MenuSceneContext({
.menuEntries = gen2MenuEntries,
.numMenuEntries = static_cast<uint32_t>(gen2MenuEntriesSize / sizeof(gen2MenuEntries[0]))
.numMenuEntries = static_cast<uint32_t>(gen2MenuEntriesSize / sizeof(gen2MenuEntries[0])),
.bButtonMeansUserWantsToSwitchCartridge = true
});
}
}

View File

@ -1,6 +1,8 @@
#include "scenes/MenuScene.h"
#include "core/FontManager.h"
#include "scenes/SceneManager.h"
#include "transferpak/TransferPakManager.h"
#include "menu/MenuFunctions.h"
#include <cstdio>
@ -78,7 +80,7 @@ void MenuScene::render(RDPQGraphics& gfx, const Rectangle& sceneBounds)
.fontId = arialId_,
.fontStyleId = fontStyleWhiteId_
};
gfx.drawText(Rectangle{40, 10, 280, 16}, "PokeMe64 by risingPhil. Very early version...", renderSettings);
gfx.drawText(Rectangle{40, 10, 280, 16}, "PokeMe64 by risingPhil. Version 0.1", renderSettings);
}
bool MenuScene::handleUserInput(joypad_port_t port, const joypad_inputs_t& inputs)
@ -98,7 +100,22 @@ bool MenuScene::handleUserInput(joypad_port_t port, const joypad_inputs_t& input
{
// b button release occurred. Switch back to previous scene.
bButtonPressed_ = false;
deps_.sceneManager.goBackToPreviousScene();
if(context_->bButtonMeansUserWantsToSwitchCartridge)
{
DialogData* diag = new DialogData{
.shouldDeleteWhenDone = true
};
setDialogDataText(*diag, "Please turn the console off to switch gameboy cartridges!");
showDialog(diag);
}
else
{
// now do the actual switch back to the previous scene
deps_.sceneManager.goBackToPreviousScene();
}
return true;
}
return false;
@ -199,10 +216,9 @@ void MenuScene::setupDialog(DialogWidgetStyle& style)
void MenuScene::showDialog(DialogData* diagData)
{
SceneWithDialogWidget::showDialog(diagData);
menuList_.setVisible(false);
cursorWidget_.setVisible(false);
dialogWidget_.setVisible(true);
dialogWidget_.setData(diagData);
setFocusChain(&dialogFocusChainSegment_);
}

View File

@ -29,11 +29,16 @@ SceneManager::~SceneManager()
unloadScene(scene_);
}
void SceneManager::switchScene(SceneType type, void (*deleteContextFunc)(void*), void* sceneContext)
void SceneManager::switchScene(SceneType type, void (*deleteContextFunc)(void*), void* sceneContext, bool deleteHistory)
{
newSceneType_ = type;
newSceneContext_ = sceneContext;
if(deleteHistory)
{
clearHistory();
}
sceneHistory_.push_back(SceneHistorySegment{
.type = type,
.context = sceneContext,

View File

@ -73,6 +73,12 @@ void SceneWithDialogWidget::advanceDialog()
dialogWidget_.advanceDialog();
}
void SceneWithDialogWidget::showDialog(DialogData* diagData)
{
dialogWidget_.setVisible(true);
dialogWidget_.setData(diagData);
}
void SceneWithDialogWidget::setupFonts()
{
arialId_ = deps_.fontManager.getFont("rom://Arial.font64");

View File

@ -10,7 +10,7 @@ static const uint16_t sramBankStartGBAddress = 0xA000;
TransferPakManager::TransferPakManager()
: port_(JOYPAD_PORT_1)
, wasPoweredAtLeastOnce_(false)
, isPoweredOn_(false)
, currentSRAMBank_(0)
, readBufferBankOffset_(0xFFFF)
, writeBufferSRAMBankOffset_(0xFFFF)
@ -30,8 +30,11 @@ joypad_port_t TransferPakManager::getPort() const
void TransferPakManager::setPort(joypad_port_t port)
{
if(isPoweredOn_)
{
setPower(false);
}
port_ = port;
wasPoweredAtLeastOnce_ = false;
}
bool TransferPakManager::hasTransferPak()
@ -47,31 +50,48 @@ bool TransferPakManager::hasTransferPak()
return (type == JOYPAD_ACCESSORY_TYPE_TRANSFER_PAK);
}
bool TransferPakManager::isPoweredOn() const
{
return isPoweredOn_;
}
bool TransferPakManager::setPower(bool on)
{
uint8_t status;
int ret;
if(!wasPoweredAtLeastOnce_)
if(on)
{
if(!on)
{
return true;
}
ret = tpak_init(static_cast<int>(port_));
if(ret)
{
debugf("[TransferPakManager]: %s: tpak_init got error %d\r\n", __FUNCTION__, ret);
}
wasPoweredAtLeastOnce_ = true;
status = getStatus();
while(!(status | TPAK_STATUS_READY))
{
debugf("[TransferPakManager]: %s: ERROR: transfer pak not ready yet. Current status is %hu\r\n", __FUNCTION__, status);
status = getStatus();
}
isPoweredOn_ = true;
}
else
{
ret = tpak_set_power(port_, on);
ret = tpak_set_access(port_, false);
if(ret)
{
debugf("[TransferPakManager]: %s: tpak_set_power got error %d\r\n", __FUNCTION__, ret);
debugf("[TransferPakManager]: %s: tpak_set_access got error %d\r\n", __FUNCTION__, ret);
}
ret = tpak_set_power(port_, false);
if(ret)
{
debugf("[TransferPakManager]: %s: tpak_set_access got error %d\r\n", __FUNCTION__, ret);
}
isPoweredOn_ = false;
}
return (!ret);
}

View File

@ -1,5 +1,4 @@
#include "widget/DialogWidget.h"
#include "widget/ListItemFiller.h"
#include "core/RDPQGraphics.h"
#include <cstdarg>
@ -33,6 +32,7 @@ static void releaseEntry(DialogData* data, bool releaseAllEntries)
DialogWidget::DialogWidget(AnimationManager& animationManager)
: dialogOptionList_(animationManager)
, dialogOptionListFiller_(dialogOptionList_)
, animationManager_(animationManager)
, bounds_({0})
, style_({0})
@ -76,6 +76,8 @@ void DialogWidget::setStyle(const DialogWidgetStyle& style)
void DialogWidget::setData(DialogData* data)
{
dialogOptionList_.clearWidgets();
// the filler is the owner of the widgets. They need to be properly destroyed
dialogOptionListFiller_.deleteWidgets();
dialogOptionList_.setVisible(false);
dialogOptionList_.setFocused(false);
releaseEntry(data_, true);
@ -84,9 +86,7 @@ void DialogWidget::setData(DialogData* data)
if(data_ && data->options.items)
{
ListItemFiller<VerticalList, MenuItemData, MenuItemWidget, MenuItemStyle> filler(dialogOptionList_);
filler.addItems(data->options.items, data->options.number, style_.dialogOptions.style);
dialogOptionListFiller_.addItems(data->options.items, data->options.number, style_.dialogOptions.style);
dialogOptionList_.setVisible(true);
dialogOptionList_.setFocused(focused_);
}
@ -171,13 +171,13 @@ void DialogWidget::advanceDialog()
releaseEntry(oldEntry, false);
dialogOptionList_.clearWidgets();
// the filler is the owner of the widgets. They need to be properly destroyed
dialogOptionListFiller_.deleteWidgets();
dialogOptionList_.setFocused(false);
dialogOptionList_.setVisible(false);
if(data_->options.items)
{
ListItemFiller<VerticalList, MenuItemData, MenuItemWidget, MenuItemStyle> filler(dialogOptionList_);
filler.addItems(data_->options.items, data_->options.number, style_.dialogOptions.style);
dialogOptionListFiller_.addItems(data_->options.items, data_->options.number, style_.dialogOptions.style);
dialogOptionList_.setVisible(true);
dialogOptionList_.setFocused(focused_);
}

View File

@ -3,7 +3,7 @@
MenuItemWidget::MenuItemWidget()
: data_()
, style_()
, style_({0})
, focused_(false)
, visible_(true)
, aButtonPressed_(false)
@ -70,9 +70,8 @@ bool MenuItemWidget::handleUserInput(const joypad_inputs_t& userInput)
}
else if(aButtonPressed_)
{
execute();
aButtonPressed_ = false;
return true;
return execute();
}
return false;
}
@ -104,7 +103,12 @@ void MenuItemWidget::render(RDPQGraphics& gfx, const Rectangle& parentBounds)
gfx.drawText(myBounds, data_.title, (focused_) ? style_.titleFocused : style_.titleNotFocused);
}
void MenuItemWidget::execute()
bool MenuItemWidget::execute()
{
data_.onConfirmAction(data_.context, data_.itemParam);
if(data_.onConfirmAction)
{
data_.onConfirmAction(data_.context, data_.itemParam);
return true;
}
return false;
}

310
src/widget/ScrollWidget.cpp Normal file
View File

@ -0,0 +1,310 @@
#include "widget/ScrollWidget.h"
#include "core/common.h"
#include "scenes/AbstractUIScene.h"
#include "animations/AnimationManager.h"
#include "core/RDPQGraphics.h"
#include <algorithm>
static uint8_t ANALOG_STICK_MIN_THRESHOLD = 30;
static uint8_t ANALOG_STICK_MAX_THRESHOLD = 60;
static FloatVector determineScrollDirection(const joypad_inputs_t& userInput)
{
// this value should end up giving a semi-normalized vector when used for both x and y coords
const float DPAD_2_DIRECTIONS_PRESSED_COORD = 0.7071f;
const float STICK_RANGE_UNIT = 1.f / (ANALOG_STICK_MAX_THRESHOLD - ANALOG_STICK_MIN_THRESHOLD);
if(userInput.btn.d_left)
{
if(userInput.btn.d_up)
{
return FloatPoint{ -DPAD_2_DIRECTIONS_PRESSED_COORD, -DPAD_2_DIRECTIONS_PRESSED_COORD};
}
else if(userInput.btn.d_down)
{
return FloatPoint{ -DPAD_2_DIRECTIONS_PRESSED_COORD, DPAD_2_DIRECTIONS_PRESSED_COORD};
}
else
{
return FloatPoint{-1.f, 0.f};
}
}
else if(userInput.btn.d_right)
{
if(userInput.btn.d_up)
{
return FloatPoint{ DPAD_2_DIRECTIONS_PRESSED_COORD, -DPAD_2_DIRECTIONS_PRESSED_COORD};
}
else if(userInput.btn.d_down)
{
return FloatPoint{ DPAD_2_DIRECTIONS_PRESSED_COORD, DPAD_2_DIRECTIONS_PRESSED_COORD};
}
else
{
return FloatPoint{1.f, 0.f};
}
}
else if(userInput.btn.d_up)
{
return FloatPoint{0.f, -1.f};
}
else if(userInput.btn.d_down)
{
return FloatPoint{0.f, 1.f};
}
else
{
// analog stick
int8_t absXVal = static_cast<int8_t>(abs(userInput.stick_x));
int8_t absYVal = static_cast<int8_t>(abs(userInput.stick_y));
if(absXVal < ANALOG_STICK_MIN_THRESHOLD && absYVal < ANALOG_STICK_MIN_THRESHOLD)
{
return FloatPoint{0.f, 0.f};
}
// clamp the x and y values to ANALOG_STICK_MAX_THRESHOLD
absXVal = (absXVal > ANALOG_STICK_MAX_THRESHOLD) ? ANALOG_STICK_MAX_THRESHOLD : absXVal;
absYVal = (absYVal > ANALOG_STICK_MAX_THRESHOLD) ? ANALOG_STICK_MAX_THRESHOLD : absYVal;
// remove the dead zone and map the resulting range to 0.f-1.f
float resultX = static_cast<float>(absXVal - ANALOG_STICK_MIN_THRESHOLD) * STICK_RANGE_UNIT;
float resultY = static_cast<float>(absYVal - ANALOG_STICK_MIN_THRESHOLD) * STICK_RANGE_UNIT;
if(userInput.stick_x < 0)
{
resultX = -resultX;
}
if(userInput.stick_y > 0)
{
// up in the UI means negative y values
resultY = -resultY;
}
return FloatPoint{resultX, resultY};
}
return FloatPoint{0.f, 0.f};
}
MoveScrollWidgetWindowAnimation::MoveScrollWidgetWindowAnimation(ScrollWidget* list)
: AbstractAnimation(1.f)
, list_(list)
, windowStartPoint_({0})
, windowEndPoint_({0})
{
}
MoveScrollWidgetWindowAnimation::~MoveScrollWidgetWindowAnimation()
{
}
AnimationDistanceTimeFunctionType MoveScrollWidgetWindowAnimation::getDistanceTimeFunctionType() const
{
return AnimationDistanceTimeFunctionType::LINEAR;
}
uint32_t MoveScrollWidgetWindowAnimation::getDurationInMs() const
{
return AbstractUIScene::MINIMUM_TIME_BETWEEN_INPUT_EVENTS;
}
void MoveScrollWidgetWindowAnimation::start(const Point& startPoint, const Point& endPoint)
{
windowStartPoint_ = startPoint;
windowEndPoint_ = endPoint;
}
void MoveScrollWidgetWindowAnimation::apply(float pos)
{
const int px = static_cast<int>(ceil(windowStartPoint_.x + (pos * (windowEndPoint_.x - windowStartPoint_.x))));
const int py = static_cast<int>(ceil(windowStartPoint_.y + (pos * (windowEndPoint_.y - windowStartPoint_.y))));
list_->setWindowStart(Point{px, py});
}
ScrollWidget::ScrollWidget(AnimationManager& animManager)
: windowAnimation_(this)
, widgets_()
, style_({0})
, animManager_(animManager)
, bounds_({0})
, windowBounds_({0})
, visible_(true)
, focused_(false)
{
animManager_.add(&windowAnimation_);
}
ScrollWidget::~ScrollWidget()
{
animManager_.remove(&windowAnimation_);
}
bool ScrollWidget::isFocused() const
{
return focused_;
}
void ScrollWidget::setFocused(bool isFocused)
{
focused_ = isFocused;
}
bool ScrollWidget::isVisible() const
{
return visible_;
}
void ScrollWidget::setVisible(bool visible)
{
visible_ = visible;
}
Rectangle ScrollWidget::getBounds() const
{
return bounds_;
}
void ScrollWidget::setBounds(const Rectangle& bounds)
{
bounds_ = bounds;
}
Dimensions ScrollWidget::getSize() const
{
return Dimensions{.width = bounds_.width, .height = bounds_.height};
}
void ScrollWidget::setStyle(const ScrollWidgetStyle& style)
{
style_ = style;
}
void ScrollWidget::addWidget(IWidget* widget)
{
widgets_.push_back(widget);
growWindow(widget);
}
void ScrollWidget::removeWidget(IWidget* widget)
{
auto it = std::find(widgets_.begin(), widgets_.end(), widget);
if(it != widgets_.end())
{
widgets_.erase(it);
}
recalculateWindowSize();
}
bool ScrollWidget::handleUserInput(const joypad_inputs_t& userInput)
{
if(!focused_ || style_.scrollStep == 0)
{
return false;
}
const FloatVector scrollDirection = determineScrollDirection(userInput);
if(scrollDirection.x == 0.f && scrollDirection.y == 0.f)
{
return false;
}
if(!windowAnimation_.isFinished())
{
windowAnimation_.skipToEnd();
}
const Point windowStartPoint = {.x = windowBounds_.x, .y = windowBounds_.y};
Point windowEndPoint = {.x = windowBounds_.x, .y = windowBounds_.y};
windowEndPoint.x += static_cast<int>(ceil(scrollDirection.x * style_.scrollStep));
windowEndPoint.y += static_cast<int>(ceil(scrollDirection.y * style_.scrollStep));
windowAnimation_.start(windowStartPoint, windowEndPoint);
return true;
}
void ScrollWidget::render(RDPQGraphics& gfx, const Rectangle& parentBounds)
{
if(!visible_)
{
return;
}
const Rectangle myAbsoluteBounds = addOffset(bounds_, parentBounds);
// we "correct" the x/y coords here because the child widget
// will use addOffset() to apply its relative bounds (which may have higher xy coords than the width and height of the window) to the parentBounds
// by doing this correction, we compensate for the window start offset
const Rectangle absoluteWindowStartBounds = Rectangle{
.x = myAbsoluteBounds.x - windowBounds_.x,
.y = myAbsoluteBounds.y - windowBounds_.y,
.width = myAbsoluteBounds.width,
.height = myAbsoluteBounds.height
};
// the bounds of the widget instances are all relative to the top of the scroll widget.
// so is the windowStart_
// so we need to define a rectangle to define the overlap in order to decide whether a widget needs to be rendered at all.
const Rectangle relativeWindowRect = {
.x = windowBounds_.x,
.y = windowBounds_.y,
.width = bounds_.width,
.height = bounds_.height
};
const Rectangle prevClip = gfx.getClippingRectangle();
gfx.setClippingRectangle(myAbsoluteBounds);
for(IWidget* widget : widgets_)
{
if(doRectanglesOverlap(relativeWindowRect, widget->getBounds()))
{
widget->render(gfx, absoluteWindowStartBounds);
}
}
gfx.setClippingRectangle(prevClip);
}
void ScrollWidget::setWindowStart(const Point& windowStart)
{
windowBounds_.x = windowStart.x;
windowBounds_.y = windowStart.y;
}
float ScrollWidget::getWindowProgressX() const
{
const float maxX = windowBounds_.width - bounds_.width;
return static_cast<float>(windowBounds_.x) / maxX;
}
float ScrollWidget::getWindowProgressY() const
{
const double maxY = windowBounds_.height - bounds_.height;
return static_cast<float>(windowBounds_.y) / maxY;
}
void ScrollWidget::growWindow(IWidget* widget)
{
const Rectangle widgetBounds = widget->getBounds();
const int widgetMaxX = widgetBounds.x + widgetBounds.width;
const int widgetMaxY = widgetBounds.y + widgetBounds.height;
if(widgetMaxX > windowBounds_.width)
{
windowBounds_.width = widgetMaxX;
}
if(widgetMaxY > windowBounds_.height)
{
windowBounds_.height = widgetMaxY;
}
}
void ScrollWidget::recalculateWindowSize()
{
windowBounds_.width = 0;
windowBounds_.height = 0;
for(IWidget* widget : widgets_)
{
growWindow(widget);
}
}