From ebea968fad0ec4ac5280e5a02f01bb511c08fc24 Mon Sep 17 00:00:00 2001 From: Madeline Hall Date: Sat, 17 May 2025 13:09:57 -0400 Subject: [PATCH 001/146] Fix build for linux on later versions of GCC --- BattleNetwork/bnHitProperties.h | 7 ++++++- BattleNetwork/cxxopts/cxxopts.hpp | 4 ++++ BattleNetwork/overworld/bnIdentityManager.h | 4 ++++ CMakeLists.txt | 5 +++++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/BattleNetwork/bnHitProperties.h b/BattleNetwork/bnHitProperties.h index 4e9a3b162..0783f64d3 100644 --- a/BattleNetwork/bnHitProperties.h +++ b/BattleNetwork/bnHitProperties.h @@ -2,6 +2,11 @@ #include "bnElements.h" #include "bnDirection.h" +#ifdef __GNUC__ + #include +#endif + + // forward declare using EntityID_t = long; @@ -63,4 +68,4 @@ namespace Hit { Direction::none, true }; -} \ No newline at end of file +} diff --git a/BattleNetwork/cxxopts/cxxopts.hpp b/BattleNetwork/cxxopts/cxxopts.hpp index 6d230f062..f677738dd 100644 --- a/BattleNetwork/cxxopts/cxxopts.hpp +++ b/BattleNetwork/cxxopts/cxxopts.hpp @@ -39,6 +39,10 @@ THE SOFTWARE. #include #include +#ifdef __GNUC__ + #include +#endif + #ifdef __cpp_lib_optional #include #define CXXOPTS_HAS_OPTIONAL diff --git a/BattleNetwork/overworld/bnIdentityManager.h b/BattleNetwork/overworld/bnIdentityManager.h index 7aeb7f13d..2b95ddba0 100644 --- a/BattleNetwork/overworld/bnIdentityManager.h +++ b/BattleNetwork/overworld/bnIdentityManager.h @@ -1,5 +1,9 @@ #include +#ifdef __GNUC__ + #include +#endif + namespace Overworld { /** * Identities should not be shared beyond a single server diff --git a/CMakeLists.txt b/CMakeLists.txt index a9a662f52..29c6db322 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,11 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) +# Consider adding directive to detect for GNU/Linux specifically for these modifications +# IF(GNU) +set(CMAKE_CXX_FLAGS "-fpermissive -Wchanges-meaning") +# ENDIF() + set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake) include(FindLua) From 2fcf2cbbce3cfbd8a565c25dd763754356f3fd99 Mon Sep 17 00:00:00 2001 From: Madeline Hall Date: Sat, 17 May 2025 16:10:23 -0400 Subject: [PATCH 002/146] Add hacky AppImage handling code --- BattleNetwork/main.cpp | 34 +++++++++++++++++++++++++++++++++- CMakeLists.txt | 12 ++++++++---- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/BattleNetwork/main.cpp b/BattleNetwork/main.cpp index 914d90c9a..ac663e3d7 100644 --- a/BattleNetwork/main.cpp +++ b/BattleNetwork/main.cpp @@ -26,6 +26,10 @@ #include #include +#ifdef APPIMAGE +#include +#endif + // Launches the standard game with full setup and configuration int LaunchGame(Game& g, const cxxopts::ParseResult& results); @@ -49,6 +53,30 @@ static cxxopts::Options options("ONB", "Open Net Battle Engine"); int main(int argc, char** argv) { // Create help and other generic flags + +#ifdef APPIMAGE + // Change the working directory to the XDG_CONFIG_HOME path. + // This is a hack to get the program functioning in AppImage. + std::string USER_HOME = std::getenv("HOME"); + std::string ONB_CONFIG_PATH = USER_HOME + "/.config/OpenNetBattle"; + std::string ONB_RESOURCE_PATH = USER_HOME + "/.config/OpenNetBattle/resources"; + + std::cout << "This is an AppImage build. YMMV." << std::endl; + + if(!std::filesystem::exists(ONB_CONFIG_PATH) || !std::filesystem::exists(ONB_RESOURCE_PATH)) { + std::filesystem::create_directory(ONB_CONFIG_PATH); + std::filesystem::create_directory(ONB_RESOURCE_PATH); + std::cout << "First run detected." << endl; + std::cout << endl; + std::cout << "Please extract the resource datafiles to " << ONB_RESOURCE_PATH << " and run the AppImage again." << endl; + return EXIT_FAILURE; + } + + std::filesystem::current_path(ONB_CONFIG_PATH); + +#endif + + options.add_options() ("h,help", "Print all options") ("e,errorLevel", "Set the level to filter error messages [silent|info|warning|critical|debug] (default is `critical`)", cxxopts::value()->default_value("warning|critical")) @@ -87,7 +115,11 @@ int main(int argc, char** argv) { } DrawWindow win; - win.Initialize("Open Net Battle v2.0a", DrawWindow::WindowMode::window); + #ifdef APPIMAGE + win.Initialize("Open Net Battle v2.0a (AppImage Build)", DrawWindow::WindowMode::window); + #else + win.Initialize("Open Net Battle v2.0a", DrawWindow::WindowMode::window); + #endif Game game{ win }; // Go the the title screen to kick off the rest of the app diff --git a/CMakeLists.txt b/CMakeLists.txt index 29c6db322..7606719bf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,9 +6,13 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) # Consider adding directive to detect for GNU/Linux specifically for these modifications -# IF(GNU) -set(CMAKE_CXX_FLAGS "-fpermissive -Wchanges-meaning") -# ENDIF() +IF(UNIX) + set(CMAKE_CXX_FLAGS "-fpermissive -Wchanges-meaning") +ENDIF() + +IF(APPIMAGE) + add_compile_definitions(APPIMAGE) +endif() set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake) @@ -91,4 +95,4 @@ set_target_properties(BattleNetwork ) include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/Compiler.cmake) -include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/PostBuild.cmake) +include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/PostBuild.cmake) \ No newline at end of file From f660980ad699795a0a5516c84cc925ee7f486120 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sun, 27 Jul 2025 20:47:38 -0700 Subject: [PATCH 003/146] Disable Shader option until fixed. Shaders cannot be turned off. Turning off shaders currently crashes on boot and forces someone to fix it through their config.ini if they changed it. Until fixed, take away the button. --- BattleNetwork/bnConfigScene.cpp | 5 +++++ BattleNetwork/bnConfigSettings.cpp | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/BattleNetwork/bnConfigScene.cpp b/BattleNetwork/bnConfigScene.cpp index 89c4fbdd7..5f36032ef 100644 --- a/BattleNetwork/bnConfigScene.cpp +++ b/BattleNetwork/bnConfigScene.cpp @@ -240,6 +240,10 @@ ConfigScene::ConfigScene(swoosh::ActivityController& controller) : sfxItem->SetValueRange(1, 4); primaryMenu.push_back(std::move(sfxItem)); + /* + Turning shaders off causes the engine to crash on boot. + Don't let them be turned off for now. + // Shaders shaderLevel = configSettings.GetShaderLevel(); auto shadersItem = std::make_shared( @@ -250,6 +254,7 @@ ConfigScene::ConfigScene(swoosh::ActivityController& controller) : shadersItem->SetValueRange(0, 1); //shadersItem->SetColor(DISABLED_TEXT_COLOR); primaryMenu.push_back(shadersItem); + */ // Keyboard primaryMenu.push_back(std::make_shared( diff --git a/BattleNetwork/bnConfigSettings.cpp b/BattleNetwork/bnConfigSettings.cpp index ada150ef6..47a600ca9 100644 --- a/BattleNetwork/bnConfigSettings.cpp +++ b/BattleNetwork/bnConfigSettings.cpp @@ -25,7 +25,10 @@ const int ConfigSettings::GetSFXLevel() const { return sfxLevel; } const int ConfigSettings::GetShaderLevel() const { - return shaderLevel; + // Turning shaders off causes the engine to crash on boot. + // Don't let them be turned off for now. + return 1; + //return shaderLevel; } const bool ConfigSettings::TestKeyboard() const { From 3a3ea281b4f9065f375156767765ddbc77754612 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sun, 27 Jul 2025 21:38:17 -0700 Subject: [PATCH 004/146] Pull folder and pack sorting Pulled changes from these commits: 1ee8139572b01b54dd8ae266576c18a7f1fab8fc cc134193753373bba1848ec57a9dcd89ab1c2846 --- BattleNetwork/bnFolderEditScene.cpp | 362 +++++++++++++----- BattleNetwork/bnFolderEditScene.h | 140 +++++-- .../overworld/bnOverworldPersonalMenu.cpp | 85 ++-- BattleNetwork/resources/ui/folder_sort.png | Bin 1127 -> 717 bytes 4 files changed, 413 insertions(+), 174 deletions(-) diff --git a/BattleNetwork/bnFolderEditScene.cpp b/BattleNetwork/bnFolderEditScene.cpp index ecead95a1..15b30ad73 100644 --- a/BattleNetwork/bnFolderEditScene.cpp +++ b/BattleNetwork/bnFolderEditScene.cpp @@ -7,6 +7,7 @@ #include #include "bnFolderEditScene.h" +#include "bnGameSession.h" #include "Segues/BlackWashFade.h" #include "bnCardFolder.h" #include "bnCardPackageManager.h" @@ -34,8 +35,7 @@ FolderEditScene::FolderEditScene(swoosh::ActivityController& controller, CardFol cardDescFont(Font::Style::thin), cardDesc("", cardDescFont), numberFont(Font::Style::thick), - numberLabel(Font::Style::gradient) -{ + numberLabel(Font::Style::gradient) { // Move card data into their appropriate containers for easier management PlaceFolderDataIntoCardSlots(); PlaceLibraryDataIntoBuckets(); @@ -43,6 +43,9 @@ FolderEditScene::FolderEditScene(swoosh::ActivityController& controller, CardFol // We must account for existing card data to accurately represent what's left from our pool ExcludeFolderDataFromPool(); + // Add sort options + ComposeSortOptions(); + // Menu name font menuLabel.setPosition(sf::Vector2f(20.f, 8.0f)); menuLabel.setScale(2.f, 2.f); @@ -92,6 +95,8 @@ FolderEditScene::FolderEditScene(swoosh::ActivityController& controller, CardFol packCursor.setPosition((2.f * 90.f) + 480.0f, 64.0f); packSwapCursor = packCursor; + sortCursor = folderCursor; + folderNextArrow = sf::Sprite(*Textures().LoadFromFile(TexturePaths::FOLDER_NEXT_ARROW)); folderNextArrow.setScale(2.f, 2.f); @@ -104,11 +109,17 @@ FolderEditScene::FolderEditScene(swoosh::ActivityController& controller, CardFol folderCardCountBox.setOrigin(folderCardCountBox.getLocalBounds().width / 2.0f, folderCardCountBox.getLocalBounds().height / 2.0f); cardHolder = sf::Sprite(*Textures().LoadFromFile(TexturePaths::FOLDER_CHIP_HOLDER)); + cardHolder.setPosition(16.f, 35.f); cardHolder.setScale(2.f, 2.f); packCardHolder = sf::Sprite(*Textures().LoadFromFile(TexturePaths::FOLDER_CHIP_HOLDER)); + packCardHolder.setPosition(310.f + 480.f, 35.f); packCardHolder.setScale(2.f, 2.f); + folderSort = sf::Sprite(*Textures().LoadFromFile(TexturePaths::FOLDER_SORT)); + folderSort.setScale(2.f, 2.f); + folderSort.setPosition(cardHolder.getPosition() + sf::Vector2f(11 * 2, 5 * 2)); + element = sf::Sprite(*Textures().LoadFromFile(TexturePaths::ELEMENT_ICON)); element.setScale(2.f, 2.f); @@ -166,8 +177,54 @@ void FolderEditScene::onUpdate(double elapsed) { camera.Update((float)elapsed); setView(camera.GetView()); + // update the folder sort cursor + sf::Vector2f sortCursorOffset = sf::Vector2f(0, 2.0 * (14.0 + (cursorSortIndex * 16.0))); + sortCursor.setPosition(folderSort.getPosition() + sortCursorOffset); + // Scene keyboard controls if (canInteract) { + if (isInSortMenu) { + ISortOptions* options = &poolSortOptions; + + if (currViewMode == ViewMode::folder) { + options = &folderSortOptions; + } + + if (Input().Has(InputEvents::pressed_ui_up)) { + if (cursorSortIndex > 0) { + cursorSortIndex--; + } + else { + cursorSortIndex = options->size() - 1; + } + } + if (Input().Has(InputEvents::pressed_ui_down)) { + if (cursorSortIndex + 1 < options->size()) { + cursorSortIndex++; + } + else { + cursorSortIndex = 0; + } + } + + if (Input().Has(InputEvents::pressed_confirm)) { + options->SelectOption(cursorSortIndex); + Audio().Play(AudioType::CHIP_DESC); + } + + if (Input().Has(InputEvents::pressed_cancel)) { + Audio().Play(AudioType::CHIP_DESC_CLOSE); + isInSortMenu = false; + } + return; + } + else if (Input().Has(InputEvents::pressed_pause)) { + Audio().Play(AudioType::CHIP_DESC); + isInSortMenu = true; + cursorSortIndex = 0; + return; + } + CardView* view = nullptr; if (currViewMode == ViewMode::folder) { @@ -177,6 +234,39 @@ void FolderEditScene::onUpdate(double elapsed) { view = &packView; } + // If CTRL+C is pressed during this scene, copy the folder contents in discord-friendly format + if (Input().HasSystemCopyEvent()) { + std::string buffer; + const std::string& nickname = getController().Session().GetNick(); + const CardPackageManager& manager = getController().CardPackagePartitioner().GetPartition(Game::LocalPartition); + + buffer += "```\n"; + buffer += "# Folder by " + nickname + "\n"; + + if (folderView.numOfCards == 0) { + buffer += "# [NONE] \n"; + } + + for (int i = 0; i < folderView.numOfCards; i++) { + const Battle::Card& card = folderCardSlots[i].ViewCard(); + const std::string& uuid = card.GetUUID(); + + if (!manager.HasPackage(uuid)) continue; + + const CardMeta& meta = manager.FindPackageByID(uuid); + buffer += uuid + " " + meta.GetPackageFingerprint() + " " + card.GetCode() + "\n"; + } + + buffer += "```"; + + if (buffer != Input().GetClipboard()) { + Input().SetClipboard(buffer); + Audio().Play(AudioType::NEW_GAME); + } + + return; + } + if (Input().Has(InputEvents::pressed_ui_up) || Input().Has(InputEvents::held_ui_up)) { if (lastKey != InputEvents::pressed_ui_up) { lastKey = InputEvents::pressed_ui_up; @@ -185,22 +275,22 @@ void FolderEditScene::onUpdate(double elapsed) { selectInputCooldown -= elapsed; - if (selectInputCooldown <= 0) { if (!extendedHold) { selectInputCooldown = maxSelectInputCooldown; extendedHold = true; } - - if (--view->currCardIndex >= 0) { + //Set the index. + view->currCardIndex = std::max(0, view->currCardIndex - 1); + //Check the index's validity. If proper, play the sound and reset the timer. + if (view->currCardIndex >= 0) { Audio().Play(AudioType::CHIP_SELECT); cardRevealTimer.reset(); } - - if (view->currCardIndex < view->lastCardOnScreen) { - --view->lastCardOnScreen; + //Condition: if we're at the top of the screen, decrement the last card on screen. + if (view->currCardIndex < view->firstCardOnScreen) { + --view->firstCardOnScreen; } - } } else if (Input().Has(InputEvents::pressed_ui_down) || Input().Has(InputEvents::held_ui_down)) { @@ -216,52 +306,65 @@ void FolderEditScene::onUpdate(double elapsed) { selectInputCooldown = maxSelectInputCooldown; extendedHold = true; } + //Adjust the math to use std::min so that the current card index is always set to numOfCards-1 at most. + //Otherwise, if available, increment the index by 1. + view->currCardIndex = std::min(view->numOfCards - 1, view->currCardIndex + 1); - if (++view->currCardIndex < view->numOfCards) { + if (view->currCardIndex < view->numOfCards) { Audio().Play(AudioType::CHIP_SELECT); cardRevealTimer.reset(); } - - if (view->currCardIndex > view->lastCardOnScreen + view->maxCardsOnScreen - 1) { - ++view->lastCardOnScreen; + //Condition: If we're at the bottom of the menu, increment the last card on screen. + if (view->currCardIndex > view->firstCardOnScreen + view->maxCardsOnScreen - 1) { + ++view->firstCardOnScreen; } } } - else if (Input().Has(InputEvents::pressed_shoulder_left)) { - extendedHold = false; + else if (Input().Has(InputEvents::pressed_shoulder_left) || Input().Has(InputEvents::held_shoulder_left)) { + if (lastKey != InputEvents::pressed_shoulder_left) { + lastKey = InputEvents::pressed_shoulder_left; + extendedHold = false; + } selectInputCooldown -= elapsed; if (selectInputCooldown <= 0) { - selectInputCooldown = maxSelectInputCooldown; - view->currCardIndex -= view->maxCardsOnScreen; - - view->currCardIndex = std::max(view->currCardIndex, 0); - - Audio().Play(AudioType::CHIP_SELECT); - - while (view->currCardIndex < view->lastCardOnScreen) { - --view->lastCardOnScreen; + if (!extendedHold) { + selectInputCooldown = maxSelectInputCooldown; + extendedHold = true; } + //Adjust the math to use std::max so that the current card index is always set to 0 at least. + view->currCardIndex = std::max(view->currCardIndex - view->maxCardsOnScreen, 0); - cardRevealTimer.reset(); + if (view->currCardIndex < view->numOfCards) { + Audio().Play(AudioType::CHIP_SELECT); + cardRevealTimer.reset(); + } + //Set last card to either the current last card minus the amount of cards on screen, or the first card in the pool. + view->firstCardOnScreen = std::max(view->firstCardOnScreen - view->maxCardsOnScreen, 0); } } - else if (Input().Has(InputEvents::pressed_shoulder_right)) { - extendedHold = false; + else if (Input().Has(InputEvents::pressed_shoulder_right) || Input().Has(InputEvents::held_shoulder_right)) { + if (lastKey != InputEvents::pressed_shoulder_right) { + lastKey = InputEvents::pressed_shoulder_right; + extendedHold = false; + } selectInputCooldown -= elapsed; if (selectInputCooldown <= 0) { - selectInputCooldown = maxSelectInputCooldown; - view->currCardIndex += view->maxCardsOnScreen; + if (!extendedHold) { + selectInputCooldown = maxSelectInputCooldown; + extendedHold = true; + } + Audio().Play(AudioType::CHIP_SELECT); - view->currCardIndex = std::min(view->currCardIndex, view->numOfCards - 1); + //Adjust the math to use std::min so that the current card index is always set to numOfCards-1 at most. + view->currCardIndex = std::min(view->numOfCards - 1, view->currCardIndex + view->maxCardsOnScreen); - while (view->currCardIndex > view->lastCardOnScreen + view->maxCardsOnScreen - 1) { - ++view->lastCardOnScreen; - } + //Set the last card on screen to be one page down or the true final card in the pack. + view->firstCardOnScreen = std::min(view->firstCardOnScreen + view->maxCardsOnScreen, view->numOfCards - view->maxCardsOnScreen); cardRevealTimer.reset(); } @@ -543,8 +646,8 @@ void FolderEditScene::onUpdate(double elapsed) { view->prevIndex = view->currCardIndex; - view->lastCardOnScreen = std::max(0, view->lastCardOnScreen); - view->lastCardOnScreen = std::min(view->numOfCards - 1, view->lastCardOnScreen); + view->firstCardOnScreen = std::max(0, view->firstCardOnScreen); + view->firstCardOnScreen = std::min(view->numOfCards - 1, view->firstCardOnScreen); bool gotoLastScene = false; @@ -585,6 +688,7 @@ void FolderEditScene::onUpdate(double elapsed) { else { prevViewMode = currViewMode; canInteract = true; + folderSort.setPosition(cardHolder.getPosition() + sf::Vector2f(11 * 2, 5 * 2)); } } else if (currViewMode == ViewMode::pool) { @@ -594,6 +698,7 @@ void FolderEditScene::onUpdate(double elapsed) { else { prevViewMode = currViewMode; canInteract = true; + folderSort.setPosition(packCardHolder.getPosition() + sf::Vector2f(11 * 2, 5 * 2)); } } else { @@ -608,12 +713,10 @@ void FolderEditScene::onLeave() { } -void FolderEditScene::onExit() -{ +void FolderEditScene::onExit() { } -void FolderEditScene::onEnter() -{ +void FolderEditScene::onEnter() { folderView.currCardIndex = 0; RefreshCurrentCardDock(folderView, folderCardSlots); } @@ -631,8 +734,8 @@ void FolderEditScene::onDraw(sf::RenderTexture& surface) { surface.draw(folderCardCountBox); if (int(0.5 + folderCardCountBox.getScale().y) == 2) { - auto nonempty = (decltype(folderCardSlots))(folderCardSlots.size()); - auto iter = std::copy_if(folderCardSlots.begin(), folderCardSlots.end(), nonempty.begin(), [](auto in) { return !in.IsEmpty(); }); + std::vector nonempty = (decltype(folderCardSlots))(folderCardSlots.size()); + auto iter = std::copy_if(folderCardSlots.begin(), folderCardSlots.end(), nonempty.begin(), [](const auto& in) { return !in.IsEmpty(); }); nonempty.resize(std::distance(nonempty.begin(), iter)); // shrink container to new size std::string str = std::to_string(nonempty.size()); @@ -691,21 +794,25 @@ void FolderEditScene::onDraw(sf::RenderTexture& surface) { DrawFolder(surface); DrawPool(surface); + + if (isInSortMenu) { + surface.draw(folderSort); + surface.draw(sortCursor); + } } void FolderEditScene::DrawFolder(sf::RenderTarget& surface) { cardDesc.setPosition(sf::Vector2f(26.f, 175.0f)); scrollbar.setPosition(410.f, 60.f); - cardHolder.setPosition(16.f, 32.f); element.setPosition(2.f * 28.f, 136.f); - card.setPosition(96.f, 88.f); + card.setPosition(96.f, 93.f); surface.draw(folderDock); surface.draw(cardHolder); // ScrollBar limits: Top to bottom screen position when selecting first and last card respectively - float top = 50.0f; float bottom = 230.0f; - float depth = ((float)folderView.lastCardOnScreen / (float)folderView.numOfCards) * bottom; + float top = 60.0f; float bottom = 260.0f; + float depth = (bottom - top) * (((float)folderView.firstCardOnScreen) / ((float)folderView.numOfCards - 7)); scrollbar.setPosition(452.f, top + depth); surface.draw(scrollbar); @@ -713,14 +820,14 @@ void FolderEditScene::DrawFolder(sf::RenderTarget& surface) { // Move the card library iterator to the current highlighted card auto iter = folderCardSlots.begin(); - for (int j = 0; j < folderView.lastCardOnScreen; j++) { + for (int j = 0; j < folderView.firstCardOnScreen; j++) { iter++; if (iter == folderCardSlots.end()) return; } // Now that we are at the viewing range, draw each card in the list - for (int i = 0; i < folderView.maxCardsOnScreen && folderView.lastCardOnScreen + i < folderView.numOfCards; i++) { + for (int i = 0; i < folderView.maxCardsOnScreen && folderView.firstCardOnScreen + i < folderView.numOfCards; i++) { if (!iter->IsEmpty()) { const Battle::Card& copy = iter->ViewCard(); bool hasID = getController().CardPackagePartitioner().GetPartition(Game::LocalPartition).HasPackage(copy.GetUUID()); @@ -769,10 +876,10 @@ void FolderEditScene::DrawFolder(sf::RenderTarget& surface) { surface.draw(limitLabel2); } // Draw card at the cursor - if (folderView.lastCardOnScreen + i == folderView.currCardIndex) { - auto y = swoosh::ease::interpolate((float)frameElapsed * 7.f, folderCursor.getPosition().y, 64.0f + (32.f * i)); - auto bounce = std::sin((float)totalTimeElapsed * 10.0f) * 5.0f; - float scaleFactor = (float)swoosh::ease::linear(cardRevealTimer.getElapsed().asSeconds(), 0.25f, 1.0f); + if (folderView.firstCardOnScreen + i == folderView.currCardIndex) { + float y = swoosh::ease::interpolate((float)frameElapsed * 7.f, folderCursor.getPosition().y, 64.0f + (32.f * i)); + float bounce = std::sin((float)totalTimeElapsed * 10.0f) * 5.0f; + float scaleFactor = (float)swoosh::ease::linear(cardRevealTimer.getElapsed().asSeconds() + 0.01f, 0.25f, 1.0f); // +0.01 to start partially open float xscale = scaleFactor * 2.f; auto interp_position = [scaleFactor, this](sf::Vector2f pos) { @@ -782,7 +889,10 @@ void FolderEditScene::DrawFolder(sf::RenderTarget& surface) { }; folderCursor.setPosition((2.f * 90.f) + bounce, y); - surface.draw(folderCursor); + + if (!isInSortMenu) { + surface.draw(folderCursor); + } if (!iter->IsEmpty()) { const Battle::Card& copy = iter->ViewCard(); @@ -796,13 +906,13 @@ void FolderEditScene::DrawFolder(sf::RenderTarget& surface) { cardLabel.SetString(std::to_string(copy.GetDamage())); cardLabel.setOrigin(cardLabel.GetLocalBounds().width + cardLabel.GetLocalBounds().left, 0); cardLabel.setScale(xscale, 2.f); - cardLabel.setPosition(interp_position(sf::Vector2f{ 2.f * 80.f, 142.f })); + cardLabel.setPosition(interp_position(sf::Vector2f{ 2.f * 77.f, 145.f })); surface.draw(cardLabel); } cardLabel.setOrigin(0, 0); cardLabel.SetColor(sf::Color::Yellow); - cardLabel.setPosition(interp_position(sf::Vector2f{ 2.f * 16.f, 142.f })); + cardLabel.setPosition(interp_position(sf::Vector2f{ 2.f * 20.f, 145.f })); cardLabel.SetString(std::string() + copy.GetCode()); cardLabel.setScale(xscale, 2.f); surface.draw(cardLabel); @@ -813,13 +923,13 @@ void FolderEditScene::DrawFolder(sf::RenderTarget& surface) { int offset = (int)(copy.GetElement()); element.setTextureRect(sf::IntRect(14 * offset, 0, 14, 14)); - element.setPosition(interp_position(sf::Vector2f{ 2.f * 32.f, 138.f })); + element.setPosition(interp_position(sf::Vector2f{ 2.f * 32.f, 142.f })); element.setScale(xscale, 2.f); surface.draw(element); } } - if (folderView.lastCardOnScreen + i == folderView.swapCardIndex && (int(totalTimeElapsed * 1000) % 2 == 0)) { - auto y = 64.0f + (32.f * i); + if (folderView.firstCardOnScreen + i == folderView.swapCardIndex && (int(totalTimeElapsed * 1000) % 2 == 0)) { + float y = 64.0f + (32.f * i); folderSwapCursor.setPosition((2.f * 95.f) + 2.0f, y); folderSwapCursor.setColor(sf::Color(255, 255, 255, 200)); @@ -834,31 +944,32 @@ void FolderEditScene::DrawFolder(sf::RenderTarget& surface) { void FolderEditScene::DrawPool(sf::RenderTarget& surface) { cardDesc.setPosition(sf::Vector2f(320.f + 480.f, 175.0f)); - packCardHolder.setPosition(310.f + 480.f, 35.f); element.setPosition(400.f + 2.f * 20.f + 480.f, 146.f); card.setPosition(389.f + 480.f, 93.f); surface.draw(packDock); surface.draw(packCardHolder); - // ScrollBar limits: Top to bottom screen position when selecting first and last card respectively - float top = 50.0f; float bottom = 230.0f; - float depth = ((float)packView.lastCardOnScreen / (float)packView.numOfCards) * bottom; - scrollbar.setPosition(292.f + 480.f, top + depth); - - surface.draw(scrollbar); - if (packView.numOfCards == 0) return; + // Per BN6, don't draw the scrollbar itself if you can't scroll in the pack. + if (packView.numOfCards > 7) { + // ScrollBar limits: Top to bottom screen position when selecting first and last card respectively + float top = 60.0f; float bottom = 260.0f; + float depth = (bottom - top) * (((float)packView.firstCardOnScreen) / ((float)packView.numOfCards - 7)); + scrollbar.setPosition(292.f + 480.f, top + depth); + surface.draw(scrollbar); + } + // Move the card library iterator to the current highlighted card auto iter = poolCardBuckets.begin(); - for (int j = 0; j < packView.lastCardOnScreen; j++) { + for (int j = 0; j < packView.firstCardOnScreen; j++) { iter++; } // Now that we are at the viewing range, draw each card in the list - for (int i = 0; i < packView.maxCardsOnScreen && packView.lastCardOnScreen + i < packView.numOfCards; i++) { + for (int i = 0; i < packView.maxCardsOnScreen && packView.firstCardOnScreen + i < packView.numOfCards; i++) { int count = iter->GetCount(); const Battle::Card& copy = iter->ViewCard(); @@ -907,10 +1018,10 @@ void FolderEditScene::DrawPool(sf::RenderTarget& surface) { surface.draw(cardLabel); // This draws the currently highlighted card - if (packView.lastCardOnScreen + i == packView.currCardIndex) { + if (packView.firstCardOnScreen + i == packView.currCardIndex) { float y = swoosh::ease::interpolate((float)frameElapsed * 7.f, packCursor.getPosition().y, 64.0f + (32.f * i)); float bounce = std::sin((float)totalTimeElapsed * 10.0f) * 2.0f; - float scaleFactor = (float)swoosh::ease::linear(cardRevealTimer.getElapsed().asSeconds(), 0.25f, 1.0f); + float scaleFactor = (float)swoosh::ease::linear(cardRevealTimer.getElapsed().asSeconds() + 0.01f, 0.25f, 1.0f); // + 0.01 to start partially open float xscale = scaleFactor * 2.f; auto interp_position = [scaleFactor, this](sf::Vector2f pos) { @@ -918,10 +1029,13 @@ void FolderEditScene::DrawPool(sf::RenderTarget& surface) { pos.x = ((scaleFactor * pos) + ((1.0f - scaleFactor) * center)).x; return pos; }; - + // draw the cursor where the entry is located and bounce packCursor.setPosition(bounce + 480.f + 2.f, y); - surface.draw(packCursor); + + if (!isInSortMenu) { + surface.draw(packCursor); + } card.setTexture(*GetPreviewForCard(poolCardBuckets[packView.currCardIndex].ViewCard().GetUUID())); card.setTextureRect(sf::IntRect{ 0,0,56,48 }); @@ -955,8 +1069,8 @@ void FolderEditScene::DrawPool(sf::RenderTarget& surface) { surface.draw(cardDesc); } - if (packView.lastCardOnScreen + i == packView.swapCardIndex && (int(totalTimeElapsed * 1000) % 2 == 0)) { - auto y = 64.0f + (32.f * i); + if (packView.firstCardOnScreen + i == packView.swapCardIndex && (int(totalTimeElapsed * 1000) % 2 == 0)) { + float y = 64.0f + (32.f * i); packSwapCursor.setPosition(485.f + 2.f + 2.f, y); packSwapCursor.setColor(sf::Color(255, 255, 255, 200)); @@ -968,11 +1082,88 @@ void FolderEditScene::DrawPool(sf::RenderTarget& surface) { } } +void FolderEditScene::ComposeSortOptions() { + auto sortByID = [](const ICardView& first, const ICardView& second) -> bool { + return first.ViewCard().GetUUID() < second.ViewCard().GetUUID(); + }; + + auto sortByAlpha = [](const ICardView& first, const ICardView& second) -> bool { + return first.ViewCard().GetShortName() < second.ViewCard().GetShortName(); + }; + + auto sortByCode = [](const ICardView& first, const ICardView& second) -> bool { + return first.ViewCard().GetCode() < second.ViewCard().GetCode(); + }; + + auto sortByAttack = [](const ICardView& first, const ICardView& second) -> bool { + return first.ViewCard().GetDamage() < second.ViewCard().GetDamage(); + }; + + auto sortByElement = [](const ICardView& first, const ICardView& second) -> bool { + return first.ViewCard().GetElement() < second.ViewCard().GetElement(); + }; + + auto sortByFolderCopies = [this](const ICardView& first, const ICardView& second) -> bool { + size_t firstCount{}, secondCount{}; + + firstCount = std::count_if(folderCardSlots.cbegin(), folderCardSlots.cend(), [&first](auto& entry) { + return entry.ViewCard().GetUUID() == first.ViewCard().GetUUID(); + }); + + secondCount = std::count_if(folderCardSlots.cbegin(), folderCardSlots.cend(), [&second](auto& entry) { + return entry.ViewCard().GetUUID() == second.ViewCard().GetUUID(); + }); + + return firstCount < secondCount; + }; + + auto sortByPoolCopies = [this](const ICardView& first, const ICardView& second) -> bool { + size_t firstCount{}, secondCount{}; + + auto iter = std::find_if(poolCardBuckets.cbegin(), poolCardBuckets.cend(), [&first](auto& entry) { + return entry.ViewCard().GetUUID() == first.ViewCard().GetUUID(); + }); + + auto iter2 = std::find_if(poolCardBuckets.cbegin(), poolCardBuckets.cend(), [&second](auto& entry) { + return entry.ViewCard().GetUUID() == second.ViewCard().GetUUID(); + }); + + if (iter != poolCardBuckets.cend()) { + firstCount = iter->GetCount(); + } + + if (iter2 != poolCardBuckets.cend()) { + secondCount = iter2->GetCount(); + } + + return firstCount < secondCount; + }; + + auto sortByMax = [](const ICardView& first, const ICardView& second) -> bool { + return first.ViewCard().GetLimit() < second.ViewCard().GetLimit(); + }; + + folderSortOptions.AddOption(sortByID); + folderSortOptions.AddOption(sortByAlpha); + folderSortOptions.AddOption(sortByCode); + folderSortOptions.AddOption(sortByAttack); + folderSortOptions.AddOption(sortByElement); + folderSortOptions.AddOption(sortByFolderCopies); + folderSortOptions.AddOption(sortByMax); + + poolSortOptions.AddOption(sortByID); + poolSortOptions.AddOption(sortByAlpha); + poolSortOptions.AddOption(sortByCode); + poolSortOptions.AddOption(sortByAttack); + poolSortOptions.AddOption(sortByElement); + poolSortOptions.AddOption(sortByPoolCopies); + poolSortOptions.AddOption(sortByMax); +} + void FolderEditScene::onEnd() { } -void FolderEditScene::ExcludeFolderDataFromPool() -{ +void FolderEditScene::ExcludeFolderDataFromPool() { Battle::Card mock; // will not be used for (auto& f : folderCardSlots) { auto iter = std::find_if(poolCardBuckets.begin(), poolCardBuckets.end(), [&f](PoolBucket& pack) { return pack.ViewCard() == f.ViewCard(); }); @@ -982,8 +1173,7 @@ void FolderEditScene::ExcludeFolderDataFromPool() } } -void FolderEditScene::PlaceFolderDataIntoCardSlots() -{ +void FolderEditScene::PlaceFolderDataIntoCardSlots() { CardFolder::Iter iter = folder.Begin(); while (iter != folder.End() && folderCardSlots.size() < 30) { @@ -998,8 +1188,7 @@ void FolderEditScene::PlaceFolderDataIntoCardSlots() } } -void FolderEditScene::PlaceLibraryDataIntoBuckets() -{ +void FolderEditScene::PlaceLibraryDataIntoBuckets() { auto& packageManager = getController().CardPackagePartitioner().GetPartition(Game::LocalPartition); std::string packageId = packageManager.FirstValidPackage(); @@ -1020,8 +1209,7 @@ void FolderEditScene::PlaceLibraryDataIntoBuckets() } while (packageId != packageManager.FirstValidPackage()); } -void FolderEditScene::WriteNewFolderData() -{ +void FolderEditScene::WriteNewFolderData() { folder = CardFolder(); for (auto iter = folderCardSlots.begin(); iter != folderCardSlots.end(); iter++) { @@ -1031,8 +1219,7 @@ void FolderEditScene::WriteNewFolderData() } } -std::shared_ptr FolderEditScene::GetIconForCard(const std::string& uuid) -{ +std::shared_ptr FolderEditScene::GetIconForCard(const std::string& uuid) { auto& packageManager = getController().CardPackagePartitioner().GetPartition(Game::LocalPartition); if (!packageManager.HasPackage(uuid)) @@ -1041,8 +1228,7 @@ std::shared_ptr FolderEditScene::GetIconForCard(const std::string& auto& meta = packageManager.FindPackageByID(uuid); return meta.GetIconTexture(); } -std::shared_ptr FolderEditScene::GetPreviewForCard(const std::string& uuid) -{ +std::shared_ptr FolderEditScene::GetPreviewForCard(const std::string& uuid) { auto& packageManager = getController().CardPackagePartitioner().GetPartition(Game::LocalPartition); if (!packageManager.HasPackage(uuid)) @@ -1061,13 +1247,13 @@ void FolderEditScene::StartupTouchControls() { rightSide.onTouch([]() { INPUTx.VirtualKeyEvent(InputEvent::RELEASED_A); - }); + }); rightSide.onRelease([this](sf::Vector2i delta) { if (!releasedB) { INPUTx.VirtualKeyEvent(InputEvent::PRESSED_A); } - }); + }); rightSide.onDrag([this](sf::Vector2i delta) { if (delta.x < -25 && !releasedB) { @@ -1075,7 +1261,7 @@ void FolderEditScene::StartupTouchControls() { INPUTx.VirtualKeyEvent(InputEvent::RELEASED_B); releasedB = true; } - }); + }); } void FolderEditScene::ShutdownTouchControls() { diff --git a/BattleNetwork/bnFolderEditScene.h b/BattleNetwork/bnFolderEditScene.h index e1a2cfacf..5c7f13cca 100644 --- a/BattleNetwork/bnFolderEditScene.h +++ b/BattleNetwork/bnFolderEditScene.h @@ -19,13 +19,13 @@ * @date 04/05/19 * @brief Edit folder contents and select from card pool * @important the games, the card pool card count is not shared by folders - * + * * User can select card and switch to the card pool on the right side of the scene to select cards - * to swap out for. - * + * to swap out for. + * * Before leaving the user is prompted to save changes */ - + class FolderEditScene : public Scene { private: enum class ViewMode : int { @@ -33,26 +33,40 @@ class FolderEditScene : public Scene { pool }; + // abstract interface class + class ICardView { + private: + Battle::Card info; + protected: + void SetCard(const Battle::Card& other) { info = other; } + public: + ICardView(const Battle::Card& info) : info(info) {} + virtual ~ICardView() {} + + virtual const bool IsEmpty() const = 0; + virtual const bool GetCard(Battle::Card& copy) = 0; + + const Battle::Card& ViewCard() const { return info; } + }; + /** * @class PackBucket - * @brief Cards in a pool avoid listing duplicates by bundling them in a counted bucket - * - * Users can select up to all of the cards in a bucket. The bucket will remain in the list but at 0. + * @brief Cards in a pool avoid listing duplicates by bundling them in a counted bucket + * + * Users can select up to all of the cards in a bucket. The bucket will remain in the list but at 0. */ - class PoolBucket { + class PoolBucket : public ICardView { private: - unsigned size; - unsigned maxSize; - Battle::Card info; + unsigned size{}; + unsigned maxSize{}; public: - PoolBucket(unsigned size, Battle::Card info) : size(size), maxSize(size), info(info) { } - ~PoolBucket() { } + PoolBucket(unsigned size, const Battle::Card& info) : ICardView(info), size(size), maxSize(size) {} + ~PoolBucket() {} - const bool IsEmpty() const { return size == 0; } - const bool GetCard(Battle::Card& copy) { if (IsEmpty()) return false; else copy = Battle::Card(info); size--; return true; } - void AddCard() { size++; size = std::min(size, maxSize); } - const Battle::Card& ViewCard() const { return info; } + const bool IsEmpty() const override { return size == 0; } + const bool GetCard(Battle::Card& copy) override { if (IsEmpty()) return false; else copy = Battle::Card(ViewCard()); size--; return true; } + void AddCard() { size++; size = std::min(size, maxSize); } const unsigned GetCount() const { return size; } }; @@ -60,40 +74,38 @@ class FolderEditScene : public Scene { * @class FolderSlot * @brief A selectable row in the folder to place new cards. When removing cards, an empty slot is left behind */ - class FolderSlot { + class FolderSlot : public ICardView { private: - bool occupied; - Battle::Card info; + bool occupied{}; public: + FolderSlot() : ICardView(Battle::Card()) {} + void AddCard(Battle::Card other) { - info = other; + SetCard(other); occupied = true; } - const bool GetCard(Battle::Card& copy) { + const bool GetCard(Battle::Card& copy) override { if (!occupied) return false; - copy = Battle::Card(info); + copy = Battle::Card(ViewCard()); occupied = false; - info = Battle::Card(); // null card + SetCard(Battle::Card()); // null card return true; } - const bool IsEmpty() const { + const bool IsEmpty() const override { return !occupied; } - - const Battle::Card& ViewCard() { - return info; - } }; private: std::vector folderCardSlots; /*!< Rows in the folder that can be inserted with cards or replaced */ std::vector poolCardBuckets; /*!< Rows in the pack that represent how many of a card are left */ - bool hasFolderChanged; /*!< Flag if folder needs to be saved before quitting screen */ + bool hasFolderChanged{}; /*!< Flag if folder needs to be saved before quitting screen */ + bool isInSortMenu{}; /*!< Flag if in the sort menu */ Camera camera; CardFolder& folder; @@ -111,7 +123,7 @@ class FolderEditScene : public Scene { Font numberFont; Text numberLabel; - + Text limitLabel; Text limitLabel2; @@ -130,6 +142,7 @@ class FolderEditScene : public Scene { sf::Sprite folderNextArrow; sf::Sprite packNextArrow; sf::Sprite folderCardCountBox; + sf::Sprite folderSort, sortCursor; // Current card graphic data sf::Sprite card; @@ -143,7 +156,7 @@ class FolderEditScene : public Scene { struct CardView { int maxCardsOnScreen{ 0 }; int currCardIndex{ 0 }; - int lastCardOnScreen{ 0 }; // index + int firstCardOnScreen{ 0 }; //!< index, the topmost card seen in the list int prevIndex{ -1 }; // for effect int numOfCards{ 0 }; int swapCardIndex{ -1 }; // -1 for unselected, otherwise ID @@ -154,11 +167,62 @@ class FolderEditScene : public Scene { double totalTimeElapsed; double frameElapsed; - - bool extendedHold{ false }; //!< If held for a 2nd pass, scroll quickly + InputEvent lastKey{}; + bool extendedHold{ false }; //!< If held for a 2nd pass, scroll quickly bool canInteract; + template + class ISortOptions { + protected: + using filter = std::function; + using base_type_t = BaseType; + std::array filters; + bool invert{}; + size_t freeIdx{}, lastIndex{}; + public: + virtual ~ISortOptions() {} + + size_t size() { return sz; } + bool AddOption(const filter& filter) { if (freeIdx >= filters.size()) return false; filters.at(freeIdx++) = filter; return true; } + virtual void SelectOption(size_t index) = 0; + }; + + template + class SortOptions : public ISortOptions { + std::vector& container; + public: + SortOptions(std::vector& ref) : ISortOptions(), container(ref) {}; + + void SelectOption(size_t index) override { + if (index >= sz) return; + + invert = !invert; + if (index != lastIndex) { + invert = false; + lastIndex = index; + } + + if (invert) { + std::sort(container.begin(), container.end(), filters.at(index)); + } + else { + std::sort(container.rbegin(), container.rend(), filters.at(index)); + } + + // push empty slots at the bottom + auto pivot = [](const ICardView& el) { + return !el.IsEmpty(); + }; + + std::partition(container.begin(), container.end(), pivot); + } + }; + + size_t cursorSortIndex{}; + SortOptions folderSortOptions{ folderCardSlots }; + SortOptions poolSortOptions{ poolCardBuckets }; + #ifdef __ANDROID__ bool canSwipe; bool touchStart; @@ -177,6 +241,7 @@ class FolderEditScene : public Scene { void DrawFolder(sf::RenderTarget& surface); void DrawPool(sf::RenderTarget& surface); + void ComposeSortOptions(); void ExcludeFolderDataFromPool(); void PlaceFolderDataIntoCardSlots(); void PlaceLibraryDataIntoBuckets(); @@ -199,11 +264,10 @@ class FolderEditScene : public Scene { ~FolderEditScene(); }; -template -void FolderEditScene::RefreshCurrentCardDock(FolderEditScene::CardView& view, const std::vector& list) -{ +template +void FolderEditScene::RefreshCurrentCardDock(FolderEditScene::CardView& view, const std::vector& list) { if (view.currCardIndex < list.size()) { - ElementType slot = list[view.currCardIndex]; // copy data, do not mutate it + T slot = list[view.currCardIndex]; // copy data, do not mutate it // If we have selected a new card, display the appropriate texture for its type if (view.currCardIndex != view.prevIndex) { diff --git a/BattleNetwork/overworld/bnOverworldPersonalMenu.cpp b/BattleNetwork/overworld/bnOverworldPersonalMenu.cpp index 5332342ea..7268afcfb 100644 --- a/BattleNetwork/overworld/bnOverworldPersonalMenu.cpp +++ b/BattleNetwork/overworld/bnOverworldPersonalMenu.cpp @@ -11,8 +11,7 @@ namespace Overworld { infoText(Font::Style::thin), areaLabel(Font::Style::thin), areaLabelThick(Font::Style::thick), - time(Font::Style::thick) - { + time(Font::Style::thick) { // Load resources areaLabel.setPosition(127, 119); infoText = areaLabel; @@ -93,8 +92,7 @@ namespace Overworld { selectInputCooldown = maxSelectInputCooldown; } - PersonalMenu::~PersonalMenu() - { + PersonalMenu::~PersonalMenu() { } using namespace swoosh; @@ -120,8 +118,7 @@ namespace Overworld { - all the folder options have expanded - ease in animation is complete */ - void PersonalMenu::QueueAnimTasks(const PersonalMenu::state& state) - { + void PersonalMenu::QueueAnimTasks(const PersonalMenu::state& state) { easeInTimer.clear(); if (state == PersonalMenu::state::opening) { @@ -199,7 +196,7 @@ namespace Overworld { for (auto&& opts : optionIcons) { opts->Reveal(); } - }); + }); t8f.doTask([=](sf::Time elapsed) { for (size_t i = 0; i < options.size(); i++) { @@ -207,7 +204,7 @@ namespace Overworld { options[i]->setPosition(36, 26 + (y * (i * 16))); optionIcons[i]->setPosition(16, 26 + (y * (i * 16))); } - }).withDuration(frames(12)); + }).withDuration(frames(12)); } else { t8f.doTask([=](sf::Time elapsed) { @@ -253,10 +250,10 @@ namespace Overworld { easeInTimer .at(time_cast(frames(14))) .doTask([=](sf::Time elapsed) { - infoBox->Reveal(); - infoBoxAnim.SyncTime(from_seconds(elapsed.asSeconds())); - infoBoxAnim.Refresh(infoBox->getSprite()); - }).withDuration(frames(4)); + infoBox->Reveal(); + infoBoxAnim.SyncTime(from_seconds(elapsed.asSeconds())); + infoBoxAnim.Refresh(infoBox->getSprite()); + }).withDuration(frames(4)); // // on frame 20 change state flag @@ -266,12 +263,11 @@ namespace Overworld { .at(frames(20)) .doTask([=](sf::Time elapsed) { currState = state::opened; - }); + }); } } - void PersonalMenu::CreateOptions() - { + void PersonalMenu::CreateOptions() { options.reserve(optionsList.size() * 2); optionIcons.reserve(optionsList.size() * 2); @@ -299,8 +295,7 @@ namespace Overworld { } - void PersonalMenu::Update(double elapsed) - { + void PersonalMenu::Update(double elapsed) { frameTick += from_seconds(elapsed); if (frameTick.count() >= 60) { frameTick = frames(0); @@ -370,7 +365,8 @@ namespace Overworld { selectInputCooldown = maxSelectInputCooldown / 4.0; } - CursorMoveUp() ? Audio().Play(AudioType::CHIP_SELECT) : 0; + CursorMoveUp(); + Audio().Play(AudioType::CHIP_SELECT); } } else if (input.Has(InputEvents::pressed_ui_down) || input.Has(InputEvents::held_ui_down)) { @@ -383,7 +379,8 @@ namespace Overworld { selectInputCooldown = maxSelectInputCooldown / 4.0; } - CursorMoveDown() ? Audio().Play(AudioType::CHIP_SELECT) : 0; + CursorMoveDown(); + Audio().Play(AudioType::CHIP_SELECT); } } else if (input.Has(InputEvents::pressed_confirm)) { @@ -417,8 +414,7 @@ namespace Overworld { } - void PersonalMenu::draw(sf::RenderTarget& target, sf::RenderStates states) const - { + void PersonalMenu::draw(sf::RenderTarget& target, sf::RenderStates states) const { if (IsHidden()) return; states.transform *= getTransform(); @@ -511,8 +507,7 @@ namespace Overworld { DrawTime(target); } - void PersonalMenu::SetPlayerDisplay(PlayerDisplay mode) - { + void PersonalMenu::SetPlayerDisplay(PlayerDisplay mode) { switch (mode) { case PlayerDisplay::PlayerHealth: { @@ -520,7 +515,7 @@ namespace Overworld { icon->Hide(); } break; - case PlayerDisplay::PlayerIcon: + case PlayerDisplay::PlayerIcon: { healthUI.Hide(); icon->Reveal(); @@ -529,8 +524,7 @@ namespace Overworld { } } - void PersonalMenu::DrawTime(sf::RenderTarget& target) const - { + void PersonalMenu::DrawTime(sf::RenderTarget& target) const { auto shadowColor = sf::Color(105, 105, 105); std::string format = (frameTick.count() < 30) ? "%OI:%OM %p" : "%OI %OM %p"; std::string timeStr = CurrentTime::AsFormattedString(format); @@ -561,8 +555,7 @@ namespace Overworld { target.draw(time); } - void PersonalMenu::SetArea(const std::string& name) - { + void PersonalMenu::SetArea(const std::string& name) { auto bounds = areaLabelThick.GetLocalBounds(); areaName = name; @@ -576,14 +569,12 @@ namespace Overworld { areaLabelThick.setPosition(240 - 1.f, 160 - 2.f); } - void PersonalMenu::UseIconTexture(const std::shared_ptr iconTexture) - { + void PersonalMenu::UseIconTexture(const std::shared_ptr iconTexture) { this->iconTexture = iconTexture; this->icon->setTexture(iconTexture, true); } - void PersonalMenu::ResetIconTexture() - { + void PersonalMenu::ResetIconTexture() { iconTexture.reset(); optionAnim << "PET"; @@ -591,8 +582,7 @@ namespace Overworld { optionAnim.SetFrame(1, icon->getSprite()); } - bool PersonalMenu::ExecuteSelection() - { + bool PersonalMenu::ExecuteSelection() { if (selectExit) { if (currState == state::opened) { Close(); @@ -612,8 +602,7 @@ namespace Overworld { return false; } - bool PersonalMenu::SelectExit() - { + bool PersonalMenu::SelectExit() { if (!selectExit) { Audio().Play(AudioType::CHIP_SELECT); @@ -634,8 +623,7 @@ namespace Overworld { return false; } - bool PersonalMenu::SelectOptions() - { + bool PersonalMenu::SelectOptions() { if (selectExit) { selectExit = false; row = 0; @@ -647,8 +635,7 @@ namespace Overworld { return false; } - bool PersonalMenu::CursorMoveUp() - { + bool PersonalMenu::CursorMoveUp() { if (!selectExit) { if (--row < 0) { row = static_cast(optionsList.size() - 1); @@ -657,24 +644,27 @@ namespace Overworld { return true; } - row = std::max(row, 0); + // else if exit is selected + selectExit = false; return false; } - bool PersonalMenu::CursorMoveDown() - { + bool PersonalMenu::CursorMoveDown() { if (!selectExit) { row = (row + 1u) % (int)optionsList.size(); return true; } + // else if exit is selected + + selectExit = false; + return false; } - void PersonalMenu::Open() - { + void PersonalMenu::Open() { if (currState == state::closed) { Audio().Play(AudioType::CHIP_DESC); currState = state::opening; @@ -684,12 +674,11 @@ namespace Overworld { } } - void PersonalMenu::Close() - { + void PersonalMenu::Close() { if (currState == state::opened) { currState = state::closing; QueueAnimTasks(currState); easeInTimer.start(); } } -} \ No newline at end of file +} diff --git a/BattleNetwork/resources/ui/folder_sort.png b/BattleNetwork/resources/ui/folder_sort.png index dca2067ebb468a0c2826712115aca48df5ed6333..772266d769f7a5c0167e0d70d02a4d28846f1b75 100644 GIT binary patch delta 678 zcmV;X0$Kg%2+akM7Ycp^0{{R3Y-^-qks%;|4^T{0MG#tJImXua_xBKUdk|7z+Z*HX5E3lKP5&rOwhv_ zFc3H0qoJ-7q7gRTqoJ-~q>sm6NBdUlS#hIP*ZPBDG?YQoE4WkFHEP&Z9jrVa?@+(Z z;0wi0cGjLoC7hWtqF&ht<(Ag^b!}@)%qGy+FPoQeF=VlAlg`#v&6Pt3^;d0wY1D60 z&yJvmEe-XPQodij%AAD481jZ!+zzO4&4_PPLStMl_;y zgGPg5-vSCi0IrMSNScgX#J-umM0eAH67Nz_upC8Z_hkatei<(-}Syfg=kpOluA zmo&VR$=NLH*VM*zJbr-swFTpyW;crySeFH7ZZOpj-kw$us+GFE4pv4lBk$n-LHEoY zqHLXA=Q(pjC_A|DkVfVh9%qN!9lhpPX`a6J`L#Iz{r^^r@p$|JQo2YH8kWoKY5)KL M07*qoM6N<$f=b*}D1N=!u zK~!i%?U+Ay+b|5q)4m=cNAQ?2$LQEGW5$jhJ7&z-v17)L9ea$98GVEvp#VP!f zinu;sw_%dk*WbTj?1#HA4=}F#)@FQ?zcI(B?}w*n>7=e$344jqtExx-P6uu@MNpZNhg~#r{5a{`EwL8zF+mx`e`j-%A_YEv~V9MzdNUeKkvwBw9b5mSkkZk?jx zfE*NGYMk=9Uo;Wv6NYiP)d*vynJ5l>>vqXJQC1qV)lM<5C;)rk+1$7#GC0A1=4MoP zM0yMLy&%W`CU8p#cUX6;NB;Rjm2abXb{Pg8fkK~_+y|&0`8yp?)$xKsGkC=gc(x8s>LrV6!z z@<2dVNsu#zh?e>*6i-L2O!Jg_>O~XvG{RVUam|@cmbD9HTHDD~-e>8$OZ}xOb8#$> zlu@S;i|f^%2^-$M>AKt8YhHUux!jqtTZTZ3LAv$pJ)v<)BpxGL%Yy7xsy4T)~m}(f0C5;TP?+k0w9CSU^p9K zXg)x}n0BF{p$k;Zb=jyJsGVPF6y0)?jq=r_lk36(bJGp59S1b(ZM$0Sw-vpACEU(A zblE>Ywjv;HY2;CFBg|`mo(#=X0sAe9CICy(GNw|@YnBWfdWe3jO6R}FOWwco1Sw}o ziS4QlkTxe$S1bj$S7XupSG9@RE^H+iNpVA|J>}&0uYl~R3tL+*qN`XtX6asV4z96o z!@G!;x1X#*hr`=b$}4RxuE}%9J(AvvS`Fw^M%$K@7j1v_8ZTOZX^$;$j5Y_jnrB77 zRd%+DZr6NaN-0(@P>U%58kdI~a)5oG419&V<``9WRz*B@(yZQk>U9c@OLL&5{;+oH z2_xwxXX7TQIAwXgQUYufHK5Bm;V2m-8I0zb$||gW;~~rJQzW^_+F6onLb( zC8Q%U&cR*E4U2UyCY(*AZs)t=vl7O-XWHIt?*`7Rm^Q*V0i!l002ov JPDHLkV1mK!3|jyI From 3016e696545ca0cc95d9758a2d24b0e03fbe40dd Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sun, 27 Jul 2025 21:40:04 -0700 Subject: [PATCH 005/146] Pull vulnerability patch from be9d7c0 Pulled changes form commit be9d7c09b844c0f32234f75a152e4f17f96d6c24. --- BattleNetwork/bnScriptResourceManager.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/BattleNetwork/bnScriptResourceManager.cpp b/BattleNetwork/bnScriptResourceManager.cpp index 63c584c48..3385d1dc6 100644 --- a/BattleNetwork/bnScriptResourceManager.cpp +++ b/BattleNetwork/bnScriptResourceManager.cpp @@ -94,6 +94,11 @@ void ScriptResourceManager::SetSystemFunctions(ScriptPackage& scriptPackage) state.open_libraries(sol::lib::base, sol::lib::math, sol::lib::table); + // vulnerability patching, why is this included with lib::base :( + state["load"] = nullptr; + state["loadfile"] = nullptr; + state["dofile"] = nullptr; + state["math"]["randomseed"] = []{ Logger::Log(LogLevel::warning, "math.random uses the engine's random number generator and does not need to be seeded"); }; From e61a39b2eab2738058302a6789166e3ce66d77d3 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sun, 27 Jul 2025 21:47:00 -0700 Subject: [PATCH 006/146] Pull 5851d92 to fix find_nearest_characters Pulled changes from 5851d92dde30ecbe406e9f8945631e8e5e38a4a1. --- BattleNetwork/bindings/bnUserTypeField.cpp | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/BattleNetwork/bindings/bnUserTypeField.cpp b/BattleNetwork/bindings/bnUserTypeField.cpp index 16362e570..2d01e11b8 100644 --- a/BattleNetwork/bindings/bnUserTypeField.cpp +++ b/BattleNetwork/bindings/bnUserTypeField.cpp @@ -14,15 +14,21 @@ static sol::as_table_t>> FindNearestCharacters(WeakWrapper& field, std::shared_ptr test, sol::stack_object queryObject) { sol::protected_function query = queryObject; - // store entities in a temp to avoid issues if the scripter mutates entities in this loop - std::vector> characters; - - field.Unwrap()->FindNearestCharacters(test, [&characters] (std::shared_ptr& character) -> bool { - characters.push_back(WeakWrapper(character)); - return false; + // prevent mutating during loop by getting a copy of the characters sorted first, not expecting many characters to be on the field anyway + // alternative is collecting into a weak wrapper list, filtering, converting to a shared_ptr list, sorting, converting to a weak wrapper list + std::vector> characters = field.Unwrap()->FindNearestCharacters(test, [&characters](std::shared_ptr& character) -> bool { + return true; }); - return FilterEntities(characters, queryObject); + // convert to weak wrapper + std::vector> wrappedCharacters; + + for (auto& character : characters) { + wrappedCharacters.push_back(WeakWrapper(character)); + } + + // filter the sorted list + return FilterEntities(wrappedCharacters, queryObject); } void DefineFieldUserType(sol::table& battle_namespace) { From a41ca860149741b6e01e14006eb122041e530e41 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sun, 27 Jul 2025 21:50:59 -0700 Subject: [PATCH 007/146] Pull 0be428a to fix clang compilation Pulled changes from 0be428a0dc7d5a22b771467d3bd447bf311cac91. --- BattleNetwork/battlescene/States/bnCardSelectBattleState.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/BattleNetwork/battlescene/States/bnCardSelectBattleState.cpp b/BattleNetwork/battlescene/States/bnCardSelectBattleState.cpp index 66482d2da..c016e5e73 100644 --- a/BattleNetwork/battlescene/States/bnCardSelectBattleState.cpp +++ b/BattleNetwork/battlescene/States/bnCardSelectBattleState.cpp @@ -212,7 +212,8 @@ void CardSelectBattleState::onUpdate(double elapsed) } else { // Send off the remaining hands to any scene implementations that need it - scene.OnSelectNewCards(player, ui->GetRemainingCards()); + std::vector cards = ui->GetRemainingCards(); + scene.OnSelectNewCards(player, cards); } } From d7f7c4b5ceb79d93cb3d7acfc0a6590d9744c468 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sun, 27 Jul 2025 22:08:14 -0700 Subject: [PATCH 008/146] Pull 1c86537, round holy panel damage up Pull change from 1c86537c81554927b42caa33945d54f9caa10dba. --- BattleNetwork/bnTile.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/BattleNetwork/bnTile.cpp b/BattleNetwork/bnTile.cpp index a9ef6d127..6feb30c6e 100644 --- a/BattleNetwork/bnTile.cpp +++ b/BattleNetwork/bnTile.cpp @@ -1035,6 +1035,7 @@ namespace Battle { // We make sure to apply any tile bonuses at this stage if (GetState() == TileState::holy) { Hit::Properties props = attacker->GetHitboxProperties(); + props.damage += 1; // rounds integer damage up -> `1 / 2 = 0`, but `(1 + 1) / 2 = 1` props.damage /= 2; attacker->SetHitboxProperties(props); } From 8fad7640f9489df43e9ccde21267919d532c3fff Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sun, 27 Jul 2025 22:14:13 -0700 Subject: [PATCH 009/146] NoCounter flag available in Lua --- BattleNetwork/bindings/bnUserTypeHitbox.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/BattleNetwork/bindings/bnUserTypeHitbox.cpp b/BattleNetwork/bindings/bnUserTypeHitbox.cpp index ddeabff33..088610fe5 100644 --- a/BattleNetwork/bindings/bnUserTypeHitbox.cpp +++ b/BattleNetwork/bindings/bnUserTypeHitbox.cpp @@ -140,7 +140,8 @@ void DefineHitboxUserTypes(sol::state& state, sol::table& battle_namespace) { "Bubble", Hit::bubble, "Freeze", Hit::freeze, "Drag", Hit::drag, - "Blind", Hit::blind + "Blind", Hit::blind, + "NoCounter", Hit::no_counter ); state.new_usertype("Drag", From ba150d007bafaa3d5a906c625bf565033b1a31f2 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sun, 27 Jul 2025 22:16:56 -0700 Subject: [PATCH 010/146] Pull 0c62014 to fix Linux compilation Pull change from 0c62014f9516b2483d1966e556f1bedc6538a979. --- BattleNetwork/bnSceneNode.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/BattleNetwork/bnSceneNode.cpp b/BattleNetwork/bnSceneNode.cpp index 3304f3b0a..2fd5c2ea0 100644 --- a/BattleNetwork/bnSceneNode.cpp +++ b/BattleNetwork/bnSceneNode.cpp @@ -1,4 +1,5 @@ #include "bnSceneNode.h" +#include SceneNode::SceneNode() : show(true), layer(0), parent(nullptr), childNodes() { From e7d66a92f47c9dd662e1fdf7b9ddf11f2321efd9 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sun, 27 Jul 2025 22:21:45 -0700 Subject: [PATCH 011/146] Pull 20e5b81, switch to stable_sort and Linux fix Pull changes from 20e5b81d26361a97c6b7728c318f34155d078695. --- BattleNetwork/bnFolderEditScene.h | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/BattleNetwork/bnFolderEditScene.h b/BattleNetwork/bnFolderEditScene.h index 5c7f13cca..7747aef9b 100644 --- a/BattleNetwork/bnFolderEditScene.h +++ b/BattleNetwork/bnFolderEditScene.h @@ -175,7 +175,7 @@ class FolderEditScene : public Scene { template class ISortOptions { protected: - using filter = std::function; + using filter = std::function; using base_type_t = BaseType; std::array filters; bool invert{}; @@ -197,17 +197,17 @@ class FolderEditScene : public Scene { void SelectOption(size_t index) override { if (index >= sz) return; - invert = !invert; - if (index != lastIndex) { - invert = false; - lastIndex = index; + this->invert = !this->invert; + if (index != this->lastIndex) { + this->invert = false; + this->lastIndex = index; } - if (invert) { - std::sort(container.begin(), container.end(), filters.at(index)); + if (this->invert) { + std::stable_sort(this->container.begin(), this->container.end(), this->filters.at(index)); } else { - std::sort(container.rbegin(), container.rend(), filters.at(index)); + std::stable_sort(this->container.rbegin(), this->container.rend(), this->filters.at(index)); } // push empty slots at the bottom @@ -215,7 +215,7 @@ class FolderEditScene : public Scene { return !el.IsEmpty(); }; - std::partition(container.begin(), container.end(), pivot); + std::partition(this->container.begin(), this->container.end(), pivot); } }; From eedad66d9edcf9b53e9ab4d4a9d59f8e64ede0db Mon Sep 17 00:00:00 2001 From: Alrysc Date: Mon, 28 Jul 2025 11:06:04 -0700 Subject: [PATCH 012/146] Pull 41facef to fix mugshot issue Pulled changes from 41facef4f8d63ddf9ff6bda1d26d79f21d581284, which fixes an issue where mugshots would sometimes bounce. --- BattleNetwork/bnAnimatedTextBox.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BattleNetwork/bnAnimatedTextBox.cpp b/BattleNetwork/bnAnimatedTextBox.cpp index 5d9dd4740..ec5411dab 100644 --- a/BattleNetwork/bnAnimatedTextBox.cpp +++ b/BattleNetwork/bnAnimatedTextBox.cpp @@ -291,6 +291,8 @@ void AnimatedTextBox::draw(sf::RenderTarget& target, sf::RenderStates states) co } if (canDraw) { + mugAnimator.Refresh(lastSpeaker); + sf::Vector2f oldpos = lastSpeaker.getPosition(); auto pos = oldpos; pos += getPosition(); @@ -308,8 +310,6 @@ void AnimatedTextBox::draw(sf::RenderTarget& target, sf::RenderStates states) co lastSpeaker.setPosition(pos); - mugAnimator.Update(0, lastSpeaker); - if (lightenMug) { lastSpeaker.setColor(sf::Color(255, 255, 255, 125)); } From ba9f9d804c84a1c019ea50fbc1e11839ac013f51 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Mon, 28 Jul 2025 11:50:52 -0700 Subject: [PATCH 013/146] Pull 2c19c51, stream_music start and end are optional Pulled changes from 2c19c51a32cba189e7f6071b9dd0bc699adb20da. --- BattleNetwork/bnScriptResourceManager.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/BattleNetwork/bnScriptResourceManager.cpp b/BattleNetwork/bnScriptResourceManager.cpp index 3385d1dc6..8f22a7170 100644 --- a/BattleNetwork/bnScriptResourceManager.cpp +++ b/BattleNetwork/bnScriptResourceManager.cpp @@ -373,7 +373,9 @@ void ScriptResourceManager::ConfigureEnvironment(ScriptPackage& scriptPackage) { return self.CreateSpawner(namespaceId, fqn, rank); }, "set_background", &ScriptedMob::SetBackground, - "stream_music", &ScriptedMob::StreamMusic, + "stream_music", [](ScriptedMob& mob, const std::string& path, std::optional startMs, std::optional endMs) { + mob.StreamMusic(path, startMs.value_or(-1), endMs.value_or(-1)); + }, "get_field", [](ScriptedMob& o) { return WeakWrapper(o.GetField()); }, "enable_freedom_mission", &ScriptedMob::EnableFreedomMission, "spawn_player", &ScriptedMob::SpawnPlayer @@ -449,7 +451,7 @@ void ScriptResourceManager::ConfigureEnvironment(ScriptPackage& scriptPackage) { sol::factories( [](const std::string& path, std::optional loop, std::optional startMs, std::optional endMs) { static ResourceHandle handle; - return handle.Audio().Stream(path, loop.value_or(true), startMs.value_or(0), endMs.value_or(0)); + return handle.Audio().Stream(path, loop.value_or(true), startMs.value_or(-1), endMs.value_or(-1)); }) ); From e9354fde4e15f544a8cf204a308c0a5e6dd0a42b Mon Sep 17 00:00:00 2001 From: Alrysc Date: Mon, 28 Jul 2025 15:44:30 -0700 Subject: [PATCH 014/146] Fix incorrect discard indexes when filter_hand_step discards multiple. Fixes case where card calling discard right and then discard incoming would delete the left and right card instead of itself and the right card. This also fixes a crash where a card calling all discard functions at a certain index would read before the beginning of the vector. --- .../battlescene/bnBattleSceneBase.cpp | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/BattleNetwork/battlescene/bnBattleSceneBase.cpp b/BattleNetwork/battlescene/bnBattleSceneBase.cpp index e59ed91de..01df5e7c2 100644 --- a/BattleNetwork/battlescene/bnBattleSceneBase.cpp +++ b/BattleNetwork/battlescene/bnBattleSceneBase.cpp @@ -566,18 +566,39 @@ void BattleSceneBase::FilterSupportCards(const std::shared_ptr& player, meta.filterHandStep(cards[i].props, adjCards); } + size_t this_card = i; + /* + Whether or not to do another loop on this index. + True when left, right, or this card were deleted. + + By setting true on left and right delete, the filter + step will run for this card again so that it can run + with the new adjacent cards. + + By setting true when deleting itself, the new card + coming into this index won't be skipped. + */ + bool check_again = false; + if (adjCards.deleteLeft) { - cards.erase(cards.begin() + i - 1u); - i--; + cards.erase(cards.begin() + this_card - 1u); + this_card--; + check_again = true; } if (adjCards.deleteRight) { - cards.erase(cards.begin() + i + 1u); - i--; + cards.erase(cards.begin() + this_card + 1u); + // This card index hasn't changed + + check_again = true; } if (adjCards.deleteThisCard) { - cards.erase(cards.begin() + i); + cards.erase(cards.begin() + this_card); + check_again = true; + } + + if (check_again) { i--; } } From 752f9781db8188b906737be2672a5e27af000966 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Mon, 28 Jul 2025 16:06:21 -0700 Subject: [PATCH 015/146] FreedomMissionMobScene may set results in onEnd FreedomMission does not have a reward state, so results were never set on a win. Now the onEnd does the same logic, as long as the Player was not deleted. --- .../battlescene/bnFreedomMissionMobScene.cpp | 18 ++++++++++++++++++ .../battlescene/bnFreedomMissionMobScene.h | 2 ++ 2 files changed, 20 insertions(+) diff --git a/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp b/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp index ad598264b..804812ae0 100644 --- a/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp +++ b/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp @@ -214,6 +214,24 @@ void FreedomMissionMobScene::onLeave() BattleSceneBase::onLeave(); } +void FreedomMissionMobScene::onEnd() { + if (!IsPlayerDeleted()) { + BattleResults& results = BattleResultsObj(); + std::shared_ptr player = GetLocalPlayer(); + results.battleLength = sf::seconds(GetElapsedBattleFrames().count() / 60.f); + results.moveCount = player->GetMoveCount(); + results.hitCount = playerHitCount; + results.turns = GetTurnCount(); + results.counterCount = GetCounterCount(); + results.doubleDelete = DoubleDelete(); + results.tripleDelete = TripleDelete(); + results.finalEmotion = player->GetEmotion(); + + results.CalculateScore(results, props.mobs.at(0)); + } + BattleSceneBase::onEnd(); +} + void FreedomMissionMobScene::IncrementTurnCount() { BattleSceneBase::IncrementTurnCount(); diff --git a/BattleNetwork/battlescene/bnFreedomMissionMobScene.h b/BattleNetwork/battlescene/bnFreedomMissionMobScene.h index 36912809f..c004ceaf2 100644 --- a/BattleNetwork/battlescene/bnFreedomMissionMobScene.h +++ b/BattleNetwork/battlescene/bnFreedomMissionMobScene.h @@ -56,6 +56,8 @@ class FreedomMissionMobScene final : public BattleSceneBase { void onResume() override; void onLeave() override; + void onEnd() override; + // // override // From 3fffaa9dd25d89a272a88f3caa1c72b950e575c6 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Mon, 28 Jul 2025 16:17:17 -0700 Subject: [PATCH 016/146] Pull be42175, loads Blue Team Mob in Freedom battles Pulled changes from be421751653ef01fc7aff3af52b4d9c45e1e796a. --- BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp b/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp index 804812ae0..abc976bc8 100644 --- a/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp +++ b/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp @@ -135,6 +135,7 @@ void FreedomMissionMobScene::Init() } else { SpawnLocalPlayer(2, 2); + LoadBlueTeamMob(mob); } // Run block programs on the remote player now that they are spawned From 14f0a8828e3bffe6b3089e3ec3517607f25d5ed2 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Mon, 28 Jul 2025 17:55:10 -0700 Subject: [PATCH 017/146] Pull some changes from 29706ac. See comment. Pulled some changes from 29706ac8e57e5228ed5c3a1388e25790896dc803. This gives Lua access to AlertSymbol, some Health UI changes, freedom battle can launch from command line, and fixes behavior where Player could use a Card while flinching. The PlayerCustScene changes this commit made were not included, since they were not finished. There is also an issue where the Player will always start in FullSynchro. --- .../States/bnTimeFreezeBattleState.cpp | 11 ++-- .../battlescene/bnBattleSceneBase.cpp | 50 +++++++++++-------- BattleNetwork/battlescene/bnBattleSceneBase.h | 1 + .../battlescene/bnFreedomMissionMobScene.cpp | 4 +- .../battlescene/bnMobBattleScene.cpp | 4 +- ...nElementalDamage.cpp => bnAlertSymbol.cpp} | 10 ++-- .../{bnElementalDamage.h => bnAlertSymbol.h} | 8 +-- BattleNetwork/bnCharacter.cpp | 2 +- BattleNetwork/bnPlayerControlledState.cpp | 2 +- BattleNetwork/bnPlayerHealthUI.cpp | 4 +- BattleNetwork/bnPlayerHealthUI.h | 1 + BattleNetwork/bnScriptResourceManager.cpp | 10 ++++ BattleNetwork/main.cpp | 15 ++++++ .../battlescene/bnNetworkBattleScene.cpp | 4 +- 14 files changed, 84 insertions(+), 42 deletions(-) rename BattleNetwork/{bnElementalDamage.cpp => bnAlertSymbol.cpp} (76%) rename BattleNetwork/{bnElementalDamage.h => bnAlertSymbol.h} (78%) diff --git a/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp b/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp index eb7ec305b..e54ca5851 100644 --- a/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp +++ b/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp @@ -74,7 +74,7 @@ void TimeFreezeBattleState::ProcessInputs() // convert meta data into a useable action object const Battle::Card& card = *maybe_card; - if (card.IsTimeFreeze() && CanCounter(p)) { + if (card.IsTimeFreeze() && CanCounter(p) && summonTick > summonTextLength) { if (std::shared_ptr action = CardToAction(card, p, &GetScene().getController().CardPackagePartitioner(), card.props)) { OnCardActionUsed(action, CurrentTime::AsMilli()); cardsUI->DropNextCard(); @@ -228,14 +228,19 @@ void TimeFreezeBattleState::onDraw(sf::RenderTexture& surface) BattleSceneBase& scene = GetScene(); const auto& first = tfEvents.begin(); - double tfcTimerScale = swoosh::ease::linear(summonTick.asSeconds().value, summonTextLength.asSeconds().value, 1.0); + double tfcTimerScale = 0; + + if (summonTick.asSeconds().value > fadeInOutLength.asSeconds().value) { + tfcTimerScale = swoosh::ease::linear((double)(summonTick - fadeInOutLength).value, (double)summonTextLength.asSeconds().value, 1.0); + } + double scale = swoosh::ease::linear(summonTick.asSeconds().value, fadeInOutLength.asSeconds().value, 1.0); scale = std::min(scale, 1.0); bar = sf::RectangleShape({ 100.f * static_cast(1.0 - tfcTimerScale), 2.f }); bar.setScale(2.f, 2.f); - if (summonTick >= summonTextLength - fadeInOutLength) { + if (summonTick >= summonTextLength) { scale = swoosh::ease::linear((summonTextLength - summonTick).asSeconds().value, fadeInOutLength.asSeconds().value, 1.0); scale = std::max(scale, 0.0); } diff --git a/BattleNetwork/battlescene/bnBattleSceneBase.cpp b/BattleNetwork/battlescene/bnBattleSceneBase.cpp index 01df5e7c2..1a074a464 100644 --- a/BattleNetwork/battlescene/bnBattleSceneBase.cpp +++ b/BattleNetwork/battlescene/bnBattleSceneBase.cpp @@ -230,27 +230,7 @@ void BattleSceneBase::OnCounter(Entity& victim, Entity& aggressor) victim.ToggleCounter(false); // disable counter frame for the victim victim.Stun(frames(150)); - if (p->IsInForm() == false && p->GetEmotion() != Emotion::evil) { - if (p == localPlayer) { - field->RevealCounterFrames(true); - } - - // node positions are relative to the parent node's origin - sf::FloatRect bounds = p->getLocalBounds(); - counterReveal->setPosition(0, -bounds.height / 4.0f); - p->AddNode(counterReveal); - - std::shared_ptr cardUI = p->GetFirstComponent(); - - if (cardUI) { - cardUI->SetMultiplier(2); - } - - p->SetEmotion(Emotion::full_synchro); - - // when players get hit by impact, battle scene takes back counter blessings - p->AddDefenseRule(counterCombatRule); - } + PreparePlayerFullSynchro(p); } } @@ -472,6 +452,8 @@ void BattleSceneBase::SpawnLocalPlayer(int x, int y) allPlayerTeamHash[localPlayer.get()] = team; HitListener::Subscribe(*localPlayer); + + PreparePlayerFullSynchro(localPlayer); } void BattleSceneBase::SpawnOtherPlayer(std::shared_ptr player, int x, int y) @@ -498,6 +480,8 @@ void BattleSceneBase::SpawnOtherPlayer(std::shared_ptr player, int x, in allPlayerTeamHash[player.get()] = team; HitListener::Subscribe(*player); + + PreparePlayerFullSynchro(player); } void BattleSceneBase::LoadRedTeamMob(Mob& mob) @@ -1056,6 +1040,30 @@ void BattleSceneBase::DrawWithPerspective(sf::Shape& shape, sf::RenderTarget& su shape.setOrigin(origin); } +void BattleSceneBase::PreparePlayerFullSynchro(const std::shared_ptr& player) { + if (player->IsInForm() == true || player->GetEmotion() == Emotion::evil) return; + + if (player == localPlayer) { + field->RevealCounterFrames(true); + } + + // node positions are relative to the parent node's origin + sf::FloatRect bounds = player->getLocalBounds(); + counterReveal->setPosition(0, -bounds.height / 4.0f); + player->AddNode(counterReveal); + + std::shared_ptr cardUI = player->GetFirstComponent(); + + if (cardUI) { + cardUI->SetMultiplier(2); + } + + player->SetEmotion(Emotion::full_synchro); + + // when players get hit by impact, battle scene takes back counter blessings + player->AddDefenseRule(counterCombatRule); +} + void BattleSceneBase::DrawWithPerspective(sf::Sprite& sprite, sf::RenderTarget& surf) { sf::Vector2f position = sprite.getPosition(); diff --git a/BattleNetwork/battlescene/bnBattleSceneBase.h b/BattleNetwork/battlescene/bnBattleSceneBase.h index d5c826499..624d7d4a0 100644 --- a/BattleNetwork/battlescene/bnBattleSceneBase.h +++ b/BattleNetwork/battlescene/bnBattleSceneBase.h @@ -383,6 +383,7 @@ class BattleSceneBase : bool TrackOtherPlayer(std::shared_ptr& other); void UntrackOtherPlayer(std::shared_ptr& other); void UntrackMobCharacter(std::shared_ptr& character); + void PreparePlayerFullSynchro(const std::shared_ptr& player); bool IsPlayerDeleted() const; std::shared_ptr GetLocalPlayer(); diff --git a/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp b/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp index abc976bc8..739bacc4c 100644 --- a/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp +++ b/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp @@ -1,6 +1,6 @@ #include "bnFreedomMissionMobScene.h" #include "../bnMob.h" -#include "../bnElementalDamage.h" +#include "../bnAlertSymbol.h" #include "../../bnPlayer.h" #include "States/bnTimeFreezeBattleState.h" @@ -170,7 +170,7 @@ void FreedomMissionMobScene::OnHit(Entity& victim, const Hit::Properties& props) } if (victim.IsSuperEffective(props.element) && props.damage > 0) { - std::shared_ptr seSymbol = std::make_shared(); + std::shared_ptr seSymbol = std::make_shared(); seSymbol->SetLayer(-100); seSymbol->SetHeight(victim.GetHeight()+(victim.getLocalBounds().height*0.5f)); // place it at sprite height GetField()->AddEntity(seSymbol, victim.GetTile()->GetX(), victim.GetTile()->GetY()); diff --git a/BattleNetwork/battlescene/bnMobBattleScene.cpp b/BattleNetwork/battlescene/bnMobBattleScene.cpp index 8a64f325e..62451e7ef 100644 --- a/BattleNetwork/battlescene/bnMobBattleScene.cpp +++ b/BattleNetwork/battlescene/bnMobBattleScene.cpp @@ -1,6 +1,6 @@ #include "bnMobBattleScene.h" #include "../bnMob.h" -#include "../bnElementalDamage.h" +#include "../bnAlertSymbol.h" #include "../../bnPlayer.h" #include "States/bnRewardBattleState.h" @@ -182,7 +182,7 @@ void MobBattleScene::OnHit(Entity& victim, const Hit::Properties& props) bool superEffective = victim.IsSuperEffective(props.element) && props.damage > 0; if (freezeBreak || superEffective) { - std::shared_ptr seSymbol = std::make_shared(); + std::shared_ptr seSymbol = std::make_shared(); seSymbol->SetLayer(-100); seSymbol->SetHeight(victim.GetHeight()+(victim.getLocalBounds().height*0.5f)); // place it at sprite height GetField()->AddEntity(seSymbol, victim.GetTile()->GetX(), victim.GetTile()->GetY()); diff --git a/BattleNetwork/bnElementalDamage.cpp b/BattleNetwork/bnAlertSymbol.cpp similarity index 76% rename from BattleNetwork/bnElementalDamage.cpp rename to BattleNetwork/bnAlertSymbol.cpp index ac47e94c4..38fde28c4 100644 --- a/BattleNetwork/bnElementalDamage.cpp +++ b/BattleNetwork/bnAlertSymbol.cpp @@ -1,4 +1,4 @@ -#include "bnElementalDamage.h" +#include "bnAlertSymbol.h" #include "bnTextureResourceManager.h" #include "bnAudioResourceManager.h" #include "bnField.h" @@ -9,7 +9,7 @@ using sf::IntRect; -ElementalDamage::ElementalDamage() : +AlertSymbol::AlertSymbol() : Artifact() { SetLayer(0); @@ -19,7 +19,7 @@ ElementalDamage::ElementalDamage() : progress = 0; } -void ElementalDamage::OnUpdate(double _elapsed) { +void AlertSymbol::OnUpdate(double _elapsed) { progress += _elapsed; float alpha = swoosh::ease::wideParabola(static_cast(progress), 0.5f, 4.0f); @@ -32,10 +32,10 @@ void ElementalDamage::OnUpdate(double _elapsed) { Entity::drawOffset = {-30.0f, -30.0f }; } -void ElementalDamage::OnDelete() +void AlertSymbol::OnDelete() { } -ElementalDamage::~ElementalDamage() +AlertSymbol::~AlertSymbol() { } diff --git a/BattleNetwork/bnElementalDamage.h b/BattleNetwork/bnAlertSymbol.h similarity index 78% rename from BattleNetwork/bnElementalDamage.h rename to BattleNetwork/bnAlertSymbol.h index 5d69b3ba5..7da5dfbc8 100644 --- a/BattleNetwork/bnElementalDamage.h +++ b/BattleNetwork/bnAlertSymbol.h @@ -5,19 +5,19 @@ class Field; /** - * @class ElementalDamage + * @class AlertSymbol * @author mav * @date 04/05/19 * @brief symbol that appears on field when elemental damage occurs */ -class ElementalDamage : public Artifact +class AlertSymbol : public Artifact { private: double progress; public: - ElementalDamage(); - ~ElementalDamage(); + AlertSymbol(); + ~AlertSymbol(); /** * @brief Grow and shrink quickly. Appear over the sprite. diff --git a/BattleNetwork/bnCharacter.cpp b/BattleNetwork/bnCharacter.cpp index 65b8c7d4a..d4811468c 100644 --- a/BattleNetwork/bnCharacter.cpp +++ b/BattleNetwork/bnCharacter.cpp @@ -7,7 +7,7 @@ #include "bnSpell.h" #include "bnTile.h" #include "bnField.h" -#include "bnElementalDamage.h" +#include "bnAlertSymbol.h" #include "bnShaderResourceManager.h" #include "bnAnimationComponent.h" #include "bnShakingEffect.h" diff --git a/BattleNetwork/bnPlayerControlledState.cpp b/BattleNetwork/bnPlayerControlledState.cpp index 69ce9ae26..706f33cd0 100644 --- a/BattleNetwork/bnPlayerControlledState.cpp +++ b/BattleNetwork/bnPlayerControlledState.cpp @@ -41,7 +41,7 @@ void PlayerControlledState::OnUpdate(double _elapsed, Player& player) { // Are we creating an action this frame? if (player.InputState().Has(InputEvents::pressed_use_chip)) { std::shared_ptr cardsUI = player.GetFirstComponent(); - if (cardsUI && cardsUI->UseNextCard()) { + if (player.CanAttack() && cardsUI && cardsUI->UseNextCard()) { player.chargeEffect->SetCharging(false); isChargeHeld = false; } diff --git a/BattleNetwork/bnPlayerHealthUI.cpp b/BattleNetwork/bnPlayerHealthUI.cpp index 145f91bc6..0ed859985 100644 --- a/BattleNetwork/bnPlayerHealthUI.cpp +++ b/BattleNetwork/bnPlayerHealthUI.cpp @@ -28,6 +28,7 @@ PlayerHealthUI::~PlayerHealthUI() void PlayerHealthUI::SetFontStyle(Font::Style style) { + glyphs.SetFont(style); } void PlayerHealthUI::SetHP(int hp) @@ -127,6 +128,7 @@ PlayerHealthUIComponent::~PlayerHealthUIComponent() { void PlayerHealthUIComponent::Inject(BattleSceneBase& scene) { scene.Inject(shared_from_base()); + this->scene - &scene; } void PlayerHealthUIComponent::draw(sf::RenderTarget& target, sf::RenderStates states) const { @@ -172,7 +174,7 @@ void PlayerHealthUIComponent::OnUpdate(double elapsed) { ui.SetFontStyle(Font::Style::gradient_gold); // If HP is low, play beep with high priority - if (player->GetHealth() <= startHP * 0.25 && !isBattleOver) { + if (player->GetHealth() <= startHP * 0.25 && !isBattleOver && scene && scene->GetSelectedCardsUI().IsHidden()) { ResourceHandle().Audio().Play(AudioType::LOW_HP, AudioPriority::high); } } diff --git a/BattleNetwork/bnPlayerHealthUI.h b/BattleNetwork/bnPlayerHealthUI.h index a46e6cb3f..0e15c4b68 100644 --- a/BattleNetwork/bnPlayerHealthUI.h +++ b/BattleNetwork/bnPlayerHealthUI.h @@ -51,6 +51,7 @@ class PlayerHealthUI : public SceneNode { }; class PlayerHealthUIComponent : public UIComponent { + BattleSceneBase* scene{ nullptr }; PlayerHealthUI ui; int startHP{}; /*!< HP of target when this component was attached */ bool isBattleOver{}; /*!< flag when battle scene ends to stop beeping */ diff --git a/BattleNetwork/bnScriptResourceManager.cpp b/BattleNetwork/bnScriptResourceManager.cpp index 8f22a7170..62c6cda9f 100644 --- a/BattleNetwork/bnScriptResourceManager.cpp +++ b/BattleNetwork/bnScriptResourceManager.cpp @@ -21,6 +21,7 @@ #include "bnField.h" #include "bnParticlePoof.h" #include "bnPlayerCustScene.h" +#include "bnAlertSymbol.h" #include "bnRandom.h" #include "bindings/bnLuaLibrary.h" @@ -683,6 +684,15 @@ void ScriptResourceManager::ConfigureEnvironment(ScriptPackage& scriptPackage) { }) ); + const auto& alertsymbol_record = battle_namespace.new_usertype("AlertSymbol", + sol::factories([]() -> WeakWrapper { + std::shared_ptr bang = std::make_shared(); + auto wrappedArtifact = WeakWrapper(bang); + wrappedArtifact.Own(); + return wrappedArtifact; + }) + ); + const auto& particle_poof = battle_namespace.new_usertype("ParticlePoof", sol::factories([]() -> WeakWrapper { std::shared_ptr artifact = std::make_shared(); diff --git a/BattleNetwork/main.cpp b/BattleNetwork/main.cpp index 8ac4e9600..82e9cf76f 100644 --- a/BattleNetwork/main.cpp +++ b/BattleNetwork/main.cpp @@ -1,5 +1,6 @@ #include "bnGame.h" #include "battlescene/bnMobBattleScene.h" +#include "battlescene/bnFreedomMissionMobScene.h" #include "bindings/bnScriptedMob.h" #include "bindings/bnScriptedBlock.h" #include "bindings/bnScriptedPlayer.h" @@ -286,6 +287,20 @@ int HandleBattleOnly(Game& g, TaskGroup tasks, const std::string& playerpath, co static PA programAdvance; + if (mob->IsFreedomMission()) { + FreedomMissionProps props{ + { player, programAdvance, std::move(folder), field, mob->GetBackground() }, + { mob }, + mob->GetTurnLimit(), + sf::Sprite(*mugshot), + mugshotAnim, + emotions, + }; + + g.push(std::move(props)); + return EXIT_SUCCESS; + } + MobBattleProperties props{ { player, programAdvance, std::move(folder), field, mob->GetBackground() }, MobBattleProperties::RewardBehavior::take, diff --git a/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp b/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp index 89e1941d9..56273e9b3 100644 --- a/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp +++ b/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp @@ -7,7 +7,7 @@ #include "../bnBufferReader.h" #include "../bnBufferWriter.h" #include "../../bnFadeInState.h" -#include "../../bnElementalDamage.h" +#include "../../bnAlertSymbol.h" #include "../../bnBlockPackageManager.h" #include "../../bnPlayerHealthUI.h" @@ -216,7 +216,7 @@ void NetworkBattleScene::OnHit(Entity& victim, const Hit::Properties& props) { bool superEffective = victim.IsSuperEffective(props.element) && props.damage > 0; if (freezeBreak || superEffective) { - std::shared_ptr seSymbol = std::make_shared(); + std::shared_ptr seSymbol = std::make_shared(); seSymbol->SetLayer(-100); seSymbol->SetHeight(victim.GetHeight() + (victim.getLocalBounds().height * 0.5f)); // place it at sprite height GetField()->AddEntity(seSymbol, victim.GetTile()->GetX(), victim.GetTile()->GetY()); From 8f8f2cf9c6fc9fad7f139f6ee0b857b739df0e24 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Mon, 28 Jul 2025 18:12:41 -0700 Subject: [PATCH 018/146] Avoid preparing FullSynchro unless the Player is in that emotion --- BattleNetwork/battlescene/bnBattleSceneBase.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/BattleNetwork/battlescene/bnBattleSceneBase.cpp b/BattleNetwork/battlescene/bnBattleSceneBase.cpp index 1a074a464..fd428bbed 100644 --- a/BattleNetwork/battlescene/bnBattleSceneBase.cpp +++ b/BattleNetwork/battlescene/bnBattleSceneBase.cpp @@ -453,7 +453,9 @@ void BattleSceneBase::SpawnLocalPlayer(int x, int y) HitListener::Subscribe(*localPlayer); - PreparePlayerFullSynchro(localPlayer); + if (localPlayer->GetEmotion() == Emotion::full_synchro) { + PreparePlayerFullSynchro(localPlayer); + } } void BattleSceneBase::SpawnOtherPlayer(std::shared_ptr player, int x, int y) @@ -481,7 +483,9 @@ void BattleSceneBase::SpawnOtherPlayer(std::shared_ptr player, int x, in HitListener::Subscribe(*player); - PreparePlayerFullSynchro(player); + if (player->GetEmotion() == Emotion::full_synchro) { + PreparePlayerFullSynchro(player); + } } void BattleSceneBase::LoadRedTeamMob(Mob& mob) From 530c2bcd850fc4a05424f39502100dbc84dad08a Mon Sep 17 00:00:00 2001 From: Alrysc Date: Mon, 28 Jul 2025 18:24:41 -0700 Subject: [PATCH 019/146] Set quitting true before onEnd iscalled. Freedom battles no longer set extra results if battle is quit. --- BattleNetwork/battlescene/bnBattleSceneBase.cpp | 4 +--- BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp | 8 +++++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/BattleNetwork/battlescene/bnBattleSceneBase.cpp b/BattleNetwork/battlescene/bnBattleSceneBase.cpp index fd428bbed..34968b430 100644 --- a/BattleNetwork/battlescene/bnBattleSceneBase.cpp +++ b/BattleNetwork/battlescene/bnBattleSceneBase.cpp @@ -1275,6 +1275,7 @@ std::vector> BattleSceneBase::BlueTeamMo void BattleSceneBase::Quit(const FadeOut& mode) { if(quitting) return; + quitting = true; // end the current state if(current) { @@ -1285,7 +1286,6 @@ void BattleSceneBase::Quit(const FadeOut& mode) { // NOTE: swoosh quirk if (getController().getStackSize() == 1) { getController().pop(); - quitting = true; return; } @@ -1301,8 +1301,6 @@ void BattleSceneBase::Quit(const FadeOut& mode) { // mode == FadeOut::pixelate getController().pop>(); } - - quitting = true; } diff --git a/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp b/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp index 739bacc4c..ce49899e3 100644 --- a/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp +++ b/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp @@ -216,7 +216,13 @@ void FreedomMissionMobScene::onLeave() } void FreedomMissionMobScene::onEnd() { - if (!IsPlayerDeleted()) { + /* + Handle results here, because there is no rewards state. + Do not do anything if the Player was defeated or the + scene is ending by the player quitting. This avoids + sending a score to a server when one isn't appropriate. + */ + if (!IsQuitting() && !IsPlayerDeleted()) { BattleResults& results = BattleResultsObj(); std::shared_ptr player = GetLocalPlayer(); results.battleLength = sf::seconds(GetElapsedBattleFrames().count() / 60.f); From 8f7984b3e4cc861bfe152bb70fbb5aefea5329e4 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Mon, 28 Jul 2025 18:59:12 -0700 Subject: [PATCH 020/146] Freedom battle does not time out combat if enemies are being deleted --- .../battlescene/bnBattleSceneBase.cpp | 8 ++++ BattleNetwork/battlescene/bnBattleSceneBase.h | 2 + .../battlescene/bnFreedomMissionMobScene.cpp | 40 +++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/BattleNetwork/battlescene/bnBattleSceneBase.cpp b/BattleNetwork/battlescene/bnBattleSceneBase.cpp index 34968b430..6188acb38 100644 --- a/BattleNetwork/battlescene/bnBattleSceneBase.cpp +++ b/BattleNetwork/battlescene/bnBattleSceneBase.cpp @@ -1130,6 +1130,14 @@ std::vector> BattleSceneBase::GetAllPlayers() return result; } +Mob* BattleSceneBase::GetRedTeamMob() { + return redTeamMob; +} + +Mob* BattleSceneBase::GetBlueTeamMob() { + return blueTeamMob; +} + std::shared_ptr BattleSceneBase::GetField() { diff --git a/BattleNetwork/battlescene/bnBattleSceneBase.h b/BattleNetwork/battlescene/bnBattleSceneBase.h index 624d7d4a0..be7be8635 100644 --- a/BattleNetwork/battlescene/bnBattleSceneBase.h +++ b/BattleNetwork/battlescene/bnBattleSceneBase.h @@ -390,6 +390,8 @@ class BattleSceneBase : const std::shared_ptr GetLocalPlayer() const; std::vector> GetOtherPlayers(); std::vector> GetAllPlayers(); + Mob* GetRedTeamMob(); + Mob* GetBlueTeamMob(); std::shared_ptr GetField(); const std::shared_ptr GetField() const; CardSelectionCust& GetCardSelectWidget(); diff --git a/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp b/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp index ce49899e3..d12c0c1f0 100644 --- a/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp +++ b/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp @@ -221,6 +221,11 @@ void FreedomMissionMobScene::onEnd() { Do not do anything if the Player was defeated or the scene is ending by the player quitting. This avoids sending a score to a server when one isn't appropriate. + + Currently, this results in slightly different behavior + compared to the MobBattleScene. In a MobBattle, quitting + while the last enemies are deleting counts as a victory. + Here, it counts as running away, because IsQuitting is true. */ if (!IsQuitting() && !IsPlayerDeleted()) { BattleResults& results = BattleResultsObj(); @@ -316,6 +321,24 @@ std::function FreedomMissionMobScene::HookFormChangeStart(CharacterTrans std::function FreedomMissionMobScene::HookTurnLimitReached() { auto outOfTurns = [this]() mutable { + Mob* redTeam = GetRedTeamMob(); + Mob* blueTeam = GetBlueTeamMob(); + + /* + Explicitly check Mob::IsCleared instead of BattleSceneBase's Cleared functions. + This makes a distinction on whether or not to watch for Entities which + are currently being deleted. + + By using this, the battle will not end while all enemies are untracked + but still deleting. + */ + bool redTeamCleared = redTeam && redTeam->IsCleared(); + bool blueTeamCleared = blueTeam && blueTeam->IsCleared(); + + if (redTeamCleared || blueTeamCleared) { + return false; + } + if (GetCustomBarProgress() >= GetCustomBarDuration() && GetTurnCount() == FreedomMissionMobScene::props.maxTurns) { overStatePtr->context = FreedomMissionOverState::Conditions::player_failed; return true; @@ -330,6 +353,23 @@ std::function FreedomMissionMobScene::HookTurnLimitReached() std::function FreedomMissionMobScene::HookTurnTimeout() { auto cardGaugeIsFull = [this]() mutable { + Mob* redTeam = GetRedTeamMob(); + Mob* blueTeam = GetBlueTeamMob(); + + /* + Explicitly check Mob::IsCleared instead of BattleSceneBase's Cleared functions. + This makes a distinction on whether or not to watch for Entities which + are currently being deleted. + + By using this, the turn will not time out while all enemies are untracked + but still deleting. + */ + bool redTeamCleared = redTeam && redTeam->IsCleared(); + bool blueTeamCleared = blueTeam && blueTeam->IsCleared(); + + if (redTeamCleared || blueTeamCleared) { + return false; + } return GetCustomBarProgress() >= GetCustomBarDuration(); }; From 336e7651e9a16a6f420fda679d9f94859ab5e9d3 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Mon, 28 Jul 2025 21:22:49 -0700 Subject: [PATCH 021/146] Fix TFC crash. Player cannot TFC until tfcStartFrame has passed. --- .../battlescene/States/bnTimeFreezeBattleState.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp b/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp index e54ca5851..c7efed80a 100644 --- a/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp +++ b/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp @@ -64,6 +64,10 @@ void TimeFreezeBattleState::ProcessInputs() for (std::shared_ptr& p : this->GetScene().GetAllPlayers()) { p->InputState().Process(); + if (summonTick < tfcStartFrame) { + continue; + } + if (p->InputState().Has(InputEvents::pressed_use_chip)) { Logger::Logf(LogLevel::info, "InputEvents::pressed_use_chip for player %i", player_idx); std::shared_ptr cardsUI = p->GetFirstComponent(); @@ -74,7 +78,7 @@ void TimeFreezeBattleState::ProcessInputs() // convert meta data into a useable action object const Battle::Card& card = *maybe_card; - if (card.IsTimeFreeze() && CanCounter(p) && summonTick > summonTextLength) { + if (card.IsTimeFreeze() && CanCounter(p)) { if (std::shared_ptr action = CardToAction(card, p, &GetScene().getController().CardPackagePartitioner(), card.props)) { OnCardActionUsed(action, CurrentTime::AsMilli()); cardsUI->DropNextCard(); From 4e2c254fc56126f8c4f2051695ff5cf8a7059fe1 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Mon, 28 Jul 2025 21:30:36 -0700 Subject: [PATCH 022/146] Pull 52a8f9e, TF chips can be marked uncounterable Pull changes from 52a8f9e239ccaf345115b0b4b56cd13f4306ef03, which adds the "counterable" card property. --- .../States/bnTimeFreezeBattleState.cpp | 15 ++++++++++----- BattleNetwork/bindings/bnUserTypeCardMeta.cpp | 1 + BattleNetwork/bnCard.h | 1 + BattleNetwork/bnPA.cpp | 1 + 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp b/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp index c7efed80a..982c0b715 100644 --- a/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp +++ b/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp @@ -276,7 +276,7 @@ void TimeFreezeBattleState::onDraw(sf::RenderTexture& surface) summonsLabel.setPosition(position); scene.DrawWithPerspective(summonsLabel, surface); - if (currState == state::display_name) { + if (currState == state::display_name && first->action->GetMetaData().counterable) { // draw TF bar underneath bar.setPosition(position + sf::Vector2f(0.f + 2.f, 12.f + 2.f)); bar.setFillColor(sf::Color::Black); @@ -425,21 +425,26 @@ const bool TimeFreezeBattleState::CanCounter(std::shared_ptr user) // tfc window ended if (summonTick > summonTextLength) return false; - bool addEvent = true; + // bool addEvent = true; if (!tfEvents.empty()) { + std::shared_ptr action = tfEvents.begin()->action; + + // some actions cannot be countered + if (!action->GetMetaData().counterable) return false; + // only opposing players can counter - std::shared_ptr lastActor = tfEvents.begin()->action->GetActor(); + std::shared_ptr lastActor = action->GetActor(); if (!lastActor->Teammate(user->GetTeam())) { playerCountered = true; Logger::Logf(LogLevel::info, "Player was countered!"); } else { - addEvent = false; + return false; } } - return addEvent; + return true; } void TimeFreezeBattleState::HandleTimeFreezeCounter(std::shared_ptr action, uint64_t timestamp) diff --git a/BattleNetwork/bindings/bnUserTypeCardMeta.cpp b/BattleNetwork/bindings/bnUserTypeCardMeta.cpp index 10efe9a3b..d59dcab71 100644 --- a/BattleNetwork/bindings/bnUserTypeCardMeta.cpp +++ b/BattleNetwork/bindings/bnUserTypeCardMeta.cpp @@ -39,6 +39,7 @@ void DefineCardMetaUserTypes(ScriptResourceManager* scriptManager, sol::state& s "shortname", &Battle::Card::Properties::shortname, "time_freeze", &Battle::Card::Properties::timeFreeze, "skip_time_freeze_intro", &Battle::Card::Properties::skipTimeFreezeIntro, + "counterable", &Battle::Card::Properties::counterable, "long_description", &Battle::Card::Properties::verboseDescription ); diff --git a/BattleNetwork/bnCard.h b/BattleNetwork/bnCard.h index bce3cd087..f06f3c4fe 100644 --- a/BattleNetwork/bnCard.h +++ b/BattleNetwork/bnCard.h @@ -37,6 +37,7 @@ namespace Battle { bool canBoost{ true }; /*!< Can this card be boosted by other cards? */ bool timeFreeze{ false }; /*!< Does this card rely on action items to resolve before resuming the battle scene? */ bool skipTimeFreezeIntro{ false }; /*! Skips the fade in/out and name appearing for this card */ + bool counterable{ true }; /*!< During the tf intro, can this card be countered? */ string shortname; string action; string description; diff --git a/BattleNetwork/bnPA.cpp b/BattleNetwork/bnPA.cpp index 6a94b725b..e1de265d4 100644 --- a/BattleNetwork/bnPA.cpp +++ b/BattleNetwork/bnPA.cpp @@ -122,6 +122,7 @@ const int PA::FindPA(std::vector& input) iter->canBoost, iter->timeFreeze, false, + true, iter->name, iter->action, iter->action, From 072ad9472eb04d3a9ca4fce4922b54099bce29c7 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Mon, 28 Jul 2025 22:57:44 -0700 Subject: [PATCH 023/146] Partial pull from 9fbac73 Pulled most changes from 9fbac73be790d4182095ef44573ff1d0fd007723. Charge animates during TF, HP changes color at low HP. Did not pull folder scene changes because a different commit was pulled which was later in the history, so these changes are already present. Did not pull bnTile.cpp cooldown changes because they require another commit's changes first. --- .../battlescene/States/bnTimeFreezeBattleState.cpp | 5 +++++ BattleNetwork/bnChargeEffectSceneNode.cpp | 7 +++++-- BattleNetwork/bnChargeEffectSceneNode.h | 1 + BattleNetwork/bnPlayerHealthUI.cpp | 3 +-- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp b/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp index 982c0b715..1148a71dc 100644 --- a/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp +++ b/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp @@ -220,6 +220,11 @@ void TimeFreezeBattleState::onUpdate(double elapsed) } break; } + + for (std::shared_ptr& player : GetScene().GetAllPlayers()) { + ChargeEffectSceneNode& chargeNode = player->GetChargeComponent(); + chargeNode.Animate(elapsed); + } } void TimeFreezeBattleState::onDraw(sf::RenderTexture& surface) diff --git a/BattleNetwork/bnChargeEffectSceneNode.cpp b/BattleNetwork/bnChargeEffectSceneNode.cpp index dbdddec82..a4f4c0611 100644 --- a/BattleNetwork/bnChargeEffectSceneNode.cpp +++ b/BattleNetwork/bnChargeEffectSceneNode.cpp @@ -23,14 +23,13 @@ ChargeEffectSceneNode::~ChargeEffectSceneNode() { void ChargeEffectSceneNode::Update(double _elapsed) { if (charging) { chargeCounter += from_seconds(_elapsed); - if (chargeCounter >= maxChargeTime + i10) { if (isCharged == false) { // We're switching states + setColor(chargeColor); Audio().Play(AudioType::BUSTER_CHARGED); animation.SetAnimation("CHARGED"); animation << Animator::Mode::Loop; - setColor(chargeColor); SetShader(Shaders().GetShader(ShaderType::ADDITIVE)); } @@ -54,6 +53,10 @@ void ChargeEffectSceneNode::Update(double _elapsed) { } } + Animate(_elapsed); +} + +void ChargeEffectSceneNode::Animate(double _elapsed) { animation.Update(_elapsed, getSprite()); } diff --git a/BattleNetwork/bnChargeEffectSceneNode.h b/BattleNetwork/bnChargeEffectSceneNode.h index b06cbf128..d2aeb41d3 100644 --- a/BattleNetwork/bnChargeEffectSceneNode.h +++ b/BattleNetwork/bnChargeEffectSceneNode.h @@ -24,6 +24,7 @@ class ChargeEffectSceneNode : public SpriteProxyNode, public ResourceHandle { ~ChargeEffectSceneNode(); void Update(double _elapsed); + void Animate(double _elapsed); /** * @brief If true, the component begins to charge. Otherwise, cancels charge diff --git a/BattleNetwork/bnPlayerHealthUI.cpp b/BattleNetwork/bnPlayerHealthUI.cpp index 0ed859985..81bdef55f 100644 --- a/BattleNetwork/bnPlayerHealthUI.cpp +++ b/BattleNetwork/bnPlayerHealthUI.cpp @@ -152,6 +152,7 @@ void PlayerHealthUIComponent::OnUpdate(double elapsed) { // if battle is ongoing and valid, play high pitch sound when hp is low isBattleOver = Injected() ? (Scene()->IsRedTeamCleared() || Scene()->IsBlueTeamCleared()) : true; + ui.Update(elapsed); if (auto player = GetOwnerAs()) { ui.SetHP(player->GetHealth()); @@ -179,6 +180,4 @@ void PlayerHealthUIComponent::OnUpdate(double elapsed) { } } } - - ui.Update(elapsed); } From 3198683e3ca983936fb4154ff12c18c40fa57bee Mon Sep 17 00:00:00 2001 From: Alrysc Date: Mon, 28 Jul 2025 23:09:09 -0700 Subject: [PATCH 024/146] Partial pull from 1e86e04 Pulled some changes from 1e86e045625215db150f5b7711b5c2c080c0c5a4. Various Tile fields now use frame_time_t. Elec element no longer does extra damage on Ice. Pulled COOLDOWN value from 9fbac73. Sea panel code and related files were not pulled. --- BattleNetwork/bnAnimation.cpp | 19 ++++++++++++++---- BattleNetwork/bnEntity.cpp | 4 +++- BattleNetwork/bnField.cpp | 4 ++-- BattleNetwork/bnTile.cpp | 36 +++++++++++++++++------------------ BattleNetwork/bnTile.h | 12 ++++++------ 5 files changed, 44 insertions(+), 31 deletions(-) diff --git a/BattleNetwork/bnAnimation.cpp b/BattleNetwork/bnAnimation.cpp index c8294935f..d8a268072 100644 --- a/BattleNetwork/bnAnimation.cpp +++ b/BattleNetwork/bnAnimation.cpp @@ -145,6 +145,20 @@ static float GetFloatValue(std::string_view line, std::string_view key) { return std::strtof(valueView.data(), nullptr); } +static frame_time_t GetFrameValue(std::string_view line, std::string_view key) { + std::string_view valueView = GetValue(line, key); + + if (valueView.empty()) return frames(0); + + // frame value + if (valueView.at(valueView.size() - 1) == 'f') { + valueView = valueView.substr(0, valueView.size() - 1); + return frames(static_cast(std::strtof(valueView.data(), nullptr))); + } + + return from_seconds(std::fabs(std::strtof(valueView.data(), nullptr))); +} + static bool GetBoolValue(std::string_view line, std::string_view key) { std::string_view valueView = GetValue(line, key); return valueView == "1" || valueView == "true"; @@ -216,10 +230,7 @@ void Animation::LoadWithData(const string& data) continue; } - float duration = GetFloatValue(line, "duration"); - - // prevent negative frame numbers - frame_time_t currentFrameDuration = from_seconds(std::fabs(duration)); + frame_time_t currentFrameDuration = GetFrameValue(line, "duration"); frameLists.at(frameAnimationIndex).Add(currentFrameDuration, IntRect{}, sf::Vector2f{ 0, 0 }, false, false); } diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index 0293691d5..4ceb2ee26 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -1303,7 +1303,9 @@ void Entity::ResolveFrameBattleDamage() GetTile()->SetState(TileState::normal); } - if (props.filtered.element == Element::elec + // TODO: Replace with Sea state check when Sea panels + // are added. Disabling bonus damage for now. + if (false && props.filtered.element == Element::elec && GetTile()->GetState() == TileState::ice) { tileDamage = props.filtered.damage; } diff --git a/BattleNetwork/bnField.cpp b/BattleNetwork/bnField.cpp index 1a7657f44..5c775cf95 100644 --- a/BattleNetwork/bnField.cpp +++ b/BattleNetwork/bnField.cpp @@ -373,7 +373,7 @@ void Field::Update(double _elapsed) { for (int i = 0; i < tiles.size(); i++) { for (int j = 0; j < tiles[i].size(); j++) { Battle::Tile* t = tiles[i][j]; - if (t->teamCooldown > 0) { + if (t->teamCooldown > frames(0)) { syncCol.insert(syncCol.begin(), j); } else if(t->GetTeam() != t->ogTeam){ @@ -451,7 +451,7 @@ void Field::Update(double _elapsed) { // sync stolen tiles with their corresponding columns for (int col : syncCol) { - double maxTimer = 0.0; + frame_time_t maxTimer = frames(0); for (size_t i = 1; i <= GetHeight(); i++) { Battle::Tile* t = tiles[i][col]; maxTimer = std::max(maxTimer, t->teamCooldown); diff --git a/BattleNetwork/bnTile.cpp b/BattleNetwork/bnTile.cpp index 6feb30c6e..793c990f9 100644 --- a/BattleNetwork/bnTile.cpp +++ b/BattleNetwork/bnTile.cpp @@ -23,13 +23,13 @@ #define START_X 0.0f #define START_Y 144.f #define Y_OFFSET 10.0f -#define COOLDOWN 10.0 -#define FLICKER 3.0 +#define COOLDOWN frames(1800) +#define FLICKER frames(180) namespace Battle { - double Tile::brokenCooldownLength = COOLDOWN; - double Tile::teamCooldownLength = COOLDOWN; - double Tile::flickerTeamCooldownLength = FLICKER; + frame_time_t Tile::brokenCooldownLength = COOLDOWN; + frame_time_t Tile::teamCooldownLength = COOLDOWN; + frame_time_t Tile::flickerTeamCooldownLength = FLICKER; Tile::Tile(int _x, int _y) : SpriteProxyNode(), @@ -54,8 +54,8 @@ namespace Battle { willHighlight = false; isTimeFrozen = true; isBattleOver = false; - brokenCooldown = 0; - flickerTeamCooldown = teamCooldown = 0; + brokenCooldown = frames(0); + flickerTeamCooldown = teamCooldown = frames(0); red_team_atlas = blue_team_atlas = nullptr; // Set by field burncycle = 0.12; // milliseconds @@ -241,7 +241,7 @@ namespace Battle { flickerTeamCooldown = flickerTeamCooldownLength; } else { - flickerTeamCooldown = 0; // cancel + flickerTeamCooldown = frames(0); // cancel teamCooldown = teamCooldownLength; } } @@ -327,11 +327,11 @@ namespace Battle { Team otherTeam = (team == Team::unknown) ? Team::unknown : (team == Team::red) ? Team::blue : Team::red; std::string prevAnimState = animState; - ((int)(flickerTeamCooldown * 100) % 2 == 0 && flickerTeamCooldown <= flickerTeamCooldownLength) ? currTeam : currTeam = otherTeam; + (((flickerTeamCooldown.count() % 4) < 2) && flickerTeamCooldown <= flickerTeamCooldownLength) ? currTeam : currTeam = otherTeam; if (state == TileState::broken) { // Broken tiles flicker when they regen - animState = ((int)(brokenCooldown * 100) % 2 == 0 && brokenCooldown <= FLICKER) ? std::move(GetAnimState(TileState::normal)) : std::move(GetAnimState(state)); + animState = (((brokenCooldown.count() % 4) < 2) && brokenCooldown <= FLICKER) ? std::move(GetAnimState(TileState::normal)) : std::move(GetAnimState(state)); } else { animState = std::move(GetAnimState(state)); @@ -531,20 +531,20 @@ namespace Battle { // Update our tile animation and texture if (!isTimeFrozen) { - if (teamCooldown > 0) { - teamCooldown -= 1.0 * _elapsed; - if (teamCooldown < 0) teamCooldown = 0; + if (teamCooldown > frames(0)) { + teamCooldown -= frames(1); + if (teamCooldown < frames(0)) teamCooldown = frames(0); } - if (flickerTeamCooldown > 0) { - flickerTeamCooldown -= 1.0 * _elapsed; - if (flickerTeamCooldown < 0) flickerTeamCooldown = 0; + if (flickerTeamCooldown > frames(0)) { + flickerTeamCooldown -= frames(1); + if (flickerTeamCooldown < frames(0)) flickerTeamCooldown = frames(0); } if (state == TileState::broken) { - brokenCooldown -= 1.0f* _elapsed; + brokenCooldown -= frames(1); - if (brokenCooldown < 0) { brokenCooldown = 0; state = TileState::normal; } + if (brokenCooldown < frames(0)) { brokenCooldown = frames(0); state = TileState::normal; } } } diff --git a/BattleNetwork/bnTile.h b/BattleNetwork/bnTile.h index f10959d34..1bc7d2946 100644 --- a/BattleNetwork/bnTile.h +++ b/BattleNetwork/bnTile.h @@ -335,12 +335,12 @@ namespace Battle { bool isPerspectiveFlipped{ false }; float width{}; float height{}; - static double teamCooldownLength; - static double brokenCooldownLength; - static double flickerTeamCooldownLength; - double teamCooldown{}; - double brokenCooldown{}; - double flickerTeamCooldown{}; + static frame_time_t teamCooldownLength; + static frame_time_t brokenCooldownLength; + static frame_time_t flickerTeamCooldownLength; + frame_time_t teamCooldown{}; + frame_time_t brokenCooldown{}; + frame_time_t flickerTeamCooldown{}; double totalElapsed{}; double elapsedBurnTime{}; double burncycle{}; From 4519a50b214654b9bf60d124d6e2f32402ba4548 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Mon, 28 Jul 2025 23:11:19 -0700 Subject: [PATCH 025/146] Pull f6b2706. Fire Element is not harmed by lava Tiles. Pulled changes from f6b270664c514d396a14e989538623657e516a6a. --- BattleNetwork/bnTile.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BattleNetwork/bnTile.cpp b/BattleNetwork/bnTile.cpp index 793c990f9..f8d97704b 100644 --- a/BattleNetwork/bnTile.cpp +++ b/BattleNetwork/bnTile.cpp @@ -685,7 +685,7 @@ namespace Battle { elapsedBurnTime = 0; } - if (GetState() == TileState::lava) { + if (GetState() == TileState::lava && character.GetElement() != Element::fire) { Hit::Properties props = { 50, Hit::flash | Hit::flinch, Element::none, 0, Direction::none }; if (character.HasCollision(props)) { character.Hit(props); From b2c781abdada8edbe97935f3583b736027e9dbed Mon Sep 17 00:00:00 2001 From: Alrysc Date: Mon, 28 Jul 2025 23:26:17 -0700 Subject: [PATCH 026/146] Pull most of b60afa0, elemental hitbox interactions Pulled most changes from b60afa0752b44c721ed7a0485730c9652d52e3ed. Excluded code related to Sand Tiles, which are not in this history yet. --- BattleNetwork/bnEntity.cpp | 20 ++++++--------- BattleNetwork/bnTile.cpp | 51 ++++++++++++++++++++++++-------------- 2 files changed, 39 insertions(+), 32 deletions(-) diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index 4ceb2ee26..de7d0f7b4 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -860,6 +860,7 @@ void Entity::SetTeam(Team _team) { void Entity::SetPassthrough(bool state) { passthrough = state; + Reveal(); } bool Entity::IsPassthrough() @@ -1345,6 +1346,9 @@ void Entity::ResolveFrameBattleDamage() flagCheckThunk(Hit::impact); } + // exclude this from the next processing step + props.filtered.flags &= ~Hit::impact; + // Requeue drag if already sliding by drag or in the middle of a move if ((props.filtered.flags & Hit::drag) == Hit::drag) { if (IsSliding()) { @@ -1366,11 +1370,11 @@ void Entity::ResolveFrameBattleDamage() } flagCheckThunk(Hit::drag); - - // exclude this from the next processing step - props.filtered.flags &= ~Hit::drag; } + // exclude this from the next processing step + props.filtered.flags &= ~Hit::drag; + bool flashAndFlinch = ((props.filtered.flags & Hit::flash) == Hit::flash) && ((props.filtered.flags & Hit::flinch) == Hit::flinch); frameFreezeCancel = frameFreezeCancel || flashAndFlinch; @@ -1388,11 +1392,6 @@ void Entity::ResolveFrameBattleDamage() append.push({ props.hitbox, { 0, props.filtered.flags } }); } else { - // TODO: this is a specific (and expensive) check. Is there a way to prioritize this defense rule? - /*for (auto&& d : this->defenses) { - hasSuperArmor = hasSuperArmor || dynamic_cast(d); - }*/ - // assume some defense rule strips out flinch, prevent abuse of stun frameStunCancel = frameStunCancel ||(props.filtered.flags & Hit::flinch) == 0 && (props.hitbox.flags & Hit::flinch) == Hit::flinch; @@ -1509,11 +1508,6 @@ void Entity::ResolveFrameBattleDamage() // exclude blind from the next processing step props.filtered.flags &= ~Hit::blind; - // todo: for confusion - //if ((props.filtered.flags & Hit::confusion) == Hit::confusion) { - // frameStunCancel = true; - //} - /* flags already accounted for: - impact diff --git a/BattleNetwork/bnTile.cpp b/BattleNetwork/bnTile.cpp index f8d97704b..da4beda5c 100644 --- a/BattleNetwork/bnTile.cpp +++ b/BattleNetwork/bnTile.cpp @@ -959,24 +959,31 @@ namespace Battle { // Spells dont cause damage when the battle is over if (isBattleOver) return; + bool hitByWind{}, hitByFire{}, hitByAqua{}; + // Now that spells and characters have updated and moved, they are due to check for attack outcomes std::vector> characters_copy = characters; // may be modified after hitboxes are resolved - for (std::shared_ptr& character : characters_copy) { - // the entity is a character (can be hit) and the team isn't the same - // we see if it passes defense checks, then call attack + for (Entity::ID_t ID: queuedAttackers) { + std::shared_ptr attacker = field.GetEntity(ID); - bool retangible = false; - DefenseFrameStateJudge judge; // judge for this character's defenses + if (!attacker) { + Logger::Logf(LogLevel::debug, "Attacker %d missing from field", ID); + continue; + } - for (Entity::ID_t ID : queuedAttackers) { - std::shared_ptr attacker = field.GetEntity(ID); + Hit::Properties props = attacker->GetHitboxProperties(); - if (!attacker) { - Logger::Logf(LogLevel::debug, "Attacker %d missing from field", ID); - continue; - } + hitByWind = hitByWind || props.element == Element::wind; + hitByFire = hitByFire || props.element == Element::fire; + hitByAqua = hitByAqua || props.element == Element::aqua; + + bool retangible = false; + DefenseFrameStateJudge judge; // judge for this character's defenses + for (std::shared_ptr& character : characters_copy) { + // the entity is a character (can be hit) and the team isn't the same + // we see if it passes defense checks, then call attack if (!character->IsHitboxAvailable()) continue; @@ -1006,7 +1013,6 @@ namespace Battle { // Collision here means "we are able to hit" // either with a hitbox that can pierce a defense or by tangibility - Hit::Properties props = attacker->GetHitboxProperties(); if (!character->HasCollision(props)) continue; // Obstacles can hit eachother, even on the same team @@ -1053,17 +1059,29 @@ namespace Battle { attacker->SetHitboxProperties(props); } + if (retangible) character->SetPassthrough(false); + judge.PrepareForNextAttack(); } // end each spell loop judge.ExecuteAllTriggers(); - if (retangible) character->SetPassthrough(false); } // end each character loop // empty previous frame queue to be used this current frame queuedAttackers.clear(); - // taggedAttackers.clear(); + + // TODO: Uncomment when Sand is in. +// if (GetState() == TileState::sand && hitByWind) { +// SetState(TileState::normal); +// } +// else + if (GetState() == TileState::grass && hitByFire) { + SetState(TileState::normal); + } + else if (GetState() == TileState::volcano && hitByAqua) { + SetState(TileState::normal); + } } void Tile::UpdateSpells(Field& field, const double elapsed) @@ -1077,11 +1095,6 @@ namespace Battle { highlightMode = (TileHighlight)request; } - Element hitboxElement = spell->GetElement(); - if (hitboxElement == Element::aqua && state == TileState::volcano) { - SetState(TileState::normal); - } - field.UpdateEntityOnce(*spell, elapsed); } } From 36a51858a5248ddf09b769e3177d5f66deff0843 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Tue, 29 Jul 2025 00:52:19 -0700 Subject: [PATCH 027/146] Partial pull from 314fb87 and 998ea83, Partially pulled changes from 314fb877e3d74e3e2472ce2c20b079c1f3ebebdd. is_actionable available from Lua, more frame_time conversions for Tile fields, changed how damage is handled. Game class uses appName, had to pull Game changes from 998ea83f6f4219fc1431bdd4dde6c4cc2c7ddd26, which added a switch to filesystem. Did not pull changes related to Tile position or offset, which have some related code in another commit not included. --- .../bindings/bnUserTypeBasicPlayer.cpp | 3 + BattleNetwork/bnEntity.cpp | 75 ++++++++++--------- BattleNetwork/bnEntity.h | 7 +- BattleNetwork/bnGame.cpp | 32 ++++---- BattleNetwork/bnGame.h | 20 +++-- BattleNetwork/bnSpell.cpp | 4 - BattleNetwork/bnTile.cpp | 35 +++++---- BattleNetwork/bnTile.h | 11 ++- 8 files changed, 102 insertions(+), 85 deletions(-) diff --git a/BattleNetwork/bindings/bnUserTypeBasicPlayer.cpp b/BattleNetwork/bindings/bnUserTypeBasicPlayer.cpp index aa8ceb730..b2af44a81 100644 --- a/BattleNetwork/bindings/bnUserTypeBasicPlayer.cpp +++ b/BattleNetwork/bindings/bnUserTypeBasicPlayer.cpp @@ -17,6 +17,9 @@ void DefineBasicPlayerUserType(sol::table& battle_namespace) { player.Unwrap()->AddAction(CardEvent{ cardAction.UnwrapAndRelease() }, order); } ), + "is_actionable", [](WeakWrapper& player) -> bool { + return player.Unwrap()->IsActionable(); + }, "get_attack_level", [](WeakWrapper& player) -> unsigned int { return player.Unwrap()->GetAttackLevel(); }, diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index de7d0f7b4..9b941faac 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -1203,7 +1203,46 @@ const bool Entity::Hit(Hit::Properties props) { props.damage *= 2; } - SetHealth(GetHealth() - props.damage); + int tileDamage = 0; + int extraDamage = 0; + + // Calculate elemental damage if the tile the character is on is super effective to it + if (props.element == Element::fire + && GetTile()->GetState() == TileState::grass) { + tileDamage = props.damage; + GetTile()->SetState(TileState::normal); + } + + // TODO: Replace with Sea state check when Sea panels + // are added. Disabling bonus damage for now. + if (false && props.element == Element::elec + && GetTile()->GetState() == TileState::ice) { + tileDamage = props.damage; + } + + /* + if (props.element == Element::aqua + && GetTile()->GetState() == TileState::ice + && !frameFreezeCancel) { + willFreeze = true; + GetTile()->SetState(TileState::normal); + } + + if ((props.flags & Hit::breaking) == Hit::breaking && IsIceFrozen()) { + extraDamage = props.damage; + frameFreezeCancel = true; + } + */ + + int totalDamage = props.damage + (tileDamage + extraDamage); + + // Broadcast the hit before we apply statuses and change the entity's state flags + if (totalDamage > 0) { + SetHealth(GetHealth() - (tileDamage + extraDamage)); + HitPublisher::Broadcast(*this, props); + } + + SetHealth(GetHealth() - totalDamage); if (IsTimeFrozen()) { props.flags |= Hit::no_counter; @@ -1294,40 +1333,6 @@ void Entity::ResolveFrameBattleDamage() } }; - int tileDamage = 0; - int extraDamage = 0; - - // Calculate elemental damage if the tile the character is on is super effective to it - if (props.filtered.element == Element::fire - && GetTile()->GetState() == TileState::grass) { - tileDamage = props.filtered.damage; - GetTile()->SetState(TileState::normal); - } - - // TODO: Replace with Sea state check when Sea panels - // are added. Disabling bonus damage for now. - if (false && props.filtered.element == Element::elec - && GetTile()->GetState() == TileState::ice) { - tileDamage = props.filtered.damage; - } - - if (props.filtered.element == Element::aqua - && GetTile()->GetState() == TileState::ice - && !frameFreezeCancel) { - willFreeze = true; - GetTile()->SetState(TileState::normal); - } - - if ((props.filtered.flags & Hit::breaking) == Hit::breaking && IsIceFrozen()) { - extraDamage = props.filtered.damage; - frameFreezeCancel = true; - } - - // Broadcast the hit before we apply statuses and change the entity's state flags - if (props.filtered.damage > 0) { - SetHealth(GetHealth() - (tileDamage + extraDamage)); - HitPublisher::Broadcast(*this, props.filtered); - } // start of new scope { diff --git a/BattleNetwork/bnEntity.h b/BattleNetwork/bnEntity.h index 1f99f7fd5..952338171 100644 --- a/BattleNetwork/bnEntity.h +++ b/BattleNetwork/bnEntity.h @@ -704,9 +704,10 @@ class Entity : * @brief Toggle whether or not to highlight a tile * @param mode * - * FLASH - flicker every other frame - * SOLID - Stay yellow - * NONE - this is the default. No effects are applied. + * flash - Flicker every other frame + * solid - Stay yellow + * none - This is the default. No effects are applied. + * automatic - Highlight yellow when attacking. This will auto disable when done. */ void HighlightTile(Battle::TileHighlight mode); diff --git a/BattleNetwork/bnGame.cpp b/BattleNetwork/bnGame.cpp index 21b3eead9..a37b1e8af 100644 --- a/BattleNetwork/bnGame.cpp +++ b/BattleNetwork/bnGame.cpp @@ -531,44 +531,44 @@ void Game::SetSubtitle(const std::string& subtitle) window.SetSubtitle(subtitle); } -const std::string Game::AppDataPath() +const std::filesystem::path Game::AppDataPath() { - return sago::getDataHome() + "/" + window.GetTitle(); + return std::filesystem::u8path(sago::getDataHome()) / appName; } -const std::string Game::CacheDataPath() +const std::filesystem::path Game::CacheDataPath() { - return sago::getCacheDir() + "/" + window.GetTitle(); + return std::filesystem::u8path(sago::getCacheDir()) / appName; } -const std::string Game::DesktopPath() +const std::filesystem::path Game::DesktopPath() { - return sago::getDesktopFolder(); + return std::filesystem::u8path(sago::getDesktopFolder()); } -const std::string Game::DownloadsPath() +const std::filesystem::path Game::DownloadsPath() { - return sago::getDownloadFolder(); + return std::filesystem::u8path(sago::getDownloadFolder()); } -const std::string Game::DocumentsPath() +const std::filesystem::path Game::DocumentsPath() { - return sago::getDocumentsFolder(); + return std::filesystem::u8path(sago::getDocumentsFolder()); } -const std::string Game::VideosPath() +const std::filesystem::path Game::VideosPath() { - return sago::getVideoFolder(); + return std::filesystem::u8path(sago::getVideoFolder()); } -const std::string Game::PicturesPath() +const std::filesystem::path Game::PicturesPath() { - return sago::getPicturesFolder(); + return std::filesystem::u8path(sago::getPicturesFolder()); } -const std::string Game::SaveGamesPath() +const std::filesystem::path Game::SaveGamesPath() { - return sago::getSaveGamesFolder1(); + return std::filesystem::u8path(sago::getSaveGamesFolder1()); } CardPackagePartitioner& Game::CardPackagePartitioner() diff --git a/BattleNetwork/bnGame.h b/BattleNetwork/bnGame.h index 27c76fdb0..affbddd0e 100644 --- a/BattleNetwork/bnGame.h +++ b/BattleNetwork/bnGame.h @@ -2,6 +2,7 @@ #include #include #include +#include #include "cxxopts/cxxopts.hpp" #include "bnTaskGroup.h" @@ -17,6 +18,8 @@ #include "bnInputManager.h" #include "bnPackageManager.h" +#define APP_NAME "OpenNetBattle" + #define ONB_REGION_JAPAN 0 #define ONB_ENABLE_PIXELATE_GFX 0 @@ -64,6 +67,7 @@ class Game final : public ActivityController { bool frameByFrame{}, isDebug{}, quitting{ false }; bool singlethreaded{ false }; bool isRecording{}, isRecordOutSaving{}, recordPressed{}; + const char* appName{ APP_NAME }; TextureResourceManager textureManager; AudioResourceManager audioManager; @@ -146,14 +150,14 @@ class Game final : public ActivityController { void Record(bool enabled = true); void SetSubtitle(const std::string& subtitle); - const std::string AppDataPath(); - const std::string CacheDataPath(); - const std::string DesktopPath(); - const std::string DownloadsPath(); - const std::string DocumentsPath(); - const std::string VideosPath(); - const std::string PicturesPath(); - const std::string SaveGamesPath(); + const std::filesystem::path AppDataPath(); + const std::filesystem::path CacheDataPath(); + const std::filesystem::path DesktopPath(); + const std::filesystem::path DownloadsPath(); + const std::filesystem::path DocumentsPath(); + const std::filesystem::path VideosPath(); + const std::filesystem::path PicturesPath(); + const std::filesystem::path SaveGamesPath(); CardPackagePartitioner& CardPackagePartitioner(); PlayerPackagePartitioner& PlayerPackagePartitioner(); diff --git a/BattleNetwork/bnSpell.cpp b/BattleNetwork/bnSpell.cpp index e92daada9..009da03b7 100644 --- a/BattleNetwork/bnSpell.cpp +++ b/BattleNetwork/bnSpell.cpp @@ -9,9 +9,5 @@ Spell::Spell(Team team) : Entity() } void Spell::OnUpdate(double _elapsed) { - //if (IsTimeFrozen()) return; - - //OnUpdate(_elapsed); - setPosition(getPosition().x, getPosition().y - GetHeight()); } diff --git a/BattleNetwork/bnTile.cpp b/BattleNetwork/bnTile.cpp index da4beda5c..2705f08dc 100644 --- a/BattleNetwork/bnTile.cpp +++ b/BattleNetwork/bnTile.cpp @@ -34,7 +34,7 @@ namespace Battle { Tile::Tile(int _x, int _y) : SpriteProxyNode(), animation() { - totalElapsed = 0; + totalElapsed = frames(0); x = _x; y = _y; @@ -58,7 +58,7 @@ namespace Battle { flickerTeamCooldown = teamCooldown = frames(0); red_team_atlas = blue_team_atlas = nullptr; // Set by field - burncycle = 0.12; // milliseconds + burncycle = frames(1); // milliseconds elapsedBurnTime = burncycle; highlightMode = TileHighlight::none; @@ -66,10 +66,10 @@ namespace Battle { volcanoSprite = std::make_shared(); volcanoErupt = Animation("resources/tiles/volcano.animation"); - auto resetVolcanoThunk = [this](int seconds) { + auto resetVolcanoThunk = [this](int frames) { if (!isBattleOver) { this->volcanoErupt.SetFrame(1, this->volcanoSprite->getSprite()); // start over - volcanoEruptTimer = seconds; + volcanoEruptTimer = ::frames(frames); std::shared_ptr field = fieldWeak.lock(); @@ -83,15 +83,15 @@ namespace Battle { }; if (team == Team::blue) { - resetVolcanoThunk(1); // blue goes first + resetVolcanoThunk(60); // blue goes first } else { - resetVolcanoThunk(2); // then red + resetVolcanoThunk(120); // then red } // On anim end, reset the timer volcanoErupt << "FLICKER" << Animator::Mode::Loop << [this, resetVolcanoThunk]() { - resetVolcanoThunk(2); + resetVolcanoThunk(120); }; volcanoSprite->setTexture(Textures().LoadFromFile("resources/tiles/volcano.png")); @@ -504,16 +504,16 @@ namespace Battle { void Tile::Update(Field& field, double _elapsed) { willHighlight = false; - totalElapsed += _elapsed; + totalElapsed += from_seconds(_elapsed); if (!isTimeFrozen && isBattleStarted) { // LAVA TILES - elapsedBurnTime -= _elapsed; + elapsedBurnTime -= from_seconds(_elapsed); // VOLCANO - volcanoEruptTimer -= _elapsed; + volcanoEruptTimer -= from_seconds(_elapsed); - if (volcanoEruptTimer <= 0) { + if (volcanoEruptTimer <= frames(0)) { volcanoErupt.Update(_elapsed, volcanoSprite->getSprite()); } @@ -549,7 +549,7 @@ namespace Battle { } RefreshTexture(); - animation.SyncTime(from_seconds(totalElapsed)); + animation.SyncTime(totalElapsed); animation.Refresh(this->getSprite()); switch (highlightMode) { @@ -557,7 +557,7 @@ namespace Battle { willHighlight = true; break; case TileHighlight::flash: - willHighlight = (int)(totalElapsed * 15) % 2 == 0; + willHighlight = (totalElapsed.count() % 4 < 2); break; default: willHighlight = false; @@ -675,14 +675,14 @@ namespace Battle { // LAVA & POISON TILES if (!character.HasFloatShoe()) { if (GetState() == TileState::poison) { - if (elapsedBurnTime <= 0) { + if (elapsedBurnTime <= frames(0)) { if (character.Hit(Hit::Properties({ 1, Hit::pierce, Element::none, 0, Direction::none }))) { elapsedBurnTime = burncycle; } } } else { - elapsedBurnTime = 0; + elapsedBurnTime = frames(0); } if (GetState() == TileState::lava && character.GetElement() != Element::fire) { @@ -1055,6 +1055,11 @@ namespace Battle { attacker->Attack(character); + // Special case: highlight the tile when attacking on a frame + if (attacker->GetTileHighlightMode() == TileHighlight::automatic) { + highlightMode = TileHighlight::solid; + } + // we restore the hitbox properties attacker->SetHitboxProperties(props); } diff --git a/BattleNetwork/bnTile.h b/BattleNetwork/bnTile.h index 1bc7d2946..d74c62009 100644 --- a/BattleNetwork/bnTile.h +++ b/BattleNetwork/bnTile.h @@ -49,6 +49,7 @@ namespace Battle { none = 0, flash = 1, solid = 2, + automatic = 3 }; class Tile : public SpriteProxyNode, public ResourceHandle { @@ -328,6 +329,8 @@ namespace Battle { int x{}; /**< Column number*/ int y{}; /**< Row number*/ + float offsetX{}; + float offsetY{}; bool willHighlight{ false }; /**< Highlights when there is a spell occupied in this tile */ bool isTimeFrozen{ false }; bool isBattleOver{ false }; @@ -341,9 +344,9 @@ namespace Battle { frame_time_t teamCooldown{}; frame_time_t brokenCooldown{}; frame_time_t flickerTeamCooldown{}; - double totalElapsed{}; - double elapsedBurnTime{}; - double burncycle{}; + frame_time_t totalElapsed{}; + frame_time_t elapsedBurnTime{}; + frame_time_t burncycle{}; std::weak_ptr fieldWeak; std::shared_ptr red_team_atlas, red_team_perm; std::shared_ptr blue_team_atlas, blue_team_perm; @@ -368,7 +371,7 @@ namespace Battle { Animation animation; Animation volcanoErupt; - double volcanoEruptTimer{ 4 }; // seconds + frame_time_t volcanoEruptTimer{ 240 }; std::shared_ptr volcanoSprite; }; From 609196c12a09c038866889a76766d740ca4a15a1 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Tue, 29 Jul 2025 01:03:47 -0700 Subject: [PATCH 028/146] Pull 59d7262. Scripted Artifact, Obstacle, and Spell have battle start and end funcs. --- BattleNetwork/bindings/bnScriptedArtifact.cpp | 26 ++++++++ BattleNetwork/bindings/bnScriptedArtifact.h | 60 ++++++++++--------- BattleNetwork/bindings/bnScriptedObstacle.cpp | 26 ++++++++ BattleNetwork/bindings/bnScriptedObstacle.h | 2 + BattleNetwork/bindings/bnScriptedSpell.cpp | 26 ++++++++ BattleNetwork/bindings/bnScriptedSpell.h | 2 + 6 files changed, 113 insertions(+), 29 deletions(-) diff --git a/BattleNetwork/bindings/bnScriptedArtifact.cpp b/BattleNetwork/bindings/bnScriptedArtifact.cpp index 78bda70cf..86f182fde 100644 --- a/BattleNetwork/bindings/bnScriptedArtifact.cpp +++ b/BattleNetwork/bindings/bnScriptedArtifact.cpp @@ -43,6 +43,32 @@ void ScriptedArtifact::OnSpawn(Battle::Tile& tile) } } +void ScriptedArtifact::OnBattleStart() { + if (battle_start_func.valid()) + { + auto result = CallLuaCallback(battle_start_func, weakWrap); + + if (result.is_error()) { + Logger::Log(LogLevel::critical, result.error_cstr()); + } + } + + Artifact::OnBattleStart(); +} + +void ScriptedArtifact::OnBattleStop() { + Artifact::OnBattleStop(); + + if (battle_end_func.valid()) + { + auto result = CallLuaCallback(battle_end_func, weakWrap); + + if (result.is_error()) { + Logger::Log(LogLevel::critical, result.error_cstr()); + } + } +} + void ScriptedArtifact::OnDelete() { if (delete_func.valid()) diff --git a/BattleNetwork/bindings/bnScriptedArtifact.h b/BattleNetwork/bindings/bnScriptedArtifact.h index 80bad0564..6f69380c2 100644 --- a/BattleNetwork/bindings/bnScriptedArtifact.h +++ b/BattleNetwork/bindings/bnScriptedArtifact.h @@ -7,11 +7,11 @@ #include "../bnAnimationComponent.h" #include "bnWeakWrapper.h" - /** - * \class ScriptedArtifact - * \brief An object in control of battlefield visual effects, with support for Lua scripting. - * - */ +/** +* \class ScriptedArtifact +* \brief An object in control of battlefield visual effects, with support for Lua scripting. +* +*/ class ScriptedArtifact final : public Artifact, public dynamic_object { std::shared_ptr animationComponent{ nullptr }; @@ -20,29 +20,31 @@ class ScriptedArtifact final : public Artifact, public dynamic_object public: ScriptedArtifact(); - ~ScriptedArtifact(); - - void Init() override; - - /** - * Centers the animation on the tile, offsets it by its internal offsets, then invokes the function assigned to onUpdate if present. - * @param _elapsed: The amount of elapsed time since the last frame. - */ - void OnUpdate(double _elapsed) override; - void OnDelete() override; - bool CanMoveTo(Battle::Tile* next) override; - void OnSpawn(Battle::Tile& spawn) override; - - void SetAnimation(const std::string& path); - Animation& GetAnimationObject(); - Battle::Tile* GetCurrentTile() const; - - sol::object update_func; - sol::object on_spawn_func; - sol::object delete_func; - sol::object can_move_to_func; - sol::object battle_start_func; - sol::object battle_end_func; + ~ScriptedArtifact(); + + void Init() override; + + /** + * Centers the animation on the tile, offsets it by its internal offsets, then invokes the function assigned to onUpdate if present. + * @param _elapsed: The amount of elapsed time since the last frame. + */ + void OnUpdate(double _elapsed) override; + void OnDelete() override; + bool CanMoveTo(Battle::Tile* next) override; + void OnSpawn(Battle::Tile& spawn) override; + void OnBattleStart() override; + void OnBattleStop() override; + + void SetAnimation(const std::string& path); + Animation& GetAnimationObject(); + Battle::Tile* GetCurrentTile() const; + + sol::object update_func; + sol::object on_spawn_func; + sol::object delete_func; + sol::object can_move_to_func; + sol::object battle_start_func; + sol::object battle_end_func; }; -#endif \ No newline at end of file +#endif diff --git a/BattleNetwork/bindings/bnScriptedObstacle.cpp b/BattleNetwork/bindings/bnScriptedObstacle.cpp index ae4181f74..c790b9085 100644 --- a/BattleNetwork/bindings/bnScriptedObstacle.cpp +++ b/BattleNetwork/bindings/bnScriptedObstacle.cpp @@ -101,6 +101,32 @@ void ScriptedObstacle::OnSpawn(Battle::Tile& spawn) } } +void ScriptedObstacle::OnBattleStart() { + if (battle_start_func.valid()) + { + auto result = CallLuaCallback(battle_start_func, weakWrap); + + if (result.is_error()) { + Logger::Log(LogLevel::critical, result.error_cstr()); + } + } + + Obstacle::OnBattleStart(); +} + +void ScriptedObstacle::OnBattleStop() { + Obstacle::OnBattleStop(); + + if (battle_end_func.valid()) + { + auto result = CallLuaCallback(battle_end_func, weakWrap); + + if (result.is_error()) { + Logger::Log(LogLevel::critical, result.error_cstr()); + } + } +} + const float ScriptedObstacle::GetHeight() const { return height; diff --git a/BattleNetwork/bindings/bnScriptedObstacle.h b/BattleNetwork/bindings/bnScriptedObstacle.h index 487800a2e..515d6ce8d 100644 --- a/BattleNetwork/bindings/bnScriptedObstacle.h +++ b/BattleNetwork/bindings/bnScriptedObstacle.h @@ -24,6 +24,8 @@ class ScriptedObstacle final : public Obstacle, public dynamic_object { void OnCollision(const std::shared_ptr other) override; void Attack(std::shared_ptr e) override; void OnSpawn(Battle::Tile& spawn) override; + void OnBattleStart() override; + void OnBattleStop() override; const float GetHeight() const; void SetHeight(const float height); diff --git a/BattleNetwork/bindings/bnScriptedSpell.cpp b/BattleNetwork/bindings/bnScriptedSpell.cpp index 99fd2200d..15fe5c2d0 100644 --- a/BattleNetwork/bindings/bnScriptedSpell.cpp +++ b/BattleNetwork/bindings/bnScriptedSpell.cpp @@ -93,6 +93,32 @@ void ScriptedSpell::OnSpawn(Battle::Tile& spawn) } } +void ScriptedSpell::OnBattleStart() { + if (battle_start_func.valid()) + { + auto result = CallLuaCallback(battle_start_func, weakWrap); + + if (result.is_error()) { + Logger::Log(LogLevel::critical, result.error_cstr()); + } + } + + Spell::OnBattleStart(); +} + +void ScriptedSpell::OnBattleStop() { + Spell::OnBattleStop(); + + if (battle_end_func.valid()) + { + auto result = CallLuaCallback(battle_end_func, weakWrap); + + if (result.is_error()) { + Logger::Log(LogLevel::critical, result.error_cstr()); + } + } +} + const float ScriptedSpell::GetHeight() const { return height; diff --git a/BattleNetwork/bindings/bnScriptedSpell.h b/BattleNetwork/bindings/bnScriptedSpell.h index 5f07bc221..4204d73d9 100644 --- a/BattleNetwork/bindings/bnScriptedSpell.h +++ b/BattleNetwork/bindings/bnScriptedSpell.h @@ -24,6 +24,8 @@ class ScriptedSpell final : public Spell, public dynamic_object { bool CanMoveTo(Battle::Tile * next) override; void Attack(std::shared_ptr e) override; void OnSpawn(Battle::Tile& spawn) override; + void OnBattleStart() override; + void OnBattleStop() override; const float GetHeight() const; void SetHeight(const float height); From 88b37c82bbd0da87aa340fc54bc8425ba7b0ad10 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Tue, 29 Jul 2025 01:06:42 -0700 Subject: [PATCH 029/146] Pull dbc635b, fix freeze status callback typo Pulled from dbc635bb115fedf8b6b1c503b9c1e230e5ad747e --- BattleNetwork/bnEntity.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index 9b941faac..4fcf22402 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -1426,7 +1426,7 @@ void Entity::ResolveFrameBattleDamage() // this will strip out flash in the next step frameFlashCancel = true; willFreeze = true; - flagCheckThunk(Hit::flinch); + flagCheckThunk(Hit::freeze); } } From f3b1a2ee813bee0b49ee161c910b53d452f815d9 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Tue, 29 Jul 2025 01:10:13 -0700 Subject: [PATCH 030/146] Pull 74094c5, TF actions call EndAction. Moved slightly compared to commit. Pulled 74094c52ff844348b64cf46ac19a169daeca89c1, then moved the line that EndAction is called. It should be called before deallocating the stunt double, in case it is reference in cleanup steps. This can be common for actions to do. --- BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp b/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp index 1148a71dc..bbc82c13d 100644 --- a/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp +++ b/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp @@ -200,6 +200,7 @@ void TimeFreezeBattleState::onUpdate(double elapsed) } else{ first->user->Reveal(); + first->action->EndAction(); scene.UntrackMobCharacter(first->stuntDouble); scene.GetField()->DeallocEntity(first->stuntDouble->GetID()); From 60d631af36d403eea4330262839cf31c96e0b9fe Mon Sep 17 00:00:00 2001 From: Alrysc Date: Tue, 29 Jul 2025 01:12:54 -0700 Subject: [PATCH 031/146] Pull e4febdb, removes chance for a duplicate Entity to be added when ending TF actions Pulled e4febdbe8b7f9de98e69f8f34d3cc825c929ccbf --- BattleNetwork/bnCardAction.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/BattleNetwork/bnCardAction.cpp b/BattleNetwork/bnCardAction.cpp index 5c5398019..d2d2dbaea 100644 --- a/BattleNetwork/bnCardAction.cpp +++ b/BattleNetwork/bnCardAction.cpp @@ -233,8 +233,9 @@ void CardAction::EndAction() OnActionEnd(); if (std::shared_ptr actorPtr = actor.lock()) { - actorPtr->GetTile()->RemoveEntityByID(actorPtr->GetID()); - startTile->AddEntity(actorPtr); + if (actorPtr->GetTile()->RemoveEntityByID(actorPtr->GetID())) { + startTile->AddEntity(actorPtr); + } } if (std::shared_ptr user = userWeak.lock()) { From c5008ef8f4dc035ce8b0f9470cfffd4ba68e633a Mon Sep 17 00:00:00 2001 From: Alrysc Date: Tue, 29 Jul 2025 01:21:08 -0700 Subject: [PATCH 032/146] Pull 454a8f7, charge does not reset during TFC Pull 454a8f7d5cf596e30205291d394d86ab821e84d4 --- BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp b/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp index bbc82c13d..1a1acc622 100644 --- a/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp +++ b/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp @@ -85,8 +85,6 @@ void TimeFreezeBattleState::ProcessInputs() } } } - - p->GetChargeComponent().SetCharging(false); } } player_idx++; From 0c256a32df5b69e63efa2843c9c31d71b52dfb84 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Tue, 29 Jul 2025 11:37:04 -0700 Subject: [PATCH 033/146] Pull 4c98afd, expose shoulder buttons to Lua Pulled changes from 4c98afd6e014a96ffce7d4081d5b28d096623eca --- BattleNetwork/bnScriptResourceManager.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/BattleNetwork/bnScriptResourceManager.cpp b/BattleNetwork/bnScriptResourceManager.cpp index 62c6cda9f..c30c24191 100644 --- a/BattleNetwork/bnScriptResourceManager.cpp +++ b/BattleNetwork/bnScriptResourceManager.cpp @@ -545,7 +545,9 @@ void ScriptResourceManager::ConfigureEnvironment(ScriptPackage& scriptPackage) { "Down", InputEvents::pressed_move_down, "Use", InputEvents::pressed_use_chip, "Special", InputEvents::pressed_special, - "Shoot", InputEvents::pressed_shoot + "Shoot", InputEvents::pressed_shoot, + "Left_Shoulder", InputEvents::pressed_shoulder_left, + "Right_Shoulder", InputEvents::pressed_shoulder_right ); input_event_record.new_enum("Held", @@ -555,7 +557,9 @@ void ScriptResourceManager::ConfigureEnvironment(ScriptPackage& scriptPackage) { "Down", InputEvents::held_move_down, "Use", InputEvents::held_use_chip, "Special", InputEvents::held_special, - "Shoot", InputEvents::held_shoot + "Shoot", InputEvents::pressed_shoot, + "Left_Shoulder", InputEvents::pressed_shoulder_left, + "Right_Shoulder", InputEvents::pressed_shoulder_right ); input_event_record.new_enum("Released", @@ -565,7 +569,9 @@ void ScriptResourceManager::ConfigureEnvironment(ScriptPackage& scriptPackage) { "Down", InputEvents::released_move_down, "Use", InputEvents::released_use_chip, "Special", InputEvents::released_special, - "Shoot", InputEvents::released_shoot + "Shoot", InputEvents::pressed_shoot, + "Left_Shoulder", InputEvents::pressed_shoulder_left, + "Right_Shoulder", InputEvents::pressed_shoulder_right ); const auto& character_rank_record = state.new_enum("Rank", From d6da20351dcbaddebc768fb8ac0e2adfefed32f3 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Tue, 29 Jul 2025 12:55:35 -0700 Subject: [PATCH 034/146] Process inputs during TF in Freedom battles --- BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp b/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp index d12c0c1f0..434b36580 100644 --- a/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp +++ b/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp @@ -179,10 +179,14 @@ void FreedomMissionMobScene::OnHit(Entity& victim, const Hit::Properties& props) void FreedomMissionMobScene::onUpdate(double elapsed) { - if (GetCurrentState() == combatPtr) { + const BattleSceneState* cur = GetCurrentState(); + + // Must process inputs for combat and subcombat states, + // but only the combat state lets player flip. + if (combatPtr->IsStateCombat(cur)) { ProcessLocalPlayerInputQueue(); - if (playerCanFlip) { + if (cur == combatPtr && playerCanFlip) { std::shared_ptr localPlayer = GetLocalPlayer(); if (localPlayer->IsActionable() && localPlayer->InputState().Has(InputEvents::pressed_option)) { localPlayer->SetFacing(localPlayer->GetFacingAway()); From 8b472ebca854160d564340b959805474d3ff8a57 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Tue, 29 Jul 2025 14:02:26 -0700 Subject: [PATCH 035/146] Fix incorrect TF bar length --- .../battlescene/States/bnTimeFreezeBattleState.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp b/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp index 1a1acc622..2881d6185 100644 --- a/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp +++ b/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp @@ -238,8 +238,12 @@ void TimeFreezeBattleState::onDraw(sf::RenderTexture& surface) double tfcTimerScale = 0; - if (summonTick.asSeconds().value > fadeInOutLength.asSeconds().value) { - tfcTimerScale = swoosh::ease::linear((double)(summonTick - fadeInOutLength).value, (double)summonTextLength.asSeconds().value, 1.0); + double summonTickSeconds = summonTick.asSeconds().value; + double fadeSeconds = fadeInOutLength.asSeconds().value; + + + if (summonTickSeconds > fadeSeconds) { + tfcTimerScale = swoosh::ease::linear((summonTickSeconds - fadeSeconds), (double)(summonTextLength.asSeconds().value - fadeSeconds), 1.0); } double scale = swoosh::ease::linear(summonTick.asSeconds().value, fadeInOutLength.asSeconds().value, 1.0); @@ -249,7 +253,7 @@ void TimeFreezeBattleState::onDraw(sf::RenderTexture& surface) bar.setScale(2.f, 2.f); if (summonTick >= summonTextLength) { - scale = swoosh::ease::linear((summonTextLength - summonTick).asSeconds().value, fadeInOutLength.asSeconds().value, 1.0); + scale = swoosh::ease::linear((summonTextLength - summonTick).asSeconds().value, fadeSeconds, 1.0); scale = std::max(scale, 0.0); } @@ -289,6 +293,7 @@ void TimeFreezeBattleState::onDraw(sf::RenderTexture& surface) bar.setPosition(position + sf::Vector2f(0.f, 12.f)); sf::Uint8 b = (sf::Uint8)swoosh::ease::interpolate((1.0-tfcTimerScale), 0.0, 255.0); + bar.setFillColor(sf::Color(255, 255, b)); scene.DrawWithPerspective(bar, surface); } From 74fe4ab1f68dd25efde5b69c54f1368f829693c1 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Tue, 29 Jul 2025 15:36:53 -0700 Subject: [PATCH 036/146] Pull grass heal from ad7f0a5, exclude other changes --- BattleNetwork/bnEntity.cpp | 16 ++++++++++++++++ BattleNetwork/bnEntity.h | 1 + 2 files changed, 17 insertions(+) diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index 4fcf22402..40095c80e 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -339,6 +339,22 @@ void Entity::Init() { void Entity::Update(double _elapsed) { ResolveFrameBattleDamage(); + // Wood entities heal on grass tiles over a specific time + if (GetElement() == Element::wood && GetTile()->GetState() == TileState::grass) { + grassHealCooldown -= from_seconds(_elapsed); + + // Heal and reset based on current health + if (grassHealCooldown <= frames(0)) { + SetHealth(GetHealth() + 1); + if (GetHealth() >= 9) { + grassHealCooldown = frames(20); + } + else { + grassHealCooldown = frames(180); + } + } + } + if (fieldStart && ((maxHealth > 0 && health <= 0) || IsDeleted())) { // Ensure entity is deleted if health is zero if (manualDelete == false) { diff --git a/BattleNetwork/bnEntity.h b/BattleNetwork/bnEntity.h index 952338171..d48da4ed5 100644 --- a/BattleNetwork/bnEntity.h +++ b/BattleNetwork/bnEntity.h @@ -769,6 +769,7 @@ class Entity : ActionQueue actionQueue; frame_time_t moveStartupDelay{}; std::optional moveEndlagDelay; + frame_time_t grassHealCooldown{ 0 }; /*!< Timer until next healing is allowed */ frame_time_t stunCooldown{ 0 }; /*!< Timer until stun is over */ frame_time_t rootCooldown{ 0 }; /*!< Timer until root is over */ frame_time_t freezeCooldown{ 0 }; /*!< Timer until freeze is over */ From 3daf261b1976a4c00ad401b1c505faea9dc61e9b Mon Sep 17 00:00:00 2001 From: Alrysc Date: Tue, 29 Jul 2025 16:40:34 -0700 Subject: [PATCH 037/146] Partial pull from c1edf79, dying doesn't decross and now clears statuses Pulled some changes from c1edf79faedb0a41facc6529250d1be14ce3673e. Excluded bnFrameRecorder changes, as that file isn't in this history yet. --- .../battlescene/bnFreedomMissionMobScene.cpp | 6 ++---- BattleNetwork/battlescene/bnMobBattleScene.cpp | 6 ++---- BattleNetwork/bnEntity.cpp | 3 +++ .../netplay/battlescene/bnNetworkBattleScene.cpp | 12 ++---------- 4 files changed, 9 insertions(+), 18 deletions(-) diff --git a/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp b/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp index 434b36580..3dbe33f6e 100644 --- a/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp +++ b/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp @@ -283,7 +283,7 @@ std::function FreedomMissionMobScene::HookIntro(MobIntroBattleState& int std::function FreedomMissionMobScene::HookFormChangeEnd(CharacterTransformBattleState& form, CardSelectBattleState& cardSelect) { auto lambda = [&form, &cardSelect, this]() mutable { - bool triggered = form.IsFinished() && (GetLocalPlayer()->GetHealth() == 0 || playerDecross); + bool triggered = form.IsFinished() && playerDecross; if (triggered) { playerDecross = false; // reset our decross flag @@ -306,9 +306,7 @@ std::function FreedomMissionMobScene::HookFormChangeStart(CharacterTrans std::shared_ptr localPlayer = GetLocalPlayer(); TrackedFormData& formData = GetPlayerFormData(localPlayer); - bool changeState = localPlayer->GetHealth() == 0; - changeState = changeState || playerDecross; - changeState = changeState && (formData.selectedForm != -1); + bool changeState = playerDecross && (formData.selectedForm != -1); if (changeState) { formData.selectedForm = -1; diff --git a/BattleNetwork/battlescene/bnMobBattleScene.cpp b/BattleNetwork/battlescene/bnMobBattleScene.cpp index 62451e7ef..13510c623 100644 --- a/BattleNetwork/battlescene/bnMobBattleScene.cpp +++ b/BattleNetwork/battlescene/bnMobBattleScene.cpp @@ -258,7 +258,7 @@ std::function MobBattleScene::HookRetreat(RetreatBattleState& retreat, F std::function MobBattleScene::HookFormChangeEnd(CharacterTransformBattleState& form, CardSelectBattleState& cardSelect) { auto lambda = [&form, &cardSelect, this]() mutable { - bool triggered = form.IsFinished() && (GetLocalPlayer()->GetHealth() == 0 || playerDecross); + bool triggered = form.IsFinished() && playerDecross; if (triggered) { playerDecross = false; // reset our decross flag @@ -281,9 +281,7 @@ std::function MobBattleScene::HookFormChangeStart(CharacterTransformBatt std::shared_ptr localPlayer = GetLocalPlayer(); TrackedFormData& formData = GetPlayerFormData(localPlayer); - bool changeState = localPlayer->GetHealth() == 0; - changeState = changeState || playerDecross; - changeState = changeState && (formData.selectedForm != -1); + bool changeState = playerDecross && (formData.selectedForm != -1); if (changeState) { formData.selectedForm = -1; diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index 40095c80e..3254d4277 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -957,6 +957,9 @@ void Entity::Delete() deleted = true; + // zero all blocking statuses + freezeCooldown = stunCooldown = rootCooldown = blindCooldown = frames(0); + OnDelete(); } diff --git a/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp b/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp index 56273e9b3..72297b329 100644 --- a/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp +++ b/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp @@ -771,14 +771,6 @@ std::function NetworkBattleScene::HookPlayerDecrosses(CharacterTransform for (std::shared_ptr player : GetAllPlayers()) { TrackedFormData& formData = GetPlayerFormData(player); - bool decross = player->GetHealth() == 0 && (formData.selectedForm != -1); - - // ensure we decross if their HP is zero and they have not yet - if (decross) { - formData.selectedForm = -1; - formData.animationComplete = false; - } - // If the anim form data is configured to decross, then we will bool myChangeState = (formData.selectedForm == -1 && formData.animationComplete == false); @@ -810,8 +802,8 @@ std::function NetworkBattleScene::HookOnCardSelectEvent() { std::function NetworkBattleScene::HookFormChangeEnd(CharacterTransformBattleState& form, CardSelectBattleState& cardSelect) { auto lambda = [&form, &cardSelect, this]() mutable { - bool localTriggered = (GetLocalPlayer()->GetHealth() == 0 || localPlayerDecross); - bool remoteTriggered = (remotePlayer->GetHealth() == 0 || remotePlayerDecross); + bool localTriggered = localPlayerDecross; + bool remoteTriggered = remotePlayerDecross; bool triggered = form.IsFinished() && (localTriggered || remoteTriggered); if (triggered) { From cc80415828abd13ac0311cd857d6ca833582ec80 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Tue, 29 Jul 2025 22:24:45 -0700 Subject: [PATCH 038/146] Some more changes from 9fbac73. Entity.set_counter_frame_range, buster flare layer change, and all card codes uppercased from 9fbac73be790d4182095ef44573ff1d0fd007723. --- BattleNetwork/bindings/bnUserTypeEntity.h | 4 ++++ BattleNetwork/bnBusterCardAction.cpp | 2 +- BattleNetwork/bnCardPackageManager.cpp | 4 ++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/BattleNetwork/bindings/bnUserTypeEntity.h b/BattleNetwork/bindings/bnUserTypeEntity.h index d220b52c9..a6452d1ed 100644 --- a/BattleNetwork/bindings/bnUserTypeEntity.h +++ b/BattleNetwork/bindings/bnUserTypeEntity.h @@ -270,6 +270,10 @@ void DefineEntityFunctionsOn(sol::basic_usertype, sol::basic_refe auto& animation = animationComponent->GetAnimationObject(); return AnimationWrapper(entity.GetWeak(), animation); }; + entity_table["set_counter_frame_range"] = [](WeakWrapper& entity, int frameStart, int frameEnd) { + auto animationComponent = entity.Unwrap()->template GetFirstComponent(); + animationComponent->SetCounterFrameRange(frameStart, frameEnd); + }; entity_table["create_node"] = [](WeakWrapper& entity) -> WeakWrapper { auto child = std::make_shared(); entity.Unwrap()->AddNode(child); diff --git a/BattleNetwork/bnBusterCardAction.cpp b/BattleNetwork/bnBusterCardAction.cpp index 204aa7030..16bbef92d 100644 --- a/BattleNetwork/bnBusterCardAction.cpp +++ b/BattleNetwork/bnBusterCardAction.cpp @@ -58,7 +58,7 @@ void BusterCardAction::OnExecute(std::shared_ptr user) { std::shared_ptr flare = attachment.GetSpriteNode(); flare->setTexture(Textures().LoadFromFile(NODE_PATH)); - flare->SetLayer(-1); + flare->SetLayer(-2); Animation& flareAnim = attachment.GetAnimationObject(); flareAnim = Animation(NODE_ANIM); diff --git a/BattleNetwork/bnCardPackageManager.cpp b/BattleNetwork/bnCardPackageManager.cpp index 0a146b5c5..11b950a6b 100644 --- a/BattleNetwork/bnCardPackageManager.cpp +++ b/BattleNetwork/bnCardPackageManager.cpp @@ -29,6 +29,10 @@ CardMeta& CardMeta::SetIconTexture(const std::shared_ptr icon) CardMeta& CardMeta::SetCodes(const std::vector codes) { this->codes = codes; + + // codes can only be upper case + std::transform(this->codes.begin(), this->codes.end(), this->codes.begin(), ::toupper); + return *this; } From 07048c98fed807746d2f748b20d8173a7c02a303 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Tue, 29 Jul 2025 22:55:04 -0700 Subject: [PATCH 039/146] Pull 02b6503, 5e892db Character.can_attack and Player.can_attack for Lua --- BattleNetwork/bindings/bnUserTypeBasicCharacter.cpp | 4 ++++ BattleNetwork/bindings/bnUserTypeBasicPlayer.cpp | 7 ++++--- BattleNetwork/bindings/bnUserTypeScriptedCharacter.cpp | 4 ++++ BattleNetwork/bindings/bnUserTypeScriptedPlayer.cpp | 4 ++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/BattleNetwork/bindings/bnUserTypeBasicCharacter.cpp b/BattleNetwork/bindings/bnUserTypeBasicCharacter.cpp index 753995e3e..49390a104 100644 --- a/BattleNetwork/bindings/bnUserTypeBasicCharacter.cpp +++ b/BattleNetwork/bindings/bnUserTypeBasicCharacter.cpp @@ -16,6 +16,10 @@ void DefineBasicCharacterUserType(sol::table& battle_namespace) { character.Unwrap()->AddAction(CardEvent{ cardAction.UnwrapAndRelease() }, order); } ), + "can_attack", [](WeakWrapper& character) { + auto characterPtr = character.Unwrap(); + return characterPtr->CanAttack(); + }, "get_rank", [](WeakWrapper& character) -> Character::Rank { return character.Unwrap()->GetRank(); } diff --git a/BattleNetwork/bindings/bnUserTypeBasicPlayer.cpp b/BattleNetwork/bindings/bnUserTypeBasicPlayer.cpp index b2af44a81..a99bee0dd 100644 --- a/BattleNetwork/bindings/bnUserTypeBasicPlayer.cpp +++ b/BattleNetwork/bindings/bnUserTypeBasicPlayer.cpp @@ -17,9 +17,10 @@ void DefineBasicPlayerUserType(sol::table& battle_namespace) { player.Unwrap()->AddAction(CardEvent{ cardAction.UnwrapAndRelease() }, order); } ), - "is_actionable", [](WeakWrapper& player) -> bool { - return player.Unwrap()->IsActionable(); - }, + "can_attack", [](WeakWrapper& player) { + auto playerPtr = player.Unwrap(); + return playerPtr->CanAttack(); + }, "get_attack_level", [](WeakWrapper& player) -> unsigned int { return player.Unwrap()->GetAttackLevel(); }, diff --git a/BattleNetwork/bindings/bnUserTypeScriptedCharacter.cpp b/BattleNetwork/bindings/bnUserTypeScriptedCharacter.cpp index 959b64c32..bce950198 100644 --- a/BattleNetwork/bindings/bnUserTypeScriptedCharacter.cpp +++ b/BattleNetwork/bindings/bnUserTypeScriptedCharacter.cpp @@ -84,6 +84,10 @@ void DefineScriptedCharacterUserType(ScriptResourceManager* scriptManager, const character.Unwrap()->AddAction(CardEvent{ cardAction.UnwrapAndRelease() }, order); } ), + "can_attack", [](WeakWrapper& character) { + auto characterPtr = character.Unwrap(); + return characterPtr->CanAttack(); + }, "get_rank", [](WeakWrapper& character) -> Character::Rank { return character.Unwrap()->GetRank(); }, diff --git a/BattleNetwork/bindings/bnUserTypeScriptedPlayer.cpp b/BattleNetwork/bindings/bnUserTypeScriptedPlayer.cpp index f0c7bfdc0..948076628 100644 --- a/BattleNetwork/bindings/bnUserTypeScriptedPlayer.cpp +++ b/BattleNetwork/bindings/bnUserTypeScriptedPlayer.cpp @@ -55,6 +55,10 @@ void DefineScriptedPlayerUserType(sol::state& state, sol::table& battle_namespac player.Unwrap()->AddAction(CardEvent{ cardAction.UnwrapAndRelease() }, order); } ), + "can_attack", [](WeakWrapper& player) { + auto playerPtr = player.Unwrap(); + return playerPtr->CanAttack(); + }, "get_attack_level", [](WeakWrapper& player) -> unsigned int { return player.Unwrap()->GetAttackLevel(); }, From 439da7d4d4dbabd93a9b56f49fa78d19f47fc545 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Tue, 29 Jul 2025 22:58:19 -0700 Subject: [PATCH 040/146] Pull 8b807b7, long card description used by default in battle --- BattleNetwork/bnBattleTextbox.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/BattleNetwork/bnBattleTextbox.cpp b/BattleNetwork/bnBattleTextbox.cpp index 03844dd33..078c2d09b 100644 --- a/BattleNetwork/bnBattleTextbox.cpp +++ b/BattleNetwork/bnBattleTextbox.cpp @@ -16,7 +16,11 @@ void Battle::TextBox::DescribeCard(Battle::Card* card) DequeMessage(); } - EnqueMessage(mug, anim, new Message(card->GetVerboseDescription())); + // use the long description unless it is not provided (empty) otherwise + // use the short card description instead + const std::string& longDescr = card->GetVerboseDescription(); + const std::string& shortDescr = card->GetDescription(); + EnqueMessage(mug, anim, new Message(longDescr.empty() ? shortDescr : longDescr)); Open(); } From 53a36b50201cddff3c74b42bfe30a935ab9ac6da Mon Sep 17 00:00:00 2001 From: Alrysc Date: Tue, 29 Jul 2025 23:02:52 -0700 Subject: [PATCH 041/146] Pull 641e48e, allow Flinch + Stun --- BattleNetwork/bnEntity.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index 3254d4277..2aa566003 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -1416,8 +1416,6 @@ void Entity::ResolveFrameBattleDamage() append.push({ props.hitbox, { 0, props.filtered.flags } }); } else { - // assume some defense rule strips out flinch, prevent abuse of stun - frameStunCancel = frameStunCancel ||(props.filtered.flags & Hit::flinch) == 0 && (props.hitbox.flags & Hit::flinch) == Hit::flinch; if ((props.filtered.flags & Hit::flash) == Hit::flash && frameStunCancel) { // cancel stun From bb1d0b15de1204bbede5bba98f698bde65d135e3 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Tue, 29 Jul 2025 23:07:35 -0700 Subject: [PATCH 042/146] Pull 2e7e5e4, HasAnimation uppercases. Applied same fix to HasPoint. --- BattleNetwork/bnAnimation.cpp | 8 ++++++-- BattleNetwork/bnAnimation.h | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/BattleNetwork/bnAnimation.cpp b/BattleNetwork/bnAnimation.cpp index d8a268072..ccb6e67cd 100644 --- a/BattleNetwork/bnAnimation.cpp +++ b/BattleNetwork/bnAnimation.cpp @@ -464,8 +464,10 @@ sf::Vector2f Animation::GetPoint(const std::string & pointName) return res; } -const bool Animation::HasPoint(const std::string& pointName) +const bool Animation::HasPoint(std::string pointName) { + std::transform(pointName.begin(), pointName.end(), pointName.begin(), ::toupper); + return animator.HasPoint(pointName); } @@ -510,8 +512,10 @@ void Animation::SetInterruptCallback(const std::function onInterrupt) interruptCallback = onInterrupt; } -const bool Animation::HasAnimation(const std::string& state) const +const bool Animation::HasAnimation(std::string state) const { + std::transform(state.begin(), state.end(), state.begin(), ::toupper); + return animations.find(state) != animations.end(); } diff --git a/BattleNetwork/bnAnimation.h b/BattleNetwork/bnAnimation.h index 38cf2b1fb..2682d4f92 100644 --- a/BattleNetwork/bnAnimation.h +++ b/BattleNetwork/bnAnimation.h @@ -172,7 +172,7 @@ class Animation { void operator<<(const std::function& onFinish); sf::Vector2f GetPoint(const std::string& pointName); - const bool HasPoint(const std::string& pointName); + const bool HasPoint(std::string pointName); char GetMode(); @@ -184,7 +184,7 @@ class Animation { void SetInterruptCallback(const std::function onInterrupt); - const bool HasAnimation(const std::string& state) const; + const bool HasAnimation(std::string state) const; const double GetPlaybackSpeed() const; void SetPlaybackSpeed(double factor); From eec9e1d7029b7e42c369dea279addcbbb9cd0a62 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Tue, 29 Jul 2025 23:53:00 -0700 Subject: [PATCH 043/146] Pull 8285a01, anger and counter not consumed unless card is boostable --- BattleNetwork/battlescene/bnBattleSceneBase.cpp | 4 +++- BattleNetwork/bnCounterCombatRule.cpp | 2 +- BattleNetwork/bnPlayerSelectedCardsUI.cpp | 3 ++- BattleNetwork/bnSelectedCardsUI.cpp | 3 +-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/BattleNetwork/battlescene/bnBattleSceneBase.cpp b/BattleNetwork/battlescene/bnBattleSceneBase.cpp index 50078ade0..11ffbb547 100644 --- a/BattleNetwork/battlescene/bnBattleSceneBase.cpp +++ b/BattleNetwork/battlescene/bnBattleSceneBase.cpp @@ -390,7 +390,9 @@ std::shared_ptr BattleSceneBase::GetPlayerFromEntityID(Entity::ID_t ID) void BattleSceneBase::OnCardActionUsed(std::shared_ptr action, uint64_t timestamp) { - HandleCounterLoss(*action->GetActor(), true); + if (action->GetMetaData().canBoost) { + HandleCounterLoss(*action->GetActor(), true); + } } sf::Vector2f BattleSceneBase::PerspectiveOffset(const sf::Vector2f& pos) diff --git a/BattleNetwork/bnCounterCombatRule.cpp b/BattleNetwork/bnCounterCombatRule.cpp index 860ac633a..65e884d4d 100644 --- a/BattleNetwork/bnCounterCombatRule.cpp +++ b/BattleNetwork/bnCounterCombatRule.cpp @@ -11,6 +11,6 @@ CounterCombatRule::~CounterCombatRule() { } void CounterCombatRule::CanBlock(DefenseFrameStateJudge& judge, std::shared_ptr attacker, std::shared_ptr owner) { // we lose counter ability if hit by an impact attack if ((attacker->GetHitboxProperties().flags & Hit::impact) == Hit::impact) { - battleScene->HandleCounterLoss(*owner, false); // see if battle scene has blessed this character with an ability + battleScene->HandleCounterLoss(*owner, false); // see if battle scene had blessed this character with an ability } } diff --git a/BattleNetwork/bnPlayerSelectedCardsUI.cpp b/BattleNetwork/bnPlayerSelectedCardsUI.cpp index 7c4d4f152..8ff1b78f9 100644 --- a/BattleNetwork/bnPlayerSelectedCardsUI.cpp +++ b/BattleNetwork/bnPlayerSelectedCardsUI.cpp @@ -216,7 +216,8 @@ void PlayerSelectedCardsUI::Broadcast(std::shared_ptr action) { std::shared_ptr player = GetOwnerAs(); - if (player && player->GetEmotion() == Emotion::angry) { + bool angry = player && player->GetEmotion() == Emotion::angry; + if (angry && action->GetMetaData().canBoost) { player->SetEmotion(Emotion::normal); } diff --git a/BattleNetwork/bnSelectedCardsUI.cpp b/BattleNetwork/bnSelectedCardsUI.cpp index 57ea27743..12f8b5419 100644 --- a/BattleNetwork/bnSelectedCardsUI.cpp +++ b/BattleNetwork/bnSelectedCardsUI.cpp @@ -142,10 +142,9 @@ bool SelectedCardsUI::UseNextCard() { if (card.CanBoost()) { card.MultiplyDamage(multiplierValue); + multiplierValue = 1; // multiplier is reset because it has been consumed } - multiplierValue = 1; // reset - // add a peek event to the action queue owner->AddAction(PeekCardEvent{ this }, ActionOrder::voluntary); return true; From 0fb7a272b39363b23ff44001e0165b500b9527cb Mon Sep 17 00:00:00 2001 From: Alrysc Date: Wed, 30 Jul 2025 00:21:09 -0700 Subject: [PATCH 044/146] Fix add_tags and remove_tags --- BattleNetwork/bindings/bnUserTypeSpriteNode.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BattleNetwork/bindings/bnUserTypeSpriteNode.cpp b/BattleNetwork/bindings/bnUserTypeSpriteNode.cpp index 51380cb97..395fe5497 100644 --- a/BattleNetwork/bindings/bnUserTypeSpriteNode.cpp +++ b/BattleNetwork/bindings/bnUserTypeSpriteNode.cpp @@ -40,10 +40,10 @@ void DefineSpriteNodeUserType(sol::state& state, sol::table& engine_namespace) { "remove_node", [](WeakWrapper& node, WeakWrapper& child) { node.Unwrap()->RemoveNode(child.Unwrap().get()); }, - "add_tags", [](WeakWrapper& node, std::initializer_list tags) { + "add_tags", [](WeakWrapper& node, std::vector tags) { node.Unwrap()->AddTags(tags); }, - "remove_tags", [](WeakWrapper& node, std::initializer_list tags) { + "remove_tags", [](WeakWrapper& node, std::vector tags) { node.Unwrap()->RemoveTags(tags); }, "has_tag", [](WeakWrapper& node, const std::string& tag) -> bool{ From ee9c976bed5c932cb3da391d1a846539808dc674 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Wed, 30 Jul 2025 10:28:53 -0700 Subject: [PATCH 045/146] Simplify integer frame read --- BattleNetwork/bnAnimation.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BattleNetwork/bnAnimation.cpp b/BattleNetwork/bnAnimation.cpp index ccb6e67cd..5ca808a38 100644 --- a/BattleNetwork/bnAnimation.cpp +++ b/BattleNetwork/bnAnimation.cpp @@ -151,9 +151,9 @@ static frame_time_t GetFrameValue(std::string_view line, std::string_view key) { if (valueView.empty()) return frames(0); // frame value - if (valueView.at(valueView.size() - 1) == 'f') { + if (valueView.at(valueView.size() - 1) == 'f') { valueView = valueView.substr(0, valueView.size() - 1); - return frames(static_cast(std::strtof(valueView.data(), nullptr))); + return frames(std::atoi(valueView.data())); } return from_seconds(std::fabs(std::strtof(valueView.data(), nullptr))); From 2a3f6cdbaf86ceb7fc6dd86829c7f4f9b6c39b27 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Wed, 30 Jul 2025 13:29:17 -0700 Subject: [PATCH 046/146] Convert GetRed and BlueMobTeam return to references --- BattleNetwork/battlescene/bnBattleSceneBase.cpp | 12 ++++++++---- BattleNetwork/battlescene/bnBattleSceneBase.h | 4 ++-- .../battlescene/bnFreedomMissionMobScene.cpp | 16 ++++++++-------- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/BattleNetwork/battlescene/bnBattleSceneBase.cpp b/BattleNetwork/battlescene/bnBattleSceneBase.cpp index 11ffbb547..e61e520e2 100644 --- a/BattleNetwork/battlescene/bnBattleSceneBase.cpp +++ b/BattleNetwork/battlescene/bnBattleSceneBase.cpp @@ -1132,12 +1132,16 @@ std::vector> BattleSceneBase::GetAllPlayers() return result; } -Mob* BattleSceneBase::GetRedTeamMob() { - return redTeamMob; +Mob& BattleSceneBase::GetRedTeamMob() { + assert(redTeamMob != nullptr && "redTeamMob was nullptr!"); + + return *redTeamMob; } -Mob* BattleSceneBase::GetBlueTeamMob() { - return blueTeamMob; +Mob& BattleSceneBase::GetBlueTeamMob() { + assert(blueTeamMob != nullptr && "blueTeamMob was nullptr!"); + + return *blueTeamMob; } diff --git a/BattleNetwork/battlescene/bnBattleSceneBase.h b/BattleNetwork/battlescene/bnBattleSceneBase.h index be7be8635..d56622d3d 100644 --- a/BattleNetwork/battlescene/bnBattleSceneBase.h +++ b/BattleNetwork/battlescene/bnBattleSceneBase.h @@ -390,8 +390,8 @@ class BattleSceneBase : const std::shared_ptr GetLocalPlayer() const; std::vector> GetOtherPlayers(); std::vector> GetAllPlayers(); - Mob* GetRedTeamMob(); - Mob* GetBlueTeamMob(); + Mob& GetRedTeamMob(); + Mob& GetBlueTeamMob(); std::shared_ptr GetField(); const std::shared_ptr GetField() const; CardSelectionCust& GetCardSelectWidget(); diff --git a/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp b/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp index 3dbe33f6e..6365b922d 100644 --- a/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp +++ b/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp @@ -323,8 +323,8 @@ std::function FreedomMissionMobScene::HookFormChangeStart(CharacterTrans std::function FreedomMissionMobScene::HookTurnLimitReached() { auto outOfTurns = [this]() mutable { - Mob* redTeam = GetRedTeamMob(); - Mob* blueTeam = GetBlueTeamMob(); + Mob redTeam = GetRedTeamMob(); + Mob blueTeam = GetBlueTeamMob(); /* Explicitly check Mob::IsCleared instead of BattleSceneBase's Cleared functions. @@ -334,8 +334,8 @@ std::function FreedomMissionMobScene::HookTurnLimitReached() By using this, the battle will not end while all enemies are untracked but still deleting. */ - bool redTeamCleared = redTeam && redTeam->IsCleared(); - bool blueTeamCleared = blueTeam && blueTeam->IsCleared(); + bool redTeamCleared = redTeam.IsCleared(); + bool blueTeamCleared = blueTeam.IsCleared(); if (redTeamCleared || blueTeamCleared) { return false; @@ -355,8 +355,8 @@ std::function FreedomMissionMobScene::HookTurnLimitReached() std::function FreedomMissionMobScene::HookTurnTimeout() { auto cardGaugeIsFull = [this]() mutable { - Mob* redTeam = GetRedTeamMob(); - Mob* blueTeam = GetBlueTeamMob(); + Mob redTeam = GetRedTeamMob(); + Mob blueTeam = GetBlueTeamMob(); /* Explicitly check Mob::IsCleared instead of BattleSceneBase's Cleared functions. @@ -366,8 +366,8 @@ std::function FreedomMissionMobScene::HookTurnTimeout() By using this, the turn will not time out while all enemies are untracked but still deleting. */ - bool redTeamCleared = redTeam && redTeam->IsCleared(); - bool blueTeamCleared = blueTeam && blueTeam->IsCleared(); + bool redTeamCleared = redTeam.IsCleared(); + bool blueTeamCleared = blueTeam.IsCleared(); if (redTeamCleared || blueTeamCleared) { return false; From 1d64eccf41866516bb9a93a19cbb43aecdc2f6cd Mon Sep 17 00:00:00 2001 From: Alrysc Date: Wed, 30 Jul 2025 14:06:51 -0700 Subject: [PATCH 047/146] Move Freedom mission result handling to FreedomMissionOverState --- .../States/bnFreedomMissionOverState.cpp | 31 +++++++++++++++-- .../battlescene/bnBattleSceneBase.cpp | 3 +- .../battlescene/bnFreedomMissionMobScene.cpp | 33 ++++--------------- .../battlescene/bnFreedomMissionMobScene.h | 4 +-- 4 files changed, 39 insertions(+), 32 deletions(-) diff --git a/BattleNetwork/battlescene/States/bnFreedomMissionOverState.cpp b/BattleNetwork/battlescene/States/bnFreedomMissionOverState.cpp index 74cbac9cd..bb4472a94 100644 --- a/BattleNetwork/battlescene/States/bnFreedomMissionOverState.cpp +++ b/BattleNetwork/battlescene/States/bnFreedomMissionOverState.cpp @@ -8,6 +8,7 @@ #include "../../bnTextureResourceManager.h" #include +#include "../bnFreedomMissionMobScene.h" FreedomMissionOverState::FreedomMissionOverState() : BattleTextIntroState() @@ -17,15 +18,41 @@ void FreedomMissionOverState::onStart(const BattleSceneState* _) { BattleTextIntroState::onStart(_); - auto& results = GetScene().BattleResultsObj(); + FreedomMissionMobScene& scene = static_cast(GetScene()); + BattleResults& results = scene.BattleResultsObj(); results.runaway = false; - if (GetScene().IsPlayerDeleted()) { + if (scene.IsPlayerDeleted()) { context = Conditions::player_deleted; } + else { + /* + Handle results here, because there is no rewards state. + If player was deleted, no other results should matter. + + This will still set results if the player failed, which may + be useful to a server so that emotion can be kept and so on. + The rest of the data is unlikely to be useful in a failure, + but is added anyway. + */ + std::shared_ptr player = scene.GetLocalPlayer(); + results.battleLength = sf::seconds(scene.GetElapsedBattleFrames().count() / 60.f); + results.moveCount = player->GetMoveCount(); + results.hitCount = scene.GetPlayerHitCount(); + results.turns = scene.GetTurnCount(); + results.counterCount = scene.GetCounterCount(); + results.doubleDelete = scene.DoubleDelete(); + results.tripleDelete = scene.TripleDelete(); + results.finalEmotion = player->GetEmotion(); + } Audio().StopStream(); + // Only calculate score on successful mission + if (!(context == Conditions::player_deleted || context == Conditions::player_failed)) { + results.CalculateScore(results, scene.GetProps().mobs.at(0)); + } + switch (context) { case Conditions::player_deleted: SetIntroText(GetScene().GetLocalPlayer()->GetName() + " deleted!"); diff --git a/BattleNetwork/battlescene/bnBattleSceneBase.cpp b/BattleNetwork/battlescene/bnBattleSceneBase.cpp index e61e520e2..112e09a68 100644 --- a/BattleNetwork/battlescene/bnBattleSceneBase.cpp +++ b/BattleNetwork/battlescene/bnBattleSceneBase.cpp @@ -1289,7 +1289,6 @@ std::vector> BattleSceneBase::BlueTeamMo void BattleSceneBase::Quit(const FadeOut& mode) { if(quitting) return; - quitting = true; // end the current state if(current) { @@ -1297,6 +1296,8 @@ void BattleSceneBase::Quit(const FadeOut& mode) { current = nullptr; } + quitting = true; + // NOTE: swoosh quirk if (getController().getStackSize() == 1) { getController().pop(); diff --git a/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp b/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp index 6365b922d..dca9424cd 100644 --- a/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp +++ b/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp @@ -219,33 +219,12 @@ void FreedomMissionMobScene::onLeave() BattleSceneBase::onLeave(); } -void FreedomMissionMobScene::onEnd() { - /* - Handle results here, because there is no rewards state. - Do not do anything if the Player was defeated or the - scene is ending by the player quitting. This avoids - sending a score to a server when one isn't appropriate. - - Currently, this results in slightly different behavior - compared to the MobBattleScene. In a MobBattle, quitting - while the last enemies are deleting counts as a victory. - Here, it counts as running away, because IsQuitting is true. - */ - if (!IsQuitting() && !IsPlayerDeleted()) { - BattleResults& results = BattleResultsObj(); - std::shared_ptr player = GetLocalPlayer(); - results.battleLength = sf::seconds(GetElapsedBattleFrames().count() / 60.f); - results.moveCount = player->GetMoveCount(); - results.hitCount = playerHitCount; - results.turns = GetTurnCount(); - results.counterCount = GetCounterCount(); - results.doubleDelete = DoubleDelete(); - results.tripleDelete = TripleDelete(); - results.finalEmotion = player->GetEmotion(); - - results.CalculateScore(results, props.mobs.at(0)); - } - BattleSceneBase::onEnd(); +int FreedomMissionMobScene::GetPlayerHitCount() { + return playerHitCount; +} + +FreedomMissionProps& FreedomMissionMobScene::GetProps() { + return props; } void FreedomMissionMobScene::IncrementTurnCount() diff --git a/BattleNetwork/battlescene/bnFreedomMissionMobScene.h b/BattleNetwork/battlescene/bnFreedomMissionMobScene.h index c004ceaf2..918e8bc07 100644 --- a/BattleNetwork/battlescene/bnFreedomMissionMobScene.h +++ b/BattleNetwork/battlescene/bnFreedomMissionMobScene.h @@ -55,8 +55,8 @@ class FreedomMissionMobScene final : public BattleSceneBase { void onEnter() override; void onResume() override; void onLeave() override; - - void onEnd() override; + FreedomMissionProps& GetProps(); + int GetPlayerHitCount(); // // override From c7267a765ad433c8c51ceb515c53d63b84883054 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Wed, 30 Jul 2025 21:17:45 -0700 Subject: [PATCH 048/146] Use references forMobs returned by GetRed/BlueTeamMob --- BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp b/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp index dca9424cd..aa039490b 100644 --- a/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp +++ b/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp @@ -302,8 +302,8 @@ std::function FreedomMissionMobScene::HookFormChangeStart(CharacterTrans std::function FreedomMissionMobScene::HookTurnLimitReached() { auto outOfTurns = [this]() mutable { - Mob redTeam = GetRedTeamMob(); - Mob blueTeam = GetBlueTeamMob(); + Mob& redTeam = GetRedTeamMob(); + Mob& blueTeam = GetBlueTeamMob(); /* Explicitly check Mob::IsCleared instead of BattleSceneBase's Cleared functions. @@ -334,8 +334,8 @@ std::function FreedomMissionMobScene::HookTurnLimitReached() std::function FreedomMissionMobScene::HookTurnTimeout() { auto cardGaugeIsFull = [this]() mutable { - Mob redTeam = GetRedTeamMob(); - Mob blueTeam = GetBlueTeamMob(); + Mob& redTeam = GetRedTeamMob(); + Mob& blueTeam = GetBlueTeamMob(); /* Explicitly check Mob::IsCleared instead of BattleSceneBase's Cleared functions. From 3b28c5f0bd15f2f8843a1ee326ef88c5dcf49bf9 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Wed, 30 Jul 2025 23:50:47 -0700 Subject: [PATCH 049/146] Moved grass healing to bnTile, adjusted heal timing --- BattleNetwork/bnEntity.cpp | 16 ---------------- BattleNetwork/bnTile.cpp | 25 +++++++++++++++++++++++++ BattleNetwork/bnTile.h | 2 ++ 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index 2aa566003..0c8268956 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -339,22 +339,6 @@ void Entity::Init() { void Entity::Update(double _elapsed) { ResolveFrameBattleDamage(); - // Wood entities heal on grass tiles over a specific time - if (GetElement() == Element::wood && GetTile()->GetState() == TileState::grass) { - grassHealCooldown -= from_seconds(_elapsed); - - // Heal and reset based on current health - if (grassHealCooldown <= frames(0)) { - SetHealth(GetHealth() + 1); - if (GetHealth() >= 9) { - grassHealCooldown = frames(20); - } - else { - grassHealCooldown = frames(180); - } - } - } - if (fieldStart && ((maxHealth > 0 && health <= 0) || IsDeleted())) { // Ensure entity is deleted if health is zero if (manualDelete == false) { diff --git a/BattleNetwork/bnTile.cpp b/BattleNetwork/bnTile.cpp index 2705f08dc..c7c284424 100644 --- a/BattleNetwork/bnTile.cpp +++ b/BattleNetwork/bnTile.cpp @@ -504,9 +504,14 @@ namespace Battle { void Tile::Update(Field& field, double _elapsed) { willHighlight = false; + totalElapsed += from_seconds(_elapsed); if (!isTimeFrozen && isBattleStarted) { + // Grass Tiles + grassHealCooldown1 -= from_seconds(_elapsed); + grassHealCooldown2 -= from_seconds(_elapsed); + // LAVA TILES elapsedBurnTime -= from_seconds(_elapsed); @@ -578,6 +583,9 @@ namespace Battle { HandleTileBehaviors(field, *character); } } + + if (grassHealCooldown1 <= frames(0)) grassHealCooldown1 = frames(20); + if (grassHealCooldown2 <= frames(0)) grassHealCooldown1 = frames(180); } void Tile::ToggleTimeFreeze(bool state) @@ -725,6 +733,23 @@ namespace Battle { } } } + const int health = character.GetHealth(); + const Element charElement = character.GetElement(); + + const bool doGrassCheck = + charElement == Element::wood + && state == TileState::grass; + + const bool heal = doGrassCheck && + ( + (grassHealCooldown1 == frames(0) && health <= 9) + || + (grassHealCooldown2 == frames(0) && health > 9) + ); + + if (heal) { + character.SetHealth(health + 1); + } } std::vector> Tile::FindEntities(std::function& e)> query) diff --git a/BattleNetwork/bnTile.h b/BattleNetwork/bnTile.h index d74c62009..0d67aaf65 100644 --- a/BattleNetwork/bnTile.h +++ b/BattleNetwork/bnTile.h @@ -347,6 +347,8 @@ namespace Battle { frame_time_t totalElapsed{}; frame_time_t elapsedBurnTime{}; frame_time_t burncycle{}; + frame_time_t grassHealCooldown1{ 180 }; /**< Heal cooldown with <= 9 HP*/ + frame_time_t grassHealCooldown2{ 20 }; /**< Heal cooldown with > 9 HP*/ std::weak_ptr fieldWeak; std::shared_ptr red_team_atlas, red_team_perm; std::shared_ptr blue_team_atlas, blue_team_perm; From 0daeba59344791248c6358b5a641076bf41e58a2 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Thu, 31 Jul 2025 12:34:32 -0700 Subject: [PATCH 050/146] Recalculate charge times after Player init and when charge level changes. Cancel charging when form changing. --- BattleNetwork/bnPlayer.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/BattleNetwork/bnPlayer.cpp b/BattleNetwork/bnPlayer.cpp index 71cf429da..765ff4ad4 100644 --- a/BattleNetwork/bnPlayer.cpp +++ b/BattleNetwork/bnPlayer.cpp @@ -76,6 +76,7 @@ void Player::Init() { animationComponent->SetPath(RESOURCE_PATH); animationComponent->Reload(); + Charge(false); FinishConstructor(); } @@ -221,6 +222,7 @@ int Player::GetMoveCount() const return Entity::GetMoveCount(); } + void Player::Charge(bool state) { frame_time_t maxCharge = CalculateChargeTime(GetChargeLevel()); @@ -245,6 +247,13 @@ const unsigned Player::GetAttackLevel() void Player::SetChargeLevel(unsigned lvl) { stats.charge = std::min(PlayerStats::MAX_CHARGE_LEVEL, lvl); + + frame_time_t maxCharge = CalculateChargeTime(stats.charge); + if (activeForm) { + maxCharge = activeForm->CalculateChargeTime(GetChargeLevel()); + } + + chargeEffect->SetMaxChargeTime(maxCharge); } const unsigned Player::GetChargeLevel() @@ -382,6 +391,10 @@ void Player::ActivateFormAt(int index) } } + // Cancel charging. This will also refresh charge times + // for the new form. + Charge(false); + // Find nodes that do not have tags, those are newly added for (std::shared_ptr& node : GetChildNodes()) { // if untagged and not the charge effect... From d954de233d10d84b6a7db9fd92a1092850ca88ce Mon Sep 17 00:00:00 2001 From: Alrysc Date: Thu, 31 Jul 2025 12:49:35 -0700 Subject: [PATCH 051/146] Drag has endlag --- BattleNetwork/bnEntity.cpp | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index 0c8268956..e5a21bada 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -1550,20 +1550,28 @@ void Entity::ResolveFrameBattleDamage() slideFromDrag = true; Battle::Tile* dest = GetTile() + postDragEffect.dir; - if (CanMoveTo(dest)) { - // Enqueue a move action at the top of our priorities - actionQueue.Add(MoveEvent{ frames(4), frames(0), frames(0), 0, dest, {}, true }, ActionOrder::immediate, ActionDiscardOp::until_resolve); - - std::queue oldQueue = statusQueue; - statusQueue = {}; - // Re-queue the drag status to be re-considered FIRST in our next combat checks - statusQueue.push({ {}, { 0, Hit::drag, Element::none, 0, postDragEffect } }); - - // append the old queue items after - while (!oldQueue.empty()) { - statusQueue.push(oldQueue.front()); - oldQueue.pop(); - } + if (!CanMoveTo(dest)) { + dest = GetTile(); + postDragEffect.count = 0; + } + + // The final drag event applies endlag + // 22 frames matches the amount of fixed frames applied to player recoil + const frame_time_t endlag = + postDragEffect.count == 0 ? frames(22) : frames(0); + + // Enqueue a move action at the top of our priorities + actionQueue.Add(MoveEvent{ frames(4), frames(0), endlag, 0, dest, {}, true }, ActionOrder::immediate, ActionDiscardOp::until_resolve); + + std::queue oldQueue = statusQueue; + statusQueue = {}; + // Re-queue the drag status to be re-considered FIRST in our next combat checks + statusQueue.push({ {}, { 0, Hit::drag, Element::none, 0, postDragEffect } }); + + // append the old queue items after + while (!oldQueue.empty()) { + statusQueue.push(oldQueue.front()); + oldQueue.pop(); } } } From 09544fe91fd115d29b689e14252aea2996c26e8f Mon Sep 17 00:00:00 2001 From: Alrysc Date: Thu, 31 Jul 2025 12:50:26 -0700 Subject: [PATCH 052/146] Lava has Impact flag --- BattleNetwork/bnTile.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BattleNetwork/bnTile.cpp b/BattleNetwork/bnTile.cpp index c7c284424..77320d926 100644 --- a/BattleNetwork/bnTile.cpp +++ b/BattleNetwork/bnTile.cpp @@ -694,7 +694,7 @@ namespace Battle { } if (GetState() == TileState::lava && character.GetElement() != Element::fire) { - Hit::Properties props = { 50, Hit::flash | Hit::flinch, Element::none, 0, Direction::none }; + Hit::Properties props = { 50, Hit::flash | Hit::flinch | Hit::impact, Element::none, 0, Direction::none }; if (character.HasCollision(props)) { character.Hit(props); field.AddEntity(std::make_shared(), GetX(), GetY()); From 182e7be266298391c93a4768ec18132e76c7fe8c Mon Sep 17 00:00:00 2001 From: Alrysc Date: Thu, 31 Jul 2025 13:36:57 -0700 Subject: [PATCH 053/146] Lua Entity is_stunned, is_rooted, is_frozen, is_blind --- BattleNetwork/bindings/bnUserTypeEntity.h | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/BattleNetwork/bindings/bnUserTypeEntity.h b/BattleNetwork/bindings/bnUserTypeEntity.h index a6452d1ed..7f9bc5dd7 100644 --- a/BattleNetwork/bindings/bnUserTypeEntity.h +++ b/BattleNetwork/bindings/bnUserTypeEntity.h @@ -360,6 +360,18 @@ void DefineEntityFunctionsOn(sol::basic_usertype, sol::basic_refe entity_table["shake_camera"] = [](WeakWrapper& entity, double power, float duration) { entity.Unwrap()->EventChannel().Emit(&Camera::ShakeCamera, power, sf::seconds(duration)); }; + entity_table["is_stunned"] = [](WeakWrapper& entity) -> bool { + return entity.Unwrap()->IsStunned(); + }; + entity_table["is_rooted"] = [](WeakWrapper& entity) -> bool { + return entity.Unwrap()->IsRooted(); + }; + entity_table["is_frozen"] = [](WeakWrapper& entity) -> bool { + return entity.Unwrap()->IsIceFrozen(); + }; + entity_table["is_blind"] = [](WeakWrapper& entity) -> bool { + return entity.Unwrap()->IsBlind(); + }; } #endif From 4ff06b5566737932d4d91f1de9903a9f10a09a6f Mon Sep 17 00:00:00 2001 From: Alrysc Date: Thu, 31 Jul 2025 15:57:25 -0700 Subject: [PATCH 054/146] Partial pull from 7bc1fd5, adjust TFC timing and animation Excluded changes related to backgrounds, which are not in this branch yet. Also excluded the change to flip the order of tfcEvents. --- .../States/bnTimeFreezeBattleState.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp b/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp index 2881d6185..d04b68b32 100644 --- a/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp +++ b/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp @@ -172,6 +172,12 @@ void TimeFreezeBattleState::onUpdate(double elapsed) for (TimeFreezeBattleState::EventData& e : tfEvents) { if (e.animateCounter) { e.alertFrameCount += frames(1); + //Set animation to false if we're done animating. + if (e.alertFrameCount.value > alertAnimFrames.value) { + e.animateCounter = false; + } + //Delay while animating. We can't counter right now. + summonTick = frames(0); } } } @@ -284,8 +290,8 @@ void TimeFreezeBattleState::onDraw(sf::RenderTexture& surface) summonsLabel.setPosition(position); scene.DrawWithPerspective(summonsLabel, surface); - if (currState == state::display_name && first->action->GetMetaData().counterable) { - // draw TF bar underneath + if (currState == state::display_name && first->action->GetMetaData().counterable && summonTick > tfcStartFrame) { + // draw TF bar underneath if conditions are met bar.setPosition(position + sf::Vector2f(0.f + 2.f, 12.f + 2.f)); bar.setFillColor(sf::Color::Black); scene.DrawWithPerspective(bar, surface); @@ -437,8 +443,14 @@ const bool TimeFreezeBattleState::CanCounter(std::shared_ptr user) // bool addEvent = true; if (!tfEvents.empty()) { + // Don't counter during alert symbol. BN6 accurate. See notes from Alrysc. std::shared_ptr action = tfEvents.begin()->action; + for (TimeFreezeBattleState::EventData& e : tfEvents) { + if (e.animateCounter) { + return false; + } + } // some actions cannot be countered if (!action->GetMetaData().counterable) return false; From dc1ad6340b571feadcbc70eee9fca4fc08ddbbce Mon Sep 17 00:00:00 2001 From: Alrysc Date: Thu, 31 Jul 2025 17:59:07 -0700 Subject: [PATCH 055/146] Pulled some changes from e53f2c6, d0bb8f7, and 22cafc5 to fix folder sort crash Excluded changes unrelated to the folder, and excluded changes related to text boxes and asking to equip. --- BattleNetwork/bnFolderEditScene.cpp | 151 ++++++++++++++------- BattleNetwork/bnFolderEditScene.h | 82 ++++++----- BattleNetwork/resources/ui/folder_sort.png | Bin 717 -> 733 bytes 3 files changed, 150 insertions(+), 83 deletions(-) diff --git a/BattleNetwork/bnFolderEditScene.cpp b/BattleNetwork/bnFolderEditScene.cpp index 15b30ad73..2e904a3f7 100644 --- a/BattleNetwork/bnFolderEditScene.cpp +++ b/BattleNetwork/bnFolderEditScene.cpp @@ -178,7 +178,7 @@ void FolderEditScene::onUpdate(double elapsed) { setView(camera.GetView()); // update the folder sort cursor - sf::Vector2f sortCursorOffset = sf::Vector2f(0, 2.0 * (14.0 + (cursorSortIndex * 16.0))); + sf::Vector2f sortCursorOffset = sf::Vector2f(-10.f, 2.0 * (14.0 + (cursorSortIndex * 16.0))); sortCursor.setPosition(folderSort.getPosition() + sortCursorOffset); // Scene keyboard controls @@ -225,12 +225,9 @@ void FolderEditScene::onUpdate(double elapsed) { return; } - CardView* view = nullptr; + CardView* view = &folderView; - if (currViewMode == ViewMode::folder) { - view = &folderView; - } - else if (currViewMode == ViewMode::pool) { + if (currViewMode == ViewMode::pool) { view = &packView; } @@ -626,23 +623,7 @@ void FolderEditScene::onUpdate(double elapsed) { view->currCardIndex = std::max(0, view->currCardIndex); view->currCardIndex = std::min(view->numOfCards - 1, view->currCardIndex); - switch (currViewMode) { - case ViewMode::folder: - { - using SlotType = decltype(folderCardSlots)::value_type; - RefreshCurrentCardDock(*view, folderCardSlots); - } - break; - case ViewMode::pool: - { - using SlotType = decltype(poolCardBuckets)::value_type; - RefreshCurrentCardDock(*view, poolCardBuckets); - } - break; - default: - Logger::Logf(LogLevel::critical, "No applicable view mode for folder edit scene: %i", static_cast(currViewMode)); - break; - } + RefreshCurrentCardSelection(); view->prevIndex = view->currCardIndex; @@ -718,7 +699,7 @@ void FolderEditScene::onExit() { void FolderEditScene::onEnter() { folderView.currCardIndex = 0; - RefreshCurrentCardDock(folderView, folderCardSlots); + RefreshCardDock(folderView, folderCardSlots); } void FolderEditScene::onResume() { @@ -1084,72 +1065,118 @@ void FolderEditScene::DrawPool(sf::RenderTarget& surface) { void FolderEditScene::ComposeSortOptions() { auto sortByID = [](const ICardView& first, const ICardView& second) -> bool { - return first.ViewCard().GetUUID() < second.ViewCard().GetUUID(); + const Battle::Card& a = first.ViewCard(); + const Battle::Card& b = second.ViewCard(); + return std::tie(a.GetUUID(), a.GetShortName()) < std::tie(b.GetUUID(), b.GetShortName()); }; auto sortByAlpha = [](const ICardView& first, const ICardView& second) -> bool { - return first.ViewCard().GetShortName() < second.ViewCard().GetShortName(); + const Battle::Card& a = first.ViewCard(); + const Battle::Card& b = second.ViewCard(); + char codeA = a.GetCode(); + char codeB = b.GetCode(); + return std::tie(a.GetShortName(), codeA) < std::tie(b.GetShortName(), codeB); }; auto sortByCode = [](const ICardView& first, const ICardView& second) -> bool { - return first.ViewCard().GetCode() < second.ViewCard().GetCode(); + const Battle::Card& a = first.ViewCard(); + const Battle::Card& b = second.ViewCard(); + char codeA = a.GetCode(); + char codeB = b.GetCode(); + return std::tie(codeA, a.GetShortName()) < std::tie(codeB, b.GetShortName()); }; auto sortByAttack = [](const ICardView& first, const ICardView& second) -> bool { - return first.ViewCard().GetDamage() < second.ViewCard().GetDamage(); + const Battle::Card& a = first.ViewCard(); + const Battle::Card& b = second.ViewCard(); + int attackA = a.GetDamage(); + int attackB = b.GetDamage(); + return std::tie(attackA, a.GetShortName()) < std::tie(attackB, b.GetShortName()); }; auto sortByElement = [](const ICardView& first, const ICardView& second) -> bool { - return first.ViewCard().GetElement() < second.ViewCard().GetElement(); + const Battle::Card& a = first.ViewCard(); + const Battle::Card& b = second.ViewCard(); + Element elementA = a.GetElement(); + Element elementB = b.GetElement(); + char codeA = a.GetCode(); + char codeB = b.GetCode(); + return std::tie(elementA, a.GetShortName(), codeA) < std::tie(elementB, b.GetShortName(), codeB); }; auto sortByFolderCopies = [this](const ICardView& first, const ICardView& second) -> bool { + const Battle::Card& a = first.ViewCard(); + const Battle::Card& b = second.ViewCard(); + char codeA = a.GetCode(); + char codeB = b.GetCode(); + size_t firstCount{}, secondCount{}; - firstCount = std::count_if(folderCardSlots.cbegin(), folderCardSlots.cend(), [&first](auto& entry) { - return entry.ViewCard().GetUUID() == first.ViewCard().GetUUID(); - }); + for (size_t i = 0; i < folderCardSlots.size(); i++) { + const auto& el = folderCardSlots[i]; + if (el.ViewCard().GetUUID() == first.ViewCard().GetUUID()) { + firstCount++; + + } - secondCount = std::count_if(folderCardSlots.cbegin(), folderCardSlots.cend(), [&second](auto& entry) { - return entry.ViewCard().GetUUID() == second.ViewCard().GetUUID(); - }); + if (el.ViewCard().GetUUID() == second.ViewCard().GetUUID()) { + secondCount++; + } + } - return firstCount < secondCount; + return std::tie(firstCount, a.GetShortName(), codeA) < std::tie(secondCount, b.GetShortName(), codeB); }; auto sortByPoolCopies = [this](const ICardView& first, const ICardView& second) -> bool { + const Battle::Card& a = first.ViewCard(); + const Battle::Card& b = second.ViewCard(); + char codeA = a.GetCode(); + char codeB = b.GetCode(); + size_t firstCount{}, secondCount{}; + bool firstCountFound{}, secondCountFound{}; - auto iter = std::find_if(poolCardBuckets.cbegin(), poolCardBuckets.cend(), [&first](auto& entry) { - return entry.ViewCard().GetUUID() == first.ViewCard().GetUUID(); - }); + for (size_t i = 0; i < poolCardBuckets.size(); i++) { + const auto& el = poolCardBuckets[i]; - auto iter2 = std::find_if(poolCardBuckets.cbegin(), poolCardBuckets.cend(), [&second](auto& entry) { - return entry.ViewCard().GetUUID() == second.ViewCard().GetUUID(); - }); + if (el.ViewCard() == first.ViewCard()) { + firstCount = el.GetCount(); + firstCountFound = true; + } - if (iter != poolCardBuckets.cend()) { - firstCount = iter->GetCount(); - } + if (el.ViewCard() == second.ViewCard()) { + secondCount = el.GetCount(); + secondCountFound = true; + } - if (iter2 != poolCardBuckets.cend()) { - secondCount = iter2->GetCount(); + if (firstCountFound && secondCountFound) break; } - return firstCount < secondCount; + return std::tie(firstCount, a.GetShortName(), codeA) < std::tie(secondCount, b.GetShortName(), codeB); }; - auto sortByMax = [](const ICardView& first, const ICardView& second) -> bool { - return first.ViewCard().GetLimit() < second.ViewCard().GetLimit(); + auto sortByClass = [](const ICardView& first, const ICardView& second) -> bool { + const Battle::Card& a = first.ViewCard(); + const Battle::Card& b = second.ViewCard(); + Battle::CardClass classA = a.GetClass(); + Battle::CardClass classB = b.GetClass(); + return std::tie(classA, a.GetShortName()) < std::tie(classB, b.GetShortName()); }; + // push empty slots at the bottom + auto pivoter = [](const ICardView& el) { + return !el.IsEmpty(); + }; + + folderSortOptions.SetPivotPredicate(pivoter); + folderSortOptions.AddOption(sortByID); folderSortOptions.AddOption(sortByAlpha); folderSortOptions.AddOption(sortByCode); folderSortOptions.AddOption(sortByAttack); folderSortOptions.AddOption(sortByElement); folderSortOptions.AddOption(sortByFolderCopies); - folderSortOptions.AddOption(sortByMax); + folderSortOptions.AddOption(sortByClass); poolSortOptions.AddOption(sortByID); poolSortOptions.AddOption(sortByAlpha); @@ -1157,7 +1184,7 @@ void FolderEditScene::ComposeSortOptions() { poolSortOptions.AddOption(sortByAttack); poolSortOptions.AddOption(sortByElement); poolSortOptions.AddOption(sortByPoolCopies); - poolSortOptions.AddOption(sortByMax); + poolSortOptions.AddOption(sortByClass); } void FolderEditScene::onEnd() { @@ -1238,6 +1265,26 @@ std::shared_ptr FolderEditScene::GetPreviewForCard(const std::strin return meta.GetPreviewTexture(); } +void FolderEditScene::RefreshCurrentCardSelection() { + switch (currViewMode) { + case ViewMode::folder: + { + using SlotType = decltype(folderCardSlots)::value_type; + RefreshCardDock(folderView, folderCardSlots); + } + break; + case ViewMode::pool: + { + using SlotType = decltype(poolCardBuckets)::value_type; + RefreshCardDock(packView, poolCardBuckets); + } + break; + default: + Logger::Logf(LogLevel::critical, "No applicable view mode for folder edit scene: %i", static_cast(currViewMode)); + break; + } +} + #ifdef __ANDROID__ void FolderEditScene::StartupTouchControls() { /* Android touch areas*/ diff --git a/BattleNetwork/bnFolderEditScene.h b/BattleNetwork/bnFolderEditScene.h index 7747aef9b..d956dbaf8 100644 --- a/BattleNetwork/bnFolderEditScene.h +++ b/BattleNetwork/bnFolderEditScene.h @@ -179,12 +179,16 @@ class FolderEditScene : public Scene { using base_type_t = BaseType; std::array filters; bool invert{}; + std::function pivotPred{ nullptr }; size_t freeIdx{}, lastIndex{}; public: virtual ~ISortOptions() {} size_t size() { return sz; } bool AddOption(const filter& filter) { if (freeIdx >= filters.size()) return false; filters.at(freeIdx++) = filter; return true; } + void SetPivotPredicate(const std::function& predicate) { + pivotPred = predicate; + } virtual void SelectOption(size_t index) = 0; }; @@ -203,19 +207,36 @@ class FolderEditScene : public Scene { this->lastIndex = index; } - if (this->invert) { - std::stable_sort(this->container.begin(), this->container.end(), this->filters.at(index)); - } - else { - std::stable_sort(this->container.rbegin(), this->container.rend(), this->filters.at(index)); + if (pivotPred) { + auto pivot = std::partition(this->container.begin(), this->container.end(), pivotPred); + size_t pivotDist = std::distance(this->container.begin(), pivot); + + std::vector copy = std::vector(this->container.begin(), pivot); + + std::sort(copy.begin(), copy.end(), this->filters.at(index)); + + if (this->invert) { + std::reverse(copy.begin(), copy.end()); + } + + std::vector copy_end = std::vector(this->container.begin() + pivotDist, this->container.end()); + + this->container.clear(); + this->container.reserve(copy.size() + copy_end.size()); + this->container.insert(this->container.end(), copy.begin(), copy.end()); + this->container.insert(this->container.end(), copy_end.begin(), copy_end.end()); + + return; } - // push empty slots at the bottom - auto pivot = [](const ICardView& el) { - return !el.IsEmpty(); - }; + std::vector copy = this->container; + std::sort(copy.begin(), copy.end(), this->filters.at(index)); + + if (this->invert) { + std::reverse(copy.begin(), copy.end()); + } - std::partition(this->container.begin(), this->container.end(), pivot); + this->container = copy; } }; @@ -248,7 +269,8 @@ class FolderEditScene : public Scene { void WriteNewFolderData(); template - void RefreshCurrentCardDock(CardView& view, const std::vector& list); + void RefreshCardDock(CardView& view, const std::vector& list); + void RefreshCurrentCardSelection(); public: void onStart() override; @@ -265,29 +287,27 @@ class FolderEditScene : public Scene { }; template -void FolderEditScene::RefreshCurrentCardDock(FolderEditScene::CardView& view, const std::vector& list) { +void FolderEditScene::RefreshCardDock(FolderEditScene::CardView& view, const std::vector& list) +{ if (view.currCardIndex < list.size()) { T slot = list[view.currCardIndex]; // copy data, do not mutate it - // If we have selected a new card, display the appropriate texture for its type - if (view.currCardIndex != view.prevIndex) { - sf::Sprite& sprite = currViewMode == ViewMode::folder ? cardHolder : packCardHolder; - Battle::Card card; - slot.GetCard(card); // Returns and frees the card from the bucket, this is why we needed a copy - - switch (card.GetClass()) { - case Battle::CardClass::mega: - sprite.setTexture(*Textures().LoadFromFile(TexturePaths::FOLDER_CHIP_HOLDER_MEGA)); - break; - case Battle::CardClass::giga: - sprite.setTexture(*Textures().LoadFromFile(TexturePaths::FOLDER_CHIP_HOLDER_GIGA)); - break; - case Battle::CardClass::dark: - sprite.setTexture(*Textures().LoadFromFile(TexturePaths::FOLDER_CHIP_HOLDER_DARK)); - break; - default: - sprite.setTexture(*Textures().LoadFromFile(TexturePaths::FOLDER_CHIP_HOLDER)); - } + sf::Sprite& sprite = currViewMode == ViewMode::folder ? cardHolder : packCardHolder; + Battle::Card card; + slot.GetCard(card); // Returns and frees the card from the bucket, this is why we needed a copy + + switch (card.GetClass()) { + case Battle::CardClass::mega: + sprite.setTexture(*Textures().LoadFromFile(TexturePaths::FOLDER_CHIP_HOLDER_MEGA)); + break; + case Battle::CardClass::giga: + sprite.setTexture(*Textures().LoadFromFile(TexturePaths::FOLDER_CHIP_HOLDER_GIGA)); + break; + case Battle::CardClass::dark: + sprite.setTexture(*Textures().LoadFromFile(TexturePaths::FOLDER_CHIP_HOLDER_DARK)); + break; + default: + sprite.setTexture(*Textures().LoadFromFile(TexturePaths::FOLDER_CHIP_HOLDER)); } } } \ No newline at end of file diff --git a/BattleNetwork/resources/ui/folder_sort.png b/BattleNetwork/resources/ui/folder_sort.png index 772266d769f7a5c0167e0d70d02a4d28846f1b75..8ec25c1b642cad97b9f802680aa6fedf925df0ba 100644 GIT binary patch delta 680 zcmV;Z0$2Ub1>FUZL4OiZOjJb>T4OoJ*7x`K5OaGFQfvSJ|A2E1*Z=?k32;bRa{vGi z!2kdb!2!6DYwZ940#r#vK~zY`&6f>w;~)%$vq#9@phy>}AQ!OI6J&hFLy{AE6r+@fSZ{FxQ>zjsjzUDd2 zgYx+wsHB`zqETPg%(e_^RGL!0B9+podo*;=*EAQiJfPk3<5YHiy`^aQ@S;SauaqU; z+3t`Y@ly)U>9=>ON1k1FMs-2Ph+9eRqHw z)fDOn<#<1=D)wZ`LXa1$l6qC}Z!WGc#dvXjiCr?&W@KOZqAW{Fyh+tiA0TL~|4gwIrr1EmQBBiHqM9;IDNm_D-Z52|cXaD>Yjy^IQ=U?pQmm>B z?q=a%Q|r@yx&G-qc6Z#mhJA55Prc7mT0;JHMLkn*Mh~;E_d(d4&YrpR`b6!`OGrwO zsD~%Q=^XM$?mkCD;qJ%!{92zUidz@l9mn%2k|Evl`4|5^&FG}uZhrvfB}=mhdxzTq O0000T4OoJ*7x`K5OaGFQfoF#=EML1010qNS#tmY4#EHc z4#EKyC`y0;00KuzL_t(Y$JLhsa^o-vMYBi9-k`t*ia3Gc{%2Z1jxE`eX1cpGO*K~1 zok5e+BXeXwtq0CYoFJ&OvdMbpbVt| z;LMh^kZn_%7cMEiBV`%WJsLW(H7#ORCUiKyo${`)PskY`4vLuivTXTI7PoiCiXv9( z@gT}i-O4-N$%V3TMl!w)T6656>UOc;PAQ{$ssLe!`@F1-X4vd`q4k^CorQ8X}wLkIO&ZGUOhZ&J^WpoT3C^^;P*U%bkk zgu)o|hF9Er74=)h^$mH&REw5(*Ec%Q_37K(uXmu!gRJybcA>nc+U)C{KjqDb!hSXT zmMC2|+FV~Gm&~+V*&W}Mcmtp+RY!e+r0M^ihAU3Bl%7U3qI82sgQk@G(AQU!&{n0m zzW#T}{eRy2;_ZCYQn*RfQo<#rCFPKJPSxd|o%*~q2aBJSmXwz?ypqY;EbG_Q#&kS> zfcmus Date: Thu, 31 Jul 2025 20:25:51 -0700 Subject: [PATCH 056/146] Partial pull 993de6e for Card::props access change --- .../battlescene/States/bnCombatBattleState.cpp | 2 +- .../States/bnTimeFreezeBattleState.cpp | 14 +++++++------- BattleNetwork/battlescene/bnBattleSceneBase.cpp | 8 ++++---- .../bindings/bnUserTypeBaseCardAction.cpp | 2 +- .../bindings/bnUserTypeScriptedCardAction.cpp | 2 +- BattleNetwork/bnCard.cpp | 15 +++++++++++++-- BattleNetwork/bnCard.h | 11 ++++++++--- BattleNetwork/bnCardAction.cpp | 6 +++--- BattleNetwork/bnCardAction.h | 6 +++--- BattleNetwork/bnCharacter.cpp | 2 +- BattleNetwork/bnPlayer.cpp | 2 +- BattleNetwork/bnPlayerSelectedCardsUI.cpp | 4 ++-- BattleNetwork/bnRealtimeCardUseListener.cpp | 2 +- BattleNetwork/bnSelectedCardsUI.cpp | 2 +- .../netplay/battlescene/bnNetworkBattleScene.cpp | 4 ++-- 15 files changed, 49 insertions(+), 33 deletions(-) diff --git a/BattleNetwork/battlescene/States/bnCombatBattleState.cpp b/BattleNetwork/battlescene/States/bnCombatBattleState.cpp index bde96ce81..4912847a8 100644 --- a/BattleNetwork/battlescene/States/bnCombatBattleState.cpp +++ b/BattleNetwork/battlescene/States/bnCombatBattleState.cpp @@ -203,7 +203,7 @@ void CombatBattleState::OnCardActionUsed(std::shared_ptr action, uin Logger::Logf(LogLevel::debug, "CombatBattleState::OnCardActionUsed() on frame #%i, with gauge progress %f", scene.FrameNumber().count(), this->GetScene().GetCustomBarProgress()); if (!IsMobCleared()) { - hasTimeFreeze = action->GetMetaData().timeFreeze; + hasTimeFreeze = action->GetMetaData().GetProps().timeFreeze; } } diff --git a/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp b/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp index d04b68b32..ec45ebbea 100644 --- a/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp +++ b/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp @@ -79,7 +79,7 @@ void TimeFreezeBattleState::ProcessInputs() const Battle::Card& card = *maybe_card; if (card.IsTimeFreeze() && CanCounter(p)) { - if (std::shared_ptr action = CardToAction(card, p, &GetScene().getController().CardPackagePartitioner(), card.props)) { + if (std::shared_ptr action = CardToAction(card, p, &GetScene().getController().CardPackagePartitioner(), card.GetProps())) { OnCardActionUsed(action, CurrentTime::AsMilli()); cardsUI->DropNextCard(); } @@ -103,7 +103,7 @@ void TimeFreezeBattleState::onStart(const BattleSceneState*) if (tfEvents.empty()) return; const auto& first = tfEvents.begin(); - if (first->action && first->action->GetMetaData().skipTimeFreezeIntro) { + if (first->action && first->action->GetMetaData().GetProps().skipTimeFreezeIntro) { SkipToAnimateState(); } } @@ -290,7 +290,7 @@ void TimeFreezeBattleState::onDraw(sf::RenderTexture& surface) summonsLabel.setPosition(position); scene.DrawWithPerspective(summonsLabel, surface); - if (currState == state::display_name && first->action->GetMetaData().counterable && summonTick > tfcStartFrame) { + if (currState == state::display_name && first->action->GetMetaData().GetProps().counterable && summonTick > tfcStartFrame) { // draw TF bar underneath if conditions are met bar.setPosition(position + sf::Vector2f(0.f + 2.f, 12.f + 2.f)); bar.setFillColor(sf::Color::Black); @@ -426,9 +426,9 @@ void TimeFreezeBattleState::DrawCardData(sf::RenderTarget& target) void TimeFreezeBattleState::OnCardActionUsed(std::shared_ptr action, uint64_t timestamp) { - Logger::Logf(LogLevel::info, "OnCardActionUsed(): %s, summonTick: %i, summonTextLength: %i", action->GetMetaData().shortname.c_str(), summonTick.count(), summonTextLength.count()); + Logger::Logf(LogLevel::info, "OnCardActionUsed(): %s, summonTick: %i, summonTextLength: %i", action->GetMetaData().GetProps().shortname.c_str(), summonTick.count(), summonTextLength.count()); - if (!(action && action->GetMetaData().timeFreeze)) return; + if (!(action && action->GetMetaData().GetProps().timeFreeze)) return; if (CanCounter(action->GetActor())) { HandleTimeFreezeCounter(action, timestamp); @@ -452,7 +452,7 @@ const bool TimeFreezeBattleState::CanCounter(std::shared_ptr user) } } // some actions cannot be countered - if (!action->GetMetaData().counterable) return false; + if (!action->GetMetaData().GetProps().counterable) return false; // only opposing players can counter std::shared_ptr lastActor = action->GetActor(); @@ -472,7 +472,7 @@ void TimeFreezeBattleState::HandleTimeFreezeCounter(std::shared_ptr { TimeFreezeBattleState::EventData data; data.action = action; - data.name = action->GetMetaData().shortname; + data.name = action->GetMetaData().GetProps().shortname; data.team = action->GetActor()->GetTeam(); data.user = action->GetActor(); lockedTimestamp = timestamp; diff --git a/BattleNetwork/battlescene/bnBattleSceneBase.cpp b/BattleNetwork/battlescene/bnBattleSceneBase.cpp index 112e09a68..f924aa6c2 100644 --- a/BattleNetwork/battlescene/bnBattleSceneBase.cpp +++ b/BattleNetwork/battlescene/bnBattleSceneBase.cpp @@ -390,7 +390,7 @@ std::shared_ptr BattleSceneBase::GetPlayerFromEntityID(Entity::ID_t ID) void BattleSceneBase::OnCardActionUsed(std::shared_ptr action, uint64_t timestamp) { - if (action->GetMetaData().canBoost) { + if (action->GetMetaData().GetProps().canBoost) { HandleCounterLoss(*action->GetActor(), true); } } @@ -542,18 +542,18 @@ void BattleSceneBase::FilterSupportCards(const std::shared_ptr& player, if (i > 0 && cards[i - 1u].CanBoost()) { adjCards.hasCardToLeft = true; - adjCards.leftCard = &cards[i - 1u].props; + adjCards.leftCard = &cards[i - 1u].GetProps(); } if (i < cards.size() - 1 && cards[i + 1u].CanBoost()) { adjCards.hasCardToRight = true; - adjCards.rightCard = &cards[i + 1u].props; + adjCards.rightCard = &cards[i + 1u].GetProps(); } CardMeta& meta = cardPackageManager.FindPackageByID(addr.packageId); if (meta.filterHandStep) { - meta.filterHandStep(cards[i].props, adjCards); + meta.filterHandStep(cards[i].GetProps(), adjCards); } size_t this_card = i; diff --git a/BattleNetwork/bindings/bnUserTypeBaseCardAction.cpp b/BattleNetwork/bindings/bnUserTypeBaseCardAction.cpp index 1e95fd945..1f4b03993 100644 --- a/BattleNetwork/bindings/bnUserTypeBaseCardAction.cpp +++ b/BattleNetwork/bindings/bnUserTypeBaseCardAction.cpp @@ -70,7 +70,7 @@ void DefineBaseCardActionUserType(sol::state& state, sol::table& battle_namespac cardAction.Unwrap()->SetMetaData(props); }, "copy_metadata", [](WeakWrapper& cardAction) -> Battle::Card::Properties { - return cardAction.Unwrap()->GetMetaData(); + return cardAction.Unwrap()->GetMetaData().GetProps(); } ); diff --git a/BattleNetwork/bindings/bnUserTypeScriptedCardAction.cpp b/BattleNetwork/bindings/bnUserTypeScriptedCardAction.cpp index eb8e27723..702062718 100644 --- a/BattleNetwork/bindings/bnUserTypeScriptedCardAction.cpp +++ b/BattleNetwork/bindings/bnUserTypeScriptedCardAction.cpp @@ -165,7 +165,7 @@ void DefineScriptedCardActionUserType(const std::string& namespaceId, ScriptReso cardAction.Unwrap()->SetMetaData(props); }, "copy_metadata", [](WeakWrapper& cardAction) -> Battle::Card::Properties { - return cardAction.Unwrap()->GetMetaData(); + return cardAction.Unwrap()->GetMetaData().GetProps(); }, "update_func", sol::property( [](WeakWrapper& cardAction) { return cardAction.Unwrap()->update_func; }, diff --git a/BattleNetwork/bnCard.cpp b/BattleNetwork/bnCard.cpp index ca13f35da..8ff4b9e9d 100644 --- a/BattleNetwork/bnCard.cpp +++ b/BattleNetwork/bnCard.cpp @@ -17,8 +17,19 @@ namespace Battle { props = unmodded = Card::Properties(); } - const Card::Properties& Card::GetUnmoddedProps() const - { + Card::Properties& Card::GetProps() { + return props; + } + + const Card::Properties& Card::GetProps() const { + return props; + } + + Card::Properties& Card::GetBaseProps() { + return unmodded; + } + + const Card::Properties& Card::GetBaseProps() const { return unmodded; } diff --git a/BattleNetwork/bnCard.h b/BattleNetwork/bnCard.h index f06f3c4fe..9273e5036 100644 --- a/BattleNetwork/bnCard.h +++ b/BattleNetwork/bnCard.h @@ -47,7 +47,6 @@ namespace Battle { std::vector metaClasses; /*!< Cards can be tagged with additional user information*/ }; - Properties props; /** * @brief Cards are not designed to have default or partial data. Must provide all at once. */ @@ -65,7 +64,12 @@ namespace Battle { ~Card(); - const Card::Properties& GetUnmoddedProps() const; + Card::Properties& GetProps(); // Modded props + Card::Properties& GetBaseProps(); // Unmodded props + + // const qualified + const Card::Properties& GetProps() const; + const Card::Properties& GetBaseProps() const; /** * @brief Get extra card description. Shows up on library. @@ -173,6 +177,7 @@ namespace Battle { private: Properties unmodded; - unsigned int multiplier{ 0 }; + Properties props; + unsigned int multiplier{ 1 }; }; } \ No newline at end of file diff --git a/BattleNetwork/bnCardAction.cpp b/BattleNetwork/bnCardAction.cpp index d2d2dbaea..b87393a13 100644 --- a/BattleNetwork/bnCardAction.cpp +++ b/BattleNetwork/bnCardAction.cpp @@ -145,7 +145,7 @@ const std::shared_ptr CardAction::GetActor() const return actor.lock(); } -const Battle::Card::Properties& CardAction::GetMetaData() const +const Battle::Card& CardAction::GetMetaData() const { return meta; } @@ -195,9 +195,9 @@ void CardAction::OverrideAnimationFrames(std::list frameData) } } -void CardAction::SetMetaData(const Battle::Card::Properties& props) +void CardAction::SetMetaData(const Battle::Card& card) { - meta = props; + meta = card; } void CardAction::Execute(std::shared_ptr user) diff --git a/BattleNetwork/bnCardAction.h b/BattleNetwork/bnCardAction.h index cf36fc93a..5b99fdc97 100644 --- a/BattleNetwork/bnCardAction.h +++ b/BattleNetwork/bnCardAction.h @@ -115,7 +115,7 @@ class CardAction : public stx::enable_shared_from_base, public sf::D std::weak_ptr userWeak; Attachments attachments; std::shared_ptr anim{ nullptr }; - Battle::Card::Properties meta; + Battle::Card meta; std::vector> animActions; Battle::Tile* startTile{ nullptr }; @@ -145,7 +145,7 @@ class CardAction : public stx::enable_shared_from_base, public sf::D void SetLockout(const LockoutProperties& props); void SetLockoutGroup(const LockoutGroup& group); void OverrideAnimationFrames(std::list frameData); - void SetMetaData(const Battle::Card::Properties& props); + void SetMetaData(const Battle::Card& card); void Execute(std::shared_ptr user); void EndAction(); void UseStuntDouble(std::shared_ptr stuntDouble); // can cause GetActor to return nullptr @@ -155,7 +155,7 @@ class CardAction : public stx::enable_shared_from_base, public sf::D const std::string& GetAnimState() const; const bool IsAnimationOver() const; const bool IsLockoutOver() const; - const Battle::Card::Properties& GetMetaData() const; + const Battle::Card& GetMetaData() const; const bool CanExecute() const; std::shared_ptr GetActor(); // may return nullptr const std::shared_ptr GetActor() const; // may return nullptr diff --git a/BattleNetwork/bnCharacter.cpp b/BattleNetwork/bnCharacter.cpp index d4811468c..eba77bf59 100644 --- a/BattleNetwork/bnCharacter.cpp +++ b/BattleNetwork/bnCharacter.cpp @@ -186,7 +186,7 @@ void Character::AddAction(const PeekCardEvent& event, const ActionOrder& order) void Character::HandleCardEvent(const CardEvent& event, const ActionQueue::ExecutionType& exec) { if (currCardAction == nullptr) { - if (event.action->GetMetaData().timeFreeze) { + if (event.action->GetMetaData().GetProps().timeFreeze) { CardActionUsePublisher::Broadcast(event.action, CurrentTime::AsMilli()); actionQueue.Pop(); } diff --git a/BattleNetwork/bnPlayer.cpp b/BattleNetwork/bnPlayer.cpp index 765ff4ad4..171b2d5f4 100644 --- a/BattleNetwork/bnPlayer.cpp +++ b/BattleNetwork/bnPlayer.cpp @@ -156,7 +156,7 @@ void Player::Attack() { if (action) { action->PreventCounters(); - Battle::Card::Properties props = action->GetMetaData(); + Battle::Card::Properties props = action->GetMetaData().GetProps(); if (!fullyCharged) { props.timeFreeze = false; diff --git a/BattleNetwork/bnPlayerSelectedCardsUI.cpp b/BattleNetwork/bnPlayerSelectedCardsUI.cpp index 8ff1b78f9..feef5b7fd 100644 --- a/BattleNetwork/bnPlayerSelectedCardsUI.cpp +++ b/BattleNetwork/bnPlayerSelectedCardsUI.cpp @@ -146,7 +146,7 @@ void PlayerSelectedCardsUI::draw(sf::RenderTarget& target, sf::RenderStates stat text.setPosition(2.0f, 296.0f); // Text sits at the bottom-left of the screen - int unmodDamage = currCard.GetUnmoddedProps().damage; + int unmodDamage = currCard.GetBaseProps().damage; int delta = currCard.GetDamage() - unmodDamage; sf::String dmgText = std::to_string(unmodDamage); @@ -217,7 +217,7 @@ void PlayerSelectedCardsUI::Broadcast(std::shared_ptr action) std::shared_ptr player = GetOwnerAs(); bool angry = player && player->GetEmotion() == Emotion::angry; - if (angry && action->GetMetaData().canBoost) { + if (angry && action->GetMetaData().GetProps().canBoost) { player->SetEmotion(Emotion::normal); } diff --git a/BattleNetwork/bnRealtimeCardUseListener.cpp b/BattleNetwork/bnRealtimeCardUseListener.cpp index 19d0ba1fe..fc01149a5 100644 --- a/BattleNetwork/bnRealtimeCardUseListener.cpp +++ b/BattleNetwork/bnRealtimeCardUseListener.cpp @@ -3,7 +3,7 @@ #include "bnCharacter.h" void RealtimeCardActionUseListener::OnCardActionUsed(std::shared_ptr action, uint64_t timestamp) { - if (action->GetMetaData().timeFreeze == false) { + if (action->GetMetaData().GetProps().timeFreeze == false) { action->GetActor()->AddAction(CardEvent{ action }, ActionOrder::voluntary); } } \ No newline at end of file diff --git a/BattleNetwork/bnSelectedCardsUI.cpp b/BattleNetwork/bnSelectedCardsUI.cpp index 12f8b5419..ffbf3d9bc 100644 --- a/BattleNetwork/bnSelectedCardsUI.cpp +++ b/BattleNetwork/bnSelectedCardsUI.cpp @@ -177,7 +177,7 @@ bool SelectedCardsUI::HandlePlayEvent(std::shared_ptr from) // could act on metadata later: // from->OnCard(card) - if (std::shared_ptr action = CardToAction(card, from, partition, card.props)) { + if (std::shared_ptr action = CardToAction(card, from, partition, card.GetProps())) { Broadcast(action); // tell the rest of the subsystems } diff --git a/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp b/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp index 72297b329..bb10986bd 100644 --- a/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp +++ b/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp @@ -598,11 +598,11 @@ void NetworkBattleScene::ReceiveHandshakeSignal(const Poco::Buffer& buffer if (packageManager.HasPackage(addr.packageId)) { card = packageManager.FindPackageByID(addr.packageId).GetCardProperties(); - card.props.uuid = packageManager.WithNamespace(card.props.uuid); + card.GetProps().uuid = packageManager.WithNamespace(card.GetProps().uuid); } else if (localPackageManager.HasPackage(addr.packageId)) { card = localPackageManager.FindPackageByID(addr.packageId).GetCardProperties(); - card.props.uuid = localPackageManager.WithNamespace(card.props.uuid); + card.GetProps().uuid = localPackageManager.WithNamespace(card.GetProps().uuid); } remoteHand.push_back(card); From 8246e727593eb59aedf034d68dfe0d7b91aa64d4 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Thu, 31 Jul 2025 23:31:06 -0700 Subject: [PATCH 057/146] Pull d236988 except for time freeze time limit --- .../States/bnTimeFreezeBattleState.cpp | 112 +++++++++++------- .../States/bnTimeFreezeBattleState.h | 2 +- .../bindings/bnUserTypeBaseCardAction.cpp | 4 +- .../bindings/bnUserTypeScriptedCardAction.cpp | 4 +- BattleNetwork/bnAnimation.h | 2 +- BattleNetwork/bnCard.cpp | 2 +- BattleNetwork/bnCard.h | 4 +- BattleNetwork/bnCardToActions.cpp | 2 +- BattleNetwork/bnText.cpp | 95 +++++++-------- BattleNetwork/bnText.h | 10 +- 10 files changed, 128 insertions(+), 109 deletions(-) diff --git a/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp b/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp index ec45ebbea..9095d3db6 100644 --- a/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp +++ b/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp @@ -263,33 +263,19 @@ void TimeFreezeBattleState::onDraw(sf::RenderTexture& surface) scale = std::max(scale, 0.0); } - sf::Vector2f position = sf::Vector2f(66.f, 82.f); + sf::Vector2f position = sf::Vector2f(64.f, 82.f); if (first->team == Team::blue) { position = sf::Vector2f(416.f, 82.f); bar.setOrigin(bar.getLocalBounds().width, 0.0f); } - summonsLabel.setScale(2.0f, 2.0f*(float)scale); - - if (first->team == Team::red) { - summonsLabel.setOrigin(0, summonsLabel.GetLocalBounds().height*0.5f); - } - else { - summonsLabel.setOrigin(summonsLabel.GetLocalBounds().width, summonsLabel.GetLocalBounds().height*0.5f); - } + + DrawCardData(position, sf::Vector2f(2.f, scale * 2.f), surface); scene.DrawCustGauage(surface); surface.draw(scene.GetCardSelectWidget()); - summonsLabel.SetColor(sf::Color::Black); - summonsLabel.setPosition(position.x + 2.f, position.y + 2.f); - scene.DrawWithPerspective(summonsLabel, surface); - - summonsLabel.SetColor(sf::Color::White); - summonsLabel.setPosition(position); - scene.DrawWithPerspective(summonsLabel, surface); - if (currState == state::display_name && first->action->GetMetaData().GetProps().counterable && summonTick > tfcStartFrame) { // draw TF bar underneath if conditions are met bar.setPosition(position + sf::Vector2f(0.f + 2.f, 12.f + 2.f)); @@ -334,12 +320,12 @@ void TimeFreezeBattleState::ExecuteTimeFreeze() { if (tfEvents.empty()) return; - auto first = tfEvents.begin(); + TimeFreezeBattleState::EventData& first = *tfEvents.begin(); - if (first->action && first->action->CanExecute()) { - first->user->Hide(); - if (GetScene().GetField()->AddEntity(first->stuntDouble, *first->user->GetTile()) != Field::AddEntityStatus::deleted) { - first->action->Execute(first->user); + if (first.action && first.action->CanExecute()) { + first.user->Hide(); + if (GetScene().GetField()->AddEntity(first.stuntDouble, *first.user->GetTile()) != Field::AddEntityStatus::deleted) { + first.action->Execute(first.user); } else { currState = state::fadeout; @@ -351,28 +337,51 @@ bool TimeFreezeBattleState::IsOver() { return state::fadeout == currState && FadeOutBackdrop(); } -/* -void TimeFreezeBattleState::DrawCardData(sf::RenderTarget& target) -{ +void TimeFreezeBattleState::DrawCardData(const sf::Vector2f& pos, const sf::Vector2f& scale, sf::RenderTarget& target) { + TimeFreezeBattleState::EventData& event = *tfEvents.begin(); const auto orange = sf::Color(225, 140, 0); bool canBoost{}; + float multiplierOffset = 0.f; + float dmgOffset = 0.f; + + // helper function + auto setSummonLabelOrigin = [](Team team, Text& text) { + if (team == Team::red) { + text.setOrigin(0, text.GetLocalBounds().height * 0.5f); + } + else { + text.setOrigin(text.GetLocalBounds().width, text.GetLocalBounds().height * 0.5f); + } + }; + + // We want the other text to use the origin of the + // summons label for the y so that they are all + // sitting on the same line when they render + auto setOrigin = [setSummonLabelOrigin, this](Team team, Text& text) { + setSummonLabelOrigin(team, text); + text.setOrigin(text.getOrigin().x, this->summonsLabel.getOrigin().y); + }; + + summonsLabel.SetString(event.name); + summonsLabel.setScale(scale); + setSummonLabelOrigin(event.team, summonsLabel); - summonsLabel.SetString(""); dmg.SetString(""); multiplier.SetString(""); - TimeFreezeBattleState::EventData& event = *tfEvents.begin(); - Battle::Card::Properties cardProps = event.action->GetMetaData(); - canBoost = cardProps.canBoost; + const Battle::Card& card = event.action->GetMetaData(); + canBoost = card.CanBoost(); - // Text sits at the bottom-left of the screen - summonsLabel.SetString(event.name); - summonsLabel.setOrigin(0, 0); - summonsLabel.setPosition(2.0f, 296.0f); + // Calculate the delta damage values to correctly draw the modifiers + const unsigned int multiplierValue = card.GetMultiplier(); + int unmodDamage = card.GetBaseProps().damage; + int damage = card.GetProps().damage; + + if (multiplierValue) { + damage /= multiplierValue; + } - // Text sits at the bottom-left of the screen - int unmodDamage = event.unmoddedProps.damage; // TODO: get unmodded properties?? - int delta = cardProps.damage - unmodDamage; + int delta = damage - unmodDamage; sf::String dmgText = std::to_string(unmodDamage); if (delta != 0) { @@ -382,17 +391,30 @@ void TimeFreezeBattleState::DrawCardData(sf::RenderTarget& target) // attacks that normally show no damage will show if the modifer adds damage if (delta > 0 || unmodDamage > 0) { dmg.SetString(dmgText); - dmg.setOrigin(0, 0); - dmg.setPosition((summonsLabel.GetLocalBounds().width * summonsLabel.getScale().x) + 10.f, 296.f); + dmg.setScale(scale); + setOrigin(event.team, dmg); + dmgOffset = 10.0f; } - - // TODO: multiplierValue needs to come from where? + if (multiplierValue != 1 && unmodDamage != 0) { // add "x N" where N is the multiplier std::string multStr = "x" + std::to_string(multiplierValue); multiplier.SetString(multStr); - multiplier.setOrigin(0, 0); - multiplier.setPosition(dmg.getPosition().x + (dmg.GetLocalBounds().width * dmg.getScale().x) + 3.0f, 296.0f); + multiplier.setScale(scale); + setOrigin(event.team, multiplier); + multiplierOffset = 3.0f; + } + + // based on team, render the text from left-to-right or right-to-left alignment + if (event.team == Team::red) { + summonsLabel.setPosition(pos); + dmg.setPosition(summonsLabel.getPosition().x + summonsLabel.GetWorldBounds().width + dmgOffset, pos.y); + multiplier.setPosition(dmg.getPosition().x + dmg.GetWorldBounds().width + multiplierOffset, pos.y); + } + else { /* team == Team::blue or other */ + multiplier.setPosition(pos); + dmg.setPosition(multiplier.getPosition().x - multiplier.GetWorldBounds().width - multiplierOffset, pos.y); + summonsLabel.setPosition(dmg.getPosition().x - dmg.GetWorldBounds().width - dmgOffset, pos.y); } // shadow beneath @@ -422,14 +444,14 @@ void TimeFreezeBattleState::DrawCardData(sf::RenderTarget& target) target.draw(multiplier); } } -*/ void TimeFreezeBattleState::OnCardActionUsed(std::shared_ptr action, uint64_t timestamp) { - Logger::Logf(LogLevel::info, "OnCardActionUsed(): %s, summonTick: %i, summonTextLength: %i", action->GetMetaData().GetProps().shortname.c_str(), summonTick.count(), summonTextLength.count()); + const Battle::Card::Properties& props = action->GetMetaData().GetProps(); + Logger::Logf(LogLevel::info, "OnCardActionUsed(): %s, summonTick: %i, summonTextLength: %i", props.shortname.c_str(), summonTick.count(), summonTextLength.count()); if (!(action && action->GetMetaData().GetProps().timeFreeze)) return; - + if (CanCounter(action->GetActor())) { HandleTimeFreezeCounter(action, timestamp); } diff --git a/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.h b/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.h index a251867c6..6fae23713 100644 --- a/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.h +++ b/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.h @@ -65,7 +65,7 @@ struct TimeFreezeBattleState final : public BattleSceneState, CardActionUseListe const bool FadeInBackdrop(); bool IsOver(); - // void DrawCardData(sf::RenderTarget& target); // TODO: we are missing some data from the selected UI to draw the info we need + void DrawCardData(const sf::Vector2f& pos, const sf::Vector2f& scale, sf::RenderTarget& target); std::shared_ptr CreateStuntDouble(std::shared_ptr from); }; diff --git a/BattleNetwork/bindings/bnUserTypeBaseCardAction.cpp b/BattleNetwork/bindings/bnUserTypeBaseCardAction.cpp index 1f4b03993..9084af928 100644 --- a/BattleNetwork/bindings/bnUserTypeBaseCardAction.cpp +++ b/BattleNetwork/bindings/bnUserTypeBaseCardAction.cpp @@ -66,8 +66,8 @@ void DefineBaseCardActionUserType(sol::state& state, sol::table& battle_namespac "get_actor", [](WeakWrapper& cardAction) -> WeakWrapper { return WeakWrapper(cardAction.Unwrap()->GetActor()); }, - "set_metadata", [](WeakWrapper& cardAction, const Battle::Card::Properties& props) { - cardAction.Unwrap()->SetMetaData(props); + "set_metadata", [](WeakWrapper& cardAction, const Battle::Card::Properties& meta) { + cardAction.Unwrap()->SetMetaData(Battle::Card(meta)); }, "copy_metadata", [](WeakWrapper& cardAction) -> Battle::Card::Properties { return cardAction.Unwrap()->GetMetaData().GetProps(); diff --git a/BattleNetwork/bindings/bnUserTypeScriptedCardAction.cpp b/BattleNetwork/bindings/bnUserTypeScriptedCardAction.cpp index 702062718..304a6ed9a 100644 --- a/BattleNetwork/bindings/bnUserTypeScriptedCardAction.cpp +++ b/BattleNetwork/bindings/bnUserTypeScriptedCardAction.cpp @@ -161,8 +161,8 @@ void DefineScriptedCardActionUserType(const std::string& namespaceId, ScriptReso "get_actor", [](WeakWrapper& cardAction) -> WeakWrapper { return WeakWrapper(cardAction.Unwrap()->GetActor()); }, - "set_metadata", [](WeakWrapper& cardAction, const Battle::Card::Properties& props) { - cardAction.Unwrap()->SetMetaData(props); + "set_metadata", [](WeakWrapper& cardAction, const Battle::Card::Properties& meta) { + cardAction.Unwrap()->SetMetaData(Battle::Card(meta)); }, "copy_metadata", [](WeakWrapper& cardAction) -> Battle::Card::Properties { return cardAction.Unwrap()->GetMetaData().GetProps(); diff --git a/BattleNetwork/bnAnimation.h b/BattleNetwork/bnAnimation.h index 2682d4f92..b2ab85eb4 100644 --- a/BattleNetwork/bnAnimation.h +++ b/BattleNetwork/bnAnimation.h @@ -214,5 +214,5 @@ class Animation { frame_time_t progress; /*!< Current progress of animation */ double playbackSpeed{ 1.0 }; /*!< Factor to multiply against update `dt`*/ std::map animations; /*!< Dictionary of FrameLists read from file */ - std::function interruptCallback; + std::function interruptCallback{ nullptr }; }; diff --git a/BattleNetwork/bnCard.cpp b/BattleNetwork/bnCard.cpp index 8ff4b9e9d..3001ff9f8 100644 --- a/BattleNetwork/bnCard.cpp +++ b/BattleNetwork/bnCard.cpp @@ -4,7 +4,7 @@ #include namespace Battle { - Card::Card() : props(), unmodded(props) + Card::Card() { } Card::Card(const Card::Properties& props) : props(props), unmodded(props) diff --git a/BattleNetwork/bnCard.h b/BattleNetwork/bnCard.h index 9273e5036..50a579dae 100644 --- a/BattleNetwork/bnCard.h +++ b/BattleNetwork/bnCard.h @@ -176,8 +176,8 @@ namespace Battle { friend struct Compare; private: - Properties unmodded; - Properties props; + Properties unmodded{}; + Properties props{}; unsigned int multiplier{ 1 }; }; } \ No newline at end of file diff --git a/BattleNetwork/bnCardToActions.cpp b/BattleNetwork/bnCardToActions.cpp index 7032a51ab..4d9b726f0 100644 --- a/BattleNetwork/bnCardToActions.cpp +++ b/BattleNetwork/bnCardToActions.cpp @@ -28,7 +28,7 @@ std::shared_ptr CardToAction( std::shared_ptr result = cardImpl->BuildCardAction(character, props); if (result) { - result->SetMetaData(props); + result->SetMetaData(card); return result; } } diff --git a/BattleNetwork/bnText.cpp b/BattleNetwork/bnText.cpp index 7f29998dc..4d3045a1b 100644 --- a/BattleNetwork/bnText.cpp +++ b/BattleNetwork/bnText.cpp @@ -2,18 +2,20 @@ #include #include // for control codes -void Text::AddLetterQuad(sf::Vector2f position, const sf::Color & color, char letter) const -{ +#include +#include + +void Text::AddLetterQuad(sf::Vector2f position, const sf::Color& color, uint32_t letter) const { font.SetLetter(letter); const auto texcoords = font.GetTextureCoords(); const auto origin = font.GetOrigin(); const sf::Texture& texture = font.GetTexture(); - float width = static_cast(texture.getSize().x); + float width = static_cast(texture.getSize().x); float height = static_cast(texture.getSize().y); - - float left = 0; - float top = 0; - float right = static_cast(texcoords.width); + + float left = 0; + float top = 0; + float right = static_cast(texcoords.width); float bottom = static_cast(texcoords.height); // fit tall letters on the same line @@ -37,8 +39,7 @@ void Text::AddLetterQuad(sf::Vector2f position, const sf::Color & color, char le vertices.append(sf::Vertex(sf::Vector2f(position.x + right, position.y), color, sf::Vector2f(u2, v1))); } -void Text::UpdateGeometry() const -{ +void Text::UpdateGeometry() const { if (!geometryDirty) return; vertices.clear(); @@ -54,23 +55,35 @@ void Text::UpdateGeometry() const float y = 0.f; float width = 0.f; - for (char letter : message) { + Poco::UTF8Encoding utf8Encoding; + Poco::TextIterator begin(message, utf8Encoding); + Poco::TextIterator end(message); + Poco::TextIterator it = begin; + + for (; it != end; ++it) { + uint32_t letter = *it; + // Handle special characters - if ((letter == L' ') || (letter == L'\n') || (letter == L'\t')) + if ((letter == U' ') || (letter == U'\n') || (letter == U'\t')) { switch (letter) { - case L' ': x += whitespaceWidth; break; - case L'\t': x += whitespaceWidth * 4; break; - case L'\n': y += lineSpacing; x = 0; break; + case U' ': x += whitespaceWidth; break; + case U'\t': x += whitespaceWidth * 4; break; + case U'\n': y += lineSpacing; x = 0; break; } - } else { + } + else { // skip user-defined control codes - if (letter > 0 && iscntrl(letter)) continue; + if (letter > 0 && letter <= 0xff && iscntrl(letter)) continue; AddLetterQuad(sf::Vector2f(x, y), color, letter); - x += font.GetLetterWidth() + letterSpacing; + if (it != begin) { + x += letterSpacing; + } + + x += font.GetLetterWidth(); } width = std::max(x, width); @@ -85,24 +98,21 @@ void Text::UpdateGeometry() const geometryDirty = false; } -Text::Text(const Font& font) : font(font), message(""), geometryDirty(true) -{ +Text::Text(const Font& font) : font(font), message(""), geometryDirty(true) { letterSpacing = 1.0f; lineSpacing = 1.0f; color = sf::Color::White; vertices.setPrimitiveType(sf::PrimitiveType::Triangles); } -Text::Text(const std::string& message, const Font& font) : font(font), message(message), geometryDirty(true) -{ +Text::Text(const std::string& message, const Font& font) : font(font), message(message), geometryDirty(true) { letterSpacing = 1.0f; lineSpacing = 1.0f; color = sf::Color::White; vertices.setPrimitiveType(sf::PrimitiveType::Triangles); } -Text::Text(const Text& rhs) : font(rhs.font) -{ +Text::Text(const Text& rhs) : font(rhs.font) { letterSpacing = rhs.letterSpacing; lineSpacing = rhs.lineSpacing; message = rhs.message; @@ -112,12 +122,10 @@ Text::Text(const Text& rhs) : font(rhs.font) geometryDirty = rhs.geometryDirty; } -Text::~Text() -{ +Text::~Text() { } -void Text::draw(sf::RenderTarget & target, sf::RenderStates states) const -{ +void Text::draw(sf::RenderTarget& target, sf::RenderStates states) const { UpdateGeometry(); states.transform *= getTransform(); @@ -126,26 +134,22 @@ void Text::draw(sf::RenderTarget & target, sf::RenderStates states) const target.draw(vertices, states); } -void Text::SetFont(const Font& font) -{ +void Text::SetFont(const Font& font) { geometryDirty |= Text::font.GetStyle() != font.GetStyle(); Text::font = font; } -void Text::SetString(const std::string& message) -{ +void Text::SetString(const std::string& message) { geometryDirty |= Text::message != message; Text::message = message; } -void Text::SetString(char c) -{ +void Text::SetString(char c) { geometryDirty |= Text::message != std::to_string(c); Text::message = std::string(1, c); } -void Text::SetColor(const sf::Color & color) -{ +void Text::SetColor(const sf::Color& color) { geometryDirty |= Text::color == color; Text::color = color; @@ -159,43 +163,36 @@ void Text::SetColor(const sf::Color & color) } } -void Text::SetLetterSpacing(float spacing) -{ +void Text::SetLetterSpacing(float spacing) { geometryDirty |= letterSpacing != spacing; letterSpacing = spacing; } -void Text::SetLineSpacing(float spacing) -{ +void Text::SetLineSpacing(float spacing) { geometryDirty |= lineSpacing != spacing; lineSpacing = spacing; } -const std::string & Text::GetString() const -{ +const std::string& Text::GetString() const { return message; } -const Font & Text::GetFont() const -{ +const Font& Text::GetFont() const { return font; } -const Font::Style & Text::GetStyle() const -{ +const Font::Style& Text::GetStyle() const { return font.GetStyle(); } -sf::FloatRect Text::GetLocalBounds() const -{ +sf::FloatRect Text::GetLocalBounds() const { UpdateGeometry(); return bounds; } -sf::FloatRect Text::GetWorldBounds() const -{ +sf::FloatRect Text::GetWorldBounds() const { return getTransform().transformRect(GetLocalBounds()); } \ No newline at end of file diff --git a/BattleNetwork/bnText.h b/BattleNetwork/bnText.h index 952c16c90..e7a3ac251 100644 --- a/BattleNetwork/bnText.h +++ b/BattleNetwork/bnText.h @@ -2,8 +2,9 @@ #include "bnSceneNode.h" #include "bnFont.h" -class Text : public SceneNode -{ +#include + +class Text : public SceneNode { private: mutable Font font; float letterSpacing, lineSpacing; @@ -14,7 +15,7 @@ class Text : public SceneNode mutable bool geometryDirty; //!< Flag if text needs to be recomputed due to a change in properties // Add a glyph quad to the vertex array - void AddLetterQuad(sf::Vector2f position, const sf::Color& color, char letter) const; + void AddLetterQuad(sf::Vector2f position, const sf::Color& color, uint32_t letter) const; // Computes geometry before draw void UpdateGeometry() const; @@ -38,5 +39,4 @@ class Text : public SceneNode const Font::Style& GetStyle() const; sf::FloatRect GetLocalBounds() const; sf::FloatRect GetWorldBounds() const; -}; - +}; \ No newline at end of file From 0703008883dbb3bc31f2375e4e4a87843902ba64 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Fri, 1 Aug 2025 11:34:41 -0700 Subject: [PATCH 058/146] Adjust position of text for DrawCardData. Now based on perspective instead of team. --- .../States/bnTimeFreezeBattleState.cpp | 25 ++++++++++++++----- .../battlescene/bnBattleSceneBase.cpp | 4 +++ BattleNetwork/battlescene/bnBattleSceneBase.h | 1 + 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp b/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp index 9095d3db6..7ab43b13d 100644 --- a/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp +++ b/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp @@ -265,7 +265,13 @@ void TimeFreezeBattleState::onDraw(sf::RenderTexture& surface) sf::Vector2f position = sf::Vector2f(64.f, 82.f); - if (first->team == Team::blue) { + Team leftTeam = scene.IsPerspectiveFlipped() ? Team::blue : Team::red; + bool flip = first->team != leftTeam; + + // Set position for DrawCardData based on perspective. DrawCardData + // cannot use DrawWithPerspective because the text positions will be + // incorrect, so this handles it. + if (flip) { position = sf::Vector2f(416.f, 82.f); bar.setOrigin(bar.getLocalBounds().width, 0.0f); } @@ -280,14 +286,17 @@ void TimeFreezeBattleState::onDraw(sf::RenderTexture& surface) // draw TF bar underneath if conditions are met bar.setPosition(position + sf::Vector2f(0.f + 2.f, 12.f + 2.f)); bar.setFillColor(sf::Color::Black); - scene.DrawWithPerspective(bar, surface); + + // Avoid drawing with perspective, because origin and position + // were already set based on perspective + surface.draw(bar); bar.setPosition(position + sf::Vector2f(0.f, 12.f)); sf::Uint8 b = (sf::Uint8)swoosh::ease::interpolate((1.0-tfcTimerScale), 0.0, 255.0); bar.setFillColor(sf::Color(255, 255, b)); - scene.DrawWithPerspective(bar, surface); + surface.draw(bar); } // draw the !! sprite @@ -344,9 +353,13 @@ void TimeFreezeBattleState::DrawCardData(const sf::Vector2f& pos, const sf::Vect float multiplierOffset = 0.f; float dmgOffset = 0.f; + BattleSceneBase& scene = GetScene(); + Team leftTeam = scene.IsPerspectiveFlipped() ? Team::blue : Team::red; + bool flip = event.team != leftTeam; + // helper function - auto setSummonLabelOrigin = [](Team team, Text& text) { - if (team == Team::red) { + auto setSummonLabelOrigin = [flip](Team team, Text& text) { + if (!flip) { text.setOrigin(0, text.GetLocalBounds().height * 0.5f); } else { @@ -406,7 +419,7 @@ void TimeFreezeBattleState::DrawCardData(const sf::Vector2f& pos, const sf::Vect } // based on team, render the text from left-to-right or right-to-left alignment - if (event.team == Team::red) { + if (!flip) { summonsLabel.setPosition(pos); dmg.setPosition(summonsLabel.getPosition().x + summonsLabel.GetWorldBounds().width + dmgOffset, pos.y); multiplier.setPosition(dmg.getPosition().x + dmg.GetWorldBounds().width + multiplierOffset, pos.y); diff --git a/BattleNetwork/battlescene/bnBattleSceneBase.cpp b/BattleNetwork/battlescene/bnBattleSceneBase.cpp index f924aa6c2..90d782949 100644 --- a/BattleNetwork/battlescene/bnBattleSceneBase.cpp +++ b/BattleNetwork/battlescene/bnBattleSceneBase.cpp @@ -1107,6 +1107,10 @@ void BattleSceneBase::PerspectiveFlip(bool flipped) perspectiveFlip = flipped; } +bool BattleSceneBase::IsPerspectiveFlipped() { + return perspectiveFlip; +} + bool BattleSceneBase::IsPlayerDeleted() const { return isPlayerDeleted; diff --git a/BattleNetwork/battlescene/bnBattleSceneBase.h b/BattleNetwork/battlescene/bnBattleSceneBase.h index d56622d3d..3fbd2e4a5 100644 --- a/BattleNetwork/battlescene/bnBattleSceneBase.h +++ b/BattleNetwork/battlescene/bnBattleSceneBase.h @@ -380,6 +380,7 @@ class BattleSceneBase : void DrawWithPerspective(sf::Shape& shape, sf::RenderTarget& surf); void DrawWithPerspective(Text& text, sf::RenderTarget& surf); void PerspectiveFlip(bool flipped); + bool IsPerspectiveFlipped(); bool TrackOtherPlayer(std::shared_ptr& other); void UntrackOtherPlayer(std::shared_ptr& other); void UntrackMobCharacter(std::shared_ptr& character); From a19d963060ea21d7286f50f667e586395f4066d3 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Fri, 1 Aug 2025 18:25:28 -0700 Subject: [PATCH 059/146] Finish pulling 1e86e04 except for intangibility and Root interaction change 1e86e04 removed the part of intangibility that would remove Root. That change was excluded for now. --- BattleNetwork/bindings/bnUserTypeTile.cpp | 3 +- BattleNetwork/bnEntity.cpp | 13 +- BattleNetwork/bnTile.cpp | 18 ++ BattleNetwork/bnTile.h | 2 + BattleNetwork/bnTileState.h | 1 + BattleNetwork/bnWaterSplash.cpp | 22 ++ BattleNetwork/bnWaterSplash.h | 13 + .../resources/tiles/splash.animation | 7 + BattleNetwork/resources/tiles/splash.png | Bin 0 -> 2058 bytes .../resources/tiles/tile_atlas_blue.png | Bin 51103 -> 23773 bytes .../resources/tiles/tile_atlas_red.png | Bin 22267 -> 25755 bytes .../resources/tiles/tile_atlas_unknown.png | Bin 19676 -> 23733 bytes BattleNetwork/resources/tiles/tiles.animation | 269 ++++++++++-------- 13 files changed, 225 insertions(+), 123 deletions(-) create mode 100644 BattleNetwork/bnWaterSplash.cpp create mode 100644 BattleNetwork/bnWaterSplash.h create mode 100644 BattleNetwork/resources/tiles/splash.animation create mode 100644 BattleNetwork/resources/tiles/splash.png diff --git a/BattleNetwork/bindings/bnUserTypeTile.cpp b/BattleNetwork/bindings/bnUserTypeTile.cpp index 5da3a8cc9..7c521db0f 100644 --- a/BattleNetwork/bindings/bnUserTypeTile.cpp +++ b/BattleNetwork/bindings/bnUserTypeTile.cpp @@ -134,7 +134,8 @@ void DefineTileUserType(sol::state& state) { "Lava", TileState::lava, "Normal", TileState::normal, "Poison", TileState::poison, - "Volcano", TileState::volcano + "Volcano", TileState::volcano, + "Sea", TileState::sea ); state.new_enum("Highlight", diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index e5a21bada..d4e9880c0 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -3,6 +3,7 @@ #include "bnTile.h" #include "bnField.h" #include "bnPlayer.h" +#include "bnWaterSplash.h" #include "bnShakingEffect.h" #include "bnShaderResourceManager.h" #include "bnTextureResourceManager.h" @@ -228,6 +229,11 @@ void Entity::UpdateMovement(double elapsed) copyMoveEvent = {}; } } + else if (tile->GetState() == TileState::sea) { + Root(frames(20)); + auto splash = std::make_shared(); + field.lock()->AddEntity(splash, *tile); + } else { // Invalidate the next tile pointer next = nullptr; @@ -1216,10 +1222,9 @@ const bool Entity::Hit(Hit::Properties props) { GetTile()->SetState(TileState::normal); } - // TODO: Replace with Sea state check when Sea panels - // are added. Disabling bonus damage for now. - if (false && props.element == Element::elec - && GetTile()->GetState() == TileState::ice) { + + if (props.element == Element::elec + && GetTile()->GetState() == TileState::sea) { tileDamage = props.damage; } diff --git a/BattleNetwork/bnTile.cpp b/BattleNetwork/bnTile.cpp index 77320d926..79396f2e5 100644 --- a/BattleNetwork/bnTile.cpp +++ b/BattleNetwork/bnTile.cpp @@ -25,10 +25,12 @@ #define Y_OFFSET 10.0f #define COOLDOWN frames(1800) #define FLICKER frames(180) +#define SEA_COOLDOWN frames (60*16) namespace Battle { frame_time_t Tile::brokenCooldownLength = COOLDOWN; frame_time_t Tile::teamCooldownLength = COOLDOWN; + frame_time_t Tile::seaCooldownLength = SEA_COOLDOWN; frame_time_t Tile::flickerTeamCooldownLength = FLICKER; Tile::Tile(int _x, int _y) : @@ -310,6 +312,10 @@ namespace Battle { RemoveNode(volcanoSprite); } + if (_state == TileState::sea) { + seaCooldown = seaCooldownLength; + } + state = _state; } @@ -333,6 +339,10 @@ namespace Battle { // Broken tiles flicker when they regen animState = (((brokenCooldown.count() % 4) < 2) && brokenCooldown <= FLICKER) ? std::move(GetAnimState(TileState::normal)) : std::move(GetAnimState(state)); } + else if (state == TileState::sea) { + // Sea tiles flicker when they regen + animState = (((seaCooldown.count() % 4) < 2) && seaCooldown <= FLICKER) ? std::move(GetAnimState(TileState::normal)) : std::move(GetAnimState(state)); + } else { animState = std::move(GetAnimState(state)); } @@ -546,6 +556,11 @@ namespace Battle { if (flickerTeamCooldown < frames(0)) flickerTeamCooldown = frames(0); } + if (state == TileState::sea) { + seaCooldown -= frames(1); + if (seaCooldown < frames(0)) { seaCooldown = frames(0); state = TileState::normal; }; + } + if (state == TileState::broken) { brokenCooldown -= frames(1); @@ -929,6 +944,9 @@ namespace Battle { case TileState::holy: str = str + "holy"; break; + case TileState::sea: + str = str + "sea"; + break; default: str = str + "normal"; } diff --git a/BattleNetwork/bnTile.h b/BattleNetwork/bnTile.h index 0d67aaf65..194f55338 100644 --- a/BattleNetwork/bnTile.h +++ b/BattleNetwork/bnTile.h @@ -341,8 +341,10 @@ namespace Battle { static frame_time_t teamCooldownLength; static frame_time_t brokenCooldownLength; static frame_time_t flickerTeamCooldownLength; + static frame_time_t seaCooldownLength; frame_time_t teamCooldown{}; frame_time_t brokenCooldown{}; + frame_time_t seaCooldown{}; frame_time_t flickerTeamCooldown{}; frame_time_t totalElapsed{}; frame_time_t elapsedBurnTime{}; diff --git a/BattleNetwork/bnTileState.h b/BattleNetwork/bnTileState.h index ddd6bd1f7..06a868012 100644 --- a/BattleNetwork/bnTileState.h +++ b/BattleNetwork/bnTileState.h @@ -18,6 +18,7 @@ enum class TileState : int { directionUp, directionDown, volcano, + sea, hidden, // immutable size // no a valid state! used for enum length }; \ No newline at end of file diff --git a/BattleNetwork/bnWaterSplash.cpp b/BattleNetwork/bnWaterSplash.cpp new file mode 100644 index 000000000..b202a762b --- /dev/null +++ b/BattleNetwork/bnWaterSplash.cpp @@ -0,0 +1,22 @@ +#include "bnWaterSplash.h" +#include "bnTile.h" +#include "bnTextureResourceManager.h" + +WaterSplash::WaterSplash() { + splashAnim = Animation("resources/tiles/splash.animation"); + splashAnim << "SPLASH" << [this]() { + this->Erase(); + };; + + setScale(2.f, 2.f); + SetLayer(-1); + + setTexture(Textures().LoadFromFile("resources/tiles/splash.png")); +} + +WaterSplash::~WaterSplash() { +} + +void WaterSplash::OnUpdate(double elapsed) { + splashAnim.Update(elapsed, getSprite()); +} \ No newline at end of file diff --git a/BattleNetwork/bnWaterSplash.h b/BattleNetwork/bnWaterSplash.h new file mode 100644 index 000000000..bc68105cc --- /dev/null +++ b/BattleNetwork/bnWaterSplash.h @@ -0,0 +1,13 @@ +#include "bnArtifact.h" +#include "bnAnimation.h" + +class WaterSplash : public Artifact { + Animation splashAnim; + +public: + WaterSplash(); + ~WaterSplash(); + + void OnUpdate(double elapsed) override; + void OnDelete() override {}; +}; diff --git a/BattleNetwork/resources/tiles/splash.animation b/BattleNetwork/resources/tiles/splash.animation new file mode 100644 index 000000000..ce716b4ec --- /dev/null +++ b/BattleNetwork/resources/tiles/splash.animation @@ -0,0 +1,7 @@ +animation state="SPLASH" +frame duration="0.067" x="2" y="101" w="26" h="12" originx="13" originy="6" +frame duration="0.067" x="2" y="78" w="44" h="19" originx="22" originy="11" +frame duration="0.067" x="2" y="30" w="46" h="20" originx="23" originy="12" +frame duration="0.067" x="2" y="54" w="48" h="20" originx="24" originy="12" +frame duration="0.067" x="2" y="2" w="48" h="24" originx="24" originy="16" +frame duration="0.033" x="32" y="101" w="1" h="1" originx="192" originy="192" \ No newline at end of file diff --git a/BattleNetwork/resources/tiles/splash.png b/BattleNetwork/resources/tiles/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..1328a6ccd1aafdbb8ee92b67c753dd99806768c7 GIT binary patch literal 2058 zcmV+l2=(`gP)G9enMd3z=4JRgoTCk35A7)0}JyB zfrW;JhGAadcT>fY9Xm}seY?5m%$YQGEh&~|`C1`b@P}Y`yxPA}_JXJ4q>aUJzkE18Y_8L60-lW}DE#$!_8iJVG$?SKw6TbKRaJEN zrU)IALqt?nMHkg_&1}hey@V}(&;EcbSGGjMU}`!mXkL2 zQHyB^kcnjkiz|^l9Y!++B4-%KvyAfHK4u7zq!u5*+uU9)fhLic|Sxq zx6-=pCVfhP%6`&(BEmqf07qyf{U^_V%MBpPkFvA4PH3$PDU;iN!GZ<HDMM8^N=7 zDTC*kh2PgyLxa2=?;O|2*YFpc&`|bb`qv>err$T0Dd`kK^eON}F{b#vd#ABJ{6sj0 z!3gu^&-1a&L;c&*!~b z5fN?n`oBf6=nT_z3;lk-KX#0>J6^>V`&qwVuwcQwn6z(4WK(LMb0dFijR={$1`$m# z2n(#g;-4F=9qEf9`qH``WbRnca*awZUIHXza1#Bg$`w~Y3dj83uj*~}saz3-pDMkV zLk~#7Bo_hOgE}Ch`_FcG&WO>PQq7Y51O8-r0y$P4l>=V=#Se#xW=Kvs9m^*zgz#PM zTTu!RN<78$EKPU2XoKb->hwoPOY3yM_N`&CIsj5OOVP2Qz z3dM)e#n&-v5U~4@H*dTsyfMZewk}ff&Qm5cZ?ijI?WZbNhwa;nm%N0Em3AND=r`cy z{HIOS52@U>@Z|ja07juP#`0^wT}q$Q4!Eic@<%E-LOyLoG^}=qKz?5usdZVd__fp) z*SueTpSxp>{~K(oq!HqV&PCueAGN%#5u<@_9%m%5Ee-ag><04_-D z+h&X_c+$qk7Qfv`(aYQLTc4j(%E`>(=VsyMxJ&GgS37pZh{?`FINo)*m!C^EDCD;i ziGKJV=68eszK8QdQhuqSTd?2{fV4$e>f)Xa{|ze?ZQwg>>hOp5(u*RYx3#7q7~{X{ ztCi9X+&^h!`H>@kH!gOL%HQn+lyc<2+02fkO^Z` z`|WZ$sTK`_;7S+;(6o{H{(wW2|pvOjkfWFU9ckQwIOU8c1<9$*_>?NpgIB zV(!<=wq1r>3?^=O0Nf0Z>2WW|<+w3EA%8o3zR5zsNjVu07B5LpijM{_gq>PG!|NIN z=e24-&RuoAf z%18xzMByhYDHj@TzYktm$ldx@xDiLt>djtTrEUyNC`8X)5$qdM*DYz?b`>dOh&F;z z&#s;0u9jmAQG_Jf?6uu)%iah{HK5o^U(C=YC7hdEKZkF*PXvjoDrmbcliiId!YQ2h zYIWYbYmC%peG>{-P^#!xV6iqF7+q9bp(5*eGz8a1Reh~7G7mh~$NI!w0pRs*+ifY9 zm%(je0NV3D(B_^K!7LiCvZ}!^aepNX_a+re^MAY`m%|d-mInI9=V*Z{p7Zxm7 z@L~uN7r6gbUoF4R>En^Oi&OBgiKcS_bKx#dVNNb!E_4ekXe6Tko?UB~ra)nAyYZQC zqc`hfz&Q}zT<;kpHkng6H|^jvZ%dTN1Q#kL{%I!}3)s1-Hlvf*S?E=70 o8IuL8q$za6hv|uKm-AiJVTBmIg1ENPLiX9u-wztjD6uO|GNhAJ@u0|5X4AbzB#^c(=d zM4;snJS_B8XC5~S0Ko9DRaAWTNKujPnTz8q+c#DK0QXR0e6xBdn65jc_OZJ0W@b>B zVjK?%9~F-C2LiW35__CtV4tXM@IB%rst09nBNf}_p$B&RatK`~T{3KCGUt8%eY<#y z1v>)1>>ziOdDfvd&jxRjeQVgODVYat0?g_Cxp{fZRa8Fm*jn$%JHiIu*vZQ2w`Hsx zk;rN}CS=pzh_iE!zY5@2lJ0uAOEyGiJ;|)TFLnSZ?&VxoT*l7FqA2%#ZtkAmAQulWTDM5wA}f6{)q6Kc03ZwyfIt->QRek1wPd7! z$?uw}-}2$VDc}Ca^PXKiLxrIhz&33*i@n^vb%2U&Cw)K#cbgMl<|n!WXT0QY0Yo5P z!56wVPrQO@fyEfI`UW;cAXx=cV6Bj>ZHjFooUCGBV7G#-_b1!pVx3J}vP5P8H)<&C z1O?tlq4pP1`v?@O?Q9W=LM)Tk_cQ>eC|sDgbIVh>et ziVEA?pW5GOLoF6eouPWwP(uNzg`Tr|HB_4#YVjxA6biN2i$VmTkN`j?0AK?EpbP-0 zdI56*G5~m=%w~!$xlN+C0w927%d7_&Op$r?0-hlN9=&97ZEOV<0H-!Uas^pI1zSY{ zAg%(hK9jDkfV-^#P@n0vh-gFYqa*%Jg^(RObAvwD(Ek1Z;t$=^g{tTp;5(@qx&iW29TdT66ZIsV}_Ga5del#Bt9^_e2WzI-}>6qj7EP*R;7iGc%Xp zv8rWc29uf1e$IF=QrT<93cu>X^7hVW$=%s>?*1iFQ|IrbHZ!>r_0zec)!#$uFR{MW zANu4kvHEx2?5^O=4b7#u(a$Gu2zSzBbi^ii>8*q-ts3um(T@=v3l)%IBdmvZ({|{X z$OZtuY>^!bp}KpIjodTQ*J<-i-{c{knQ;uKijkc|0lh5)#>j^&KD|nf8-qI;31=oR zfqT5dHtrMmcii5LIP~mBxRGk^QLc>UA9y(JBW$CN6pPg0{a5KrtZK5pA%{7NmRi!j z_c8g8yw_0!=PSYI>r={ZQk5kw z3AO4#S?|+IRaSN39xV?d5{fi1>#r29WK2*@2L_XUDC2%b!k?4wk!ILoZ`F8hSeg%U@G=^=QTKq)VNBqtvV@bfwMk zP#BFM5N#&KwXU~4JK&lcSg{HDnD?krbJ1)RIG6*sZA_X{8U^;3y_vp#En3HjV>Gin zS=2DcjhJ@JiCA~b0T=JQ{UL140`gryww2l1yKALI?6Q58ts->w07e4~&6S?%@*GN) z^IgB9pZ3Jt--tNc(9B3O17j_yvKA|m{)+Em;T*>pl86YvQQv}ln(3H# zQdX|jH8Sees>XozUdt33U;XhdWm;laooTc%mh!mNK2GDPBmqrxtyv!kT`u-pr7lwR zy`CX{7as&MCmwZf)G3@(mzEqNR528Pb2g$}>5Xqn5M*U}XU9*}APA=`e)6ULnVnhr zedW@C`q~-fLqgZ5wRYTr0<7}+zuT(qw07KZzL4F?e|*l?agJ*+2duz>7j$w2%5r4p zDc_NOID`FJT5cAXVNg3CPsG!&np7l}R8}ps@93p(=90Ac1o)+{Ls)_+;Jpo2w65nP z)!=|`144;UFfA~7&J&-r%6n$@c&WfCccS+%lG=Dj2@PoEX=XU}X^E?!g|H9yM5xCm zJgntS=f~N~Bmm==d;3V_mql)ZTOenfQX`mt>ifRPr&?a-FtJ{+*=FWk zd2Qww@{L~0MzhNc?eW!qf}%&&GklD7CCiDx-F?+q$X_Mn*8cpfBv)3Y_u8V*@kU)G3Lez!xvRbi93S0B#4F zGE1}v6cD7oihmbhG7YRFkryGu9%5to;>Its43*ZSZ@mZy&7VQfa>3 zF<=Aw#(!i#!^RNF5SSJP*vVol-sgU+V<}%VJ!jpxZWxPj-n$zVd8JV8Up!xMPjToBq72jG%E8h(ZNiz7bc<#u`x%*VXWyMqXgmGrS?34jOakjJrc-_qk$rLIYSb$^^x55JY-q1D5*tyk>x} zEB6*vWM^;|N^(Q{Mey{268^L1S-$Xh!bH5a!bKL1#0b7G92U4>yFD@z`tOE<18mpxFyaUAO+8P7)>Zo%!nRhuDO$j@tA&o<9$0^x{noU0`{eUD2CQEULPmyc zej4!69#H)TEAyoNyHI}ST{kkuEj^F>5N8Dgou@=8_-||!v0vwSy@r-)hP{0zSL zSwgDwhcLPC+ilm{$T_00cj9ggYR~rw_JTY%ZJn$#rCb`GVcC2-`yR-MihRNlSPQKE zrp0eZdi5H)eEy;b1@%$zc`9VjtHv>D9*A}(hjS%Z(rFPH(?Y>F9Y9o&m~w8xMXBvpbH5O5c2XB~C)baJn|LbR1`qcH#? zL~QU`Q;1imnY_;pr%KPMtRMbxpo(XofIaP8w8x7i85!v#Od7IM(k}z;!7GCKoaGd+ z4t9c49jc>fbFfS|o0bIPO;YgF3-qErWEGj+DGJ2O1Ueq@4XaS=yKJh7=$3Hy=)o0w zmbaXX!95@NlW_$I2;JU-94LVHP}zrhDaeWeTmyrQK1M^1HX)1e>Iy$`wkayq6~ z%5lp6-boLf7H9p2lgRIxMuKs_B%w5pk5p!Ucd#c1v)P5XZCz?C_-KnXR$jnG!^9zQ zVwqSVWm|r|zN^;_M?`F7_OWnEo`Q$8Aa8zyrcKoClU5ssy_9dfFuYu741WgG*|gyf zP`nQ0GH)Qg`)$=|gL2qOEl;kB;l2(`R-4ocb|k>elfJZoA>+u1Jl$6JAA6V#Gs;qS zAoFeKOb&MlxHFXbflcm3c7uShyKVg%Szi+BQP!_n&Rubj(%uoL;5&)L;UTdS{(SoL z7qyUJZJJG`jauTf1~@_Y`)bSzHOr=Xzk?S)Z*JhO}-IX z)MAMH?W*O@8Ts@RReVWP3YyFp!PaaK+bXvXn@Qi)sW+Aoycb@xUU<@EAr4NNRj^nt znd_voDerxOv%{ImJ6HigmK5Gv`{>s>7$?Iy_$ z%8@)rfzlb0jxXN#E9vlNG`=1K=bmL#UMRMI&3L5`LEj34|Sr73u)z_`ZD^bG&`IUF<5TxHTVkn_9 zWFLY{^t*Lcs1X-6zv8f+Az^6I0wh(&8L*7qB5~N=Z#GF7<@v1vf@Z{^nPva zm(S)!oT|z*#&De-Lt7jIrN1VuDoU2un$o#s9y z5ZV7#9$QAQ9?^9`w=d0`P+jc-1Y9xJmI(bq=$V>VP1=2qqo)kLb^Bs?f?t)2U5;~>;> zbHa^ibFFR6=G~E{CK@e?#3YthZB&Y(vxCs2AFu(3GKSo;f`V+m@5$+s?Hp~WH_qzY z2-5t(o+h0SD1cx-%`T?bv9e#3Y+Y=%25k^aZ_r*P!PW?G`juX$&Mi@A3l8q?6AUBE zaX(x3J)CSL)q2%pU+1G96cU_5@w>X)5bAtN2} z9j}8A*L8 zr%g*|Eb9dhMppyjn5P0u8OYgZR5Z|(bVu#EGRV~R3;q(WA+3$6&V~Ghy+?k~ z4%Gc|(w-cuIZTq!Kecc2qwB|;C*J=ziRPxcmCm@eQObJr?1h%(%gv!`>;Idyv@nMQ zwLbe`f8)15S2jAnKdtLzs=Lqy+QCU7B~y8&y8B?yV(!ti&hc)1!z(`%1$Okpi8nJgCf!r)*WuSBeqmpY z*y{c_Kh2q$WqvD6CDN9eytv3_vDG)4Q`he_ZMit?e_}Swv9qRkwk6(Sdf9DPF6ax1 zt)4WRV_YEeK1y+H={qiBv2_d~DOy?Sk70ShG90>r$l6HP-m!{_Y6r=-EF^_RDH8wH@Rhla|>W<}Yyi))%}R%v*Y^zWs-~)&ED0`G?9_ zUITJ_>qhw;F01Ez8~oUZA7mo?Y_$&eND>nTo($BQ_8`7c}_IQRnhzP z3zJBz)7tq)xf9+a5gvh$L;7>-9d!Pvy}TsTb+Y$1I_3|}is1Uef;amM#pNyCckc}s zv>CJ*!5r~kUM;nX&wC^&s%2{hR{C_p6_4pwDKNeb5UJfKe{2|jL-6q62{xIqFxw|3 zZY!(tKoj@+ zDVkx-?O91#LTVJ@)*TkJrnjQ1Zz7S11H*#0@*+kLpEauzNeg48(3l|woEu^oR&04| z$iE=1g$|1cTPX&v-)h5X5UQ$^T}LyHkVe1jX=Cnva{mI%;mPeL&eO$fzJl_suqWAO zJ<7+QkLo-vq`QR!6JOJUH!hysd2507^x<2Zw~5=r^mq}(B9(aArAPm=Z#T&%1ZGZw zDI?jYLFGf1Y}{gqZ|q^~Z2{J0mTQG!Or8RC3QNgmRG~4Ny%#!4G17EIS#coQyW862 zl81noAGve?ox@yUD$nt;`@Qs^K?)*)>&FG^$n@P^{4^EjX7j~+pTVwPW1LWa7z6D1 zZD4{8=8sLSBt-L=?btBk9gS>!k$hE%!rAkDEmeLxiSYY~R2CTi+|itBrL0-22Iv4_ z(ZG%MeAPg{s#3*!-r!tJ&)IxHCl1qF$DBEYzekzPj+*O-NL4hJ&ZBH%k%p(4i7RSC zofKQ?zTM{WGuTFwG53TYX6*)VlycMwV3abczNi2A%>9IOyVoA`AKF>8KH7<2?ZWiv zm4U*X_`-f0o6&WoEH+7|qA|%wBAV>+R;1!0093tZ-Um`D_8$&y!+_fKx4(yl>HW?o z4a7nsJ_d1ala<~mvtr^RviNY-(8qtMxm|A9^B=gfmPd2#y$>4UjJ#zRt@S`JFs2sl=Q5@9uH9_+RcP6}LACf_M62zj+ywrj|Y|-`T$Kjd_>D zoKg6f(qR+{1Iad7MKrw|yQc~+aVQLLs+&kEUOFczq!V;oWaH^=5&1dti=}WU+HCcQ zajypkT$Ap0$VJuP?|_5>E5SvA!u+O{BphXEz?1;|8)jkmK+|CrjL!M#yGAmIMWD{h zSEl|yK;AiZ3Ego9aT!%;)lIB^dj23ZX^=(VK+VYXjZm ze`%pHG?HHrqW`Y^z^qQP2xeg;v*{eh+{rV@!o|a$cB%$@KhQ&BU^`h%ns*jJM(Z=$CTw#r$tv5+D9qYD%{W1`)t|8brX- zknfR_zD;ej&ll2VeI81XodI4En9vpz8AD((2hQUn1lNWRy#!Q=yF~*_T-PykX)Z(m zPI!My=J>I&M@X58v}wzmIchV+>kE2mMSUPkxk=@&)xT6Tzn}0)ANk@<>wO|fgNM(K z==S_EPU)A7c46q+^1%=B(j!*{p%51T(Ot#XD%Obc|ACy?1KJm26*}g5CEb%06~D{aU|>F8mUR_A)u%RZ*IML=7DUi}nPk!X zQe@6^hd=i%oH!D~f9Ci>sjx+7+w&84f%xNW9kZg7rYD$VjGS6U{NrAx%ZqrBhaBPW z^~6p&qP)R*L(UO#;pvHf@0>{sj+HgYg@(V5#0uK7efjuoMyDKdalM+#aT*>)hEEj>KQ}9) zcG!=i>IPbBZmHLF5T|?IX?aCSbe0SFkwg>QTxRzfCq-EM4B=yYFtrW;Vpt(53?u!Y zxmIM3<4+OTpNm~y@{rs7%F?#Zv?c=6GG{?BFFv0ku;GFv>B)=R;zHOtfH^u`%_sXO zjP!4{0vw`(xbCAaqKVqM!Q$QRl$1W#Dk0`nnT#uo;rK0n9Et+p34R#Tb{fs3##(b? zg!^$j(Al!h4)%O~AD|>hB_1V0=CHA1_z0ZM8o{W-fLC{PP;!rLn6!(SgZSOdxrZJ_ z+~K!XD(vA@{HzjOLktiNRq&5bWZ%8ws54*6Ypwiy^VDBv%xD=L8=_?um5_3?IucJO z#+dlc=y+@ffy?Wt!b({nL5;$Z9N){KPuMh+NcdXY)aF|PMO5}9bHTteW0I&eV1dB9 z^5lOyotGs~%2WQS-9kOa?^Ps&C;4z!PArH%$eas7jwbv(PAJ;IKNI*zMICoS_4zUW zOGi|K$DtK`-ToqL|0q0cu%hJCGu(KCe6ClX?AgK(?e34s-<#MBH%t#h67_$^hmnQD zyevL)v4c9RUMVmuS)`>Wul=pqnNYQoCJ~`X+Jv+7&coq(=MSa$RS@Q8vMv^e-#D!% z3r%I7N|IIh)#z>Yh89oAQJ9M*rOg4;m5c8Ye4Tu)_da4-618@*4*S9JJ^pECm+OB2 z$&c;NsvkIv!8TC`lmI=RQ9$V?>B}qZfW+@2V7v4VIynsUJRxGl=No};O@Kk+6Rhel za(_XEJuJNT8$_&sJv_|QQ8MlcoLqFZ9h@>;RWA&yjln8!-xx`){MwuJoZOxn*EDD- zPx7U8JbxbhG>D|>PKLT`_aM93W94ettd7&Dp!~kQqlC-aI3xXJ*-+fYDeSU@W9Vo# z!JC3OetxY@hYP{P|GZ6KCWfVl0^AQ4T44g3I z6_sW9zp}1^iv_vNm?OBjy%mJA|Fim9&hm+;C(GA1ppyz-^#J)-(sxsh*2kfc#V9hH zPaLmU@haVikfe=m^mh?LssplQ7JKQ#tZsqAZ%be?$-Zpg=4Eo^eqma_1NVoOOd!eM zuF{`igJ32h`AR|7WqpMl(_ihXC#=2h9^&${e_qMY8jWb1bYSSN_N!pemh@u)mE!sA ze0lL?`+v(VB4@BP(Z|cMo+|0q18(FP+P@x9v`=fwelhszr8`lz)L|2B7UZeinJV0~ zOK0Q0%DG(+JB$dCw2qk*MC|q!PdIO{QWLulGBv);oQoGP82uQO6ihEE)gFthTubvA zzP4!fLS;~6e?>mNH28~yq=#X0{8!2=k7t&4S%t0#w7xrhhsJE!ag*om1a64Qa?g46P%hQ$SYUoHwHUZiaaxZ|VJYy3OQvZQi0>fZ zg@NT%vqi6su+_e5$8fWxd&Us6{%TeGyUBkTuCG@8#VitF>-L?zt5dz);0zQ#acoD< z_CrH5+r#4owq5q#=4mSp#>lS^91)@7WzXGv{gBGRL#n5NU$_c(gf8w@pw?-m+BY~Z z!K0L8ZaCL*F^&$Els>Xmq#`o%_WU`lBL_eD+}{4Pgy^v%sHLGxWB-}}oAdba5_aA~dR zCD#)~;D^)O69=G>(RO9S5LbZB5bm!Z#beCBjtJdm3Ca4l9k;jL0zRUu-ow)C@^#xq zX=LO2dh!pDAX9xW6Vw-x@@oUwQ|ute^*(W)@^kw}tSGjykDIy-*eRBbo~Q0SO4U!w zN7$zB&Gfn=)&5)gYb&LBHo1hmW@S~pwT!0LW$JD^ep;By;;c;9ca`)LY2}^BLds#h z)W&>+pS@qx^9y`AVeAZnu4h!djGhxmR5j)K8cG~ayXh8I97A5|*{84P=d}9;vh}&H zh;W88Qw$G(?db&AsFYd-4{3IjX3L9zU6q6yg^)LcmaMtM?#vCm-$2;Xs~m5=%gAD5 zfC{UpE9P6#4Hr|22n_7n^xYjAdof=Rr;z$wOA??ND^T`b(os`8Z>XpJi_>^d_`_%C z(3nV`=7fn*})!bvbL%a9);Ew3s!|q4fmuPI#WwRmHP+|Y|d`aNO*qWc?M4K=y zRlK~X@;;@844h=!^;2EGmQ~%)Z#iuf-rkFnJvep^N`+ynk1AunLNKF0Hj@o7PKz2K zJntsg>1_m?&l{!DzV)<&nc}VUK|@vPKH+&z2mtR8!qK@Ay6V?f@&M*c0~ zG2bT<*0&!a9WV|MMLfM29{yJ%haWUKi;mavjew5ChGE@ZN$<2W8z5M7H_5~E+#+(X znr2{3$)|Op6Uuj)B`zE$otLKMF1Qib9!=|Ur#$) zN9`YNghzBNR5*;@I23JHOW}N`jr8xDV|9lgvdwIE?8Z5`>kn7v$&IG}PgAh}Uhdva z{b=;V>rbcuZMte~jNSc}Ugf>vi%gi<-16Ef0+o;Kam+zv9S?@jEL86`?Lov1UZ>)ium2=Sk7nitT5?*b@PT| zGaOy=VPBOx`fkrG+zk4YUJll^Mic0{hQKT@C*xxEv<+pPIZz^~jwsypTITh`oeo+#Fl~=K5tVcWrBSc9ijZ`FF_m@`fDi z7KvnQ0eA;$VlYHcgQqJ_pA_RD1Z1AGGy%c;>Y>mC=S}4Ol@X+bA8zZ_n$%v?(V746 zv)}uqzxVr{(=EFsSPQoFTQ78-#n1JUcFlfYOe%jq*Arg>dTs-%H)y}=-RIkpN|rjs zw(h{!*5~5e@lI|mEYmQWCKYhkhNM+jv%c8<0teaJ%C*lvg~3Kf{6ZR1wzhKft}$Kp zp`ntk*Uplsv}cNwtVvmAazI6hzex!Erk)6P?M%OQvBJ*Wy%#{nN3r898&)JcoMqfb zbzYPLOKL*zA7AfR`{q|BgHFG3eHa`)ZSJ<%QUPhskTOITGHy!S%e{X1#Ub~o`Z~8V z86J-YeEkQ29}Q5S%i3^vHX(WojYPjZO;6r1UyOQ=F;kLCodhu2!P%rhY7wp;Ht)HC zk_ZIJvQU&dcf+^QdzrZ?l!sb1Ec|{`;O2qg(1(-4ejbm`XuR2itwJosrBB+m+oxK` z{ts5n*1LPC8UF1S<1agnLRN-7pX{Iu{G+G7pQ=h$21MbkA8jwQo)}IzuQSZW_E%pU ze0jJn;TpZ0Z#C`5qx}(<+MsfFBxEm_^q?O!mcpVy2}!AM7&*?2IRfEBbzsZA`M=WJ?Xictu=vOnTR^VCf*3bQYACUK!e6< zqQ*~{t;>)S|FAY>uQZJte(f4_UXO@5(zE_Squuj~_ul{|wvJ9KO){}WyW{D&h6i-BDch=mo!io`iO4a|hQm8me8Yw6E^E5!4K`FK zV+TQrLXLtjyS@*c%WVHc$^)+Ry(%&I)G*f7)D=a|E1U#Ru1l`8uv&jkdqg9wS2AF- zWifBj9g+A+ea^weGRL{lI2`Mcs#i=2OK$VMiD#mI8Ce?q-sC*E_AUtasH}N_cSA)> zlX)-o*nOenYGA;4VAdT$?k9;28WKgDZ-x81$M&J7^sHpQVe{2!Sk+86%psQTr zKS0+d{{%%A%|u7qO8{{n0iA`02d%E zl9j2Xc*F(f@x^$PRFj`*2E3;De5@4mBi~`?wJgj||ESQo@v|nn5IW#j zd+L?#&jR@VNP}@zEtoqzo{82#2Ic0!?r}b^j!&awU9-2bXl#n3*z+$fXJx>4%VHND z@EfJW&g&vDLHF-fF-H?f3D?i?WGQsud)h%$C7(hD($In5B#I8^FTiyTbl}f3ple{B zmu6Zbq>{GZdo7i)h%dV#Dex_oq33HyR|JRx}R;Z=?%9sI_f20a@6`%A9Wlev+4 zymtt`X!TX|6DQqPCEL;c29u0Qm1W|mE17#pCoO!j^d$$4 z5#%<5`)}npDCf=Kog8TKg&%Rpb>1kdYslbLDf33b=bD)uO2jk0NMx}*Emu=HG}(N> z3H_Dvh>>kpAAW$sc{&bS@d<_AfnOhFHES85;+LDtRv|;L+54-vc#jXcdwMW4eTg92pKZ8y zWe)1Zn0Exdt&w)VV`mIHd)ddveGRFa$>9j-*L;HEF#^Ky-8J#adObmxCFNqaDd2TJ zk7$yo1~@3^`+a*eXf(5M+Yh-|0jkCgmq@L_y}I?*vtoKZ)gp&o?^aiM!`}7a=6cJS zRDR0k{9fvR7{Bbsneh4UC0XKwkvik&^Nz)gK7sBZ9AWAUm&DAM&ec?`eGxY-g+-KR z#f%~%-|OIcCZ@+zy`XfjW>R;xV}>njDC%(^57Z_Y)lh zm>)rYC($eZSC^5%6h)2a8FV})kwqkex^)2jkG`ktB z|G*Hpg~s#7%3FWf>0W22&v;*;ScFYnQViJ6FWU9$jRuXHUC223X4GGDEVNtFPmGH& zR~59vB$twaGs2PP9Fy^*4|6kqP@-pO-7@f4%{q0(+O-M{f`u*`CXD^wVY)YBrMp+> zTG(H>ckKp*8IE7>q(W6q+zrQLNhvm~ifGNYgkns63j2#peMXpxJl7@DRHL-a_IO^C z)1&1L^1*x!oa(lmpVk|m?~cYWY_6x>*Cv1b3WM^tIsoB`X5zGS-^)`X9nePQy2I`D zY~yp7Ftpww=8@M1s^SWMUcq^XV)H$bM-as-gT?bCO9Z1DQ|-50U@;QL(qQx9WMoN* zdqqN)=YYB9K%N~EzE|+geXKhP9u(Ph?D{ zm^4@VcV%A;QM&s>-3ii^_8Z!vn1h2kn@`ijYmdfuc4emx?oPwuiHqKZDsLMUh$`Nr zpQ-P;Mj%*t1kKl16QHFdv)P8a==|(&a&e$K$tpGu=Q#Y<x}yDsya#>%K=K;nA2NCHB%QSBz&~0qJxK32BF-$e^OTj)lZ|nbK_5tSKXW3a z9A0B_2z_+tulf81@;%W%=5xz;Now3lCxz-l9g$70hOF=$3Rb7vN#D1u;P!DDE`}L! zij3wc&4v<9NcfOxcfQF)D6o!V`KJdc>0~WNjhl^X_>uGv*Rt_e%z}v;X-b=;R6Ef! zp}W%TDOjQVBe zGkCI0=*VWA?wo+?VAL!;j3R`*;T-t7|ECCd+|2&{AttAB+RUI^sZwvnunNoTjbi3D zMC>Ugnw0pGK+B+~LVG3?`M`R$Tbkng$=o&GbTzI20x_;nHu??qp&w0Dy1B}iwSnkb z(Es&%{9tRJQz6;4D%rsKd4gtm=mfs_lwzm@jFcHZE(1mCoguP}=l&S@AkB%1HWsE0 zt9e9Frx)di-#Phlkc;fMECtWN?EaL=yCl!GS?(=G{WDZ{xaZnqAAIwFbYpoy#z9{( zD%mxsfPc~2=TN9Tl@T2~a88B}HimpR-yME}d-|qpD|nOn4!)$!;Jx{^%*r?EilG?C zRLlwY*FNz!iGUj;(4l)PL=pm+rFpH<59ONDJ>(fO5sA1!i1J^ze{~UUA*e1!M&CxT zZYGDU7c;BNF!vTOqU^oC=X$o*n0_W>w)MhV#b7p@*C~W#`&rvhPm$(9UfPq}h&8gk zKX@Qp{j02{|Nfhxv%XT^1I{6jj+%o+_ZLDftMr=nVl9WjCpyF1(&*c6E7rANA|`@+ zl~z7{KME`0UwMl;IFuvF_NY1?b3l885BHVFL4tyuVy^L3ZUvgtJfA{yng?&toQ6sa z-la*vIJe9dJjVQbt!)TWHyq9+|^@<>Ljypo>!pDA!e7m@In zItZZaQf$1;2}Sly*I|5Zz0xBn0v*)AN!|?8(92nPYlZCqd)uj?)HA1Dkn4V(I2A8?wIjBP3cj*}Trc0?Z()nDm(YAQB0f-7-Q{I& z!!#U!MuZxTTOzG4oss^bNB5b-nN01oxv6HxN} zgu+BWr7g?Xbx+0bI9a+41c0jei)S!osq)cynpGktj&up5-v=_eQh} z?hW<-BSv*4r#De%bj0II2-=DMEPkkpKKoQzn>jObS<1M*n|MBl^0g6b@=*|P;$xN? z(A2-YBM+dXW^R-LksKd>v(2pgG6PMm%adAira_#u^fpym;nq_#>uk8W|9taHeM^ja z!+ul4BNbm{8genuW2_f|$a}f#U0kXWGkB(F4AFn*gN&J4$U{xN#K41q)|c&I!{6SF zmO4CWL2Q;K)6=jl1&J!hR0G}tJ3BZ!c(jV9w)~D#{4etTFE;#dtfg;MCa8=dA;GGt zcP6M5X;)@r%hO`>Q0o#EL)i}poy{|p4c~0IKWJH!Al=CP(bx)L*x9m?^7$g4PJlgi zoO29~D+fu!pX$%O#q-=oWGJWm|D2~5W;#01O=Ft-QtTlAt_eoh)8YClUOt{+Tj+zr zK&Al<-|4Q`vl2K_9+@z=(T1~WMEPKki;r$n4jEo-gl-*E28?AR4>j%FU0DFMW>_6J z!<1R{+^sV^DNClQ%y(_Qew;=yJUi>y`h2n*YT`iK!I7G@YtwsV;&Y2QSysPwoA?wO zM{2@@z6)>YX$$bI&k0k;wV6ME0>Ql6R`Sq!0@=j9iA$l%8eG21eUtQ{lp z$+6hFj}aX)Vw;+@QoJ&dGdDZyQ_%|W`u$9`Sn?)7;;`3B<{qBYZvmy{U^$d-x~h`z zmOzmm@uzRl2cL_Op(3b*oZaNbbPe|sZzd=DHQkHT1noib}i(VonpnJ zeK+JhJ)TsE0)usTzLqg%`)YQ(NGrd|<%F0OX#yLahwj4BIX`DB9Yunq{eLr`{3;J1 ziTBWuPK>;u$W&VY>G9q=d&g(%S=fCf%(MZ`jVM{0Zs(DaEOB~V&s&@qjE)p7ilwi9 zvXRO_Gr7MQ)7EFy;%$i}lAL79fRMP4=iJJH!MNi>rwRiV$#85sVw~Yli!C8jBJAfM zRFpRb!t4&#d8F@iAx!mtOF&wdp0@q>Ygf$dc`=7Tz z_QbzC6qxT~82ifB4z@X3d|M;l7yR)w)aH1DlXN2FK63N+KO#D}_t zW6<0-5w)u8^WDl9>B4(AJv!gju-saJ<+0Bp&nx-twMQS6{dKZ|>ZwbY`odgP;8<_Q_jTN!y@-CSNVR z#4AU73NKTXNF(txNeDv2>1nj}0qd6G$QOdJ2yv$SuMD1KGLwahg|pD$t}w+=m}$T1 z{t4)OksaFAE)kmZZITLirKE=aFSm3~q*Yu_3t(=R^Syl`Bf7~xkx`*fF25v$=(UZV zf1+2)iDi1lOnzde@c7;9r{r5`;zIfUAfBrSO>&d)`)=D=mVK-Lou$vAS29d#?AG=t zUbM_`K|M4%{kN@(5_4m&PTGWuKH~f{TdkS zmKaM~&SYjF_Lf>YkUfk@fUEzYri*EV$sIOwlw~q;s(iXv7;(_T6-%d{NeqUi)l1 zeY=}2EW4hK-ddATf_C?qp!ub`&%TNe8ih0M9EOaFZ73KD3{hCntjH4K5nG{16#1ai z^~xmseaJ{bG6|UDPE^JnBu)~uke{saG>4ZrIqZiOkfxb6pk7hl4$A8O z)C?rwSX~kEyPvjqyTC zd#BL=&>-&j{(~-`C`<y%v z`xG~Z)>G1T9=~oGPgHq6wrV{0{9V@Q9589QGyH0f5IG`9YgQb2|5b_uO#L7jxLehi zY12D?{*3&3-<@B?PdYWkWKFqCFA0F#5oA&9`b=b1sGB=PW2K40Ot>}jK=i1;*5;5n zqLf5bG}G4S$JObIU5{Z;&NMN}(Jp_$dNT%W>8D`*$~Kx~GBbbubHsmArY>7*Q3ji1 z)v!~d=T>XfUw7}kp|GiKGPjw;a8PxJ4zTdkUDyHYa-<|y)%fYqBL>=9KCl;ZO^vW= zF!CtQn!|JA1wrX#9HLskhnEmt69ek9JHan=i?e8Ga2UEJ0kaXijxK&P5&YtV=zYFY zJ&)1UNI+fI%TV%q7TZ4`8NG}_B+$#1r`y?xD+8D@+}p}e{1^J?qdU|)(khw;nkO2# z73MrLRbTZG1#mRlgYxX)t%2ur6|GX4s%T!?!fx2iyFLLGjM4a_*DZ=sHV180I zY>Y)QXiCgcEtE@UkP&@|e`sfS`lkUh^ZQxIdNhYUx|9Ae-BF3w%lY2Wu#Jc_kj9TU&%lS6haxQ!HN!4X2pucMwxx$+_j*wu>h5W zV07eCEjZ0z91w4mvZB3DiEXIRk&HikxyJ}Uo4?*^z3O*d@Ey{%N{vl|Es9(;mS{Hu z7!JmPMXfpP)Dl_UIIu9u3n?Bw$WFFzXEto{j>#I8jjW0?Pl<6%`PRpkV)z=Wr1Nq8 zg*t(M{Pk{&9TzK_LZXdD4wACl0ueY8of{TpsGG~Zw>GgypMjT+UtTt1!;i(`f)BQK zV?BCFRK)LhjalLPe}XVk=dH{MYaLR3^C60C@xH$YGW-Hw8l!ivoSpl6!5*Vq7R(Q`J9y+CaIJy?4D&v#!xjQ4 z<@I<%Hg3wc5x{_Xxrp7wUezv_gwFApcfi-q1->~J4SiW88&u!0qax$f^!-%2%mpx3 zEVjn(1X&2Yu}5<#H~tkC+PqE}>RzT6ImPD0vicnQQ7PCWJdb;vO~d&&UjCylkH9(E zP~1zlhk>1XOU-8uW+*-)&#{VC=Xr4cjYB43CdckBgzGCu5l%`^@GW&H>at;Sr9v`N z=V6J;VuZ-uH#d0#xNQh@T?(~%qhxLkX?C9J$i)I~<^pGXN<=|2AiJ8*YztkBTgN#n zm-lEGT@|sT%Kv2LK-_zFg%j|OqfK3!3dr$t(X z9C;5CJ&n-5qYBMpSVA-~s6$um(5(gTT0%`*Y*9UPmbm3HK&75gI=$(v6-FvoS-NL1?iP7L z7*PR;Y9TWjbVr9d!tohV8X}>&7^3io+8}yd7m(f*1HQ~lY~w^XN?II1f}DkV`yF;$ z_#jj^zjJ{d$Qoer9qrW2SFOF=78fzZB0{>gq&xDBk6Ktq<@vn^~Aa zM+xO3*h8m;St9^zNvqOL0^@c~ggB?MROvesd`Pla%tZ2|r^29pl4>gJoQ{SU^%+x2 zgtgNGLvEQ;O%%=Od$?4i%c*(}`m|PfyPSPo#?ZWY<%n1VRZx6qFKzAV~dyGr{lNQ=WSt&%HnHk35-YX6<*c$NSoZq43tG8NU*)0`K-Y^i8S zP@Ltur*>Wll)d9A2RnTVkny!v%6=Mw3hQIei!(}KZDUD^CztJqu)8xp!zUryd@3Ql z2F1?s6Ifjl=9i{Omf0^lS}cxX#`N0GK#vTDLTqPbWh@-2k~7Hzb>{K$MTF^_iyxAU zT$63O?fvbc&${AseN$czvu+#OREnhdGM9=|X90eN)NZ1yjX}uf11@&M5`AwJo7kI? zBNvfRT}}&|x`VH+Dw~j-o>FVE(IOjs0sXw0>yAK`V&oyUoHmJ&{;gDk)_#Ww3xm!k#*7hB(@2Y7tF z9ZJ4Hs2ziD;W&v5B%7{j7DMvJ)M6e&Iz&f#)_~nCLfyEfKJz=DyE1mLBooafhUeij z%lJGG{3Aaut-khJcUms6O&UT2^U$xvK6rc(kbY{oFOnN5HizR}U#_{NqB3u4H^6;O zO=dPzuV?Okr11ekV?nxmFX%M45Nxs&m~5Dr*;mX z>mS?slMw%N=LuA#smL~N=#$T|^7dT3e>zb3v8V%Xe&i}6xBElU6F&1uZ|<4!?+z|G zmN4umbAc(E;qIq?t*0EqeAcvKbg@A3u*E!#a8D9gQH^SJR5m)^_8GuuWPS?gfLQ4^ zTpC6*9L{t7R)3_E0-7i7`U2E4!$6qIqd-+;f2ULBqvBbzTK>jv*zJe9?j0 zdd9cAS?bh_YGfD&lElfXqYpq3;k&Dc+k`~=#i3jFPG7gmDZHvagrvT!`?fvB$2 ztGQxtGeYUyduc-H!Ni?$k01txOPohfFzTy;U$3J^q6y~+tEH^WO+|I-iS(f_{1 z%pe@DA|;ENL)C4k(Lq8(a^zH+s_M2|B&PDty7wp2ZEYE=>f=YqHcOu4s~=Wt;Sb-c zKv&lS^gLrTRH&{8?yO8URto^npK&kRV>zm#k@=QMN;)}0`r2D>;c@+jKH&3C#UMvT8S%nAc&o8QU_-^R3L ziIx=VsgFAS$bmU$-?(=o1QB_Re*oaSNpJF$j}EsZ<&E^U9PlD_l{ZWA7d$bMGV7B3 z@+wfvpB=lb;q*Bkn!I~cG7T{M%Y?J0c;>)3htHXl{+KsngW-R=%o?8gI6MyTsUW5- zZ9SfX7TjEA=z~WaR@NS}ehWZw*p&MqIuu5)?FgxaZ6R3i*kfz9OF7S6n$RrZ3f(M29VC!tcko_|LS{1!3v-y$l96X0ij_)Olz=G;&(`j8fo z3KWuU#N;_?Ph*V3sN5aXIxe6Dw~~#yKfxwF99N{$e?>9N8{w%Q7;D~hr2)~a90Te} z-p!@60U)Et5d2r(7lw)!wSo=y<^e`B<(Cr+jRRaCe23GxOuUlDQss)H?JJQzt}f>L zWq5Ou@%gHYbKJ|qdf!-~mOzl1e+iP0`R5>qi{u~!>5xccc7n|KgliS@P2&>$pP7un zBWKDnlu_5?g3@Rz4PSBe`=i45l^*-`qWOgW z5VZ+o({$bqt&==YfSMTACB~=V&8=s918rLQ-S@Y{gD@~9jWxrX9pOhEKLDcIXhK0I zRDesQuDq%xZK)>e>fn~((Puaj8Zny%S-(La_f?Qk`8W`BU5)^fF@RcvBPB;XXzt~Z z)Q?6;I08khJod};Q*3?e4fdy(;18YAFuT<0%3dnV(h-Jl%J}dDE^YEpPbr<*BfzBn z2_0R*F|l#+W~_>o`XmQ#Y1F@Dx1CQA-ENw7s&Ct=;c-cD0TIMNjrHVO2~K<2PU^h{ zkA{I@{2qskU3yTZi)Wm7)h==+E7aMJn{q$4z&H+-O9?&H(n{J!7#_c zJb+*g1axAXM$HsW=w0xVALhq%d#7Au0ljSX=Jf`ALhmE1rlCF^;QpQ!2bz@0Rqbsx@!1X5o;_msnilP1$O z_@{oVoUR?VLcZqQd{4ad0Bq+i-5IMBkCQ$b6$1Dd^qWZ7_Jg; z5$-G3D?-41XKDtAW1fl4Ig<={SpY{v^jw!ae_ToOdp?KhQ=P)5DvbU;?t5h?Y&@?G zfUmJ6MWo6Q1FnASnjBtOrS3wrA}%B=q{PB?eFSIu`WUH+{m4TCBqTCI#K(h{@E#xU-Ue3tNcs_2HY_J zMonFd9hjwDBBo?JK>LM$TYClpjr5ocy|;0Q1*%VYR;y+D%hFpDrOM@|?zxIg$iWA~ zIgL#zc9+_45;v`@^BBW?)g8+&e5jdldGQ6In+rP*RQ6XO+gDQHfe@Ls#)F8Li9#dJ zNj^`id0{w@_EW@_g|peVz5Rg}b_=cx57W-UauQgrpW zWeQPe0tA-u!rhDfPNI{7oyg)0^)|~Hd;RcuASmLBCf`#Q4G75zf2?Z~nqQ^QuZU`@ ztgDjS0ofK>cX6oefTKm9xSb)TC{?_j;tlVBBh$ER@QD8z(feD(;O`KhTrm>{OjABM z+ez-Q+=^kTxhMHW7{_SEbZ1gm#RT|+Nu$lcbDutu{v8>tj72O79!9Qh7u( z9j@qxG+8etXs3);48zLnjj-#gyr&Rjfp$KY6t-;2@e6x*usTjqk^ZR!TV&s}LMoNz zYYrw!v?si-2w37dTGCGperZrM4>ZlRJy{K(vsk(8`nn|%G{iMpOiAu+3{LY-B(nOC z%3B=v?B6Wf5QKUT3=_}Acg2GnG(RC4?<_|AYwiC|2aF-}8RqTvF)K9Za3~N%Y96tG zx#7$PeFg(XTnfJ}R_?2s7h{cbbfg{17Gh3=PvF?QCEotd%f!aQ zle2N>+phj!v_iNj8pR4|1bF71ZN=}z*q-2#^I>90PorN6Q__z0Gj+4c14(!jIpGN@=xaJ|m6(<5C4 zj@EH4gF#w>^jzT5uRVnZnGxwHuPJKLHcW6bqo`VrQ0Gs^2E1 zA?_PJ?PyqOI5^2ymC=pw7dmVOeg-dF)emPJY))de=zTChmxPq{y75gko5br`G4C)r zXFC)%8RHY9M*a3mem$W>@u^fbA!vwPQ;4t=0KczB#CfpX~ z*K9Vj@J8P4A1>pim5=1qtR(i6ht%5J5F%MGASkcbwJHC~#aHBXrB6x*>VP8Qah!e184JoUZ`bYjaOo-ujlOh>w1z?J0(foR z-VSptotP4_k(#s1n@cqythzgJ6CzA9VrbmP8VJ1}-dT;BmL+E(84%>td%=lj7h#z2 z!?s1*M`#5p+44b>g|SQD3J3apt_DW?7mwQ#$M@yHozXlC$Y)xL=AH8!LgQAeyVZw3 znsRTHV!a?QH{1_MVv;5K;uxX**>=_NZpT}aHGb3)5Tzdg)D2Vl%}toStI-=&MX*W* zN4{gMN>cBI*J?5o z5$cyH6`a*Ow>}fi#KnRP)ulYK`gNRfJJ8%I_j64r=zWI6rGD1tuWZCO!*~H@;@t-o z$(Xw}FH(O@+nLsuMz#23KJpmbBHC*S3<`dwcQ`^Y)b16esH^`k5c6NElv`Caq(NT4 zGA)++02YgNA<`<0^xr?AQif6ev9>CZfZpreunPtJ3Ln+@jOS4fD43&2MdA^3-)FF| zy{1Hp$Zh36p}dA7+_Ep?;il{@4}-o#Mz4_Au+u!k<5|A=T6Yb3qck6kIKyl!CP-m9 z8y*K)WlACT*O8{=3$jXkQTFUKYqJCuUYPAslzONg^M_=#=b|sLG*YREB^xYl)@NT& zq+&iuh&)>(sXTX|+4>bBu5zRj*a?^?%|6|};`+;3iS{Z(jWH%lerEg`Y$|sVMB<$n z9|n;a2LEu-!g&N05o^4j6>Ho(KJ%dZgVQgIpR~0liC4sHf{NNnzJPI+Cdj!Uh@|^@ z#!CqhCyJ!C51z4LNf6x1LeKe^X;Nqffh@&9Qt_^yOX^E3-=_qo8gb;bidn#EpLLU( z>8J~orbtd7sWIbI%uTEH=QO?t*ZeHp&Elq6iEBGLs<=m2>_@eLzfn^AWc)ubpMFB}^@cm`4v8(@Sw*2on#&Cd<53Bx2k@El9Cdum%?-{<^cSP-_ XpZz8SOw$H8ojP+4pSL@P+YZm|j zz@;aTH4Om(s-g38Ed$;8xuuMU1ONctogO^Uce1qw0B$GxCbnqbH{j}oIwz(t=|9kn z(0mvXep%C&Ys}`7bHaGRWJPOyU(3_SE<+=%_cX;rFP+f;qAR1-(5GR$bV3^{e!c$i zd;YuJzJ{H6Og3gM8v|2K870^o0|@Vv*en@>jBS1{DVm`iXgu@qNlwGoy zW+Qklm%0hNF;MGS@cvh6B4;iu{{W`fbVUVwYy0_Y#T&kl(RXjXzRIvHq>*vSI|Z*T z{@8viG%VBOV?n?@67Kokj>PMtmU-XbY-A>7UR4#ciu=@i0?oA#a;{#O=Xn_#g`)`QbiUT3h@X4`9@ohDi(&Tl-y#U6_caKWDVgq!%yK9R_ z@m|1CsdWVXq^au9P9=;U>wJ=MV??_kSK3J~QLT&30l$<0-8&xuM$gBud;kFE)_)G5 z^TbC+06+lnL{sB=0C2N~rPA${1*##-sVG8rmG%suEf+Kny!K>W?#1<3(yfl=P(v_FC^%tvD&iFBOmn~Ok zD4M)H$`!C7qRirDJ>jr*GL5O9e07X$IGj6{7&vY5A2h`so|U$oh9H%Wvx9YfM)~sG zUj`8lTD$n61b7|sbD3)8LF){)3rp$03dUdP%Z1RF{1>Lp;gx(RBg)&Zg9z-PTN}Tk zt<>QB&^=B=OIAg5_*;?5mC?_UklKdr)SbA}mdP%gY-K3?x_=aJIWIq&ytsi6`=%JM z0sD2>`8Wp0%zVu|(!FWLZGax!z(-onvYItNYw?)9x|t|*b%m5SdhZZLoZP(ozzr#bgObGQkHCI4R+3oOMDQ%SqQyBlNgu0`FpFmjxBHw zKDZ_7<6s77P6Me=#cop@NBkum_dK@&X9oX-ti~PhrkRgYs{F^1_loV_)K7y4aL2U> ziqHVZ*<$_0meRRHV$0TFMiYvS>Oo4WdtylJ=I;Dy7VxbTPn&aTTF|jG54C>dyBj94 zM4>vZVJW)&w`>*{b{^S;ChoGO#CU;Qa0)=G&57@Hy)Hq*1JviG3%xF%ga@K|ATckf z*9;&XK_T;1;0ZnL3XlCOM#*YuH7xqvBqVVEdT)IU)1xvE?ciD2`9Bu!;M3eoY2;Ca z`)J)&-&LK@Me&XvEo0of4p6~3A{)`RCe~{e7rc4#0==c)bX|NYt+K({S=@xl5?Z? zc9fQ{Pownec9`i+PgrJ+B<@W~qOZ z(4@v^4U7phM=Ssx!m8iYORUaieRVPS33916;ykNzkKvLZs|VCtAH>Zf<>lwR9DJ4x zhxkhtd_61}K|XdDLs)~EHJR)^&bG6%BKG9vBZ)&1m4~P6f)RC>Li!V>@EV)1Sm(p0 z(-uxYp($+*=#izh2G#-U!v*8X?BSNkV>6t7@T57kVrp!`!q^o+%RLuN|IUP_-zY&7GXd-`j(d$eWVR2U7ga z>1}hHM66|3nN?!>fXggyZ?O=CGKcP=dWBCtLi%Pit;pSXk@V0qNT?#;Z8eRhjB@A^Jd7Y&)LgSmHN=4v)!&yVYXe<&`se?GY_)a`O`Aoqqcui zrA-bxNHi@ARgitS(sEWyq976o_vpY;s?-H8lbrr%_z0c7H29|(xfT(NtZPz_zHL@2 zZWD{*bI-=)lzhEcgEX%-!SkRe_NR|P6T4OMxZ~rzqsM!JdMCZ$qO%6wOs~C z4Q^(>9V6JgIluuxQs9)RhMfe(~A?#Dv}v(5G_X%nHurVKKF)XwJa%CPd6B z)h5t7|$OEe0 z>;A2TE)F%L;%G}(Bjm9=Hlldm6G`V-Z@PypU$Ht}zB=X*O%o@iepfTY+okb{Thr<> zso2jK=`_ldLW0lQPWb$=v**r z?c%*%Df$Az0J?Pk1^+J%LSc)FtSvORJ*sEMm<=H;LW?2uI`a39(rJf*DprB}_kE5W z8zWY($RqzlD0%!FT>!XwC$Yb^%aHYw; ztU1Cj=|9OR@PEup26*D7v}yD0r|vprWwTOOiYtg%qWzL5-{cucL{)9w^h%v6;Iw_2 zXEi%#RnzW4b)c7HcEBVhHC7V{8Bj*fP{&tcYdxHPDm)9|Ep!nN`A0&{CMv}#c40UUCZsI zOnOvmxyKEHI^7q4q0cjK><{0?zT;nvO0N-UY>ER|c?U0znrU&vg@@*hg7)_C#;&V6 zRHH0#@i5m;>m=YJ?If%vvqh;REQ--Hi)uIyNgxw$Ieh}3WlUnc^s!!{d7#cGI*>V> zHY!-vG9{_h0u>258wrWE$y1{oX$36~xYVeX;Nr<%<&p>HE0Im|_iKfg1qI7j!X|Cv zq(E+Bg@Xp=lC6h)la#vcTYbi%0FFLS^)eUg&}m z*g^D|yy=9!pWC{mmhH4z*_v5nn;7#Qv}CLQM-Nv#f=R#g8=42i9hKbPowXQpBN)gc zuD=a5cUHp*0Oxf-gouP5Rni8oudK8`S8CiXnlMe!QRz;`*8K`Z+i?Zm$i_G&lSB|g zZoYu=MCtBrZ*`2#D4eZZ3ECWcQv23^D$i8Q78|~lls9t}Oa+$u_?oezkjsi+NGcdh z=-V=(*PmsskJ<+`(jZ}7+L0}@9A&IbtSW%ve4M&az~G`3aBNgSFHkVby?0jU0{-GU zon{^#w+oo_GF=p;Dn1b7`dw)kyVy8l?icy_TpUA4AaLXx5y15bma{ z8Ym+L7#_f<%*tzo7hNo6kNRQwNt>BPSY7h_r=W1j0M*L43lA+o)T}2l`<=IjHis|`ai#4TwBx@zTrXNLPwz8(eNHh$c6(Z)z+ z*5HcexAKr1vNj7d7xxV};qw_?bwpso*1k>R%AzPBL|uFwtXAsDWWl*wc}@85faT0g+ThX~xnSy;;MP|Izgixx1Cr)9KhDki5{=b- zeOzdST>$QFOmAq0Ezl9mu_#X$76^bJFel%lTJo+rs^>|KfC3s;qoF+YSDl>CqB3v` zMGsZU@p~X>MOn2C2C}We?o@e8h)jZubtB~#8;d`cdC|lk1hW(bujmx{g#zKMFm`py zBs`E;Q|4+^AsmoWC!oyKBn22juws4xM)ee=wtaOZJo}6p<7IAxmVlIRNR-ItonWea0l{&isA^ATlu5!Bca}l zsYTa5FE-FP!+IYVsr1t16Sm1Y2el>k5fT8YaK|khMpz4Oq)=x-YfMIucs@~?%s0z; z>i)D<5s93PSpO7JwD@XK?6$<74E_C>jg+NbFKwFxfyU77|&$@M;Quxu<^A)UeD#P1%iv*fbR>vmYcdVZOPaAC&U`*OhV`f-VrZh{Nq zx>Qv>7&wj>qz#;SaVt;SAWv_W7wq47(50U~SZlXx<@$SN@Zym|Al+2%RmK9J=B&YA z2!5}=oOV2AM$^X!JWia5`tqme7tOHZp8@&eSD(GV#JpXr^Z?oc>xv(SuY)bSA1+)C zur~tH`k4--L~n8E_X9ks3$&4Di=;bi!ch`?I6bIffF*@o-~214=)fwI9Df=m(D*jP zz5OWFN8FLx1OTC14AdwU_0aQ(Qd*phxW8q{Om*?W3J7+mI~?g`f9jinach+mV+3WZ zIN-D0MSJXGbU&WYgui&5$vM;c_+ONv;|oKoLo@CS9SN>~CC82O>~e0LP1FVZyh;?| zwBwMWs+NA{UW_}&I@Ery)@&#J`vdXuy+LZ1)NfTpJkkuBpsrB1A>ztf1$=rqgyzgi2)h%vc< zlLhR3_aUc46cy_{HJk>FSli+Q%n2dCW`#Ly5g)$Vh8^VEd9aYQ`8;aZ0jaH#!PPS3 z1eup2qO*lI{=ijq&i%_N*WH)$X-1@HJxanI0=bJn)z(T~ThjsV zUApPYY23VBMou9Y*u|~z!gV_iAnK;*3Xe3{Bqx;07o3sMEgb%qB-BmAyCORCgXwqY z8Q`722XjKplfMNq!ic$Cwi23-qKjsFl`f4`Zks7aBN($ud8NGXI~Sgf$G7_YIjMTs z87T^YVdKJw(FCMy9WA;+hKOh)# zEKVAm0De0EQi&#H{lZX#q|)SBOQsvQhL}`L@`+Dl)@{u0r{`>y->TEvBa+IBd&C7* zMRBXDd6VpRo&C-62c`|-NHJmghi!7`l)pz3BG@$P*&>U|5+YG{m!+h6B}~}?an~_B z`A!3o^g^w=~jLHTwt&&Vs^(_56_frYK^=VEM82Ik*Bg8phSUuzNkJD&T+s>0tD2rFIe?)vJ^99Jp}mn z_84+c-Wz$fq3n(IE>v4)bT}Q4OZPFxMCTgSJLTVEy{bTcU;DnWr6ji~TG04-7rGsfRu#+A$OtE;;tcwD*LS z-RO%4X}@>e=@p#X3UeD_GdXN*V*%%b7v!1u`qaA8RzubDOI7pJ8W-gW!^wVYE@{1O zF5ytUER@mrynT@1_0ICkKA(sdC)p{Gevt{iI}7CydPNe4d2wj9*{w_Ydo+{TLsgK} zJ(-_;yCh&SnzWCTXR9CbhRMJ{rK%#*JLJfh2AGxcI(7HT{qUJrO_eA0l^4$-&^ zFix0VaNbSirO)g5sA;B5nxcBSThby#8Vk7?Ab3&?)oyh^{bSuY|0m@0@Hda&k~#E^ ztFN$MSXlBc!X4~hG|`fP*i=AMQP8UNL(&1ml}7~X4ubw9$E;8wc4nj*<0|-g-0$s) z_601HYwYOkhu##i?`|(XdIdM?By;VDdE5y4`}7QGO&gWVa7ebGg_-oyz+cfOOl@3j zwX59up_sb6fBh~Bi&&L8*6wWB1e7%Mmts{qaK5WRxDY|7mNxgbU$vOtZuhuKABAR~ zL`*QdVP)n{V>RqC*ffe7iTENmIWy+@#aV!;#*UH%i@&l=5H6^|Zdkvh zY6b>f?-jw%IsX#KJdPZG4=4^-)6MD&{thf-%|CggwlLA>@D{Qm_~31Jn?1eCu17P2 z-$zxyI~Fsebk854HTggZ71!nc0zM%czi?YO>}$==DvlMt>tgh6zKVBRp}&K=X@~9} z<^9epr7W!+F|gzyTL`BJ#U~FM2t2z17(3-(AMTe!lwQwQ{m9$Xmwh|-x*Aiou)5S@ z0=+NrmC={m)OhciizYD?b4m3)!1IJsaEOK?Jf&+0cHakQE2P}&c<>-6^0KYgX;gD+ z#gC4N7)cZNhAfAW@1pg{7axj92AO%y9m_{ZrPYsS~X3+3Jp=oZiO%n$qUaOu_Z9I4)Zn9n?kGh4=x7492c4a@4m3B zIM@d*-&0!96NfS~a6WVKNs*?KZJRZqSPMQTY6cD>4cclHC3_~byJ-bV2gL8@lK!gb z8P;YzpEFo@Y3{b^6(SRACCjg z#y89D8x>cv?UV$aq{Zh&Pu<^?|44n4$5XF$*{XT~Am4&!=m=JKd)7DNHEj&Tw2u0F zq*BIOeQFK-X@?FvOw0e-FdYucSNX+v{BFr{A>Ls;40BCiC3i*DtP5;D1>JqgR0Fi5 z66@OjOlLx!u(DBBm)d;*ETBb(n?F?-X+;!|-Lh))Lhjr-!oO`8>nm>B=-|VLq~Nr{ zu%*X%z@L!ULq;dJ<9(r$N@PGnm*nkKfoyXO_;r+2sg^#Yhsfj0`=4QYP;Q0dWCy5( zJM&}7SnasZZ%8=O(+?wejDX756V3CDolB zb0MbhxC0&G9rWTPj)gKbBsW$W6Dzr^Axt_(y1iSf@rs<=x@xr?p-VW-t1tY9Se*Y` z*7I_~;R#Dht7v$2@-~%io_+uOiYKge=^0dUq=^EVbcCeLLLG2u#s9ZUaewn8bqot) zPjs)s&rwg2J_b0I!T$Y4{mOG&k(wBFxm;F36csXuO+7uN&uSju7}(gqv8DD*ZJ4X# zy4v*jg*jquy}pU0hBWx-(z;YbMKXInUz?1|49u}r79RH?Tv}sGITJOwoeI;`uwp#g zce2dAJ$jJ)`i+*jZH!O@yMq|25M9Zl?DNNXI-VS_)I3oIzI>bdPKnm?>J{q=>W-T< zt3cDNh_}u$-p&uO0p2-bE}~eVf>O8>mc6PgN>cIu%%y!LC^hT+n#>SWY#VOV`)(RT zkgP$f&$EW+vtWdtfdsG;=Di;j?6dy#=vCaquD;?v81eB;TTL#--pqH+?A8mPnDRQv z`v|qKR7Yk6;pW1^l`$al^vO=z!o3qwUdr$lh(lmc9v-Z#JA4(tnYWL2IQ<5B->z1+ z1+x4I(fzRvN^Mi~(t^HUDTPtDy_~-80(lcZHY<42RdJ7@`iKF1X8Olz->Ek;IQmP^ zade-t&q#xBAaD@yI(I5=?T7?javPYCw9bATchJ)<`{LOvODmxJCCy-G9u3h7)GWBy z>>kZvw)q39*Xo{6%tFUR@Gdv>Y2tQkx%ExMR|oHL_O1oayBej!Cgqv5&)4KCH=H;F zNp?+59#DZ7Fq*IZ)cUAWpFHWWBi}h!q?BldOws?cT5`1X(S!1I0?fau zbF*HnyiMDS1uWD2IT2@hA3MNsWb@2@a;jiol$X0wI1GSF$6_lm{)YAadpJh?eTP0h2vxIz zposFy&jn&yX%ZOnErRZ-++C=;9v}gQDxu$=zG$ZaJ_2TnivK8oyQj~ol7^t>^SMS@ zepro`&j5}Td)*N`N9oVeSw4rIQ)K!m5fywu;90$lFi_>9airJQiJe)R)a&JAlV-lR{MZya#4r>fLg21#gK{Rf!(Mp zdj2kZ{)1iFbCb5&rl)Yf5FL0YflQ}vLiHSY6HHq*_J#c>(^KNGvzywAQGV?6(5?FN zox|o^UHA2J(mBD4qJ=qn2qRwan=i*e>u=Wn$n8EoIsNqwe*V?Aaft!&`!EZeEnzIu z6ZA<=)u)Nw<9&}Ipyt$wueXXgZ-H_#GdRBJh+fSwa@5#=lB=nO?-%FJ1}C?30W%c? z(U(nGLz9Gu*;IV)=;bB+W9*4GWYszT>W+z8%rFYHjbE!Lv#yqdEdOpRL5}t-iv60*wOc_-xc+;x_RHTjUD`v~^QKCxswOWoX;oaMOL7v8hSj z>Rqq%7mHd@%L*q=U~H~)@Ez?KhSV);?+*p4ZXxN1UKJJUoCsDo{>0w8KxNB*p&65S z+n%bTQZXy?DSZ~bd$Vj!i*UvVSos*pYT8Ed!bc?(#dr zgsjU{LOUg;VYh_5aYt;h3x8+_rnrj{LNxYYY4n4eI(EXGf20MDEq9tUzL2b=U4J8H z8LtQjRnEDBelR-bwZ6uJy!-1Nzb5&W78=$qy%~y!d`dee!Jkj226gHEs$((axi4>Y zC(qFkf|y>g@pkJPM9`kJ#xLY#R^T-DM>xi+lCeUE1`ezNPSm+Q*K>0-t$gq!=r3CQ z-8vd?&)2rNtDpaca0rHDvLE!mi7r>}Ce0_&G*L}gK`bvcpKrgCip=Yx{GX}G18M3m z>P7)Q8FmJbZ`X&qkU5$iZmM~Ud{k>4{yN7=RX-Bq;YOCk7zR8G?bX6n=rXZbGkDSr z!Gc&H+e)0RqKxcd{-qw%evND@nD|4ja!HWYnf@M`U-K|fXD!7hAednT@g4EvEA+*E_1DYi+|W5EecJ5D`1aA>vxe)1c`}t4-wEfa_46zCdj{!hubKzx(A6%4}_bqkWb+HAk@{9zCqG)_6u!!L#D zD{blV`R)eeZFD0p>j&ldw)T>BM5Z`@qjF((U#q65b`8~LOgU9Blt*aE>R_WYao{kO zjgK7jmMPe_^_ntJVNnrdoQ2pqEqg1umu4qGr@snxq|@*8vvKR8nGAn6Go?=W&h)fI z%UXx0ckxPsiP?FSi!sb(i)pyZb#yD_$Hw~e)1P+r&v<^b;%*=5S`&$H?+ zdV)KEEu8tYd!EcHs!<$R=S4dFtn~5~s9k-H%sgYngE9=AEMUy2S5Ql+|joaQ0?uM z>=*D~xd1y>bvhfNV~NH;gIyTGG|1KJhnUgNzf%@Sw-TV*>{JCsQn1pw<8N+ek3fW8 zL%xvx#SHTcji=L-@|+n8#vGc$i><3dY2u9s(;GfZKQ9*a#PfC+eJzGskeOgRPv(9y zoLl~$HzAN8v6fvBcx=a{o zahd9CuaUZY6{|_h2Jw2uB2z0B*DMv1V^ThRees9WQ7AgVW`TG`se{cfnbVEI`aR#h zN}VnCluNDSag%AMXI*@c4>(}wyWreN7bbl>^T#j;z28fJ|B2opWG@9~&ehF0wE1q4 zsgzpaY>%n2st+R(^|;wEQpkW|ka@gGe4tspH?F%t+=G3U=n;3T+=FI#+OsVg%2)Y( ztuD6ca@*b*r7Ub+3g)`mf|joVYq45T|3P2Y*83wEA4D^0+iIEx4B&5qGis`NEoo;S z`seHDUj<4Duvdy4wX1D0ST1R-Kb*9cds)W-G@YRONbehs$6D@8MlDA}l+# z3XoP6cQMy5T+eOUh26X`OK)(11+P*ErY_iNNU2*4o=IL^RUq&CavU;--^xn~<*^hX zzTObt>n~ba5$330x_rFzE9W++P*hxXiIR1#%B2tfJFkO;n4B)C;0kDl327;-Mk8Ze zTxN~gkm6*_oq(4OfZ^(R(>Ne56Cr{xg$v+*z}9IL9<|jlTI#UsKy0IHNL6<&#m|d> zS7pVuD=Gv`;YEwR2l>xVfnm9=Kx@krnJsihuE22e`Z_YX2aEn0v3>pyc%%0@T$mv8 zggamMEY4e{N7pwXJKzC#vsg1cd|ldd`s(d=VKqY3o;XSiWO$eu4jgi(dI6ZQZ*1c;)#5vHQ@RSI94 zjbnnjS!TP@t0VeGjm%eU@B9|dC0(>(qTYR7`n+HdqT5k)j?JBL3S@cgInuTuS6O)t>rB5Br-x$4zuySEIu@%&207#`mi z$fP)@XgKKTuMC7axKTx={`xBBGbbWXsGeLTM=FtEo*jPfUmZbyVr!? z`3R$XLs#YN6|y3d)05Xnzh?ZMe%=h{#&?VL5gL6@jKk9nKshJs@D<_!>`!xV$ZD~n zI=e}z?;^GPU%iT&72?(w+0jC%BT7jqAg+4hVHxW3_Ac3S*ul7SZdsP!9P!8gXJ8D@ zY%5vS^3!-d^%CS5K6K2=QR0UA?fY7|(4x?GB0?)*-a~!-om;nGAkp^2JQED)APT1{u zRCo+PdxC8)_ERaPjl@mXlSTP#t3Gky__=nHP~F6Ns-e|{u+5*_t`aRPgSSp+I!w-k zueRHp5C88>1N^s z6Tf5&x@(sV-(M4b6fW|fcb<}n%*5q!WSm%$HJNI71d4~4E`?gcf>x*n}uht(EL)|JDo=@VMLQZoxaL1|+$7DH_PbX&Q z@T-@8p^OZaBLVK9+*TtECFxpxPAzRJt^%L0>mqSxv_s>A>DCg!UJi7$=C9EW?VjpR zly~~VMc&V;o!{qYyw(yTmLf<)8w0HdPF^vEbdUYCZ(*h897Zo*xFF2=(W1$!amXcZ zS0*6Z)>GQDPzo|;QJtW9QVqZP$f@Q*a0|Sorjr~H13LC^fsY3^2Dq2)$;W-m!Qm(t zn4Q6hZ+E|M)!-;wN=`N`>%jenx7MbHbn#apCH~n5t}*ed3lvW1*JH&n>7kU;G1c=0 z;G-lBWAL>9%H5|)@x321wLEnGt`pixduk5KG#p<$Q4=akn0q$7s!hW2Z?=?jAEELd z#Rwx`D3L{9RqPBOZ_BiKx4`oljW^a6%O?-lL;X|5We82#?y2w)#@kKT_54^s*9h|0 zj(_U>mGAxAHT6S&Rry;IVtC4P*=a3EGQmFQSa^&Xb$n)`PuJ9Xo1-_+1 z8Z)!d*QGVR>;c{Ojop(+ncK>z@dtkGmCS%0O2_RE4Pc2r+6^*|- z-6bISwmS$r-A?0%{&w55qzZ`Gc&rLm+x3@-Zv?W3qOE-Lp78Z}rxcUcRIn+0T)!F# z!TrP@#FSTeyv#C`KT^S+_)qNhS*T1PgituPK2Px*!8wQ$`K$Y=Cl!*U%#^@@fv2a7 zRDPFywWrz5+@8)a?v*0rYjUl&m#a?2NtW>@A6(d`wL9G_tU4LN$e^7 zk$n99oeSIV=!tKZh?r3lZ^L1}yxH@Hx2Tp`1i(v#&qea+MbtxJT*=$r#2wV>)?62= zWD2Br9E)l;G1gj8I-S@}>(X>9`?<@j{^hM0?S%C}R#L@^2Oku7 z6fIL+rtC05+T~VND|h2>!DYUEh*P7U=&h4HJyjgtC2=5SuN|MDGPk_~e0DLbT_0OI z{3K4ZL!QK}toQ%TO9lxau+KIe`4EJp;tFLrp}LUlDQ#EGGO{PFLm_U^oT5nbvXQLH ziB=P~VqZ~E`u|0QW4EH@WH)T3!_;k(-z3=jUmK(UyAU9Ks1AR>Ql~tUhu1-j>5n9y zGCbO_`=)v|A}_Szto(p}d6Lqp?6rm1g}=J-Y6MCdbUrP3xPiZ|MrB1#cZu&GsROrV={Pdpwq>5`D9>d0%K1`^r_xv;F2^DXd=&k9m)VGdtiypw`j zI|P>nuJ0NbQ#v_6V7@_)^|3PlT$hzLsYUGMeSXqEp2i1!pr}oR%2T5s(j^UX@&oagSDN687SvZW8y9%Hz=8>@>!rW_O*7(X@^6f*4@tIJHzuGV^jA2C@z^Bf>hT7S@JwA34fq) zBZyk)S@evjAW8AmzQ&%Bv%;8jB9-NKtVj3;xRyIc+Z=ZF_%{_+1K~s2- z%QR|aC9^pOg@_qyLq0}ls+jj@0S1uew05;)|131p=EOg%Jsv#6Yw3(Z6@nT8HGzN2 zMtmg*5W_)HZD*wCVI^A1TMEt#qrjsV0d13ExByRCn%k~4wmnm*ecE=K(;ycG- z^Anr^0+SIx(#cBlbW<67K;4`!Dose#;|ZFNOhUxf7LnGAM~FwyN9|x1+)%yYsFCQb zHXkzWC0NxWSD>qrgGOP9y{USDzJu3Z8Ej#800~L|JMbe-5%?&ke z+GB1_yGH1?6bU)stMVW*(3u{U#mr)4quc&XoBL=1wbWke^VG-$dH*)4p#NYYfmAvw z-)nx8tiys=39mhj$e!2V={@V_^)3uBOl{j+irhm>C_MVy==SvLyUh_GUH+5s8*UX4IJNVVt+P+4?QCowAp?_}3mSVh7gE`J5<(VL#S3BU zEEp2mIHV)zZs#d^-cfkef0W9L?9O0G@VU>2q4uE;)zDy~lRI7gIoh@9J%q~Vg<)R) z`GB!+!GYRL2pupj!M?Wo8{7H;)iPCVPE*Wn$^=h_$7pZkl-AZ48 zYbYT-*7JP!th0VK;L%_Hp~_Sw|DGy9fwG8Q3zWV=a- zx*5N_J?FVuD4Z7vlUU4XxQw9=N`RkpPar!%3Vh8sZ8nvJr3LZ-TaA8->fh9!KsfnN zCpN>YaM?q}DxFO}?N#w{A!uMD%mHnf%`(jCYu9O>gsK2ju>bE+b}zDJt=H;ExRDFy zkPa|V#|%KxR|ZIX^ntDxjc?P5B4tgG!UzdDwhk(GnaaUbCow;%fiJc!(eCab__Bj3 zKIf~fHA^c^Gc`VtL1B+4EdJJAlmT7&lQ;=q?{_qf_2m7i5NzG>`+bL8{Kt$Evj;Pel-et8x-R4{)!c3HteVnbyF2vgRM`oz ztIr9E#j}OcMhiuM1%FeoB{AmiBLA%Z5nA_+kOzA!>sh>c^N~e~qhL_fo^7n_a+I7} zn+(__-Tb%7rhxp>++)3NsH)plm9ZBuQ;(ur!~Yt>U?=~g;IICjlC(P2BYi2auI|vH zxhj?_mPAgc8b%8TbjmX&Go|HEURs#yhGGaS{})3uJl{drtu!e9am8M5e%Cd}a7X}2 z(lld#tk6{%mMvIutc4iy5`jy}OR+sx)&aeyIuxrmwI_o04ep7gHl>zv|*eSLbmFcJ0O@kC=`+}yb)PPIT%xx`lKT z2+G=&^I`NQZI$@jJX@ttcoU+$0BrRtRqo^)?H(65?9;)pgNKs$ADBU)Sk*+3dg|_0 z06n0qYMaadnV|9HRMlIFC2G|tYPD;J#S8y*m>c!)eTn7M{x<=O9^{F5o{BIJtbzs& zuf!Q$*B_?nRuXYQX zAk~!y7=68qf(e(x7w`3wr+yJDa7JNbp^tRKEuYTdiCg>*PGU2PnCEv2Ro6JoY`aLa z)Q*<{m4Kz+i)|o5qRDE2x|pkkyO^t3La+J7;bbj~pmltu+Q(E*ZGf1hf<%CU<;1vc z;tH@zz4Kubtzky=^&rA`?H%3Xg>c8fDw3R~7kLnX;mJn0T1JN^?Hg zUo}WOeW*mds@WGU1h{&am$Uh{Bo15Ey$ds$GM_Va|FO^~8-6NcjqZj5K zPm5COpdcnX|AF#;?Ur7FEb|*0WHmRgpSr6#q_f_3PI2i`MO@KdH|fUu5VNY=ak69V zgC3DeTYkBfMio_i-ZUTD`R=YrC47$dAbs!mcvYGTwU6$vA{=zX`1TmMCS5ZLTW#=O%vFsNTa0&{_!h-@~Xhx_!9)VYbV^yf0E z+)vwT#Qwhly^*JO!QL#3YgaF}3I}}PFj04>>jZ_?(syNEAV}ZUAtce1rW~~zgU5B? zA2q}@>J(GJj;Q0m6+N!5{gCQ!Xx=kjd|W$6u=Qtm3>ppL<|Df_j|qH(OQ?K zrX*$!c03;~em{r`=0;S1`VsAix zS@+ZR%Kui$juw!%wUJK)F_ZhN#y2ea)yM= zDkZFdQi*L`+i!DNQdqOpR&o@r`IJ}tIn}--Vt6;^emzXow!f;Pu!t8Q`oqd}RMX(0U z)m~N%0pFdtE(h&Y_Gwk}>&}pb+!sVDjx@ML-$?5Z@n||VjCTp$QtxaSQsUun>@Kte ztFNcApyR54Bkk!@sJQ~T)|P6%PK|A+b{ zvt2gR%!Tv6$)rI7`+f9sz02U_jO%H0XptpOSmCOO6t1K-O*+X9#_QYZ4EY*_9xk6^ z=-3^IFfkmwZY@I6$s8@;O6^vdmc3PECqXr0_!4_*X5M9YpDRPkN3jNO&FNMZzw4A` z{3_nfS1eJZsG=-jiEKHW>6%LmbX{FYT?#+^0zaGSiIi9k=dL%j>m45UFvB$biVhmf z3oQQ}tI9;R?r8a)N0B`CMXpJw$y{Jq!#H#n+f&R({2u(Jsw7bW7CSd9ZH`aSkg%+? zxLc@Z*Sln7tOwruu4MsW3;o-84rKpD#;V}nPEP)1;$968v>jbQ16eY>XR)$5pc#Ys z^HhSvI2$8Djq@06bRJLO#1^G7;Ch7l-Mrasq+4!+NPSr@X*e!ujFz>f@aY?w1^LAc z#XsM+(^2cL0gcYSyhYx`arn1O{)d?>T-sgIGM}UBT$I)yth+kYSGoO)JKa?1jIcd# z(p+gVDs@9?@Ot2|@hV@@Li~!|CKu=W%*#l{>qwK@oCk#4ODHaGt zyp**_bm}>#PP#LN516yrv$SKb;AElr0%qTnb&lMS6)mgt+!$}vsO7XL5A98Wr$$7V z4eh+QXF}VrL+9T3Xw#RaDkX*8M)GY+aI4)8G1SJMBbxyAqtcBAdX|X)+jwso{ zif@$P-@jN1`lbY8J0A*Og(J|mXVJ?Vq$!!xN9}Hx6QWa@uBluVT}SfI{ZHa~Csk*! zcJrI}W6r9U8w-B-o`+}pP|R}q~Z zGFXhoG3~XV+=w&AMD2b0dG6<+AG}I~7I21#vZe@elW%lR>LZ(k?OA4)P1dP%1xbwAul56eD>Mj<&?%^UeGw%B{v0Kt=-k{Y_kdPV?5g)o zwg1tXL&PARG6+vB)|JpA_$1p8uPiOEhCk+61pIwpK1R*3q79K-cl~b>0M*Lp$l}+- zal2qS^zdl;*z59u3!Engk?|p{9yitgsir3;rPB}6zKQdnnfa{Le?@Ppw&vacQO&hu zYs+u3^^_0&36Q_f2qFnAJca;hIz?IZ+6&Y=kEhbL^qLGK8*TuiPQSNnfurP;K!C8_ z-uBVT_4xmG%m64*!{QTJmSa~V;m7}U%tf`xwt@EyE1-ow4MP~F)v>I+M%Y2}kpYoh z6)L=EDRFDwl%Em28OQXHrxr8d1ed1s+8uanz}k|j-iC=p7ec8!u>rbRF;=wI9xx@D z0a|a8ylWZwd%{R5wQskA+1qX#bR_EQbbOcpKpRa@-e!t1<}WAyoABvB!ZDF2a})qC zm{75e(|cd_o*u~Xmz&M*LW=nKz@zzO>!e%Yv2mH65w-uy1&|McDOCkqzB#qY+6mqE z9svnmUa8HV&t1X&8JWz&iF`Y>aH#l939;zjUCVl3nwMK(zEJ*VUjx!1kUG}aM0w<~ zG$kfeCKAm7%Y5I<@!lmVN=~!k)mS-``t@iTfTPP+O>~aX-1+Z23Q*)F5z^CKJ&8WCWJaA0x`L1S|oF+G?Y*;w>X`X~%>iTD< zqPi+OHk+EbdRu`=mVEm!pM)>eUVqB@zZiQDs3y9u4V0#+fHYrC=-U>AR#bV^<2g;<_%MHkNac_w`C$hcXqdTeHtB&)Bm!qfN1$sn^0; z(8cqRXfvMyW`ev^ObhLeS>4QPpB%^5RJsT;6;cN}~Sd&x|Gjnq@uQUHJF8ty~c9odA6gNOq*vM7{9!uU6BrcQqc&Pty#Ndc^U`yZcYf*G;OG58n>D@L(vN zR?+e!(+00hyog@rMwWQWAb)0;Jt;vMlI=d#E*C&Z5sdcO`)guk93p^E6!z?sT-z#Y z?8zV5W!FYS{fQz71>jBBA{&~1NbCAWMxTES>k7!O|CRPtxb}_Y%6+(H zw~X}bth!R4o2Jd~$i#)YsAF-es8^L8&`K#@eu0?}bpoD8H0{h)MJn~(Hr)kgSM~^rl zyU`38NKYdwGpwcKu-P}UxeDh28z~~ZpY6F|1jr8!7)k{0rwMY_J??+k&8EP>3FVO9 z*b}3^^>x3(%XD*lfyS_1Y%+4RqT(7?dwZ{$>Z3}13p3mu==!DPO#A@5^|RDl^BxYx z=RyWN!Dr6A_FVqb-T0|j1xp|*YX?-mjN~e_vU++WvV^GiKTI{?f1_T3$azt#^*)4o zeBi9E3pIsjXtI-}%+c+ipQli!rcTkxD((ETHXp|EyOMnPA7If_ER;BU=!|o_6eH;U zfoiv1X0@G3DhX$vUFsI>356GN>fl0VIl<6&N8TTsm-egc@|YA|X1*CTho-R)=kl*f zSvsD94`la_MOhh|{1N+}h&B9H^mNTjUOG;V1zBKyO7m8D(B+;kpcqc*C$!J}34G45 zzWMbJJpF6Uk_2S~!v19Bi?!ci0m+F+2x5Dax*1-hsIXCN9(sXhm}3|`_~l_xk9pDK zqVc)Zcw0+7IrtFeZrhKLcW^N5L15AjWq=v_@}T%mPhyr&z(L0oMgpPQ^?1(?YuiWw zI}?g&UU4=UwsS6gZ&Rl~LnY*Wjh^{hi&|CLuU}=I+8GgMV!6=_oFAxYsLsZQI-WhB z11l+oKX^tkUA5%)nCdZFq@oUd+i|;=j(!07%m8)ex{risMWmvHsaSMiAY;nVWxBHx z+Ri_<(V4$MqhnfAfRN=@HC6TFp?%FuFKu>@6B7a6I4JAtDtBRNwk@NX3JF?Fd|F@Vn z?vPIN#tTzeRANuKO!4V?Q#1Ujwf!rPNnBK6p8FUwjnjL)>F}uGhZe^{#JuVBw`{ar zY!{kJwdw~%U|iR^fGP}b)yd_xY7tLY#XV$o0sq$N*7)ry%_@Ceiyq+km42HJmx^@? zMZ+c{r={M1IIxMA>KBajCA&Ii&CjsnEvuqD+P!%Xs<<2eHs&S*y}|Ghr`uh=y=ZyP z>gzgx#KbhwzZyK#*pcHenO#5w_f=q0w!Qw$PeV!OBm~&Op2AW*cHLv_$k%kH=mgPHu7?^d-L9K##`OR+&dzOy_*bgRFsk#A%3zi1-e)ZtaPqb2-eE!;H` z1}uA7i_EEUQ*yIPxPPPl(HR2$t)g(@Zxw?4MghtS+{fxo1Z>hWqTjlQUO*(gZi@#(G#izJ7U$hf_f&E`_4av7FNtnviuh+JNz; z%_0?-nN6y16k)Zi8KsUgo?m=YX~C!Zt$RcfmZCtqf;EtLT`h$4)N?qFcdJun{4uw| zkzo;-VI#!Yt#$DD&(+C^^|oq4I>y0zBXgger)S0(o#UoOVdBhtok_$-fX4aT(Hb2^_*lNY!IjQY$9YhH^p<_n`vMxBr zbQOCczzZCWF+`7$np0}lrd%F2+fG@10Smhxb|Mb6Jh?LO5+dg1Mx;EzHwvH!b`fqg z@EOox^^?q--E+cqly8XVa#^vI-}_M>4(ZHWz4>lvg&^}8&|`^ZeH)+OYJ_?4p!Rp7 z7F;ct{?+Equ)aW7A_I?}*S#jXeif?wg;gd`IwfhK4t_sV2osz@G6|#ORb461zUm~4 z!H7JS+JIFCU=4inahe6|q(W*x!3oIkBvsa^x+Re*`KJ8L*Coxs*`9NK@bdcUd9P7>nB@2 ztcXrK_1=r*&;9mJ!~+Z>A+~`k$zviW0^}rSi5F ze9Is%=Bz^@{2r|e{**`Op#~_a=hX75i1?U|P;KqG%h&N}fx`W$qnAU*50B$cBhxvD zg>7rVVjmcW$926mz+YYA_ZLLU)5*e21Q+uZo_xQE|5u@_i}|DEDc$WCou*lR@b7|z z*hHe$HAv9fu%ulKg7|&v>a@v!6AI$+<}_CqYR-Xz@9t-C&^|eI8x5>eP!FAyEw@v1 zJc!_7RpvoFMk>r<>WcZ5-E#Ym=Ipl2yrvT8#XFfyI(RGI5z2yuGUF;N{oT40rzyhB z?l6J*VQz3)1X%)R>reb~>p%y&xfEWrb#>16^Wix(>VDS73i~20OL-7&&M8{4M-n#j z&$A9Y)w);N zed?Unn^UU;w^3I#B_%xd=$jv&w;~;Ggd%PoPq#eaW+`|12j95$55D8>VzxzmE+=Qh zrys#O=N~lm=O|_cxbFwZVO%x~r44Q7PiruoHq0`ux?y?ZPhEPc{fLe*6lFNptuHL$ z`NyBgg+2Dr<9brx8_t*I|M7-3;+;g-?1E?4^_Bd%8Vj-2g(5c2hQ&9hl6l(+Rt__R zz04Ffq*@;Qf$tfrS3u-qa=E#-wC5`wT@O1IWssB&!U#-oP>mJ1gFE!;0qS>>OLVhU z2GwkGAM@e<>46%m=3DthwG740lIxX`Zgl)aIx3Vqd=V`dR&$D~Ml_w7FdXD$_Q4t$ zTT(=*_VbCiAxO0Y)ZzG@iB~eE6CV?6T_2n3!AE z=?ZaH2rSx^sz*`eH;;~gorr+flu{oQt#X>@tcw5bi3AUvRDF8-TW+GMmJ<1IGd{g5ir z6*7M}V$0KQxBQTfiY|*f?^ImLdXtfnb8)_a456Y|K$3JZ*R@BpNol=akoJ+cLgu>a zX&(OKjn;*#v?)iDbSkDCN0-ugiSX)pUpxM=6U_m_%?9cEua!*Of6XMERgH)$i&eC6 zb_jAjxT|hK$)R^W7I-f*OvZ{3%Zat0=B#v5DZZQck<-=L4kXEu2m5R<)Q6oG#wg|7 zfv;pw{isq2w+edMLL={ejwRrG$Ge9y%G=go%;-+_J00dQ%WO@XlqC`l)CdhRt<)OV zITr`lCGRhv;ndNk{;kVZ_w$a)YWFSQU_6MDV{x%}Bg*Xf4oeh7D@$iOUYkBr z%mSxc72N3mPmNr7O!PgT-Yw}hA+hI%u2E)lU$)n(HcPEfJz8(<Mst%Oi=?j$LQ<9*La6FE4#%I$U?vE3&a zDG;RSioe&107Cpk3RmaGjv2g%Im~6HeIv!w@v7I1{QrW=O~Q<3IL(k}m}LTXyfQQQ zf^Wf`$ z1 zudg}Du^5K@tXsCL@*f!yH%E$Fxw)1|2vg}_4yg%jr)a9y59xcje!y){@98%bZsmCk z22{~E0Ze7K!dfi-w}ekW=p7y-ZhDl966u;Tht4$pf-BwA8>b90@v5cfANe=Gvtmbo z|MKlR-nF>g_=n%Zz*4eh*ja$2W~5qqR=I#Mf)Xp{Uq0BpFCqNF zf^H!H;dmdXsM*!DC55^ZAMF{wQx8|rQ3c%NK+;3)4eq@|U%;r0h z8N++4m)wOOte(l?+oBJJ>Ro5;;&&2f!Git z1U|Yb-Tk9m^?{HpA=0nnov{dL7PfmU(4Qu@W|rM}&z}yUbsQQcrDzQaZi%36zHJk0*U6#*`D%8@2Kgc)G08mN)ww*C> z&88eL-3gH5_Xxc4gfMsMI%&7IE{ERb=FA(sGt7@FDvonJ{*`y(9sSvH-X2{R@nacn z-Hb19%5<73bOVL4$x_Nb=5d(<#*;%=3!5vq)*xzCl%cCY$o;Ilz*83S1DUQPY+cQ_ z6xC!vY1b~{u#*`4kL8t?N^P^_vpLq8cgHi8B^M!T+jdy%$Rn)JHI!pKQRoEoARb>{ z2W?v;uxmJV>c$KJdL6nfu;3+!x^>+QPOM1SjH_$#i4CwwYmxoFZMR1NK{+<_AXQ{w z3VuoTFRkD5BlgfY3>o(1{e>V04&{|{lVfcbrW7%&sw-=q?%>F?v*76`*=W;qcUQn4 zfk5+06tX1n)d6+&5u)N{>I?pQRw0IrUdf5OnO3vh0kl+1KO)l9WFk+sqCI81B8&n0 zNmN=|(q~-q3E9MsSYZ}L$a%&-?5%WC(yRM!uK$*GP z_W^VQysZTm_LC%9ofu27bK7lL!&g%*P7er@5>^3|e3C~huYj-Unl*g0Vwui+$!&>1 zt09>Wi1>*A-su+beG&XG!0(jZn-C7%pXPPMj*7kMubnKs6K6c$gz<3_?O5~ANwc4? zSpz-raY2g5j#XYz-JZroVs3@bfh=P83coh^p@^JA_ttNll**Arzh~w#drw1o^qu-- z83f4dq1H7%V-pGD#~zZ6wm;5Q`{(dZ{CK_nfxuITG@-gEQkOd<$!zKmte19rbv~Wh z#OZ;r>_z|huLUvQ0Fi`fMm%Vd&cN{Wd&8aN=SfXR3B+yzMe22m{;dRQ8M2Lg(Apr^ zn+t|vo*hoxy^GwP|MUCa0{V)V$;H^IN~X<>Y)bgU2ev0Bw0lZ8IkL@mB=&!Pza>om zg_?r5@73;Vz;67QXy-PvnDA@3PFkBIKcjXR9u~4c`zXWR`TKWS)V#;$M8I@vR}PP4 z!ETP%&UF9mU$}^mI#$C3!`G9=q^D)`69G$sR!dwUC6$Zw=mFHr+$gd&>|Uf)x)eOJ z0%veMa|%|qi}?x;o3}%g?N(8?uwyPhTLT};4i%Y{SqXb;q-;-t_U5_)M3se#;oZgY zY91|d&!x(OO57fH2}STNTRX`C^3PKDr0r%SG`)`bkI$9UwA`uzv+-val}}Gi#%yS< zA17W4$gMz4Jn_%D-#JPtIIvwqkz5pIkyZSuAYqF`Sh6)QzR;x|`}cmDENa?!U%Kho z)35r=?gVY7ipq^@RN5)Z_e3s}0PUlDX4N+z)&-o}m@bu#lM z7lbVWDxSy@K1rTHFo8w(k_R6KWSfpAcU6}MaBgE3*FZWBP3Q9+IZ>#0x0`YkI6OFCuu2A~RFJKP^U;dad1-dKBa`AD`m8Ch`NvB0c!@V_*| zf44^Pbw2^jcb7>%r*4$H-fpwrQOCU{tI~h7E|{x|Nl|+MqhcObsqu9e6K8&L^n`p7 zZFh9}!4eJ+&pLb~DIAcw7R%+|ojR2*iwZw)ap08W#t}AhWb*SK`!xo#QycyKVeLzp z`N78Gp#eazm|C@jDfes%wHuO$gqPW6M-a(v->Ly_MEk)jZJ?4T8$I0_4u^7n0n7f$ zs|9l^@8NEKea5hDOIY9fCs(Ut5?=}Z=#i2!%5Fz!1v08*ZeD!gx@UILs*PTAWxOAI zG)eOlnSu!*24G~I3T{94i5qU)Z`?Q(@hR_T0(yntmc@q#?j6y+)yJNz5qr$h z5rUFlKw|aJCkPcqZ21H%IMqJ)KA-3L+_JZ<;i~;HpObs(q1Wd_5%9M)pHaD9;*4kG zm`-s67bQcwBKdc+EUMHE^5ejXDvQrQ5U+cSStfI+u?nPK^Ikdev&?mxK<=>?E-AnGYBA8%z(H?<}y37TSO0Q7m2P#O*Cz4^^0vJtjQ{ z7Qq*2&L$RSIDro}Cs@6EJ?PCcG`l5(@!yb)BOI4g9$yU6G_!D?mu)}ZIMy<@z^S@Z z_3M+?tY8YAl-&ws+DrI>p~b_SC|I#1;3s$_IljjUw9>6dU#E|ijpa>ixAT@%r zJ)A1H<0d&io1{BcHExI~J|?WBDzNYo8}MHyVY2OD#yYT@TB*{~G1ifE*t}75C^w>; ztX>-6u2$+sF0=FO-e5Xz>~#WFaR0TX_{8mj)n`vw=IEu2PypirtcmaY&%E93Lz=dRbGeynCPY7}FX7vVTY*<35d`}CGOnB}57~jL9xJYQ` z02!_;L2k)jraW}BAchYMD&v}^i*j?pHT%t@NTSvA07cUA6mEUnTL1z`NZJ3Abf-?B zL3X7L?iNH%3CKr~+w2F*&!wGsXuSWd{V2tzMVc0dSYfD(7aT5=O!K`Be>q(PJ$W|Sd2 z0;=zt44e~-RE%)$KI6BQs6FTgJEA{k@|c`WQ81cWcmbzbK9dKPKjZhWwE&2CjE$9_ zz?&EI&R-k5|F%GP_D30=2 z%&!=Ea{`FThpzBdntH*o^WEAO!jKlC;c9ZhH{~o*7TAv!c-@@K6P9U_zpi0Ly!mMh z+S`gx%ghlckd&EFqM&)GC~Wm|Sb-TE@J5aH=jNO&8jF5YvGRxrQmd+iQpsREA3Sk% zwBkuU@9HC=GRm*k$)xuTHIUCF6TB$4;i@M`u!K7}S~V+7xJKMvWA&h$B+v~4B>K8f zJU&v8TXM}3Txx7GlLZL|NwR!C`>k?P(GvkD2IKMxQ#pDEa>I5(HOfIUQqdnl<5ASM z^N;=uPOJQD06dK!9dDyn>2kI1PVR1)Vt%^nM=s4?L@$(49z6zN_-XL9{DI(D7(HxB zkcXsj8Cb&rx?hqu+yDwQO2IrevEaaY{TfMrM>4D4v3~KN;@2dPloyOFcUWMPQMG-+ zuSOKMsRkPY7aPG3XOl%Atxjl9DX>7{4Z(|bl-JFysbghWP19HRLEOGn6QTkU{1*=8 z*bi%4lU=ry3J{Dcf$oti1t83$R?01^iwM32ri#faY$BC6Rm#(A-Df^487+k}$z*6G zOT2~>+}!ldX=cx){1WZ{_;86nkYCMxDR%@2bIUCR&wphC);EPK5~@Uj*QTO4AizCL z%<~zVQMK{0t{a$DuYE$M&eCKlnR)V|%!xkZ*L9#_^eiV>KPcR$RYeHYZ|m8|XQrkS z*r+s{iys<|xNAeGw$J3~fU0ki>gq%#|F=5+$YeBmN_K_()kvwU;%lj7&tX!RM|O?p zCwrqI(L-hK(PSxA_s;?o5syUs>B35}!uE_8&~OzO7U<1H*RvhK&h&!p{_Sh1<1zZl zp}a2K2r7DOr_|s>vFVH!eeGM`Jj4rjMeN&UV#eW9p8QABsEgjum>&7_{*DNJ+!#vs z_nr3dbiN0d-7N0kW<%Ur8r;6HA}1_i_Du-T91%?&aS1Eq)867=e&g;!ikpk0D02@o zXBp630EO=V&c3jb4S(&X-?jc+4}fUg@_F-1FwuYO(R-1}fEXvgZ!b=Yi*;4ET*4&z zwYQRDj{VmQLL|8*>X4?vl=eEe3r;QaxJ{i6&t8c(EetBAw0I@LOv=cybbT|v(gXHk zwZ{Bgsic7uY;G^I=}s`i;{zIAk;1_1OThmZj5TgVpsvW}f<+4RX9PiqA8|!;_C|Zh z1_6YkLrmG&s~TCt!S(56%U)!AH&&6#hvW$p&BY*n!QNZHD56T4l zq7xZRak}cD?l(OZ=IqFBGpp3va}fy4>3%=ssvwmFEbrK2hiPD>Y@#`VoR&h7sgFz_ zTB;n#f7jn;U6I0>!dbDdmSTLI%JU-RFr-xvujH;#vB-<5x4UOE!g;8@xB0C4fN4Vs z5k)o;FgkxxkQ^=L7CB~Hk@5C_SpwmYGe1m0#ndmL9pGrf1%F2W(3Ei~5JSAT(RIby zl=k;T`M_8S#~*)ox&HX$26OY6G->hdwlVSRv4~Viwn$rXVG6VV;fb|Q`%@d95aFg$ zO6>wQOzN$9w#^Ddr?`>Y>i9B^wn{~chtD_g^=U?lB^tlj{r%Y6o&^)e3dk7m=t8QfwZe~ z?YATvy0N*IIB^*aeB znogZi-ep9hTdh%oXZt&lg~ty81^v%CtDoMW7nH?nM&vg>ur}ZRc`3Yq8$xX! z)@1^E)2HycBNwr#)LxvzWVmJZbAQ0Ln3>V@l?JGS->R=J0`#W+AfXm~I^FF_tmeu2 zG?L;e|BcSt0xy&x)aajeUmVapX&gT9|F4?9=iFfBr{7k;7(u6~Z%S?QSpq4}5Px6W zM_!{h|Fl1aOqWu$tiTA~Ke96yNHHUYc343FeX&0^*mIvW9b8)fKi66TWJUPXM*fdV zb7iwWLrT?t#d3qb;xs*HLJT%dH_F7RdXoe9GoBp{KCqP@3meP&e&wl`$sC90fGs6h zx6QTrG6dO!PA2DlW35nYd!U_O_q8elJkNOsPl*9x4C+O%wnv5eI?z{lzP96pv2;ta zJXE}=PMGiZr=RQgb`i9E^FDPr4YH7LwtY_wrTXc}5a_zQq9u3k{VhvSbh8Ew+a+20 zNS9DordOumMPcf}*?pzy=P&ddULAokk7rdSe*tI*e2NJzUsf>h;F%kw>u_ld4Z4m2Tr zNmoXX+Q3hKllbVBlb=tr&yJPpP|*He*=4!>CEmc-nDl=dv>& z(ERcCI6(< z)QXC*69p`Nd6VeU-9Sbv3AOUZuR_szJ-5JH4;U@g+6(xB_`XN^mXPt!NWPN_*2epH zrOA)&Li^d2d3aKRX#JP?GA9C6^ItMn^f4UwheDLiB)XqXfRq1r;qMxtnYAJV!8WT@ z&BAAPw+qDvlC=Q`X2-|ITGK4e_z1ux?mzf|f5Ly@UEQQ_-I6L?LG7)UNCHFGo-B6D ztJI9Pw4KZbT)z~M+wto^0tCsdcSjf)WuzeB&<5ib9iXLI!Qq1*vLnfWduU22?!>~r zmy4xqw#KA~?tu*Y&OwE%)3{4<--3hSi<=`N=70u@7w;+u}WuqLJ7d7qY1Dd~%S7Im~e&I1l;pH6PU^s69y<$i$c` z;yy)@54Xvkug#teC}L+%6i}4rRHjZ-sA&IN^swyKf?G<^*V*x#^y719XM4hB{}^%A zrrkJJ^I8e{TM*vM7LZ{KQbM`2O*`wL6My)hNL)jQzg}zpz;1s_H)OVgc=BvlrfGIO zva^tnUIuyz1@mJVfw4rAs&G`M76&=Y7xav-f97jTR(MwVa6q0N>Ow6xA`3r&(x;n1Pi^K5C-VpUIj3vLL&gFp_v*qykw^iqpBLg!Rf<>ETND6G=OQ%lM&%^QI5x zIEl1a+xI@aQl2YUl{GWXV51Z&k_qV!%CFA@Phso8>Z(JI!p!iZfxOrY(H>Z|O zSuI4Qzq-YCOcvs22~0IPHRyWI6(82>I6+q=mQZXOPJRjs5Br>664!D$1nTjb>eeT7 zx{W7uy1!}FmbZLgYOb8!EG`7>a~{J(b}>|{#IKDVfs6UAP|!bNo%|su%l?6n7%k?K z!`ffKTK381xNGw=cp&fHub0<)ny_;88Bsi{RmF9#y^`BMCjzpi)jjxt?}_DyE5z?d zz+_YoCp*%f?vshO(mKi>eg}P06GtG<+FBqLsSvQ$TL%4IYZj3_CC7}MtA?pOOB$1i zlG}$~U z&-^fuoo5f@i%>DzMO6C2qyvwdMc5ztN0!dSi16rpZ%+7y@iJ;RB2Jcsjvh^xptRS3+1XTMl!1Tw z?>DZ+8xt89Ex1+jokSosK{5Y#R;kaFJgqo0%IrSky(T1nYm4@qB*Bn4xjp;x7-&jM zpJFE_0)Wh2K=bXCs)hG-MtC%ESWiPn4ohxHU`tkr_2A53J9Vgc^T$pbE0z)seS{jl zF2>InuBgVTImTC97SgwPj%l9Vzud0weXaZ4InJW*@7RTs6*+}2v$1lv$vmcG)BAo$ ztXyL?;wSmC4C|fr@M~HU4${~hs~q(bt*Z}*S=nF3UwYiecrURO_~*#g!_~cFOjDm1 zvOjb5V!BgF)5rmF)vL*`1iz|a3%atBL?-`2F#cEp8GeBukfxEJ#*gUd6nbAt)(p!g4l@mf&s!p?6pn+A~WL7t1_k?@fWEkDsUCJ z+s*q-I1LOY?qG5yqzvK_dT`ji$$as}DvK~4A_pJlayxRH=d(~m`BDbC<05n&m7z^A z89+1>Edad)Y%^+Rz#oa3JWiT1H8H?e*Tf0bOX`oR71T260JeoCwFn#q-#*EjCyoWS zLnD)#Czxwz202`Tt=-C6uEKHMmM5JZ!`WjmMkk-k_Z0 ztvGzurIkLzwFPu_ai2|3`P3E>LN~NmyD7wJic*2k{Upr<@0*b~MGoUzixb5m!8TBA zv=w;M@LM-Z_9*Z^niF9u0iTm({ml?4HerK7+LOyIG$Kja%I(#B@lI=nh-fDhAwNn@ zTas(n?Te|{pmvvzj~{e=}H5tPA^e0-2|YQg=vU^1@o^KNbSyh(qDXgH|zcfQ{`Z-`&baqyC&e0Xjr zeMTq~Mj>uu>w}@q@7gOrm9XwoyXkXXuFh7kXLv&@<%U4nNRK`skTE5+F|%1oshoRp z3&!+4LwMlbzN4b)RXHc7AL4u3iIui|%6sYxnrWr5gnjc5>LE9NCtf?l{lilYIp~wH z@5pFHa%0-J?Vw>E>jz&1Sy;aeqIfXF@*a}qvt89Gq4|a{BOoKlE@LG$qqMT0*@PgH! z$8a9ow0eD_G&pt4>${S90+h{AnL9(P$7w!xMnp$A9Qd*@d)Ws~48ZkW(#-h_gT^TR zj(9;1d0zi0P*mi&^-<_X(Lwu;(fl@Z@RC1u!8M<~5RnV0jnu-s9Yt~0Rfkf$w4 zkmRPhVynQT#7q66_+QG+To4y=vFpaABr{sx7 z#srC(|9r>vYMlq9C9UjlwLj%Nd4~=FJMH*yu+Y|{jLPG~A0ksL?*kS(q&L+<2!PGl zgdAA+Bg(@=-f!OR2lJGFC(0Gey)Pi$tu);-9$50)pW5}@LH(u0yV(r662DCrjyqWE zN%Trr#5N(EEt;;5c>-E+En zZYwtJJ9@-FxHQA$)x%%N%u02Ce>b|r+Y6bQm2_a?LVA&YiSq(!N2Rs)>% zepIIYp?SRSDTRC!ZyUNXI>nRmh{8d$_EzAfF0w_+b% zk9BO({H;7VW8yx)EMqy=@KTbcLn%4%Z@R&97GUauYZ)~K%0X; zTAe>;-fQIkxG*c|6Fr_|@u|RHn%R=b@)dP?;B+d)oh}q>1Or_=ekPUVMde%Rf`{F_3PtAepLLdF%3 zo*i5Ob+|>!e{qb|3Vk`Id6@}hGEWa2b>H@zi=crshbG;TFH>PcgN-~<@OGQ4=@y|9 zchi>WWx+YDzD10kw+^)OeB>@3R`083xa=8j$i&MGF~9cL5Zr#u{YCR~Z^J-LN!uU1 zWgY;o=}E}H(2j(_{wC9ZnWeb1{yGKH5Yx&y zq+M_L_#ov69;-uOc1{7GFgtVO+n$lh_F8%EhbtU_W6aoGSG5;~Bo&N!SnbNmSQg6hxNXzBzLKOnuxddqH#i<6+UoeXR^pbVG0VmimWr+m$;s>?$1g^mJzTWELJ*XJW#Y_50X>8zG^> zL#us)TFrnB3Vj8{uum&cfo z0YbVrSmXC_e4UEX6k22)<;|1}7Ib*|ZUygiO0?i{ATwM~hW^6!tu^Um8T4=&ZY(y= zHsJ9VMCyM16@F2l=-WiWiy~$R?laEkHScCHA!Ah;a(A)Ktj4+yA}?ZQ>S8YR^Sxur zExh$c$oR%mgP?AyW3*Ss5_ewmySx;1e)TW?R5iBrX3op~tuM~rIB=`}^+T##r?ldB z4*R}3_Ct-b%}9Y69)GuaNiuc>SLXGWe;=G<3deE!-K>xwB@b2$lTVxa*5oQjSIAFlz;1VI!Gb~1up2d85=2&R^q zH%~k@xL?mv6JtAZm#-`U$$T1iN|#+MuRv_U2BwgqE|t9?xqNm@f!$%8^BP^toOR z1KX7GaLB~7%$ZQj!5W^{Az8zO#bCNxj_PaxaxXh2&y3*0(56Lr3pR6qdK%jCG?uFc z8*tU~rs18NS5TMOq(7uypZ#*u3D<8ew`VgPt-61wM_s+Pp@e1Ld>XrW2ArPD^~_}j z&jiI&Y;DHc~odlK7P@v;^8goob~Ll zUZ|D6Z#ASHa*^sx!OqpbZB;FO4{)%SPIod<=hSb0xptVNyYSCRuz#*N!gMp_>C+`n z*tN2ouWkdTT(thD9id0DjX%;dQl%`muS9bdOGGO?OATl?hzp46UI0nuZT{;ZU+6EC zFn25v%IGrt^ccU83oN#a8oRQX>P(OFH(Np`4ASU@`>EJO-!F}IxY6;{%c5nHSw`Q) z+O~|It)k17^P}^niSoO!|zdi08M^U~zuW4@VnO)Q69!_W=S0BXh zhO2pu;|x2AZcp)Fe|WW#?hN2>>z|h0YNCW6#P{QcPyez`wW?jH8_Tx6tj}LfZmZ~; zPkHBR0b4DM?>D`2dfBd5>f|ym7XeNxTR+l2?-TP?m+`axq%4VE{ViSxPe8cVQ7R49 z(Xu2>R2?PPRN`3O+aa?r78eh8`=GcCoTL+Xx)(B+TPKPr>;64}m<>bzxef>q^ z5J6ag42^=;3`L*twyqZSrzsq!yaTWJ;H@|mZU?!Xw0m1s=&yWGA6a&nCx(_Y(%-EX#^=;^v{)GICgq*yy zw>-9*oe|_ajo*S^t^O?d^W)U{X^KmadfhVujh`&3Fz;-A1toVX zn}~qBku>XW<1@*3D3<^1okAKO={bT(wui4Cday+;Vs+kQ`9ErGu)I3HdJWy~Xl96% z|8}`vpjkbbSg3T5OQm`VrjSXrDrdWU>5T=W@`kyXw!2zjBAxyf0~3|%vlg31@0qh3 z40w$Pzo0KCIjC#yR5AT2L~(hmfpv>DIOz=UIkP1&8vjdsu}_OGaWRN9o*Scef-YOw#*Yx;5;(cL$~)0>6#Dvmjc_Oi;&sN{n0#QRm)CjVqi2cQC+m zM--kGC+GpgV%J(k<4;sytmVSDxGURjSW&M80*9=;Bj69+up+ zJAd_A*P7Dta3=0qq(&$);ysh=$J>e)@*6DS z=R4;=hRrc_hHggDP54Lra(YY6R+2XwznI!5WM1*zB{qnunq9ab=tBEAH{WSh#hJ7H zzVP9EXLHCzc$_UT+MKlr)40BLC$-FQU!H$3x38&`Rp>?`3Wp;pnGd~yFq zVMnrrKO59hPrE#HiF@pkk{+QrrAlS)|E#*Ux%9h;K_%ds(Y<(fM3;VPLgP=c!*VUw z04kTqbN9^kLtF>Lf_7&_yXCviR$Vzpv&HbIhrgd#5fZKoOI9(biu(KVU&1VN762Cm zD7hGM0j$Ej(@S|YNaru*b8;pZ<&pyul-!SsELtTKXcr5CHmZ9_6AFx``BPvbLc!+n zb#!&iBHO_xUjtZ%130&>9BgKmkaxOLOFN~+(;$<3?%}XDIks1%_WAwD$=H{3SA@^3 zm$6j~e7Fk*muO7@HS|4}y1OadNUZd}zHsrg;$TW;6sCJv5!1v3s2Zf?%fFq(Yd zzuZzF+%M(tb+jyEt6#6WWcir^vVQLQkb3x^hun^cp{MsJMxrj`c;If>cl)g@{nun@|zi_Y9Lt$X3XbWwJ%tQkE>ulx+~kPPVa+ zotd$WF=qbH_`bj2`~KhSz258kU)SHbc;=k@oO$Lv&w0*$?$3RnqYd0UfEI_LX>!A4 zOOvsL?$2%S!#VQPHRo!tQUOgMAi)U{0jPCO_!e5iKq0%_Drn8E{4*?m#yr^-Pz$DG z9s4IV-8jqAt4>1!?T$VG;lpNNlozucaI6Kt2MK)?4A^?$gD0=Gu4-T_f=>pozIJ|@ zAVAnqa9>FsuGMKnt1ZXeZDKs745^B6j&hu+d;wm@K1@JTfjMO7R<6o zE3V-b(cQ$J`#wBn2Zm7xcXi)7PiZ}NINlxmLUoSNBaAit%?B0MH)CZd!JUV$+p3)9 zbWt6M?C*pK-gprm|5`$VJp%*XHrYn5$+JOUhW68kTPQz{Z8?Fj&$SNWw0^2|St_I* zx|Kl(O$4h>Uw9NScN??K5(?z*SFk;yb6jxTjB$4%;6fLF&&+N96x_KrOBbqM#JyVm zBz$MW0{?Yy)$1mTy=D1$8jNV!0Cwj@U3-z= zgQ8$5=k%|yamxZZ`mn&3pz*rX+4Kuny&~ivY6l&z zP;z~AW%0`b=|SLZdT{Fg{aL;wIAK_;n_l7}wU+(>g+ISk3kS)P*O0pm+?k@AlDX?D z9fY2h$%K#h1vUl7H3p{Kr_nM7=|9&_SOEqhZ*uE6s|Vtjyy|5QiI?oJ_Fh;v9K2B6 z91C8~UKR|sWZYEJ1MES+(HfgvaE-&`MO4$h`jP!MY4237FNH5y7jjwnzg|Vh@{g#{ zCdD^H_PiZ`EKzfUWVW*$p6lB-4zPwSHJw!2i{H$u4fP2oY?;Eayg`XOJ^ldoaJO)f~(W;(729!63TAQ1G3I07ti@4bN&oX zG8j!3Mf;EA6gV4pq@powMw429@>j2sMdvesO&D*8-Zr4w!+W`s_~g0PngmyG<1*b` zg$JXk!!I`Nmx)arE4c9(V>B}RErSNzv%^zHr(ZA!yaxLoSG(+1C=rPLyAlLd7M;UA ziGjZz+={vJY#k+g{|5(0oWE5$`flCnG(ik6kqeBxX(5*JZTx5Aj+|c3a@Cx(xQSp4 zzye+iC7q#`MV!8*9O6W6LU%?ev-lf_75_~z%QIIs$m_#ez3iLAg4GL;zuPr;Knj2M zyoOJ-(9M=!RPg*B{Mj>R;_2X3b%X?8^x>0O$O2_?ulS53_{(bkoE?7i*eW3x%Of7( zZ6;Yysobl8bAQj;`EsJ}wvE;kN%3jY&9LHUs9Jq79%Q7B^8nL(Yh>rJ?AI@`fia2c zCZ&)te<+T3kJ##*uXZ*TaPbHTrJT?X3Yu(fmmB6Qxos!(l84W9=RJvO=HgK6;~&*2 zq$QxeWaANI*uTwMK z4JpQSRtuyZ)fXl+x5`leXt%+WMv%-I%3&w|;4Fd-hIZvR4?JwNx6R}#ea6l9!$yY9 zP*yxM_T-h)fMubG!|T(3Fg~Ed=u%i3)$OP3271*=Y5`>N8J8d@2#?f(?MWNA^v%LN z{!*$3gY|7R?&TQ^O1pcswRvc-Kk@MJDb-hzboa2e6R16`D!Tfpnth$qVa3|UT~XQd z;E9>Ld#tlWy@HMVz}W1M^&+h@sgf|)v?U8vr)?(fd_ryQ>;$Cy;yY|Jhq+O-DsS1W z>ow@7so%Tzl+@iE2f_L}lb)EaYV5aPUibH)E?pepQ8yt)2|o2aX#iY{E9c=>*U03I z^!@dN@hiv<;_XsY;yb31N}}rW{EqVu-S7b^$ae%AslzB){$n*0J{I<(trm`1msbAa z@;jkI<|B;=dT_Y*Bf@vY8r(aWZ6WH2Ps@U)&&aP4lxw}jCH87ZP6KN$vh5A1&VWpsM@4S- zz&y#t3N(zUf$$ICAr>l%^S~#9$DSRy0$GiM*T!^#|NlSHBqwE)cM7kcK2dOMx9`8+ zasM2TJY*mPC{jQh@cZF^A;aERtL%&701>&>LBdlwi5J1AX0fp12Y*j&H&02#;%} z@gRio>lg5%N1k4~GLCvF7?W#DX-pd6)12Q^+sf+zFXBR~U7$eUn`%ka19D}e|UT>O%5()yLGa0H9 zxH+egWuZO4hP|iM$lu;?>3$}&YkHkS=*3pK1wi>ukANTH#6mU>HkHst9x#n%^1`c4 zuL~ZBc3iF*6WaCAahI(+v@FYTMp|m3F@{XWA2y9!@nn?Y*|iR*iS{FBW4NI(5;0ot z^)mm9W!>Lu^Yb9y6&lQ|h2B;&r3$C_@I3hP4%Q;0^jAfM6M01R_&}<;W%$jyJI~Ez zr%GT1F>pA5fjTQEm=;ziLB9XMyFW<9+1UN|1+J|pp7ZmYBmf6iP-fYiTRTz1buY8+ z5mPFIGYz|K3gHGZ%s-w0QDT4Q23q&?VJi1Qx%Ls0nc9g+XEncfWdRO_t4yH@`qBu< z{u~G7sYgTD<~J}>)cTy``;GhnBA9~U!2C_)PE@J5yza*?w{Ev5>)~;CIs6M7#&WN4 z1Pe-0(*l?!qKLRndX(d+S#Z$u7F9H#vFdl1O-gm7ysvChoAO|m@vhp#t)V4~MF&OX zz%Bu_l(kZwcgER0)UY-cIZwQHG3qS;*wuZj7RW=`9LYXej~f#2*+I!&1;)~!8@{1u zw>(l4OJNulc$GLNw?k20ICqipwVCzfr>;zhiPm>wV)8{DR-%a~%FY72Z>heCq3G3q zd_Oxk+o+adPj*8e_e*WLz_SUI#Q)|po!XHEmWTO@?x4Z27m(glI6>e5!-IQWbOywO z)mx;|GeqyK!1yt51gXt*LH>1K*riwntxp2V46u>$crKQQHlvIg0E$fTq};sYYIbl( z`4bCZm;k^aIK@QTToSx)`8Qn_nmYUEoeQydV({k+VwKq==lZRTrlJG>5tqf2Pu+S( zo!>X}zS(`hG`!}ehG(UIu>9c8tJep9-!s)By$|&f2g4v5_n+;xA%G^EmB8AMBG&-1VJ3; zVQkzMePkh$p7ra1vfFxGF7++hElKL))vci~kw7E|X+U?X{kN^4{fs9Q>8m zpn4hCc*M<|vL2Yq9qS}$F3E}mpzLf4AMuB36jWz0M+GZ$0ZsVW08|JR8cvuVOu~lI z*qF3zjUd7n-xc!Rr?BG#*-qw)(#Ps%E8`qBuUzPhWgz99YinywGpbcB1$5@S&zK9P z%uq=IP*R#()oPAk1Uod}xj%j>dEhRm%-2Q9t%dgQJ^3UtjtB*h5))G^qixoke81Af z&?*bCnG{}W$KjgJdH?tNETwhKP&IrPG%9Jaxb4P26==OodR>mT^J7}9SxKd}d1+G?cueHw~Z+ayDAQF zXRHU-6MhhtB!1-9cSL#|o4R$h3nko5{+NGz#;YqleyItdRMJnRdE zOjL%fovg1IChVF0QeZ zJ+F!R?^K(EqfGG5yALc~nXNoY29(eC!-xkWjn_!JU||>ZIIzm9(|!^0+wKXLr=+F} z#3jIljjp2Fz+lddB0(*U3&fX6-f!4 zYu_g@;ImW>yzL76CRsCoJJyrQxVSd-KRem5__McT}+_vGo=LC9I=`^RWw#b!`0po`E2riU#C<@+i+u`WEP&^}$ z5T|M2U8xZnb48U=!;3doCY%q;d9cm(#gG4aVy5MwW^Y#Bt@Zq;rdDAC31xl^EvRm( zCqli*v3_vpqW@6sSbt!`F{CpIg5Zt$RMwk~%_=8-60{@O_klXkQ6KlkKPmq#u>J(} z$gh4SGw^Ad+ELA6O#;qjjJDA$M-`VuXP~J95px-((clfbZJM}9+vfzp^8u!I! z)R?0HrB$kaO>s;1O zhjBv;r;@OLYu#VX2S6jG&LFMBg-vtXI|RK(Pj`gsKA4Xd5l1UPq$NGfbaSCiX9(yv zIdPDBVh7Z_az4G{1Q)!vbguIBvDmV`)mK-sK>tsPFpZH_0JxH0R-jLp{miF#Vofsj z+ZE|m-8kOkH&_kzoy6;}r%?bnrY6)0nb+7_i1v<^J(T`5WUI>D;>)y(A0|upNq}i2 z4CTuNu33b{0NJz(M;H>+Anh8&2b4;215$g2GU@`8tklK(l2=ev&SM_!iv{9w-M4J zHZS)G9%w;aqW_jlHlE2RbK@H;fP=Y-{YX_#WPT6wEVw5dkRt}OrGq;2%Lu6-Fr$md zeIt`Zfu@{Nr7m5~^-vK~zc8uv9F0XL5Ba^mA1j-?Z>#VG*jnTXzj%kRFrRi3Z$h)= zwynRGECslS0_Sqc*AdbPUXCYy5=yiS+VI<&%?G2iPQY}mKy@pw*Pexw9)>R?jzg>W zi?G}SGOY$CZ?VvK$MV?`=`aBUZJ;hkXIb^4u|T?&WcT+Qd%vtQVSLE+jkwdNY0UBL zpWF}!)3;09lNQjp^Yb3EqCvre6>GN`Q&2lx$;N--9;OQ8`%H|2I=7Q!<|P*IELdK6 z_f7kyvC>)KA<+_om5WTo3(L{?4nt|kMSq_MXRz5Hl-j*#y^ptS6|ObkCd(>k^FKvjgC~^tD_^{3E3Yc#!f`-<4f1<;;OHKcxlQ>`DBs5gPPPEP#p`HRQq)(KrxrO|I)$AG6Yrfw%g;U$Qq!(Yi> z_6WGQpsu$*nRJ35Ro&-sB|}TWT;}kgXqW8!hhvA$3jp{#R(Cs>Tn&o<1GMGiE@jjb zJ<2Q9C1BR3NDd~tVYN^p-Xr!1&U-uR)Ynx4zgIiK%4g~b^5OEKE1DL&5mG$br01pC zv4f4Dj}jckBF(}+mihfAH-9g8<~hIMBcz}f+OzeNGc}_Aa|Fxy!yAZ;z5xzdU*c?sfk6bpT`#cfXCkco=DD66-1S_u}tv_39_bAki&V zOWbpQpdCGCj%lBVjmc)5dQEW(HML9qf#r)g&WrvUjDNSp?J&kRRIlvDX7fVdVH=CD zxM!u%%kC~XAaE1V!69YkPQtRo>Nmbc1MeSD%!O)3r}9d8qhqTRp0E`Hw8lRckSM=h zwy%N-sd>S{Ihp6~FgiC#GNm{L1a}?2CPyCH7Oj;Cj7!sXo2oZedM=487~IN>5xXb- z=}e5#Og@w)CSk@2_U55C-YKSw`Kd@>3rEQ^{aj#q#w!JAidnWnXE3x{`VXE?ZlmFD_$R3b)J=$x90LfwC5alczgD5;4Q4kBMKpdpX9l@78{wf`ps| zF$HLgORw)VygFtHA{sdXDu8ijoh`GW5t){=2#~bJGsCl^(rGXSt!+C85_(8E$`@?0 zZ(Ahs~`sEEGC2KIlf_TTRnNFe9gtiCR5VhhJ&I zJriLG@}|F;6F3xkuvuG;d8=vI)x4;=q#=&3OyU#&z#}I4isy6>e!$<1A6#THqJ9PG}%-C$VQUhDFV*-LpX9^$6@dDxj}INqhTI8pEOn7?F^K_k)$> zUa}=pBxDiZ*>_Yp;Ig(7b)?roSVdknl$}Q|Rdq7$F-l629Rek)wd;zMzj!Pb2G%RCZ$t@q+L4fYEGZ{z^{Xg@0Rnw@c( zrxAt2_-Pn?FEJ+Q(|jH5Ied)syVt@EIvGq;i`gLFPhKG@SNvp8h1ortifEL*jaQmU$rSa_|-1DNH-GHmAU4Nd2 ztshKm6@>C>p6>KUY7fe#IdSP;w%K~kw!jau1N#mhNhlI~U_D_4o=ruGY0cOaXYIML zx$OKM+9(mtRet=eM5ORFTD1*woFhO7?ruHONsV#UG!<0}+-T!Sky6lmx~LY=wr%^H z^n&)e-Y~@x17o)8rPNvXj$H0Z0YuV{AfbdDscd<`NTZ2fULPP-jsy+ZPV)AQ`=qea zZzY^&Oa|Dvv{NMHEpM2Ll@kOQEfKolUUrM7CaS3wl6S!Gqo!e8=lfB>Rj3B+udsOH z)Oq4By7KToP!eN|xtdngTg^2 zFnLo|_V+-`ky3rz8E1b%bu$6*+-GLuYAFQVmH%pXd9M|obX|PAHchmN2Y(!MG?S1_ zg}te0LCiL6JAS9s-AZjua@i)Fq#sjHh^Lq17R+Z`eJi_j8yLQwSs7l@GNm1RX=;7{ z1vg*}j*B?<-5MVke)tWZpjUA{4Qx)6G?~jMv^J!bij>R&>V2dZcWad`OZIb8X?HHS zgOeUW1LG@cs(qvvH?59U>XlcE#?A#aVF~3yK;_}JgujUlLGgW4XZy+RFzRgM`0c=} zneS7uyIgA>*A4=Pl`5BRmK#~q~joAh2VvGAGYAi zSM#v(?rctv&TnnJJ;w_rnH9X$LUI_xBIgx?q@cprHtYSN`+L9Addj0k(UDiTw6?c# z3m7pk)#TPFKON=;Mv~0&g~kQIF>zDNb0ej_3C0gP{3+fEJcXA-$e?yPwG`GEK4Z{P=~>D<@Q)SX^X0Z}>xG-c+NIYn-%NO~ zHn(RM;nJOl^9l}i)w#+jT6%L+bHkFBa+#hME(B|)Va{00OehA?g z+Jr7v=0#%c$H}Lkos5(Y?B=oE`Nu#ij#`_&ui5MsOsesORy$zo<6V}>4HSr;2|Wmb z!+u1&8!2I}2vXc5)|74@xvk?sEfuNy19w<0jSxNab;m$Fzx5UYK*a*s7F^pZB&d(^ z0?knxLY{H4{%!=}9Nw6zyarpy{02+dzQ!UK0uvy9H^w9Sn2y=OU0}YicyMZ|>4MW7 z?33!t&6jX>#fwt+zLkQjAPd!HSEL+>ms_KDY>eJX{Mybx1a$WIxk7I1QDiq5ORL$g z%6wC*Kps`5zJ&Z|4T;KuorjLPD^&y_c*O?CD)3|XGEzJPVafYiyW}JBQZz7&!u`$5 zoWyjISRpb85kKGQuT^UKT}ApH%0fh2{3xm&Uw4;Y=68>#32pxoC;i@!#}j55S$8vD z>Nw%F6leI&6JUFBMz=<0L!HvM1Vtnhy}m(Wy`~wld-6ei7h31XlMgQ=0CX0;N)U?) ze6v}v)VjGWxay4JFQ6t>sdOG_B#T@9a*F-E*EhJ1t{qjzPGMjpI=;Uc4X4}y}#I|r`RpygiW29^MKT=y!7EHAr#zT`Z6>z(4v z=GyN_K`To28Z!5X!ed_NQd@k*8B8O!siKD`q52|VUH^1DCcZY@Q#M6|`&CA~rDoqL-$I?t&`V_w5WBH@doQ(7HjYew%} z>eq?9F_WaTEnZOt8#xPoJHp|QeQJ8t!TcO*;1jsjgNm*q52tVq?XTv*mRKx3 zmIT1AbT*OCt>0g^u$?opebV6|P&EM1u~kx4i<#`~4)f!LH5$QsXD;8|8}$zYQ+~O3;FT)0jp8-6|qqhAmd|=U5DWD20dj zv1I(pEVTa|M(?=(o*3QHLK-qasUW0A(!YjT%o=F&uW;ry{$889LksE>5)BWdGNH|v zDSdMzfDIiSvaI`$V`7m!9nr|%?rTUCsm%V5&*#K52^NzHa+--3 zq{3zV1_MGFOo87Ok_wkAMf}Qz7n@5@wNZNxb^y)N{$FSob$s_+@8`lT5`t{CO1bU? zD@%a|8^s4fmx+yhvy5?-ZN=vKH^V4Wpb#-ec=VW?m^%D=DiQR#mVZf!n%Gm$2^P{rv(IXmGCX?`fsqJCw zV+e$|z&krG(b6b^r^_6pJ&~!zRfM3*)}?n<^O*YafmYFq$M6`;I`i$#Hc>kJDJ z)JQ7qf>!v2Lx6dV;VV$UONP#43{d;-|5+sf(RP<}!icG1=FADJmNGJEA6(%o|Z?+R6wp!W`H#ahoU;!tGv&FG0z1R~I5t=vL9Hk=LZuuia%r zNFk)!`A;YP@;1FX4%Y*kN8t6%hugz@PswD3Ha2rwESy2S0xhx*S4aRV$QG$1&i}9B zaRf0N}HV zG3r5Xr7_`7Ts})~6diX)Q>b6q$#kYhB9cQgedD6}U{hwWgyPr~XAY+z>1(3Un`MiC z1Fog>pje%5wd1hbaF(!kNFXE<(D`1%@+aGj`B270CSEcDb~ZN~Sd zIvJNnOe*Q(JfpqbasBaw6E)WPiT#Hj)r+_>+pcxwbx`3dhPggGh*|0PVei2%s5s@i zrqOYPpV25TGT)k4`@TuqhL5%t6uk>^WqrxCZ3^CMv?ca9v>3~}hKhW%3n@pdgi~E^K#{Ox&>qx2?7(HN`^^Lih!<0#3?MtdImGWLoLx_a;8rsN+9MQ6a?FRVTSFPWm#~% zylGM2tQbJdsoxxaYKncbk;SK8+wui#`Q?BXz?KiIZ80PC0Ft>*mxcuGO&1nNl>$~yK_@%E?11G1}!O0t@z^@I2bWKu>TD=mz4)!odzbxUiGa{D_ z1BbfDK%TJSZ2u;~5>@jyM-m6gofMydF^s%dR68=3e-GG2MxWv_iFs~@WkK1h7A?(H z=K*J9_YEmkhum#@2l}-sg5N~sLaAYaqG$y=)cW0vbf(UCKZb5hZlIm_5)RP_B~9|g zw2?$xdE8I>r!yzui3^6?{)FkjF`CT@+|@CZpR;~o(4Tip%OqMIjICHGm|M6Hqrzy& zokk;C_ZjLPdvkU_KQ$RSm~XIk%2gO#^Sc7@DW9M6Ww1DRcxRjcPqZ!oS6D&M%^a<*Lyus8qM$}78mq~ay_ zN1`pD!h3F3D$F~%M_QcGa+8Y`_${zE3(tw?vVwJnV~fJg@M`uHH}ic)PyA? zK?66gg(7lD!UV>I39looIgP*RY}C%5!FlxkQ!yY;1Wv+*6TSvIDh#}PrkiFB4}dk_ z{~-|-?G@{}PHFgW2g?*-OCvfwBOP?3^XJ^3Y9pp=CNgRZ=_@_+=d%+Gn}z1ff24If z^P;x9e{vuCap2>??H~cTSRdlmFm{=ABIJe|H#B*3-!?#%o9^_T4LbBY7m3iDy6@$I zZ{u5E63ZL>)CVAiHXV<-|gPgMhfl>Nqu_4s|u2>^CEIJ6vx3adWDDphl| zv@3Rd_T5dou3<|LpHjQ!!BEds0x$%4rxP$oFf{_NyPk$3C4)AG5@hRp`<&nBYXS5z zMHZS|(4R2OJD{=WHp1zsbx&<<06>dBgi5lKpksY}px9oGqJQwgv!ttG;t_#WC&54c zzSQLsP6+;o^sVJ*N$}r$QNGyw>nobSqJ&BWaKPt&@}C)pRYl$~iC6gPR}pyC<>M#R zC4*$z)WpFI&xISmvt&ym_th zzy|M+joJEkff={3aLy8kaItIsMe7L%Y@KW9hCQF~(8U&cp)%^M zGyg@@8^HJWbT??`KaR)5%;^rB;y=6d`T}84IZ5Wg1h(fGgK@E0*d^|yTf@CW|B$Lz zq`@L5k>ig~I|xSI2YA(=3DkuTKiC`ubosBvX#x{ml~)pjdyhj71jm^CasMvl*)AB` z+BEw0bvbPkOp_@BpJRz(a$j0e1zM)JFd4PMJpLxml_Oii znbg}5nrYz(lobhtQkYZEApRj=%sWR$iT~tgi{M&nQcF+R&1cnGEO8Itjf-p><#~Zs zn6G*DXCLT;>VvN())NN0_1OSV+zNnH)G+;)T+7#igCAeE+v>1gJ3WVHQ88r$M@2pg zV;tLAWZDXY9psJN%_}NMd0n0OP0eBFmPQTR-$>S&C5Oo)^?dUs^i>DTNZ7Po)-Bbd54#f!iKeXAnQ4=$n<@299@E({HnJ) zf4xQs-{dqtza25Sf>P*ByVSc*Ol2yZt)+;Z+iQG8;fYSV&Z%hj94BM6cmBP*_A}(( z$BvEykG`J=deqPm3c;7n`Ol1Tn0^!2G&6<=sXWW#Ag>AHl`xkX0!9_;g_uO@34?~M zO3`0jh%{}5*@4ywYdinVgM(o5mgY^eiUt;2?m`vVdednII#_-`!R;`QVuY(HYEwfV z^$H#Xffs-8&+yAi^Qb=t`#z18_1y;?dy|P<3hX`X81FupFOR_%k7H%=b0e6P1VFm7 zQ$@XNu*={LuA48>@@l}lKgdH@0Z@+08|q{29@?m)NFc!1)s6$QK3PXL3Q63Ivk)o+ zme?pFs15C!QzdL_rOgtzG;P+rfMkh-ai2v<2PH6TKMfddki>M9&mu-+iW>v-f*yVz za=3FwQ6_cUm-WzUa@A;$jw5_SZ6x~aXjS0Hd=QV?0kK`HG$z3d`rVuhiIWnL#ckJs zd3YIu0~B78bARwLoG3UkC*JpkvC`Io(|d^&UW7XQ7&<)amd0dkdRgwi>6YFEwJw zB}3VQt~~%f8zoBvWEW{W$q7}a-VA#MSnq2Y;N3_IP%a=O^Sfw23hlv&ymR$@6A#y+ z*qO;n2P&<)0dfh>kpK)3%;0V$A4vKCPGo~~+Do{7&vDl|S-=w%Aw?+IuKOhSRxcv1 zrY~sdr)(aFbp4_cf8bJb_0{7U310JEC5he-DuvZ_dkys$HPCz=G5RH0Y+ikloBNrA zZcp8u;{yAxi(bKK-4l#T4BEd~)Vx*#WD0=CBIP(aU@Ab&r|^WrCf++skpibSG@DPJA_+(Fj4h#7SGMa_hsyc1 z6Mmyb4Zwd`)s}*Q)w|hSI9p*^5}e3$5kjpfr1+O>c$42u0JkVyw!f9fC;sbB0>B0x zKJ*NUw*pxU|72O0!7UdhImK+2YBL$vfx#0~dS7 z2Kzq?ykTFzO^J-=j6Os1TZP1zlWKkh3v?n=674BN0J5L047fur#ZlV~(=?(ECUjr2WITJzh$|nGsndC2$Lt-G7{m%m*X}tUcJ|Ds z?c~IXE4KGBA+k81Jw}8)EbFDRPNRE3rQ`dvm*>g2W0yaSif=sIWqC;nf<-ra)4Kwl zjnRY*KQrG_jRxkz*!IR&7ZrBTV>0y;#OE<}30cJ1p##SGbXw|S5Utldn7uk{)a9}n zL>;e;MSBW$ZE^#B@H4p?_BYwy2A}i?$$9kN5>ySabh{UF?_n$XKo|8l2B@l8*dV~? zJ(+<3G{>KUau~)Qt{1>Z^xZqH* z^}%^|tY++!@1td>0>-F%bPjXp*ofr&rYZ3SJW%%k301~!7HY7MM?nhAcYxD^Jht1C;{-w zq?Y}}`}qs1zORxu3D+*0}&WRYlwbydz$&SXOR1 zJwB~*L^u*-VGLZ+@YGSipliJr^XH$*)W5j~;G!#}PbQdSjuAH}Lm~iuR0E6dRCoDyCjOiN@SNl&J}kZ9{$ z4ES6eshVwD`xiJn9?tCvU(=<`sT9B;~h(2WtKhS_wp0mz$? zSE8Dui&sG8k$H&698%H7-)Atq^PmCErdN6lv~euIK$&i(MEm_nP3O{V@nOnd5L}T6 zl)An%R4!2pd^n?9LuPxU?E z-^Jz7zj<-RXzwDgzs<*ziVKstC(Xe6AGPoxNGLUqIw2z~F3~DN z_eS{1k#hX8Iq&c-T<$(OPVDQbDA8Nwb9nS~U59_PV%ciY5ujn`Nm`M%xd-|WzOezt zwQbEfWbsRj^ljzyzx3p7`51PIe;~U0U#>DHJY?@Nw`WqwI%3IIXA5_OpcvN zK76SX<2E9HE5mj&LvxPQ4)|Ddo))P8v}9`#2h0u?CdUp(#wy*Th3u$y{_cAa;%!z5 zcV2&Mw*cc^3~IvQf-XanIG6aj3h8$|UmZ=1F3p?wVzsfQphItuZF8Y;MDS+a7G-r9 z$@NmM3f$nn*fO2G8nn+jvkLO&*`<+{i14N5i2;?YGo5K|ozw-MwNuFUPxZljOfRrJ zJP#^cr$Zl{fqH#y@+~D+-Fl}xX$wtU8ZhSNA;k@~6s4aE@FS8{{#>(|Jn46yvQK|0 z@l?7?LD^ydPq!~7H+Wh)&=t*Rp50qyLl_k8-)&eSe-OCHwk&kXzXYcxwr>FUa%vuW zV>T}O&F!(jJ5KBOs@{6r?Ad0k+|1@E5Cq>c_}`h>UV*s&9dM*7$8^dYc4xb1Vv zJ!!iYbP|l0lKWaGAHiYPSu+^!*v&p>_}E>+_>9@x8K)-QplB1~AiO7S7I0T}*T}bv z-HdT;DhhP=c{5Ye+dZpzfNBJr{1X5Nc9b)EFe`ML$%M@z>8Mr(x}+F zxp+(>97L{GZw(OXU6SX)iTkBrx}FpB5eOU#Tzx^riRKt>m%qtEOT{K=Rc%PDOKzi7W zhl0xY_+vjlnxc4EjRSF+RVf9pPoH?EM=hjNxLjW-5ri`hR7V)H2Db2=nz9JC`R zX1l&7>jy#O+O6TK#C3OS@!#Qj) zza6tv^3>?#ov9jQXv;&VLjYk7xZbHXmK}7<54{6mj~5})#Qj^%zgh_Lx`#KqkQrmk z(rXuYIGW7A#Hxha8GlYpkQ12cCx%#@xze|nxD`bAv=rD;e5WKF1G~yMw2lBbyln+m z7_2Y{H<9g_s?^ukEqRWUHNVM>LUj-Cl`s4lG+;9L?1|#ZK_#C>rmC&f{{y@#7MZfP z57&nT#^+k5n74gyP<0SUvf$qLX~^+x+f$pa`(71tlLq}a_oJEzgHdS-=n|3eU%Y>` z$JzhpUmRv9z!6S!HE{evc^N#RLbj=nIApWhuSItd`^yM}@%~h~irU?jyX0;>oCqwl z+kXM;D$vkRFl5uc`0Nwgp?ayKoAYuqUmq%H`tJX--dY~QfY z`MAGM{O~Dg3Ac)KkJZWCH9{kEH$fg;6LTYHk;&m(Rxs@- zL=-%hHL`+G&%_BK@djnDozATch-Z#zQAO)^`C=f#63?vu+j5Ra%R36wQ9%aVlcz$x S`y`G5KStM0^~-f1y!b!*3_^DR diff --git a/BattleNetwork/resources/tiles/tile_atlas_red.png b/BattleNetwork/resources/tiles/tile_atlas_red.png index 2b6404c1443ae2062f373a4f3be56748f72d67ea..279b82868791d24d9ea8ffe4a2a20a444ef62cd1 100644 GIT binary patch delta 23148 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 diff --git a/BattleNetwork/resources/tiles/tile_atlas_unknown.png b/BattleNetwork/resources/tiles/tile_atlas_unknown.png index cfe2d68bcf66d3be813ba01fd9286b2d58982160..6f8b136f55bb024f9f77575b02ddddb3537210ba 100644 GIT binary patch literal 23733 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 URaJSfTJt{3^ERdx=TLY526{W~Bme*a literal 19676 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 diff --git a/BattleNetwork/resources/tiles/tiles.animation b/BattleNetwork/resources/tiles/tiles.animation index 7d95d81e7..17814bc41 100644 --- a/BattleNetwork/resources/tiles/tiles.animation +++ b/BattleNetwork/resources/tiles/tiles.animation @@ -1,202 +1,235 @@ -imagePath="tile_atlas_red.png" +imagePath="C:/Users/Proto/Documents/Code/OpenNetBattle/BattleNetwork/resources/tiles/tile_atlas_red.png" animation state="row_1_normal" -frame duration="1" x="0" y="0" w="40" h="30" originx="0" originy="0" +frame duration="1" x="0" y="0" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_2_normal" -frame duration="1" x="40" y="0" w="40" h="30" originx="0" originy="0" +frame duration="1" x="40" y="0" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_3_normal" -frame duration="1" x="80" y="0" w="40" h="30" originx="0" originy="0" +frame duration="1" x="80" y="0" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_1_empty" -frame duration="1" x="120" y="0" w="40" h="30" originx="0" originy="0" +frame duration="1" x="120" y="0" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_2_empty" -frame duration="1" x="160" y="0" w="40" h="30" originx="0" originy="0" +frame duration="1" x="160" y="0" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_3_empty" -frame duration="1" x="200" y="0" w="40" h="30" originx="0" originy="0" +frame duration="1" x="200" y="0" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_1_broken" -frame duration="1" x="240" y="0" w="40" h="30" originx="0" originy="0" +frame duration="1" x="240" y="0" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_2_broken" -frame duration="1" x="280" y="0" w="40" h="30" originx="0" originy="0" +frame duration="1" x="280" y="0" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_3_broken" -frame duration="1" x="320" y="0" w="40" h="30" originx="0" originy="0" +frame duration="1" x="320" y="0" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_1_cracked" -frame duration="1" x="360" y="0" w="40" h="30" originx="0" originy="0" +frame duration="1" x="360" y="0" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_2_cracked" -frame duration="1" x="400" y="0" w="40" h="30" originx="0" originy="0" +frame duration="1" x="400" y="0" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_3_cracked" -frame duration="1" x="440" y="0" w="40" h="30" originx="0" originy="0" +frame duration="1" x="440" y="0" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_1_ice" -frame duration="1" x="480" y="0" w="40" h="30" originx="0" originy="0" +frame duration="1" x="480" y="0" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_2_ice" -frame duration="1" x="520" y="0" w="40" h="30" originx="0" originy="0" +frame duration="1" x="520" y="0" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_3_ice" -frame duration="1" x="560" y="0" w="40" h="30" originx="0" originy="0" +frame duration="1" x="560" y="0" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_1_grass" -frame duration="1" x="600" y="0" w="40" h="30" originx="0" originy="0" +frame duration="1" x="600" y="0" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_2_grass" -frame duration="1" x="640" y="0" w="40" h="30" originx="0" originy="0" +frame duration="1" x="640" y="0" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_3_grass" -frame duration="1" x="680" y="0" w="40" h="30" originx="0" originy="0" +frame duration="1" x="680" y="0" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_1_poison" -frame duration="0.1" x="0" y="30" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="40" y="30" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="80" y="30" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="120" y="30" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="160" y="30" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="120" y="30" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="80" y="30" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="40" y="30" w="40" h="30" originx="0" originy="0" +frame duration="0.1" x="0" y="30" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="40" y="30" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="80" y="30" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="120" y="30" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="160" y="30" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="120" y="30" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="80" y="30" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="40" y="30" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_2_poison" -frame duration="0.1" x="200" y="30" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="240" y="30" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="280" y="30" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="320" y="30" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="360" y="30" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="320" y="30" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="280" y="30" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="240" y="30" w="40" h="30" originx="0" originy="0" +frame duration="0.1" x="200" y="30" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="240" y="30" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="280" y="30" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="320" y="30" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="360" y="30" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="320" y="30" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="280" y="30" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="240" y="30" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_3_poison" -frame duration="0.1" x="400" y="30" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="440" y="30" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="480" y="30" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="520" y="30" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="560" y="30" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="520" y="30" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="480" y="30" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="440" y="30" w="40" h="30" originx="0" originy="0" +frame duration="0.1" x="400" y="30" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="440" y="30" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="480" y="30" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="520" y="30" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="560" y="30" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="520" y="30" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="480" y="30" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="440" y="30" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_1_volcano" -frame duration="0.05" x="600" y="30" w="40" h="30" originx="0" originy="0" +frame duration="0.05" x="600" y="30" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_2_volcano" -frame duration="0.1" x="640" y="30" w="40" h="30" originx="0" originy="0" +frame duration="0.1" x="640" y="30" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_3_volcano" -frame duration="0.1" x="680" y="30" w="40" h="30" originx="0" originy="0" +frame duration="0.1" x="680" y="30" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_1_holy" -frame duration="0.1" x="0" y="60" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="40" y="60" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="80" y="60" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="120" y="60" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="160" y="60" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="200" y="60" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="240" y="60" w="40" h="30" originx="0" originy="0" +frame duration="0.1" x="0" y="60" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="40" y="60" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="80" y="60" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="120" y="60" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="160" y="60" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="200" y="60" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="240" y="60" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_2_holy" -frame duration="0.1" x="280" y="60" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="320" y="60" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="360" y="60" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="400" y="60" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="440" y="60" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="480" y="60" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="520" y="60" w="40" h="30" originx="0" originy="0" +frame duration="0.1" x="280" y="60" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="320" y="60" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="360" y="60" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="400" y="60" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="440" y="60" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="480" y="60" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="520" y="60" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_3_holy" -frame duration="0.1" x="0" y="90" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="40" y="90" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="80" y="90" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="120" y="90" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="160" y="90" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="200" y="90" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="240" y="90" w="40" h="30" originx="0" originy="0" +frame duration="0.1" x="0" y="90" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="40" y="90" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="80" y="90" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="120" y="90" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="160" y="90" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="200" y="90" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="240" y="90" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_1_direction_right" -frame duration="0.1" x="560" y="60" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="600" y="60" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="640" y="60" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="680" y="60" w="40" h="30" originx="0" originy="0" +frame duration="0.1" x="560" y="60" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="600" y="60" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="640" y="60" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="680" y="60" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_2_direction_right" -frame duration="0.1" x="280" y="90" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="320" y="90" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="360" y="90" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="400" y="90" w="40" h="30" originx="0" originy="0" +frame duration="0.1" x="280" y="90" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="320" y="90" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="360" y="90" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="400" y="90" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_3_direction_right" -frame duration="0.1" x="440" y="90" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="480" y="90" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="520" y="90" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="560" y="90" w="40" h="30" originx="0" originy="0" +frame duration="0.1" x="440" y="90" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="480" y="90" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="520" y="90" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="560" y="90" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_1_direction_left" -frame duration="0.1" x="600" y="90" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="640" y="90" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="680" y="90" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="680" y="120" w="40" h="30" originx="0" originy="0" +frame duration="0.1" x="600" y="90" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="640" y="90" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="680" y="90" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="680" y="120" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_2_direction_left" -frame duration="0.1" x="0" y="120" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="40" y="120" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="80" y="120" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="120" y="120" w="40" h="30" originx="0" originy="0" +frame duration="0.1" x="0" y="120" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="40" y="120" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="80" y="120" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="120" y="120" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_3_direction_left" -frame duration="0.1" x="160" y="120" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="200" y="120" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="240" y="120" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="280" y="120" w="40" h="30" originx="0" originy="0" +frame duration="0.1" x="160" y="120" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="200" y="120" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="240" y="120" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="280" y="120" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_1_direction_up" -frame duration="0.1" x="320" y="120" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="360" y="120" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="400" y="120" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="440" y="120" w="40" h="30" originx="0" originy="0" +frame duration="0.1" x="320" y="120" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="360" y="120" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="400" y="120" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="440" y="120" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_2_direction_up" -frame duration="0.1" x="480" y="120" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="520" y="120" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="560" y="120" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="600" y="120" w="40" h="30" originx="0" originy="0" +frame duration="0.1" x="480" y="120" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="520" y="120" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="560" y="120" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="600" y="120" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_3_direction_up" -frame duration="0.1" x="640" y="120" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="0" y="150" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="40" y="150" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="80" y="150" w="40" h="30" originx="0" originy="0" +frame duration="0.1" x="640" y="120" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="0" y="150" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="40" y="150" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="80" y="150" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_1_direction_down" -frame duration="0.1" x="120" y="150" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="160" y="150" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="200" y="150" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="240" y="150" w="40" h="30" originx="0" originy="0" +frame duration="0.1" x="120" y="150" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="160" y="150" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="200" y="150" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="240" y="150" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_2_direction_down" -frame duration="0.1" x="280" y="150" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="320" y="150" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="360" y="150" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="400" y="150" w="40" h="30" originx="0" originy="0" +frame duration="0.1" x="280" y="150" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="320" y="150" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="360" y="150" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="400" y="150" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_3_direction_down" -frame duration="0.1" x="440" y="150" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="480" y="150" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="520" y="150" w="40" h="30" originx="0" originy="0" -frame duration="0.1" x="560" y="150" w="40" h="30" originx="0" originy="0" +frame duration="0.1" x="440" y="150" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="480" y="150" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="520" y="150" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="0.1" x="560" y="150" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_1_lava" -frame duration="1" x="600" y="150" w="40" h="30" originx="0" originy="0" +frame duration="1" x="600" y="150" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_2_lava" -frame duration="1" x="640" y="150" w="40" h="30" originx="0" originy="0" +frame duration="1" x="640" y="150" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_3_lava" -frame duration="1" x="680" y="150" w="40" h="30" originx="0" originy="0" +frame duration="1" x="680" y="150" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" + +animation state="row_1_sea" +frame duration="80f" x="0" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="16f" x="120" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="16f" x="240" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="16f" x="360" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="60f" x="480" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="25f" x="480" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="20f" x="360" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="16f" x="240" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="16f" x="120" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" + +animation state="row_2_sea" +frame duration="80f" x="40" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="16f" x="160" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="16f" x="280" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="16f" x="400" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="60f" x="520" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="25f" x="520" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="20f" x="400" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="16f" x="280" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="16f" x="160" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" + +animation state="row_3_sea" +frame duration="80f" x="80" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="16f" x="200" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="16f" x="320" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="16f" x="440" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="60f" x="560" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="25f" x="560" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="20f" x="440" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="16f" x="320" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="16f" x="200" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" From 1ad31a80862d71ce3ab405b6b94f828133ca7aea Mon Sep 17 00:00:00 2001 From: Alrysc Date: Fri, 1 Aug 2025 21:18:42 -0700 Subject: [PATCH 060/146] Partial pull 5d2bb95, exclude mob boss changes --- BattleNetwork/bindings/bnUserTypeTile.cpp | 4 +- BattleNetwork/bnAnimation.cpp | 7 +--- BattleNetwork/bnEntity.cpp | 5 ++- BattleNetwork/bnTile.cpp | 19 ++++++++-- BattleNetwork/bnTileState.h | 2 + .../resources/tiles/tile_atlas_blue.png | Bin 23773 -> 25189 bytes .../resources/tiles/tile_atlas_red.png | Bin 25755 -> 27158 bytes .../resources/tiles/tile_atlas_unknown.png | Bin 23733 -> 24542 bytes BattleNetwork/resources/tiles/tiles.animation | 35 +++++++++++++----- 9 files changed, 51 insertions(+), 21 deletions(-) diff --git a/BattleNetwork/bindings/bnUserTypeTile.cpp b/BattleNetwork/bindings/bnUserTypeTile.cpp index 7c521db0f..685c66c71 100644 --- a/BattleNetwork/bindings/bnUserTypeTile.cpp +++ b/BattleNetwork/bindings/bnUserTypeTile.cpp @@ -135,7 +135,9 @@ void DefineTileUserType(sol::state& state) { "Normal", TileState::normal, "Poison", TileState::poison, "Volcano", TileState::volcano, - "Sea", TileState::sea + "Sea", TileState::sea, + "Sand", TileState::sand, + "Metal", TileState::metal ); state.new_enum("Highlight", diff --git a/BattleNetwork/bnAnimation.cpp b/BattleNetwork/bnAnimation.cpp index 5ca808a38..f61a7ba1a 100644 --- a/BattleNetwork/bnAnimation.cpp +++ b/BattleNetwork/bnAnimation.cpp @@ -151,7 +151,7 @@ static frame_time_t GetFrameValue(std::string_view line, std::string_view key) { if (valueView.empty()) return frames(0); // frame value - if (valueView.at(valueView.size() - 1) == 'f') { + if (valueView.at(valueView.size() - 1) == 'f') { valueView = valueView.substr(0, valueView.size() - 1); return frames(std::atoi(valueView.data())); } @@ -240,10 +240,7 @@ void Animation::LoadWithData(const string& data) continue; } - float duration = GetFloatValue(line, "duration"); - - // prevent negative frame numbers - frame_time_t currentFrameDuration = from_seconds(std::fabs(duration)); + frame_time_t currentFrameDuration = GetFrameValue(line, "duration"); int currentStartx = 0; int currentStarty = 0; diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index d4e9880c0..498ec350a 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -229,11 +229,14 @@ void Entity::UpdateMovement(double elapsed) copyMoveEvent = {}; } } - else if (tile->GetState() == TileState::sea) { + else if (tile->GetState() == TileState::sea && GetElement() != Element::aqua && !HasFloatShoe()) { Root(frames(20)); auto splash = std::make_shared(); field.lock()->AddEntity(splash, *tile); } + else if (tile->GetState() == TileState::sand && !HasFloatShoe()) { + Root(frames(20)); + } else { // Invalidate the next tile pointer next = nullptr; diff --git a/BattleNetwork/bnTile.cpp b/BattleNetwork/bnTile.cpp index 79396f2e5..58d72094d 100644 --- a/BattleNetwork/bnTile.cpp +++ b/BattleNetwork/bnTile.cpp @@ -294,14 +294,19 @@ namespace Battle { } if (_state == TileState::broken) { - if(characters.size() || reserved.size()) { + bool noBreak = characters.size() || reserved.size(); + noBreak = noBreak || state == TileState::metal; + + if (noBreak) { return; - } else { - brokenCooldown = brokenCooldownLength; } + + brokenCooldown = brokenCooldownLength; } - if (_state == TileState::cracked && (state == TileState::empty || state == TileState::broken)) { + bool noCrack = (state == TileState::empty) || (state == TileState::broken) || (state == TileState::metal); + + if (_state == TileState::cracked && noCrack) { return; } @@ -947,6 +952,12 @@ namespace Battle { case TileState::sea: str = str + "sea"; break; + case TileState::sand: + str = str + "sand"; + break; + case TileState::metal: + str = str + "metal"; + break; default: str = str + "normal"; } diff --git a/BattleNetwork/bnTileState.h b/BattleNetwork/bnTileState.h index 06a868012..d6bda76fc 100644 --- a/BattleNetwork/bnTileState.h +++ b/BattleNetwork/bnTileState.h @@ -19,6 +19,8 @@ enum class TileState : int { directionDown, volcano, sea, + sand, + metal, hidden, // immutable size // no a valid state! used for enum length }; \ No newline at end of file diff --git a/BattleNetwork/resources/tiles/tile_atlas_blue.png b/BattleNetwork/resources/tiles/tile_atlas_blue.png index 4c772e09a5726c5017c479feb2b99f9d0ebc4301..702fe586665946ae70c60d2b252e912d87df4b2e 100644 GIT binary patch literal 25189 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

E?((EzdSwFD0St{ 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 i=N``Q6&(@}Y4ebmA!drdw5TH2G&l52b!)X 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 bkA;g?N6r75$q}PIxp_VbaZ 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 B$@u^P literal 23733 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 URaJSfTJt{3^ERdx=TLY526{W~Bme*a diff --git a/BattleNetwork/resources/tiles/tiles.animation b/BattleNetwork/resources/tiles/tiles.animation index 17814bc41..e2debbcd4 100644 --- a/BattleNetwork/resources/tiles/tiles.animation +++ b/BattleNetwork/resources/tiles/tiles.animation @@ -207,10 +207,9 @@ frame duration="16f" x="120" y="180" w="40" h="30" originx="0" originy="0" flipx frame duration="16f" x="240" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" frame duration="16f" x="360" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" frame duration="60f" x="480" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" -frame duration="25f" x="480" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" -frame duration="20f" x="360" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" -frame duration="16f" x="240" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" -frame duration="16f" x="120" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="25f" x="360" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="20f" x="240" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="16f" x="120" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_2_sea" frame duration="80f" x="40" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" @@ -218,9 +217,8 @@ frame duration="16f" x="160" y="180" w="40" h="30" originx="0" originy="0" flipx frame duration="16f" x="280" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" frame duration="16f" x="400" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" frame duration="60f" x="520" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" -frame duration="25f" x="520" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" -frame duration="20f" x="400" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" -frame duration="16f" x="280" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="25f" x="400" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="20f" x="280" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" frame duration="16f" x="160" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" animation state="row_3_sea" @@ -229,7 +227,24 @@ frame duration="16f" x="200" y="180" w="40" h="30" originx="0" originy="0" flipx frame duration="16f" x="320" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" frame duration="16f" x="440" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" frame duration="60f" x="560" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" -frame duration="25f" x="560" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" -frame duration="20f" x="440" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" -frame duration="16f" x="320" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="25f" x="440" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" +frame duration="20f" x="320" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" frame duration="16f" x="200" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" + +animation state="row_1_sand" +frame duration="25" x="600" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" + +animation state="row_2_sand" +frame duration="25" x="640" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" + +animation state="row_3_sand" +frame duration="25" x="680" y="180" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" + +animation state="row_1_metal" +frame duration="25" x="0" y="210" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" + +animation state="row_2_metal" +frame duration="25" x="40" y="210" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" + +animation state="row_3_metal" +frame duration="25" x="80" y="210" w="40" h="30" originx="0" originy="0" flipx="0" flipy="0" \ No newline at end of file From 0d9b0a7ecdc3fac3c512ac9ea0c290c0b6f33a63 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Fri, 1 Aug 2025 22:00:27 -0700 Subject: [PATCH 061/146] Uncomment Sand reaction to Wind --- BattleNetwork/bnTile.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/BattleNetwork/bnTile.cpp b/BattleNetwork/bnTile.cpp index 58d72094d..96097e8d8 100644 --- a/BattleNetwork/bnTile.cpp +++ b/BattleNetwork/bnTile.cpp @@ -1130,11 +1130,10 @@ namespace Battle { // empty previous frame queue to be used this current frame queuedAttackers.clear(); - // TODO: Uncomment when Sand is in. -// if (GetState() == TileState::sand && hitByWind) { -// SetState(TileState::normal); -// } -// else + if (GetState() == TileState::sand && hitByWind) { + SetState(TileState::normal); + } + else if (GetState() == TileState::grass && hitByFire) { SetState(TileState::normal); } From f3d0434ae488d2830e8b10a2279ecebf20a82ef9 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Fri, 1 Aug 2025 22:54:51 -0700 Subject: [PATCH 062/146] Sea damage on Fire Entities --- BattleNetwork/bnTile.cpp | 14 ++++++++++++++ BattleNetwork/bnTile.h | 2 ++ 2 files changed, 16 insertions(+) diff --git a/BattleNetwork/bnTile.cpp b/BattleNetwork/bnTile.cpp index 96097e8d8..fc5c2ea54 100644 --- a/BattleNetwork/bnTile.cpp +++ b/BattleNetwork/bnTile.cpp @@ -26,11 +26,13 @@ #define COOLDOWN frames(1800) #define FLICKER frames(180) #define SEA_COOLDOWN frames (60*16) +#define SEA_DAMAGE_COOLDOWN frames(7) namespace Battle { frame_time_t Tile::brokenCooldownLength = COOLDOWN; frame_time_t Tile::teamCooldownLength = COOLDOWN; frame_time_t Tile::seaCooldownLength = SEA_COOLDOWN; + frame_time_t Tile::seaDamageCooldownLength = SEA_DAMAGE_COOLDOWN; frame_time_t Tile::flickerTeamCooldownLength = FLICKER; Tile::Tile(int _x, int _y) : @@ -319,6 +321,7 @@ namespace Battle { if (_state == TileState::sea) { seaCooldown = seaCooldownLength; + seaDamageCooldown = seaDamageCooldownLength; } state = _state; @@ -533,6 +536,9 @@ namespace Battle { // VOLCANO volcanoEruptTimer -= from_seconds(_elapsed); + // Sea + seaDamageCooldown -= from_seconds(_elapsed); + if (volcanoEruptTimer <= frames(0)) { volcanoErupt.Update(_elapsed, volcanoSprite->getSprite()); } @@ -721,6 +727,14 @@ namespace Battle { SetState(TileState::normal); } } + + if (GetState() == TileState::sea && character.GetElement() == Element::fire) { + if (seaDamageCooldown <= frames(0)) { + if (character.Hit(Hit::Properties({ 1, Hit::pierce, Element::none, 0, Direction::none }))) { + seaDamageCooldown = seaDamageCooldownLength; + } + } + } } // DIRECTIONAL TILES diff --git a/BattleNetwork/bnTile.h b/BattleNetwork/bnTile.h index 194f55338..68c933108 100644 --- a/BattleNetwork/bnTile.h +++ b/BattleNetwork/bnTile.h @@ -342,9 +342,11 @@ namespace Battle { static frame_time_t brokenCooldownLength; static frame_time_t flickerTeamCooldownLength; static frame_time_t seaCooldownLength; + static frame_time_t seaDamageCooldownLength; frame_time_t teamCooldown{}; frame_time_t brokenCooldown{}; frame_time_t seaCooldown{}; + frame_time_t seaDamageCooldown{}; frame_time_t flickerTeamCooldown{}; frame_time_t totalElapsed{}; frame_time_t elapsedBurnTime{}; From fba098a6cd5e96992aaef197fcbd037a0ccb0351 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sat, 2 Aug 2025 17:06:12 -0700 Subject: [PATCH 063/146] Sea bonus applies to Aqua cards --- BattleNetwork/bnCard.cpp | 26 +++++++- BattleNetwork/bnCard.h | 8 ++- BattleNetwork/bnPackageAddress.h | 8 +++ BattleNetwork/bnPlayerSelectedCardsUI.cpp | 5 +- BattleNetwork/bnSelectedCardsUI.cpp | 35 ++++++++++- BattleNetwork/bnSelectedCardsUI.h | 3 +- BattleNetwork/stx/string.cpp | 73 +++++++++++++++++++++++ BattleNetwork/stx/string.h | 4 ++ 8 files changed, 155 insertions(+), 7 deletions(-) diff --git a/BattleNetwork/bnCard.cpp b/BattleNetwork/bnCard.cpp index 3001ff9f8..49dd8adbe 100644 --- a/BattleNetwork/bnCard.cpp +++ b/BattleNetwork/bnCard.cpp @@ -103,13 +103,37 @@ namespace Battle { return iter != props.metaClasses.end(); } - void Card::ModDamage(int modifier) + void Card::ModDamage(int32_t modifier, usize id_hash) { + auto iter = prevModifiers.find(id_hash); + if (iter != prevModifiers.end()) { + int32_t& value = iter->second; + if (modifier != value) { + props.damage += modifier - value; + value = modifier; + } + return; + } + if (unmodded.damage != 0) { props.damage += modifier; + prevModifiers.insert(std::make_pair(id_hash, modifier)); } } + void Card::ClearMod(usize id_hash) + { + auto iter = prevModifiers.find(id_hash); + if (iter == prevModifiers.end()) return; + props.damage -= iter->second; + prevModifiers.erase(iter); + } + + const bool Card::HasMod(usize id_hash) + { + return prevModifiers.find(id_hash) != prevModifiers.end(); + } + void Card::MultiplyDamage(unsigned int multiplier) { this->multiplier *= multiplier; diff --git a/BattleNetwork/bnCard.h b/BattleNetwork/bnCard.h index 50a579dae..368e1257d 100644 --- a/BattleNetwork/bnCard.h +++ b/BattleNetwork/bnCard.h @@ -4,6 +4,8 @@ #include #include #include "bnElements.h" +#include "bnPackageAddress.h" +#include using std::string; @@ -29,6 +31,7 @@ namespace Battle { class Card { public: + using usize = size_t; struct Properties { std::string uuid; unsigned damage{ 0 }; @@ -169,13 +172,16 @@ namespace Battle { return std::tie(props.shortname, props.code) < std::tie(rhs.props.shortname, rhs.props.code); } - void ModDamage(int modifier); + void ModDamage(int32_t modifier, usize id_hash); + void ClearMod(usize id_hash); + const bool HasMod(usize id_hash); void MultiplyDamage(unsigned int multiplier); const unsigned GetMultiplier() const; friend struct Compare; private: + std::map prevModifiers; Properties unmodded{}; Properties props{}; unsigned int multiplier{ 1 }; diff --git a/BattleNetwork/bnPackageAddress.h b/BattleNetwork/bnPackageAddress.h index 8f80debc8..a13e36a35 100644 --- a/BattleNetwork/bnPackageAddress.h +++ b/BattleNetwork/bnPackageAddress.h @@ -16,6 +16,14 @@ struct PackageAddress { static stx::result_t FromStr(const std::string& fqn); }; +namespace InternalPackages { + static const PackageAddress sea_tile_boost = PackageAddress::FromStr("@internal/com.onb.sea_tile_boost").unwrap(); + + namespace hashes { + static const int32_t sea_tile_boost = stx::hash(InternalPackages::sea_tile_boost); + } +} + bool operator<(const PackageAddress& a, const PackageAddress& b); bool operator==(const PackageAddress& a, const PackageAddress& b); diff --git a/BattleNetwork/bnPlayerSelectedCardsUI.cpp b/BattleNetwork/bnPlayerSelectedCardsUI.cpp index feef5b7fd..0d6f9cd7e 100644 --- a/BattleNetwork/bnPlayerSelectedCardsUI.cpp +++ b/BattleNetwork/bnPlayerSelectedCardsUI.cpp @@ -151,7 +151,9 @@ void PlayerSelectedCardsUI::draw(sf::RenderTarget& target, sf::RenderStates stat sf::String dmgText = std::to_string(unmodDamage); if (delta != 0) { - dmgText = dmgText + sf::String("+") + sf::String(std::to_string(std::abs(delta))); + std::string op = sf::String(delta > 0 ? "+" : "-"); + + dmgText = dmgText + op + sf::String(std::to_string(std::abs(delta))); } // attacks that normally show no damage will show if the modifer adds damage @@ -210,6 +212,7 @@ void PlayerSelectedCardsUI::OnUpdate(double _elapsed) { } elapsed = _elapsed; + SelectedCardsUI::OnUpdate(_elapsed); } void PlayerSelectedCardsUI::Broadcast(std::shared_ptr action) diff --git a/BattleNetwork/bnSelectedCardsUI.cpp b/BattleNetwork/bnSelectedCardsUI.cpp index ffbf3d9bc..ab34b2cce 100644 --- a/BattleNetwork/bnSelectedCardsUI.cpp +++ b/BattleNetwork/bnSelectedCardsUI.cpp @@ -109,6 +109,23 @@ void SelectedCardsUI::OnUpdate(double _elapsed) { if (character->IsDeleted()) { Hide(); } + + Battle::Tile* tile = character->GetTile(); + if (tile == nullptr) return; + + MaybeCard& maybeCard = Peek(); + if (!maybeCard.has_value()) return; + + Battle::Card& data = maybeCard.value().get(); + + // TODO: Check secondary element when they are aded + if (tile->GetState() == TileState::sea && data.GetElement() == Element::aqua && data.CanBoost()) { + data.ModDamage(30, InternalPackages::hashes::sea_tile_boost); + } + else { + data.ClearMod(InternalPackages::hashes::sea_tile_boost); + } + } } @@ -140,6 +157,19 @@ bool SelectedCardsUI::UseNextCard() { Battle::Card& card = (*selectedCards)[curr]; + Battle::Tile* tile = owner->GetTile(); + + // Reset tile when card is boosted by Sea. + // It could be worth checking this under the CanBoost() check below, + // but for now, hfacing the modded damage should mean the modded damage + // will happen, even if the card has somehow become marked as unboostable + // by now. + if (tile != nullptr) { + if (tile->GetState() == TileState::sea && card.HasMod(InternalPackages::hashes::sea_tile_boost)) { + tile->SetState(TileState::normal); + } + } + if (card.CanBoost()) { card.MultiplyDamage(multiplierValue); multiplierValue = 1; // multiplier is reset because it has been consumed @@ -156,11 +186,10 @@ void SelectedCardsUI::Broadcast(std::shared_ptr action) CardActionUsePublisher::Broadcast(action, CurrentTime::AsMilli()); } -std::optional> SelectedCardsUI::Peek() +SelectedCardsUI::MaybeCard SelectedCardsUI::Peek() { if (curr < selectedCards->size()) { - using RefType = std::reference_wrapper; - return std::optional(std::ref((*selectedCards)[curr])); + return MaybeCard(std::ref((*selectedCards)[curr])); } return {}; diff --git a/BattleNetwork/bnSelectedCardsUI.h b/BattleNetwork/bnSelectedCardsUI.h index 8bc0e6415..dbfc299df 100644 --- a/BattleNetwork/bnSelectedCardsUI.h +++ b/BattleNetwork/bnSelectedCardsUI.h @@ -23,6 +23,7 @@ class BattleSceneBase; class SelectedCardsUI : public CardActionUsePublisher, public UIComponent { public: + using MaybeCard = std::optional>; /** * \param character Character to attach to */ @@ -74,7 +75,7 @@ class SelectedCardsUI : public CardActionUsePublisher, public UIComponent { * @brief Return a const reference to the next card, if valid * @preconditions Assumes the card can be used and currCard < cardCount! */ - std::optional> Peek(); + MaybeCard Peek(); //!< Returns true if there was a card to play, false if empty bool HandlePlayEvent(std::shared_ptr from); diff --git a/BattleNetwork/stx/string.cpp b/BattleNetwork/stx/string.cpp index 640ae7e03..76c17fa05 100644 --- a/BattleNetwork/stx/string.cpp +++ b/BattleNetwork/stx/string.cpp @@ -2,6 +2,32 @@ #include #include #include +#include + +// NOTE: the following code was from http://burtleburtle.net/bob/c/lookup3.c +// References: http://burtleburtle.net/bob/hash/index.html +#define rot(x,k) (((x)<<(k)) | ((x)>>(32-(k)))) + +#define mix(a,b,c) \ +{ \ + a -= c; a ^= rot(c, 4); c += b; \ + b -= a; b ^= rot(a, 6); a += c; \ + c -= b; c ^= rot(b, 8); b += a; \ + a -= c; a ^= rot(c,16); c += b; \ + b -= a; b ^= rot(a,19); a += c; \ + c -= b; c ^= rot(b, 4); b += a; \ +} + +#define final(a,b,c) \ +{ \ + c ^= b; c -= rot(b,14); \ + a ^= c; a -= rot(c,11); \ + b ^= a; b -= rot(a,25); \ + c ^= b; c -= rot(b,16); \ + a ^= c; a -= rot(c,4); \ + b ^= a; b -= rot(a,14); \ + c ^= b; c -= rot(b,24); \ +} namespace stx { std::string replace(std::string str, const std::string& from, const std::string& to) { @@ -179,4 +205,51 @@ namespace stx { return ssout.str(); } + + uint32_t hash(const std::string& str) { + static std::map cache; + + if (cache.count(str) > 0) { + return cache[str]; + } + + uint32_t sig = hashword((uint32_t*)str.c_str(), str.length(), 0); + cache.insert_or_assign(str, sig); + return sig; + } + + uint32_t hashword( + const uint32_t* k, /* the key, an array of uint32_t values */ + size_t length, /* the length of the key, in uint32_ts */ + uint32_t initval) /* the previous hash, or an arbitrary value */ + { + uint32_t a, b, c; + + /* Set up the internal state */ + a = b = c = 0xdeadbeef + (((uint32_t)length) << 2) + initval; + + /*------------------------------------------------- handle most of the key */ + while (length > 3) + { + a += k[0]; + b += k[1]; + c += k[2]; + mix(a, b, c); + length -= 3; + k += 3; + } + + /*------------------------------------------- handle the last 3 uint32_t's */ + switch (length) /* all the case statements fall through */ + { + case 3: c += k[2]; + case 2: b += k[1]; + case 1: a += k[0]; + final(a, b, c); + case 0: /* case 0: nothing left to add */ + break; + } + /*------------------------------------------------------ report the result */ + return c; + } } \ No newline at end of file diff --git a/BattleNetwork/stx/string.h b/BattleNetwork/stx/string.h index 6fdfebbb7..09edc5097 100644 --- a/BattleNetwork/stx/string.h +++ b/BattleNetwork/stx/string.h @@ -54,4 +54,8 @@ namespace stx { * @param stride determines the number of pairs per space. Default is 1 e.g. `00 AA BB`. If set to 0 there is no spacing. */ std::string as_hex(const std::string& buffer, size_t stride=1); + + uint32_t hash(const std::string& str); + + uint32_t hashword(const uint32_t* k, size_t length, uint32_t initval); } \ No newline at end of file From 2262bcfb902bddf76302688a66c5f3c0a80a7da0 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sat, 2 Aug 2025 18:49:08 -0700 Subject: [PATCH 064/146] Fix shaking causing incorrect draw position --- BattleNetwork/bnEntity.cpp | 8 ++++++++ BattleNetwork/bnEntity.h | 2 ++ BattleNetwork/bnShakingEffect.cpp | 22 ++++++++++++++-------- BattleNetwork/bnShakingEffect.h | 5 +++-- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index 498ec350a..6b2d64801 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -804,6 +804,14 @@ const sf::Vector2f Entity::GetTileOffset() const return this->tileOffset; } +void Entity::SetTileOffset(const sf::Vector2f& offset) { + tileOffset = offset; +} + +void Entity::RefreshPosition() { + setPosition(tile->getPosition() + tileOffset + drawOffset); +} + void Entity::SetDrawOffset(const sf::Vector2f& offset) { drawOffset = offset; diff --git a/BattleNetwork/bnEntity.h b/BattleNetwork/bnEntity.h index d48da4ed5..8768ef9c9 100644 --- a/BattleNetwork/bnEntity.h +++ b/BattleNetwork/bnEntity.h @@ -274,6 +274,8 @@ class Entity : Battle::Tile* GetCurrentTile() const; const sf::Vector2f GetTileOffset() const; + void SetTileOffset(const sf::Vector2f& offset); + void RefreshPosition(); void SetDrawOffset(const sf::Vector2f& offset); void SetDrawOffset(float x, float y); const sf::Vector2f GetDrawOffset() const; diff --git a/BattleNetwork/bnShakingEffect.cpp b/BattleNetwork/bnShakingEffect.cpp index 7eebe6ae6..c1d1bcad2 100644 --- a/BattleNetwork/bnShakingEffect.cpp +++ b/BattleNetwork/bnShakingEffect.cpp @@ -3,9 +3,9 @@ #include "battlescene/bnBattleSceneBase.h" ShakingEffect::ShakingEffect(std::weak_ptr owner) : - shakeDur(0.35f), + shakeDur(frames(21)), stress(3), - shakeProgress(0), + shakeProgress(frames(0)), startPos(owner.lock()->getPosition()), bscene(nullptr), isShaking(false), @@ -17,21 +17,27 @@ ShakingEffect::~ShakingEffect() { } -void ShakingEffect::OnUpdate(double _elapsed) -{ +void ShakingEffect::OnUpdate(double _elapsed) { auto owner = GetOwner(); - shakeProgress += _elapsed; + shakeProgress += frames(1); if (owner && shakeProgress <= shakeDur) { // Drop off to zero by end of shake - double currStress = stress * (1.0 - (shakeProgress / shakeDur)); + double currStress = stress * (1.0 - (shakeProgress.count() / (double)shakeDur.count())); - int randomAngle = static_cast(shakeProgress) * (rand() % 360); + int randomAngle = (int)(shakeProgress.count()) * (rand() % 360); randomAngle += (150 + (rand() % 60)); auto shakeOffset = sf::Vector2f(std::sin(static_cast(randomAngle * currStress)), std::cos(static_cast(randomAngle * currStress))); - owner->setPosition(startPos + shakeOffset); + // We add, reposition, and then reset the tile offset so that we do not + // accidentally accumulate the shake noise which will misplace the entity + // over several frames. + // Simply: the entity will snap back to its previous position the next frame. + const sf::Vector2f tileOffset = owner->GetTileOffset(); + owner->SetTileOffset(tileOffset + shakeOffset); + owner->RefreshPosition(); + owner->SetTileOffset(tileOffset); } else { Eject(); diff --git a/BattleNetwork/bnShakingEffect.h b/BattleNetwork/bnShakingEffect.h index 350477b69..d78f883a5 100644 --- a/BattleNetwork/bnShakingEffect.h +++ b/BattleNetwork/bnShakingEffect.h @@ -1,6 +1,7 @@ #pragma once #include "bnComponent.h" #include +#include "frame_time_t.h" class BattleSceneBase; class Entity; @@ -8,9 +9,9 @@ class Entity; class ShakingEffect : public Component { private: bool isShaking; - double shakeDur; /*!< Duration of shake effect */ + frame_time_t shakeDur; /*!< Duration of shake effect */ double stress; /*!< How much stress to apply to shake */ - double shakeProgress; + frame_time_t shakeProgress; sf::Vector2f startPos; BattleSceneBase* bscene; public: From b60fab046cd6cc82ebdc02594cb7e71c8515b876 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sat, 2 Aug 2025 21:40:47 -0700 Subject: [PATCH 065/146] Adjust Drag endlag placement --- BattleNetwork/bnEntity.cpp | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index 6b2d64801..9f568f401 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -1566,18 +1566,22 @@ void Entity::ResolveFrameBattleDamage() slideFromDrag = true; Battle::Tile* dest = GetTile() + postDragEffect.dir; + // The final drag event applies endlag. + // 22 frames matches the amount of fixed frames applied to player recoil + // This must be applied as move delta time instead of end delay to avoid + // opening new edge cases. When move delta is 0, IsSliding is false and + // statuses are allowed to apply earlier than intended. + // This may be made more clear by making a distinction between voluntary + // and involuntary MoveEvents. + frame_time_t movetime = frames(4); if (!CanMoveTo(dest)) { + movetime= frames(22); dest = GetTile(); postDragEffect.count = 0; } - - // The final drag event applies endlag - // 22 frames matches the amount of fixed frames applied to player recoil - const frame_time_t endlag = - postDragEffect.count == 0 ? frames(22) : frames(0); // Enqueue a move action at the top of our priorities - actionQueue.Add(MoveEvent{ frames(4), frames(0), endlag, 0, dest, {}, true }, ActionOrder::immediate, ActionDiscardOp::until_resolve); + actionQueue.Add(MoveEvent{ movetime, frames(0), frames(0), 0, dest, {}, true }, ActionOrder::immediate, ActionDiscardOp::until_resolve); std::queue oldQueue = statusQueue; statusQueue = {}; From 03a53533fb39c8717a9c5310bc3dedb85882ffe8 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sun, 3 Aug 2025 14:25:09 -0700 Subject: [PATCH 066/146] Fix charge time, added Player::IsCharging, access to Lua --- BattleNetwork/bindings/bnScriptedPlayer.cpp | 3 +++ BattleNetwork/bindings/bnUserTypeScriptedPlayer.cpp | 3 +++ BattleNetwork/bnChargeEffectSceneNode.cpp | 5 +++++ BattleNetwork/bnChargeEffectSceneNode.h | 6 ++++++ BattleNetwork/bnPlayer.cpp | 6 +++++- BattleNetwork/bnPlayer.h | 2 ++ 6 files changed, 24 insertions(+), 1 deletion(-) diff --git a/BattleNetwork/bindings/bnScriptedPlayer.cpp b/BattleNetwork/bindings/bnScriptedPlayer.cpp index 498db4a0a..8672b2cf1 100644 --- a/BattleNetwork/bindings/bnScriptedPlayer.cpp +++ b/BattleNetwork/bindings/bnScriptedPlayer.cpp @@ -18,6 +18,9 @@ void ScriptedPlayer::Init() { stx::result_t initResult = CallLuaFunction(script, "player_init", WeakWrapper(weak_from_base())); + // Recalculate charge time, which may have been changed by player_init + Charge(false); + if (initResult.is_error()) { Logger::Log(LogLevel::critical, initResult.error_cstr()); } diff --git a/BattleNetwork/bindings/bnUserTypeScriptedPlayer.cpp b/BattleNetwork/bindings/bnUserTypeScriptedPlayer.cpp index 948076628..0ec665fec 100644 --- a/BattleNetwork/bindings/bnUserTypeScriptedPlayer.cpp +++ b/BattleNetwork/bindings/bnUserTypeScriptedPlayer.cpp @@ -83,6 +83,9 @@ void DefineScriptedPlayerUserType(sol::state& state, sol::table& battle_namespac "set_charge_position", [](WeakWrapper& player, float x, float y) { player.Unwrap()->SetChargePosition(x, y); }, + "is_charging", [](WeakWrapper& player) -> bool{ + return player.Unwrap()->IsCharging(); + }, "slide_when_moving", [](WeakWrapper& player, bool enable, const frame_time_t& frames) { player.Unwrap()->SlideWhenMoving(enable, frames); }, diff --git a/BattleNetwork/bnChargeEffectSceneNode.cpp b/BattleNetwork/bnChargeEffectSceneNode.cpp index a4f4c0611..f457f1df7 100644 --- a/BattleNetwork/bnChargeEffectSceneNode.cpp +++ b/BattleNetwork/bnChargeEffectSceneNode.cpp @@ -85,6 +85,11 @@ const bool ChargeEffectSceneNode::IsFullyCharged() const return isCharged; } +const bool ChargeEffectSceneNode::IsPartiallyCharged() const +{ + return isPartiallyCharged; +} + void ChargeEffectSceneNode::SetFullyChargedColor(const sf::Color color) { chargeColor = color; diff --git a/BattleNetwork/bnChargeEffectSceneNode.h b/BattleNetwork/bnChargeEffectSceneNode.h index d2aeb41d3..4c056a870 100644 --- a/BattleNetwork/bnChargeEffectSceneNode.h +++ b/BattleNetwork/bnChargeEffectSceneNode.h @@ -50,6 +50,12 @@ class ChargeEffectSceneNode : public SpriteProxyNode, public ResourceHandle { */ const bool IsFullyCharged() const; + /** + * @brief Check partial charge time + * @return true if the charge component's charge time is above i10 + */ + const bool IsPartiallyCharged() const; + void SetFullyChargedColor(const sf::Color color); private: diff --git a/BattleNetwork/bnPlayer.cpp b/BattleNetwork/bnPlayer.cpp index 171b2d5f4..0eed3123c 100644 --- a/BattleNetwork/bnPlayer.cpp +++ b/BattleNetwork/bnPlayer.cpp @@ -76,7 +76,6 @@ void Player::Init() { animationComponent->SetPath(RESOURCE_PATH); animationComponent->Reload(); - Charge(false); FinishConstructor(); } @@ -225,6 +224,7 @@ int Player::GetMoveCount() const void Player::Charge(bool state) { + frame_time_t maxCharge = CalculateChargeTime(GetChargeLevel()); if (activeForm) { maxCharge = activeForm->CalculateChargeTime(GetChargeLevel()); @@ -234,6 +234,10 @@ void Player::Charge(bool state) chargeEffect->SetCharging(state); } +bool Player::IsCharging() { + return chargeEffect->IsPartiallyCharged(); +} + void Player::SetAttackLevel(unsigned lvl) { stats.attack = std::min(PlayerStats::MAX_ATTACK_LEVEL, lvl); diff --git a/BattleNetwork/bnPlayer.h b/BattleNetwork/bnPlayer.h index 66096dde1..b7f173fa7 100644 --- a/BattleNetwork/bnPlayer.h +++ b/BattleNetwork/bnPlayer.h @@ -116,6 +116,8 @@ class Player : public Character, public AI { */ void Charge(bool state); + bool IsCharging(); + void SetAttackLevel(unsigned lvl); const unsigned GetAttackLevel(); From 9d463ecbfd9a617e632972a8143b873b8c16b71b Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sun, 3 Aug 2025 19:51:05 -0700 Subject: [PATCH 067/146] Fix incorrect poison damage time and panel flash time --- BattleNetwork/bnTile.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BattleNetwork/bnTile.cpp b/BattleNetwork/bnTile.cpp index fc5c2ea54..129870996 100644 --- a/BattleNetwork/bnTile.cpp +++ b/BattleNetwork/bnTile.cpp @@ -62,7 +62,7 @@ namespace Battle { flickerTeamCooldown = teamCooldown = frames(0); red_team_atlas = blue_team_atlas = nullptr; // Set by field - burncycle = frames(1); // milliseconds + burncycle = frames(7); // milliseconds elapsedBurnTime = burncycle; highlightMode = TileHighlight::none; @@ -588,7 +588,7 @@ namespace Battle { willHighlight = true; break; case TileHighlight::flash: - willHighlight = (totalElapsed.count() % 4 < 2); + willHighlight = (totalElapsed.count() % 8 < 4); break; default: willHighlight = false; From ba5c93a943b75fc545d91c1e2878e06fdad519fe Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sun, 3 Aug 2025 21:50:35 -0700 Subject: [PATCH 068/146] Pull most of 6bd901a, 5a31557, and dee4fb7, extra changes to make health work better Excluded change to Entity::delete which would prevent deletion after battle is over --- .../battlescene/bnBattleSceneBase.cpp | 9 +++++-- BattleNetwork/battlescene/bnBattleSceneBase.h | 3 +++ .../battlescene/bnFreedomMissionMobScene.cpp | 25 +++++++++++++++++-- .../battlescene/bnMobBattleScene.cpp | 21 +++++++++++++++- BattleNetwork/bnEntity.cpp | 6 ----- BattleNetwork/bnPlayer.cpp | 25 +++++++++++++++++-- BattleNetwork/bnPlayerHealthUI.cpp | 17 +++++++++++-- BattleNetwork/bnPlayerHealthUI.h | 3 +++ .../battlescene/bnNetworkBattleScene.cpp | 5 +++- .../overworld/bnOverworldOnlineArea.cpp | 17 +++++++++---- 10 files changed, 110 insertions(+), 21 deletions(-) diff --git a/BattleNetwork/battlescene/bnBattleSceneBase.cpp b/BattleNetwork/battlescene/bnBattleSceneBase.cpp index 90d782949..7fbfbe085 100644 --- a/BattleNetwork/battlescene/bnBattleSceneBase.cpp +++ b/BattleNetwork/battlescene/bnBattleSceneBase.cpp @@ -424,6 +424,7 @@ void BattleSceneBase::SpawnLocalPlayer(int x, int y) Team team = field->GetAt(x, y)->GetTeam(); localPlayer->Init(); + localPlayer->ChangeState(); localPlayer->SetTeam(team); field->AddEntity(localPlayer, x, y); @@ -433,8 +434,8 @@ void BattleSceneBase::SpawnLocalPlayer(int x, int y) this->SubscribeToCardActions(*localPlayer); this->SubscribeToCardActions(*cardUI); - auto healthUI = localPlayer->CreateComponent(localPlayer); - healthUI->setScale(2.f, 2.f); // TODO: this should be upscaled by cardCustGUI transforms... why is it not? + this->healthUI = localPlayer->CreateComponent(localPlayer); + this->healthUI->setScale(2.f, 2.f); // TODO: this should be upscaled by cardCustGUI transforms... why is it not? cardCustGUI.AddNode(healthUI); @@ -1173,6 +1174,10 @@ PlayerEmotionUI& BattleSceneBase::GetEmotionWindow() return *emotionUI; } +PlayerHealthUIComponent& BattleSceneBase::GetHealthWindow() { + return *healthUI; +} + Camera& BattleSceneBase::GetCamera() { return camera; diff --git a/BattleNetwork/battlescene/bnBattleSceneBase.h b/BattleNetwork/battlescene/bnBattleSceneBase.h index 3fbd2e4a5..36bb4f014 100644 --- a/BattleNetwork/battlescene/bnBattleSceneBase.h +++ b/BattleNetwork/battlescene/bnBattleSceneBase.h @@ -13,6 +13,7 @@ #include "../bnScene.h" #include "../bnComponent.h" #include "../bnPA.h" +#include "../bnPlayerHealthUI.h" #include "../bnMobHealthUI.h" #include "../bnAnimation.h" #include "../bnCamera.h" @@ -110,6 +111,7 @@ class BattleSceneBase : RealtimeCardActionUseListener cardActionListener; /*!< Card use listener handles one card at a time */ std::shared_ptr cardUI{ nullptr }; /*!< Player's Card UI implementation */ std::shared_ptr emotionUI{ nullptr }; /*!< Player's Emotion Window */ + std::shared_ptr healthUI{ nullptr }; /*!< Player's Health Window */ Camera camera; /*!< Camera object - will shake screen */ sf::Sprite mobEdgeSprite, mobBackdropSprite; /*!< name backdrop images*/ PA& programAdvance; /*!< PA object loads PA database and returns matching PA card from input */ @@ -398,6 +400,7 @@ class BattleSceneBase : CardSelectionCust& GetCardSelectWidget(); PlayerSelectedCardsUI& GetSelectedCardsUI(); PlayerEmotionUI& GetEmotionWindow(); + PlayerHealthUIComponent& GetHealthWindow(); Camera& GetCamera(); PA& GetPA(); BattleResults& BattleResultsObj(); diff --git a/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp b/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp index aa039490b..82608d712 100644 --- a/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp +++ b/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp @@ -129,6 +129,16 @@ void FreedomMissionMobScene::Init() playerCanFlip = mob.PlayerCanFlip(); } + /* + SpawnLocalPlayer calls Lua player_init, which may set health as + a means to set max health. The localPlayer already has this health + value, or it may have a modified value from the server. To avoid + overwriting modified values from the server, undo any Lua SetHealth + calls by setting back to the previous health. + */ + const int health = GetLocalPlayer()->GetHealth(); + const int maxHealth = GetLocalPlayer()->GetMaxHealth(); + if (mob.HasPlayerSpawnPoint(1)) { Mob::PlayerSpawnData data = mob.GetPlayerSpawnPoint(1); SpawnLocalPlayer(data.tileX, data.tileY); @@ -138,15 +148,26 @@ void FreedomMissionMobScene::Init() LoadBlueTeamMob(mob); } - // Run block programs on the remote player now that they are spawned + // If maxHealth is low, assume the session had not set a health value and + // trust the player_init to avoid leaving the player at 0 HP. + if (maxHealth > 0) { + GetLocalPlayer()->SetHealth(health); + } + + + // Run block programs on the local player now that they are spawned BlockPackageManager& blockPackages = getController().BlockPackagePartitioner().GetPartition(Game::LocalPartition); for (const std::string& blockID : props.blocks) { if (!blockPackages.HasPackage(blockID)) continue; - + auto& blockMeta = blockPackages.FindPackageByID(blockID); blockMeta.mutator(*GetLocalPlayer()); } + + //This should be run to ensure Health UI snaps to the user's current health. + GetHealthWindow().ResetHP(GetLocalPlayer()->GetHealth()); + CardSelectionCust& cardSelectWidget = GetCardSelectWidget(); cardSelectWidget.PreventRetreat(); cardSelectWidget.SetSpeaker(props.mug, props.anim); diff --git a/BattleNetwork/battlescene/bnMobBattleScene.cpp b/BattleNetwork/battlescene/bnMobBattleScene.cpp index 13510c623..1592553e8 100644 --- a/BattleNetwork/battlescene/bnMobBattleScene.cpp +++ b/BattleNetwork/battlescene/bnMobBattleScene.cpp @@ -141,6 +141,16 @@ void MobBattleScene::Init() LoadBlueTeamMob(mob); } + /* + SpawnLocalPlayer calls Lua player_init, which may set health as + a means to set max health. The localPlayer already has this health + value, or it may have a modified value from the server. To avoid + overwriting modified values from the server, undo any Lua SetHealth + calls by setting back to the previous health. + */ + const int health = GetLocalPlayer()->GetHealth(); + const int maxHealth = GetLocalPlayer()->GetMaxHealth(); + if (mob.HasPlayerSpawnPoint(1)) { Mob::PlayerSpawnData data = mob.GetPlayerSpawnPoint(1); SpawnLocalPlayer(data.tileX, data.tileY); @@ -149,7 +159,13 @@ void MobBattleScene::Init() SpawnLocalPlayer(2, 2); } - // Run block programs on the remote player now that they are spawned + // If maxHealth is low, assume the session had not set a health value and + // trust the player_init to avoid leaving the player at 0 HP. + if (maxHealth > 0) { + GetLocalPlayer()->SetHealth(health); + } + + // Run block programs on the local player now that they are spawned BlockPackageManager& blockPackages = getController().BlockPackagePartitioner().GetPartition(Game::LocalPartition); for (const std::string& blockID : props.blocks) { if (!blockPackages.HasPackage(blockID)) continue; @@ -158,6 +174,9 @@ void MobBattleScene::Init() blockMeta.mutator(*GetLocalPlayer()); } + //This should be run to ensure Health UI snaps to the user's current health. + GetHealthWindow().ResetHP(GetLocalPlayer()->GetHealth()); + GetCardSelectWidget().SetSpeaker(props.mug, props.anim); GetEmotionWindow().SetTexture(props.emotion); } diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index 9f568f401..cf10828eb 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -1632,12 +1632,6 @@ void Entity::ResolveFrameBattleDamage() } void Entity::SetHealth(const int _health) { - std::shared_ptr fieldPtr = field.lock(); - - if (fieldPtr) { - if (!fieldPtr->isBattleActive) return; - } - health = _health; if (maxHealth == 0) { diff --git a/BattleNetwork/bnPlayer.cpp b/BattleNetwork/bnPlayer.cpp index 0eed3123c..d755ab318 100644 --- a/BattleNetwork/bnPlayer.cpp +++ b/BattleNetwork/bnPlayer.cpp @@ -268,8 +268,29 @@ const unsigned Player::GetChargeLevel() void Player::ModMaxHealth(int mod) { stats.moddedHP += mod; - SetMaxHealth(this->GetMaxHealth() + mod); - SetHealth(this->GetMaxHealth()); + + /* + Used to set current health to new max, but this + is undesired behavior when PlayerSession has a + different current health. + + Instead, raise health to new max iff current was + the max health. + + This will not be necessary once block mutations are + known to the session. + */ + const int oldMax = GetMaxHealth(); + const int newMax = oldMax + mod; + const int curHP = GetHealth(); + SetMaxHealth(newMax); + + if (newMax < curHP) { + SetHealth(newMax); + } + else if (oldMax == curHP) { + SetHealth(newMax); + } } const int Player::GetMaxHealthMod() diff --git a/BattleNetwork/bnPlayerHealthUI.cpp b/BattleNetwork/bnPlayerHealthUI.cpp index 81bdef55f..52afb41db 100644 --- a/BattleNetwork/bnPlayerHealthUI.cpp +++ b/BattleNetwork/bnPlayerHealthUI.cpp @@ -107,6 +107,11 @@ void PlayerHealthUI::draw(sf::RenderTarget& target, sf::RenderStates states) con target.draw(glyphs, states); } +void PlayerHealthUI::ResetHP(int newHP) { + targetHP = std::max(newHP, 0); + currHP = lastHP = newHP; +} + //////////////////////////////////// // class PlayerHealthUIComponent // //////////////////////////////////// @@ -115,12 +120,20 @@ PlayerHealthUIComponent::PlayerHealthUIComponent(std::weak_ptr _player) UIComponent(_player) { isBattleOver = false; - startHP = _player.lock()->GetHealth(); - ui.SetHP(startHP); + auto player = _player.lock(); + // startHP is compared to to decide on using the gold gradient. + // That color should be used when current <= 25% of max health. + startHP = player->GetMaxHealth(); + ui.SetHP(player->GetHealth()); SetDrawOnUIPass(false); OnUpdate(0); // refresh and prepare for the 1st frame } +void PlayerHealthUIComponent::ResetHP(int newHP) { + ui.ResetHP(newHP); + startHP = GetOwner()->GetMaxHealth(); +} + PlayerHealthUIComponent::~PlayerHealthUIComponent() { this->Eject(); } diff --git a/BattleNetwork/bnPlayerHealthUI.h b/BattleNetwork/bnPlayerHealthUI.h index 0e15c4b68..d7eb2ce3d 100644 --- a/BattleNetwork/bnPlayerHealthUI.h +++ b/BattleNetwork/bnPlayerHealthUI.h @@ -36,6 +36,7 @@ class PlayerHealthUI : public SceneNode { void SetFontStyle(Font::Style style); void SetHP(int hp); + void ResetHP(int newHP); void Update(double elapsed); void draw(sf::RenderTarget& target, sf::RenderStates states) const override final; @@ -60,6 +61,8 @@ class PlayerHealthUIComponent : public UIComponent { * \brief Sets the player owner. Sets hp tracker to current health. */ PlayerHealthUIComponent(std::weak_ptr _player); + + void ResetHP(int newHP); /** * @brief No memory needs to be freed diff --git a/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp b/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp index bb10986bd..cbe456432 100644 --- a/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp +++ b/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp @@ -415,7 +415,7 @@ void NetworkBattleScene::Init() { SpawnRemotePlayer(p, x, y); } - // Run block programs on the remote player now that they are spawned + // Run block programs on the current player now that they are spawned for (const PackageAddress& addr : blocks) { BlockPackageManager& blockPackages = partition.GetPartition(addr.namespaceId); if (!blockPackages.HasPackage(addr.packageId)) continue; @@ -427,6 +427,9 @@ void NetworkBattleScene::Init() { idx++; } + //This should be run to ensure Health UI snaps to the user's current health. + GetHealthWindow().ResetHP(GetLocalPlayer()->GetHealth()); + std::shared_ptr ui = remotePlayer->GetFirstComponent(); if (ui) { diff --git a/BattleNetwork/overworld/bnOverworldOnlineArea.cpp b/BattleNetwork/overworld/bnOverworldOnlineArea.cpp index b1e464132..5307711d2 100644 --- a/BattleNetwork/overworld/bnOverworldOnlineArea.cpp +++ b/BattleNetwork/overworld/bnOverworldOnlineArea.cpp @@ -2322,8 +2322,11 @@ void Overworld::OnlineArea::receivePVPSignal(BufferReader& reader, const Poco::B auto emotions = Textures().LoadFromFile(emotionsTexture); auto player = std::shared_ptr(meta.GetData()); - player->SetHealth(GetPlayerSession()->health); - player->SetEmotion(GetPlayerSession()->emotion); + auto& overworldSession = GetPlayerSession(); + + player->SetMaxHealth(overworldSession->maxHealth); + player->SetHealth(overworldSession->health); + player->SetEmotion(overworldSession->emotion); GameSession& session = getController().Session(); std::vector localNaviBlocks = PlayerCustScene::GetInstalledBlocks(GetCurrentNaviID(), session); @@ -2504,9 +2507,13 @@ void Overworld::OnlineArea::receiveMobSignal(BufferReader& reader, const Poco::B std::shared_ptr emotions = Textures().LoadFromFile(emotionsTexture); std::shared_ptr player = std::shared_ptr(playerMeta.GetData()); - auto& playerSession = GetPlayerSession(); - player->SetHealth(playerSession->health); - player->SetEmotion(playerSession->emotion); + auto& overworldSession = GetPlayerSession(); + + player->SetMaxHealth(overworldSession->maxHealth); + int hp = overworldSession->health; + if (hp == 0) { hp = 1; } + player->SetHealth(hp); + player->SetEmotion(overworldSession->emotion); CardFolder* newFolder = nullptr; From f33ee8faa83647f811f9c662e212cabdee6c95d4 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Mon, 4 Aug 2025 19:34:49 -0700 Subject: [PATCH 069/146] Player.charged_time_table_func is called with charge level instead of Player --- BattleNetwork/bindings/bnScriptedPlayer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BattleNetwork/bindings/bnScriptedPlayer.cpp b/BattleNetwork/bindings/bnScriptedPlayer.cpp index 8672b2cf1..b5aa3933a 100644 --- a/BattleNetwork/bindings/bnScriptedPlayer.cpp +++ b/BattleNetwork/bindings/bnScriptedPlayer.cpp @@ -189,7 +189,7 @@ frame_time_t ScriptedPlayer::CalculateChargeTime(const unsigned chargeLevel) { if (charge_time_table_func.valid()) { - stx::result_t result = CallLuaCallbackExpectingValue(charge_time_table_func, weakWrap); + stx::result_t result = CallLuaCallbackExpectingValue(charge_time_table_func, chargeLevel); if (!result.is_error()) { return result.value(); From ae7753318371a6e478f6e7a01fa0dd4e0f10d2c3 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Mon, 4 Aug 2025 19:50:58 -0700 Subject: [PATCH 070/146] Revert broken Tile cooldown to previous, correct value. Broken and Sea Tiles flicker for 60 instead of 180 frames. --- BattleNetwork/bnTile.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/BattleNetwork/bnTile.cpp b/BattleNetwork/bnTile.cpp index 129870996..a9a56a969 100644 --- a/BattleNetwork/bnTile.cpp +++ b/BattleNetwork/bnTile.cpp @@ -24,16 +24,18 @@ #define START_Y 144.f #define Y_OFFSET 10.0f #define COOLDOWN frames(1800) -#define FLICKER frames(180) +#define BROKEN_COOLDOWN frames(600) +#define TILE_FLICKER frames(60) +#define TEAM_FLICKER frames(180) #define SEA_COOLDOWN frames (60*16) #define SEA_DAMAGE_COOLDOWN frames(7) namespace Battle { - frame_time_t Tile::brokenCooldownLength = COOLDOWN; + frame_time_t Tile::brokenCooldownLength = BROKEN_COOLDOWN; frame_time_t Tile::teamCooldownLength = COOLDOWN; frame_time_t Tile::seaCooldownLength = SEA_COOLDOWN; frame_time_t Tile::seaDamageCooldownLength = SEA_DAMAGE_COOLDOWN; - frame_time_t Tile::flickerTeamCooldownLength = FLICKER; + frame_time_t Tile::flickerTeamCooldownLength = TEAM_FLICKER; Tile::Tile(int _x, int _y) : SpriteProxyNode(), @@ -345,11 +347,11 @@ namespace Battle { if (state == TileState::broken) { // Broken tiles flicker when they regen - animState = (((brokenCooldown.count() % 4) < 2) && brokenCooldown <= FLICKER) ? std::move(GetAnimState(TileState::normal)) : std::move(GetAnimState(state)); + animState = (((brokenCooldown.count() % 4) < 2) && brokenCooldown <= TILE_FLICKER) ? std::move(GetAnimState(TileState::normal)) : std::move(GetAnimState(state)); } else if (state == TileState::sea) { // Sea tiles flicker when they regen - animState = (((seaCooldown.count() % 4) < 2) && seaCooldown <= FLICKER) ? std::move(GetAnimState(TileState::normal)) : std::move(GetAnimState(state)); + animState = (((seaCooldown.count() % 4) < 2) && seaCooldown <= TILE_FLICKER) ? std::move(GetAnimState(TileState::normal)) : std::move(GetAnimState(state)); } else { animState = std::move(GetAnimState(state)); From ec56eccbeffab888a753eb99db4dd83c64c4d501 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Tue, 5 Aug 2025 15:59:11 -0700 Subject: [PATCH 071/146] Prevent using cards during flinch, cards can be queued during movement --- BattleNetwork/bnCharacter.cpp | 2 +- BattleNetwork/bnPlayer.cpp | 5 +++++ BattleNetwork/bnPlayer.h | 1 + BattleNetwork/bnPlayerControlledState.cpp | 5 +++-- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/BattleNetwork/bnCharacter.cpp b/BattleNetwork/bnCharacter.cpp index eba77bf59..36cc59548 100644 --- a/BattleNetwork/bnCharacter.cpp +++ b/BattleNetwork/bnCharacter.cpp @@ -150,7 +150,7 @@ bool Character::CanMoveTo(Battle::Tile * next) const bool Character::CanAttack() const { - return !currCardAction && IsActionable(); + return !currCardAction;//&& IsActionable(); } void Character::MakeActionable() diff --git a/BattleNetwork/bnPlayer.cpp b/BattleNetwork/bnPlayer.cpp index d755ab318..fa8156249 100644 --- a/BattleNetwork/bnPlayer.cpp +++ b/BattleNetwork/bnPlayer.cpp @@ -145,6 +145,11 @@ bool Player::IsActionable() const return animationComponent->GetAnimationString() == "PLAYER_IDLE"; } +const bool Player::CanAttack() const +{ + return animationComponent->GetAnimationString() != recoilAnimHash && Character::CanAttack(); +} + void Player::Attack() { std::shared_ptr action = nullptr; diff --git a/BattleNetwork/bnPlayer.h b/BattleNetwork/bnPlayer.h index b7f173fa7..d128cbb48 100644 --- a/BattleNetwork/bnPlayer.h +++ b/BattleNetwork/bnPlayer.h @@ -87,6 +87,7 @@ class Player : public Character, public AI { void MakeActionable() override final; bool IsActionable() const override final; + const bool CanAttack() const; /** * @brief Fires a buster diff --git a/BattleNetwork/bnPlayerControlledState.cpp b/BattleNetwork/bnPlayerControlledState.cpp index 706f33cd0..3376aacd6 100644 --- a/BattleNetwork/bnPlayerControlledState.cpp +++ b/BattleNetwork/bnPlayerControlledState.cpp @@ -41,7 +41,8 @@ void PlayerControlledState::OnUpdate(double _elapsed, Player& player) { // Are we creating an action this frame? if (player.InputState().Has(InputEvents::pressed_use_chip)) { std::shared_ptr cardsUI = player.GetFirstComponent(); - if (player.CanAttack() && cardsUI && cardsUI->UseNextCard()) { + + if (cardsUI && player.CanAttack() && cardsUI->UseNextCard()) { player.chargeEffect->SetCharging(false); isChargeHeld = false; } @@ -105,7 +106,7 @@ void PlayerControlledState::OnUpdate(double _elapsed, Player& player) { anim->SetAnimation(move_anim, idle_callback); - anim->SetInterruptCallback(idle_callback); + //anim->SetInterruptCallback(idle_callback); }; if (player.playerControllerSlide) { From 544054d1785593b96c3ac7f039d8a9fedf1c3e4e Mon Sep 17 00:00:00 2001 From: Alrysc Date: Wed, 6 Aug 2025 20:17:30 -0700 Subject: [PATCH 072/146] Reset charge tracking if charge was reset --- BattleNetwork/bnPlayerControlledState.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/BattleNetwork/bnPlayerControlledState.cpp b/BattleNetwork/bnPlayerControlledState.cpp index 3376aacd6..2cf440bbd 100644 --- a/BattleNetwork/bnPlayerControlledState.cpp +++ b/BattleNetwork/bnPlayerControlledState.cpp @@ -36,6 +36,16 @@ void PlayerControlledState::OnUpdate(double _elapsed, Player& player) { return; } + /* + This state may have been paused while the Player was stunned or + frozen (see Entity::Update). If so, the charge time may have been + reset since the last time this updated. This accounts for that by + resetting the isChargeHeld variable, avoiding scenarios where an + attack could be made because a Player had Shoot held before being + stunned and released once it had ended. + */ + isChargeHeld = isChargeHeld && player.chargeEffect->GetChargeTime() > frames(0); + bool missChargeKey = isChargeHeld && !player.InputState().Has(InputEvents::held_shoot); // Are we creating an action this frame? From 89ad63f11ab53432c5187271ad26158383c61185 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Fri, 29 Aug 2025 17:17:27 -0700 Subject: [PATCH 073/146] Add no_counter to Buster --- BattleNetwork/bnBuster.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/BattleNetwork/bnBuster.cpp b/BattleNetwork/bnBuster.cpp index 5242a0506..9bd029cf3 100644 --- a/BattleNetwork/bnBuster.cpp +++ b/BattleNetwork/bnBuster.cpp @@ -10,6 +10,7 @@ #include "bnAudioResourceManager.h" #include "bnRandom.h" +// TODO: Buster props should have an aggressor set. Buster::Buster(Team _team, bool _charged, int damage) : isCharged(_charged), Spell(_team) { SetPassthrough(true); SetLayer(-100); @@ -32,7 +33,7 @@ Buster::Buster(Team _team, bool _charged, int damage) : isCharged(_charged), Spe Audio().Play(AudioType::BUSTER_PEA, AudioPriority::high); auto props = Hit::DefaultProperties; - props.flags = props.flags & ~(Hit::flinch | Hit::flash); + props.flags = (props.flags | Hit::no_counter) & ~(Hit::flinch | Hit::flash); props.damage = damage; SetHitboxProperties(props); From a3f057ff5d01db90bf9ff36572c0b85682137ca5 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Fri, 29 Aug 2025 17:18:38 -0700 Subject: [PATCH 074/146] New StatusBehaviorDirector, handles status tick and application, statuses can apply more accurately than before --- BattleNetwork/bnStatusDirector.cpp | 212 +++++++++++++++++++++++++++++ BattleNetwork/bnStatusDirector.h | 69 ++++++++++ 2 files changed, 281 insertions(+) create mode 100644 BattleNetwork/bnStatusDirector.cpp create mode 100644 BattleNetwork/bnStatusDirector.h diff --git a/BattleNetwork/bnStatusDirector.cpp b/BattleNetwork/bnStatusDirector.cpp new file mode 100644 index 000000000..00da67207 --- /dev/null +++ b/BattleNetwork/bnStatusDirector.cpp @@ -0,0 +1,212 @@ +#include "bnStatusDirector.h" +#include "bnEntity.h" +#include "bnHitProperties.h" +#include + +StatusBehaviorDirector::StatusBehaviorDirector(Entity& owner) : owner(owner), queuedStatuses{ 0 }, currentStatuses{ 0 } { + currentStatuses = {}; +} + +void StatusBehaviorDirector::AddStatus(Hit::Flags statusFlag, frame_time_t maxCooldown) { + // Since we might use it twice, create it once. No need to repeat code. + AppliedStatus& statusToCheck = GetStatus(statusFlag); + statusToCheck.remainingTime = maxCooldown; + queuedStatuses = queuedStatuses | statusFlag; +} + +void StatusBehaviorDirector::AddStatus(Hit::Flags statusFlag) { + // Slight hack. A flag with less than 2 frames on it will not always be + // observable. This is because OnUpdate ticks the time down by 1 and then removes + // time <= 0. If they were added with frames(1), they would tick and be removed + // before status callbacks run. + AddStatus(statusFlag, frames(2)); +} + +Hit::Flags StatusBehaviorDirector::GetAppliedFlags(Hit::Flags flags) { + // Start from lowest set bit + Hit::Flags i = flags & -flags; + Hit::Flags m = ~0; + + static const Hit::Flags stunMask = ~(Hit::flinch | Hit::freeze); + static const Hit::Flags freezeMask = ~(Hit::flinch | Hit::flash); + + // NOTE: Flag order is important. stun < freeze < drag + while (i != 0) { + switch (i & flags & m) { + case Hit::stun: + m = m & stunMask; + break; + case Hit::freeze: + m = m & freezeMask; + break; + case Hit::drag: + m = (m & ~Hit::freeze) | ~freezeMask; + break; + } + i = i << 1; + } + + return m & flags; +} + +void StatusBehaviorDirector::ProcessPendingStatuses() { + // Process only Drag if it's queued + if ((queuedStatuses & Hit::drag) == Hit::drag) { + ProcessFlags(Hit::drag); + queuedStatuses &= ~Hit::drag; + return; + } + + // Do not process other flags if Drag is a current status + if((currentStatuses & Hit::drag) == Hit::drag) { + return; + } + + + if (queuedStatuses == 0) { + return; + } + + ProcessFlags(queuedStatuses); + queuedStatuses = Hit::none; +} + +void StatusBehaviorDirector::ProcessFlags(Hit::Flags attack) { + // Timestop check. + // TODO: Behavior does not currently happen here, but instead in Entity::Hit. + // Move that here, or remove this check. + if (false && (currentStatuses & Hit::freeze) == Hit::freeze) { + attack &= ~Hit::freeze; + } + + // Retangible removes active flash, but not queued. + if ((attack & Hit::retangible) == Hit::retangible) { + currentStatuses &= ~Hit::flash; + } + + // Flinch|Flash cancels existing Freeze|Stun + if ((attack & (Hit::flinch | Hit::flash)) == (Hit::flinch | Hit::flash)) { + // If stun is already active, flinch | flash will prevent it from + // being added by this attack. That also means a freeze could be + // committed. + if ((currentStatuses & Hit::stun) == Hit::stun) { + attack &= ~Hit::stun; + } + + currentStatuses &= ~(Hit::freeze | Hit::stun); + } + + // Drag cancels existing and queued Freeze + // Additionally cancels current or queued Stun and Freeze if Player has Drag + if ((attack & Hit::drag) == Hit::drag) { + currentStatuses &= ~Hit::freeze; + // TODO: It would be more correct to handle this in GetAppliedFlags, and + // GetAppliedFlags is set up to do this. But because Drag handling only looks + // at the Drag flag, the queued status is never removed. Find a way to make this + // cleaner. + queuedStatuses &= ~Hit::freeze; + + if ((currentStatuses & Hit::drag) == Hit::drag) { + currentStatuses &= ~(Hit::stun | Hit::freeze); + queuedStatuses &= ~(Hit::stun | Hit::freeze); + } + } + + Hit::Flags toApply = GetAppliedFlags(attack); + currentStatuses |= toApply; +} + +/* + TODO: Should OnUpdate tick and remove at the same time? This may result in some off-by-one error. +*/ +void StatusBehaviorDirector::OnUpdate(double elapsed) { + frame_time_t _elapsed = from_seconds(elapsed); + + if ((currentStatuses & Hit::drag) == Hit::drag) { + AppliedStatus& drag = GetStatus(Hit::drag); + + drag.remainingTime -= _elapsed; + + if (drag.remainingTime > frames(0)) { + return; + } + } + + auto keyTestThunk = [this](const InputEvent& key) { + return owner.InputState().Has(key); + }; + + bool anyKey = keyTestThunk(InputEvents::pressed_use_chip); + anyKey = anyKey || keyTestThunk(InputEvents::pressed_move_down); + anyKey = anyKey || keyTestThunk(InputEvents::pressed_move_up); + anyKey = anyKey || keyTestThunk(InputEvents::pressed_move_left); + anyKey = anyKey || keyTestThunk(InputEvents::pressed_move_right); + anyKey = anyKey || keyTestThunk(InputEvents::pressed_shoot); + anyKey = anyKey || keyTestThunk(InputEvents::pressed_special); + anyKey = anyKey || keyTestThunk(InputEvents::pressed_shoulder_left); + anyKey = anyKey || keyTestThunk(InputEvents::pressed_shoulder_right); + + Hit::Flags checkBit = 1; + while (checkBit != 0) { + Hit::Flags statusBit = currentStatuses & checkBit; + checkBit = checkBit << 1; + if (statusBit == 0) { + continue; + } + + AppliedStatus& status = GetStatus(statusBit); + + if (anyKey && (statusBit & (Hit::stun | Hit::freeze | Hit::bubble)) != 0) { + status.remainingTime -= _elapsed; + } + + status.remainingTime -= _elapsed; + if (status.remainingTime <= frames(0)) { + currentStatuses &= ~statusBit; + continue; + } + } +}; + +AppliedStatus& StatusBehaviorDirector::GetStatus(Hit::Flags flag) { + AppliedStatus& status = statusMap[flag]; + // Cover for default constructed if index did not exist + status.statusFlag = flag; + return status; +}; + +void StatusBehaviorDirector::ClearStatus() { + for (auto& [_, status] : statusMap) { + status.remainingTime = frames(0); + } + + queuedStatuses = Hit::none; + currentStatuses = Hit::none; +}; + +void StatusBehaviorDirector::ClearStatus(Hit::Flags flag) { + AppliedStatus& status = statusMap[flag]; + status.remainingTime = frames(0); + queuedStatuses &= ~flag; + currentStatuses &= ~flag; +} + +const Hit::Flags StatusBehaviorDirector::GetQueuedStatuses() const { + return queuedStatuses; +} + +const bool StatusBehaviorDirector::IsApplied(Hit::Flags flag) const { + return (currentStatuses & flag) == flag; +} + +const bool StatusBehaviorDirector::HasStatus(Hit::Flags flag) const { + return ((currentStatuses | queuedStatuses) & flag) == flag; +} + + +const Hit::Flags StatusBehaviorDirector::GetCurrentStatuses() const { + return currentStatuses; +} + +StatusBehaviorDirector::~StatusBehaviorDirector() { +}; diff --git a/BattleNetwork/bnStatusDirector.h b/BattleNetwork/bnStatusDirector.h new file mode 100644 index 000000000..b0e8ddd98 --- /dev/null +++ b/BattleNetwork/bnStatusDirector.h @@ -0,0 +1,69 @@ +#pragma once + +#include "bnLogger.h" +#include "bnHitProperties.h" +#include "bnFrameTimeUtils.h" +#include "bnInputEvent.h" +#include + +struct AppliedStatus { + Hit::Flags statusFlag; + frame_time_t remainingTime; +}; + +class Entity; + +class StatusBehaviorDirector { +public: + StatusBehaviorDirector(Entity& owner); + virtual ~StatusBehaviorDirector(); + void AddStatus(Hit::Flags statusFlag, frame_time_t maxCooldown); + void AddStatus(Hit::Flags statusFlag); + + /* + Ticks timers on all current statuses. The result of GetCurrentStatuses + may be different before and after calling this. + + It is expected that ProcessPendingStatuses is called before this. These + two functions are separate so that statuses may be updated separately + from processing, for example when the Entity is sliding from Drag. + + If Hit::drag is a current status, only its associated timer will be reduced. + Remaining statuses will be skipped unless this is called while Hit::drag is + not a current status. This means that a call to OnUpdate that results in Hit::drag + being removed from the current statuses will not tick any other timers. + */ + void OnUpdate(double elapsed); + AppliedStatus& GetStatus(Hit::Flags flag); + const Hit::Flags GetQueuedStatuses() const; + const Hit::Flags GetCurrentStatuses() const; + void ClearStatus(); + void ClearStatus(Hit::Flags flag); + /* + Process current queuedStatuses. The result of GetQueuedStatuses and + GetCurrentStatuses may be different before and after calling this. + + If Hit::drag is queued or is a current status, statuses will only be partially + processed. + */ + void ProcessPendingStatuses(); + /* + Returns true if flag is uncommitted or applied. + */ + const bool HasStatus(Hit::Flags flag) const; + /* + Returns true only if flag isapplied. + */ + const bool IsApplied(Hit::Flags flag) const; +private: + Entity& owner; + std::vector lastFrameStates; + std::map statusMap; + Hit::Flags currentStatuses; + Hit::Flags queuedStatuses; + + void ProcessFlags(Hit::Flags attack); + + // Returns Hit::Flags that would be applied from input flags. + Hit::Flags GetAppliedFlags(Hit::Flags flags); +}; From 9b7a3b773d41caf337aaf614b5ccfbe1fc778467 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Fri, 29 Aug 2025 17:19:32 -0700 Subject: [PATCH 075/146] Swap stun and freeze enum value so status order is correct --- BattleNetwork/bnHitProperties.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BattleNetwork/bnHitProperties.h b/BattleNetwork/bnHitProperties.h index 3bae2a660..189f9ab02 100644 --- a/BattleNetwork/bnHitProperties.h +++ b/BattleNetwork/bnHitProperties.h @@ -10,11 +10,11 @@ namespace Hit { const Flags none = 0x00000000; const Flags retangible = 0x00000001; - const Flags freeze = 0x00000002; + const Flags stun = 0x00000002; const Flags pierce = 0x00000004; const Flags flinch = 0x00000008; const Flags shake = 0x00000010; - const Flags stun = 0x00000020; + const Flags freeze = 0x00000020; const Flags flash = 0x00000040; const Flags breaking = 0x00000080; // NOTE: this is what we refer to as "true breaking" const Flags impact = 0x00000100; From ff015a006e0d5ae1087770b750495ac8f2893650 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Fri, 29 Aug 2025 17:20:22 -0700 Subject: [PATCH 076/146] Entity uses StatusBehaviorDirector. Many changes to statuses and reactions to getting hit. --- BattleNetwork/bnEntity.cpp | 531 ++++++++++++++++--------------------- BattleNetwork/bnEntity.h | 35 +-- BattleNetwork/bnTile.cpp | 5 - 3 files changed, 233 insertions(+), 338 deletions(-) diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index cf10828eb..af9614447 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -32,7 +32,8 @@ Entity::Entity() : channel(nullptr), mode(Battle::TileHighlight::none), hitboxProperties(Hit::DefaultProperties), - CounterHitPublisher() + CounterHitPublisher(), + statuses(*this) { ID = ++Entity::numOfIDs; @@ -230,12 +231,12 @@ void Entity::UpdateMovement(double elapsed) } } else if (tile->GetState() == TileState::sea && GetElement() != Element::aqua && !HasFloatShoe()) { - Root(frames(20)); + statuses.AddStatus(Hit::root, frames(20)); auto splash = std::make_shared(); field.lock()->AddEntity(splash, *tile); } else if (tile->GetState() == TileState::sand && !HasFloatShoe()) { - Root(frames(20)); + statuses.AddStatus(Hit::root, frames(20)); } else { // Invalidate the next tile pointer @@ -358,79 +359,116 @@ void Entity::Update(double _elapsed) { health = 0; // Ensure status effects do not play out - stunCooldown = frames(0); - rootCooldown = frames(0); - invincibilityCooldown = frames(0); + statuses.ClearStatus(); } // reset base color setColor(NoopCompositeColor(GetColorMode())); - RefreshShader(); + statusShaderTimer++; - if (!hit) { - if (invincibilityCooldown > frames(0)) { - unsigned frame = invincibilityCooldown.count() % 4; - if (frame < 2) { - Reveal(); - } - else { - Hide(); - } + // Used to determine if Sprite should be revealed this frame, + // when flashing was active and became inactive this frame. + bool wasFlashing = statuses.IsApplied(Hit::flash); - invincibilityCooldown -= from_seconds(_elapsed); + Hit::Flags queuedStatuses = statuses.GetQueuedStatuses(); - if (invincibilityCooldown <= frames(0)) { - Reveal(); - } - } + statuses.ProcessPendingStatuses(); + + if (currentDrag.dir == Direction::none) { + // Tick all statuses at once + statuses.OnUpdate(_elapsed); + } + + Hit::Flags currentStatuses = statuses.GetCurrentStatuses(); + Hit::Flags newStatuses = queuedStatuses & currentStatuses; + + // Some statuses clear the action queue. + // TODO: Consider if they should also call FinishMove. + if ((newStatuses & (Hit::freeze | Hit::stun)) != 0) { + actionQueue.ClearQueue(ActionQueue::CleanupType::allow_interrupts); + } + + if ((newStatuses & Hit::freeze) == Hit::freeze) { + IceFreeze(); } - if(rootCooldown > frames(0)) { - rootCooldown -= from_seconds(_elapsed); - // Root is cancelled if these conditions are met - if (rootCooldown <= frames(0) || invincibilityCooldown > frames(0) || IsPassthrough()) { - rootCooldown = frames(0); + if ((newStatuses & Hit::blind) == Hit::blind) { + Blind(); + } + + if ((newStatuses & Hit::retangible) == Hit::retangible) { + SetPassthrough(false); + } + + // a re-usable thunk for custom status effects + auto flagCheckThunk = [this](const Hit::Flags& toCheck) { + if (Entity::StatusCallback& func = statusCallbackHash[toCheck]) { + func(); + } + }; + + Hit::Flags statusCheck = newStatuses; + /// Run all status callbacks, starting from lowest set bit + Hit::Flags checkIdx = statusCheck & -statusCheck; + while (statusCheck > 0) { + if ((statusCheck & checkIdx) != 0) { + flagCheckThunk(checkIdx); } + + statusCheck = statusCheck & ~checkIdx; + checkIdx = checkIdx << 1; } - bool canUpdateThisFrame = true; + RefreshShader(); + + bool stunned = statuses.IsApplied(Hit::stun); + bool frozen = statuses.IsApplied(Hit::freeze); + bool blind = statuses.IsApplied(Hit::blind); + + // TODO: Determine if Drag should also be checked here. + // The answer is likely yes. + bool canUpdateThisFrame = !(frozen || stunned); + + /* + TODO: Using Hide and Reveal here may conflict with user mods that + attempt to hide an Entity. Find some way to play nice. - if(stunCooldown > frames(0)) { - canUpdateThisFrame = false; - stunCooldown -= from_seconds(_elapsed); + Example: AntiDamage mod, which hides the Character using it for + some time. If triggered while flashing, they will unhide before + they become vulnerable again. + */ + if (!hit) { + AppliedStatus& flash = statuses.GetStatus(Hit::flash); - if (stunCooldown <= frames(0)) { - stunCooldown = frames(0); + if (statuses.IsApplied(Hit::flash)) { + unsigned frame = flash.remainingTime.count() % 4; + if (frame < 2 || statuses.IsApplied(Hit::drag)) { + Reveal(); + } + else { + Hide(); + } + } + // Flash became inactive this frame. Reveal. + else if (wasFlashing) { + Reveal(); } } // assume this is hidden, will flip to visible if not iceFx->Hide(); - if (freezeCooldown > frames(0)) { + if (frozen) { iceFxAnimation.Update(_elapsed, iceFx->getSprite()); iceFx->Reveal(); - - canUpdateThisFrame = false; - freezeCooldown -= from_seconds(_elapsed); - - if (freezeCooldown <= frames(0)) { - freezeCooldown = frames(0); - } } // assume this is hidden, will flip to visible if not blindFx->Hide(); - if (blindCooldown > frames(0)) { + if (blind) { blindFxAnimation.Update(_elapsed, blindFx->getSprite()); blindFx->Reveal(); - - blindCooldown -= from_seconds(_elapsed); - - if (blindCooldown <= frames(0)) { - blindCooldown = frames(0); - } } if(canUpdateThisFrame) { @@ -535,19 +573,30 @@ void Entity::RefreshShader() smartShader.SetUniform("swapPalette", swapPalette); smartShader.SetUniform("palette", palette); + AppliedStatus& flash = statuses.GetStatus(Hit::flash); + bool flashing = statuses.IsApplied(Hit::flash); + bool stunned = statuses.IsApplied(Hit::stun); + bool frozen = statuses.IsApplied(Hit::freeze); + bool rooted = statuses.IsApplied(Hit::root); + // state checks - unsigned stunFrame = stunCooldown.count() % 4; - unsigned rootFrame = rootCooldown.count() % 4; + bool stunFrame = statusShaderTimer % 4 < 2; + bool rootFrame = statusShaderTimer % 4 < 2; counterFrameFlag = counterFrameFlag % 4; counterFrameFlag++; - bool iframes = invincibilityCooldown > frames(0); + /* + TODO: Flash uses its own timer. + Stun and Root use the same timer as each other, and only stun colors if both active. + Freeze overrides Root color as well. + */ + bool whiteout = hit && !isTimeFrozen; vector states = { - static_cast(whiteout), // WHITEOUT - static_cast(rootCooldown > frames(0) && (iframes || rootFrame)), // BLACKOUT - static_cast(stunCooldown > frames(0) && (iframes || stunFrame)), // HIGHLIGHT - static_cast(freezeCooldown > frames(0)) // ICEOUT + static_cast(whiteout), // WHITEOUT + static_cast(rooted && (flashing || rootFrame)), // BLACKOUT + static_cast(stunned && (flashing || stunFrame)), // HIGHLIGHT + static_cast(frozen) // ICEOUT }; smartShader.SetUniform("states", states); @@ -958,8 +1007,7 @@ void Entity::Delete() deleted = true; - // zero all blocking statuses - freezeCooldown = stunCooldown = rootCooldown = blindCooldown = frames(0); + statuses.ClearStatus(); OnDelete(); } @@ -1012,6 +1060,9 @@ void Entity::AdoptNextTile() // Slide if the tile we are moving to is ICE if (next->GetState() != TileState::ice || HasFloatShoe()) { + // TODO: Determine if this should only be incremented when + // move is voluntary. Does your rank go down when pushed? + // If not using animations, then // adopting a tile is the last step in the move procedure // Increase the move count @@ -1239,32 +1290,52 @@ const bool Entity::Hit(Hit::Properties props) { tileDamage = props.damage; } - /* + if (props.element == Element::aqua - && GetTile()->GetState() == TileState::ice - && !frameFreezeCancel) { - willFreeze = true; + && GetTile()->GetState() == TileState::ice) { + props.flags |= Hit::freeze; GetTile()->SetState(TileState::normal); } - if ((props.flags & Hit::breaking) == Hit::breaking && IsIceFrozen()) { + if ((props.flags & Hit::breaking) == Hit::breaking && statuses.IsApplied(Hit::freeze)) { extraDamage = props.damage; - frameFreezeCancel = true; + // Breaking immediately ends freeze, before the next Entity update. + // Seen by freeze being cleared during time freeze. + // TODO: Likely related to this, breaking clears frozen even when + // damage is blocked by defenses. Find out if this can be done, and also how + // defenses that trigger actions interact with this, and compare to stun. + statuses.ClearStatus(Hit::freeze); + iceFx->Hide(); + + // Remove flinch from breaking attack if it did not have flinch | flash. + // Attacks that break freeze but don't flinch and flash should not flinch. + // This is here instead of in the StatusBehaviorDirector because freeze would + // have been cleared before flags are processed this frame, making it impossible + // to tell freeze was ended. + if ((props.flags & (Hit::flash | Hit::flinch)) != (Hit::flash | Hit::flinch)) { + props.flags = props.flags & ~Hit::flinch; + } } - */ + int totalDamage = props.damage + (tileDamage + extraDamage); // Broadcast the hit before we apply statuses and change the entity's state flags if (totalDamage > 0) { - SetHealth(GetHealth() - (tileDamage + extraDamage)); + SetHealth(GetHealth() - totalDamage); HitPublisher::Broadcast(*this, props); } - SetHealth(GetHealth() - totalDamage); - if (IsTimeFrozen()) { props.flags |= Hit::no_counter; + + // Frozen Entities cannot be refrozen during timefreeze. + // This is currently handled here instead of in the StatusBehaviorDirector + // because statuses will never update during timefreeze, and so cannot + // tell this flag was affected by it. + if (IsIceFrozen()) { + props.flags = props.flags & ~Hit::freeze; + } } // Add to status queue for state resolution @@ -1306,7 +1377,7 @@ const bool Entity::HasCollision(const Hit::Properties & props) { // Pierce status hits even when passthrough or flinched if ((props.flags & Hit::pierce) != Hit::pierce) { - if (invincibilityCooldown > frames(0) || IsPassthrough() || !hitboxEnabled) return false; + if (statuses.IsApplied(Hit::flash) || IsPassthrough() || !hitboxEnabled) return false; } return true; @@ -1328,243 +1399,122 @@ const int Entity::GetMaxHealth() const void Entity::ResolveFrameBattleDamage() { - if(statusQueue.empty() || IsDeleted()) return; + if(IsDeleted()) return; std::shared_ptr frameCounterAggressor = nullptr; - bool frameStunCancel = false; - bool frameFlashCancel = false; - bool frameFreezeCancel = false; - bool willFreeze = false; - Hit::Drag postDragEffect{}; std::queue append; - while (!statusQueue.empty() && !IsSliding()) { + bool dragWasReplaced = false; + + while (!statusQueue.empty()) { CombatHitProps props = statusQueue.front(); statusQueue.pop(); - // a re-usable thunk for custom status effects - auto flagCheckThunk = [props, this](const Hit::Flags& toCheck) { - if ((props.filtered.flags & toCheck) == toCheck) { - if (Entity::StatusCallback& func = statusCallbackHash[toCheck]) { - func(); - } - } - }; - - // start of new scope { - // Only register counter if: - // 1. Hit type is impact - // 2. The hitbox is allowed to counter - // 3. The character is on a counter frame - // 4. Hit properties has an aggressor - // This will set the counter aggressor to be the first non-impact hit and not check again this frame - if (IsCountered() && (props.filtered.flags & Hit::impact) == Hit::impact && !frameCounterAggressor) { - if ((props.hitbox.flags & Hit::no_counter) == 0 && props.filtered.aggressor) { - frameCounterAggressor = GetField()->GetCharacter(props.filtered.aggressor); - } - OnCountered(); - flagCheckThunk(Hit::impact); + bool countered = IsCountered() + && (props.hitbox.flags & Hit::no_counter) == 0 // This is the original instead of filtered + && (props.filtered.flags & Hit::impact) == Hit::impact + && !frameCounterAggressor + && props.filtered.aggressor; + if (countered) { + // Only consider a counter if there was an aggressor + if (frameCounterAggressor = GetField()->GetCharacter(props.filtered.aggressor)) { + statuses.AddStatus(Hit::stun, frames(150)); + OnCountered(); + } } - // exclude this from the next processing step - props.filtered.flags &= ~Hit::impact; - - // Requeue drag if already sliding by drag or in the middle of a move - if ((props.filtered.flags & Hit::drag) == Hit::drag) { - if (IsSliding()) { - append.push({ props.hitbox, { 0, Hit::drag, Element::none, 0, props.filtered.drag } }); - } - else { - // requeue counter hits, if any (frameCounterAggressor is null when no counter was present) - if (frameCounterAggressor) { - append.push({ props.hitbox, { 0, Hit::impact, Element::none, frameCounterAggressor->GetID() } }); - frameCounterAggressor = nullptr; - } - - // requeue drag if count is > 0 - if(props.filtered.drag.count > 0) { - // Apply drag effect post status resolution - postDragEffect.dir = props.filtered.drag.dir; - postDragEffect.count = props.filtered.drag.count - 1u; - } - } - - flagCheckThunk(Hit::drag); + // Drag replaces current Drag effects. + // Do not consider Drag if it has no direction + if ((props.filtered.flags & Hit::drag) == Hit::drag && props.filtered.drag.dir != Direction::none) { + dragWasReplaced = true; + currentDrag = props.filtered.drag; } - // exclude this from the next processing step - props.filtered.flags &= ~Hit::drag; - - bool flashAndFlinch = ((props.filtered.flags & Hit::flash) == Hit::flash) && ((props.filtered.flags & Hit::flinch) == Hit::flinch); - frameFreezeCancel = frameFreezeCancel || flashAndFlinch; - - /** - While an attack that only flinches will not cancel stun, - an attack that both flinches and flashes will cancel stun. - This applies if the entity doesn't have SuperArmor installed. - If they do have armor, stun isn't cancelled. - - This effect is requeued for another frame if currently dragging - */ - if ((props.filtered.flags & Hit::stun) == Hit::stun) { - if (postDragEffect.dir != Direction::none) { - // requeue these statuses if in the middle of a slide - append.push({ props.hitbox, { 0, props.filtered.flags } }); - } - else { - - if ((props.filtered.flags & Hit::flash) == Hit::flash && frameStunCancel) { - // cancel stun - stunCooldown = frames(0); - } - else { - // refresh stun - stunCooldown = frames(120); - flagCheckThunk(Hit::stun); - } + props.filtered.flags = props.filtered.flags & ~Hit::drag; - actionQueue.ClearQueue(ActionQueue::CleanupType::allow_interrupts); - } + const bool hasFlash = ((props.filtered.flags & Hit::flash) == Hit::flash); + if (hasFlash) { + statuses.AddStatus(Hit::flash, frames(120)); } - // exclude this from the next processing step - props.filtered.flags &= ~Hit::stun; + props.filtered.flags = props.filtered.flags & ~Hit::flash; if ((props.filtered.flags & Hit::freeze) == Hit::freeze) { - if (postDragEffect.dir != Direction::none) { - // requeue these statuses if in the middle of a slide - append.push({ props.hitbox, { 0, props.filtered.flags } }); - } - else { - // this will strip out flash in the next step - frameFlashCancel = true; - willFreeze = true; - flagCheckThunk(Hit::freeze); - } - } - - // exclude this from the next processing step - props.filtered.flags &= ~Hit::freeze; - - // Always negate flash if frozen this frame - if (frameFlashCancel) { - props.filtered.flags &= ~Hit::flash; - } - - // Flash can be queued if dragging this frame - if ((props.filtered.flags & Hit::flash) == Hit::flash) { - if (postDragEffect.dir != Direction::none) { - append.push({ props.hitbox, { 0, props.filtered.flags } }); - } - else { - invincibilityCooldown = frames(120); // used as a `flash` status time - flagCheckThunk(Hit::flash); - } + statuses.AddStatus(Hit::freeze, frames(150)); } - // exclude this from the next processing step - props.filtered.flags &= ~Hit::flash; - - // Flinch is canceled if retangibility is applied - if ((props.filtered.flags & Hit::retangible) == Hit::retangible) { - invincibilityCooldown = frames(0); + props.filtered.flags = props.filtered.flags & ~Hit::freeze; - flagCheckThunk(Hit::retangible); + if ((props.filtered.flags & Hit::stun) == Hit::stun) { + statuses.AddStatus(Hit::stun, frames(120)); } - // exclude this from the next processing step - props.filtered.flags &= ~Hit::retangible; + props.filtered.flags = props.filtered.flags & ~Hit::stun; if ((props.filtered.flags & Hit::bubble) == Hit::bubble) { - if (postDragEffect.dir != Direction::none) { - // requeue these statuses if in the middle of a slide - append.push({ props.hitbox, { 0, props.filtered.flags } }); - } - else { - flagCheckThunk(Hit::bubble); - } + statuses.AddStatus(Hit::bubble, frames(150)); } - // exclude this from the next processing step - props.filtered.flags &= ~Hit::bubble; + props.filtered.flags = props.filtered.flags & ~Hit::bubble; if ((props.filtered.flags & Hit::root) == Hit::root) { - rootCooldown = frames(120); - flagCheckThunk(Hit::root); + statuses.AddStatus(Hit::root, frames(120)); } - // exclude this from the next processing step - props.filtered.flags &= ~Hit::root; + props.filtered.flags = props.filtered.flags & ~Hit::root; - // Only if not in time freeze, consider this status for delayed effect after sliding - if ((props.filtered.flags & Hit::shake) == Hit::shake && !IsTimeFrozen()) { - if (postDragEffect.dir != Direction::none) { - // requeue these statuses if in the middle of a slide - append.push({ props.hitbox, { 0, props.filtered.flags } }); - } - else { - CreateComponent(weak_from_this()); - flagCheckThunk(Hit::shake); - } + if ((props.filtered.flags & Hit::blind) == Hit::blind) { + statuses.AddStatus(Hit::blind, frames(300)); } - // exclude this from the next processing step - props.filtered.flags &= ~Hit::shake; + props.filtered.flags = props.filtered.flags & ~Hit::blind; - // blind check - if ((props.filtered.flags & Hit::blind) == Hit::blind) { - if (postDragEffect.dir != Direction::none) { - // requeue these statuses if in the middle of a slide/drag - append.push({ props.hitbox, { 0, props.filtered.flags } }); - } - else { - Blind(frames(300)); - flagCheckThunk(Hit::blind); + // Add the rest, starting from lowest set bit + Hit::Flags curFlag = props.filtered.flags & -props.filtered.flags; + while (props.filtered.flags > 0) { + if ((props.filtered.flags & curFlag) != 0) { + statuses.AddStatus(curFlag); + props.filtered.flags = props.filtered.flags & ~curFlag; } - } - // exclude blind from the next processing step - props.filtered.flags &= ~Hit::blind; - - /* - flags already accounted for: - - impact - - stun - - freeze - - flash - - drag - - retangible - - bubble - - root - - shake - - blind - Now check if the rest were triggered and invoke the - corresponding status callbacks - */ - flagCheckThunk(Hit::breaking); - flagCheckThunk(Hit::pierce); - flagCheckThunk(Hit::flinch); + curFlag = curFlag << 1; + } if (GetHealth() == 0) { - postDragEffect.dir = Direction::none; // Cancel slide post-status if blowing up + currentDrag.dir = Direction::none; // Cancel slide post-status if blowing up + dragWasReplaced = true; } } } // end while-loop - if (!append.empty()) { - statusQueue = append; + // A new Drag should immediately end current movement + // TODO: Drag forcibly ends the movement. Find out if that counts as a movement, because FinishMove + // calls AdoptTile, which increases moveCount. + if (dragWasReplaced) { + FinishMove(); + statuses.AddStatus(Hit::drag, frames(22)); } - if (postDragEffect.dir != Direction::none) { + // TODO: Drag during wind push will not overwrite? What about other movement, like ice, conveyor? + if (!IsSliding() && currentDrag.dir != Direction::none) { // enemies and objects on opposing side of field are granted immunity from drag if (Teammate(GetTile()->GetTeam())) { actionQueue.ClearQueue(ActionQueue::CleanupType::allow_interrupts); slideFromDrag = true; - Battle::Tile* dest = GetTile() + postDragEffect.dir; + + if (currentDrag.count > 0u) { + currentDrag.count -= 1u; + } + else { + currentDrag.dir = Direction::none; + } + + Battle::Tile* dest = GetTile() + currentDrag.dir; + // The final drag event applies endlag. // 22 frames matches the amount of fixed frames applied to player recoil @@ -1573,32 +1523,25 @@ void Entity::ResolveFrameBattleDamage() // statuses are allowed to apply earlier than intended. // This may be made more clear by making a distinction between voluntary // and involuntary MoveEvents. + // TODO: Having higher endlag acts badly with ice slide. Ice slide should + // act the same as if the currentDrag.count did not go down when reaching + // that Tile. frame_time_t movetime = frames(4); - if (!CanMoveTo(dest)) { - movetime= frames(22); + if (currentDrag.dir == Direction::none || !CanMoveTo(dest)) { + movetime = frames(22); dest = GetTile(); - postDragEffect.count = 0; + currentDrag.count = 0; + currentDrag.dir = Direction::none; } // Enqueue a move action at the top of our priorities actionQueue.Add(MoveEvent{ movetime, frames(0), frames(0), 0, dest, {}, true }, ActionOrder::immediate, ActionDiscardOp::until_resolve); - - std::queue oldQueue = statusQueue; - statusQueue = {}; - // Re-queue the drag status to be re-considered FIRST in our next combat checks - statusQueue.push({ {}, { 0, Hit::drag, Element::none, 0, postDragEffect } }); - - // append the old queue items after - while (!oldQueue.empty()) { - statusQueue.push(oldQueue.front()); - oldQueue.pop(); - } } } if (GetHealth() == 0) { // We are dying. Prevent special fx and status animations from triggering. - frameFreezeCancel = frameFlashCancel = frameStunCancel = true; + statuses.ClearStatus(); while(statusQueue.size() > 0) { statusQueue.pop(); @@ -1614,21 +1557,6 @@ void Entity::ResolveFrameBattleDamage() } else if (frameCounterAggressor) { CounterHitPublisher::Broadcast(*this, *frameCounterAggressor); } - - if (frameFreezeCancel) { - freezeCooldown = frames(0); // end freeze effect - } - else if (willFreeze) { - IceFreeze(frames(150)); // start freeze effect - } - - if (frameFlashCancel) { - invincibilityCooldown = frames(0); // end flash effect - } - - if (frameStunCancel) { - stunCooldown = frames(0); // end stun effect - } } void Entity::SetHealth(const int _health) { @@ -1715,46 +1643,38 @@ void Entity::NeverFlip(bool enabled) neverFlip = enabled; } +// TODO: Replace all of these with one HasStatus bool Entity::IsStunned() { - return stunCooldown > frames(0); + return statuses.HasStatus(Hit::stun); } bool Entity::IsRooted() { - return rootCooldown > frames(0); + return statuses.HasStatus(Hit::root); } bool Entity::IsIceFrozen() { - return freezeCooldown > frames(0); + return statuses.HasStatus(Hit::freeze); } bool Entity::IsBlind() { - return blindCooldown > frames(0); -} - -void Entity::Stun(frame_time_t maxCooldown) -{ - invincibilityCooldown = frames(0); // cancel flash - freezeCooldown = frames(0); // cancel freeze - stunCooldown = maxCooldown; + return statuses.HasStatus(Hit::blind); } -void Entity::Root(frame_time_t maxCooldown) -{ - rootCooldown = maxCooldown; -} -void Entity::IceFreeze(frame_time_t maxCooldown) +void Entity::IceFreeze() { - invincibilityCooldown = frames(0); // cancel flash - stunCooldown = frames(0); // cancel stun - freezeCooldown = maxCooldown; - const float height = GetHeight(); static std::shared_ptr freezesfx = Audio().LoadFromFile(SoundPaths::ICE_FX); + // Becoming frozen instantly ends flashing, which includes removing its passthrough effect. + // Removing stun here may be redundant with how the StatusBehaviorDirector filters + // freeze and stun. + SetPassthrough(false); + statuses.ClearStatus(Hit::flash); + statuses.ClearStatus(Hit::stun); Audio().Play(freezesfx, AudioPriority::highest); if (height <= 48) { @@ -1773,7 +1693,7 @@ void Entity::IceFreeze(frame_time_t maxCooldown) iceFxAnimation.Refresh(iceFx->getSprite()); } -void Entity::Blind(frame_time_t maxCooldown) +void Entity::Blind() { float height = -GetHeight()/2.f; std::shared_ptr anim = GetFirstComponent(); @@ -1782,7 +1702,6 @@ void Entity::Blind(frame_time_t maxCooldown) height = (anim->GetPoint("head") - anim->GetPoint("origin")).y; } - blindCooldown = maxCooldown; blindFx->setPosition(0, height); blindFxAnimation << "default" << Animator::Mode::Loop; blindFxAnimation.Refresh(blindFx->getSprite()); @@ -1790,7 +1709,7 @@ void Entity::Blind(frame_time_t maxCooldown) bool Entity::IsCountered() { - return (counterable && stunCooldown <= frames(0)); + return (counterable && !statuses.IsApplied(Hit::stun)); } const Battle::TileHighlight Entity::GetTileHighlightMode() const { diff --git a/BattleNetwork/bnEntity.h b/BattleNetwork/bnEntity.h index 8768ef9c9..a1faf4ab1 100644 --- a/BattleNetwork/bnEntity.h +++ b/BattleNetwork/bnEntity.h @@ -37,6 +37,7 @@ using std::string; #include "bnDefenseRule.h" #include "bnHitProperties.h" #include "stx/memory.h" +#include "bnStatusDirector.h" namespace Battle { class Tile; @@ -772,11 +773,8 @@ class Entity : frame_time_t moveStartupDelay{}; std::optional moveEndlagDelay; frame_time_t grassHealCooldown{ 0 }; /*!< Timer until next healing is allowed */ - frame_time_t stunCooldown{ 0 }; /*!< Timer until stun is over */ - frame_time_t rootCooldown{ 0 }; /*!< Timer until root is over */ - frame_time_t freezeCooldown{ 0 }; /*!< Timer until freeze is over */ - frame_time_t blindCooldown{ 0 }; /*!< Timer until blind is over */ - frame_time_t invincibilityCooldown{ 0 }; /*!< Timer until invincibility is over */ + StatusBehaviorDirector statuses; + bool counterable{}; bool neverFlip{}; bool hit{}; /*!< Was hit this frame */ @@ -798,29 +796,13 @@ class Entity : const int GetMoveCount() const; /*!< Total intended movements made. Used to calculate rank*/ - /** - * @brief Stun a character for maxCooldown seconds - * @param maxCooldown - * Used internally by class - * - */ - void Stun(frame_time_t maxCooldown); - - /** - * @brief Stop a character from moving for maxCooldown seconds - * @param maxCooldown - * Used internally by class - * - */ - void Root(frame_time_t maxCooldown); - /** * @brief Stop a character from moving for maxCooldown seconds * @param maxCooldown * Used internally by class * */ - void IceFreeze(frame_time_t maxCooldown); + void IceFreeze(); /** * @brief This entity should not see opponents for maxCooldown seconds @@ -828,7 +810,7 @@ class Entity : * Used internally by class * */ - void Blind(frame_time_t maxCooldown); + void Blind(); /** * @brief Query if an attack successfully countered a Character @@ -898,12 +880,11 @@ class Entity : sf::Vector2f counterSlideOffset{ 0.f, 0.f }; /*!< Used when enemies delete on counter - they slide back */ std::vector> defenses; /* statusQueue; + Hit::Drag currentDrag{}; sf::Shader* whiteout{ nullptr }; /*!< Flash white when hit */ sf::Shader* stun{ nullptr }; /*!< Flicker yellow with luminance values when stun */ diff --git a/BattleNetwork/bnTile.cpp b/BattleNetwork/bnTile.cpp index a9a56a969..23f9c6160 100644 --- a/BattleNetwork/bnTile.cpp +++ b/BattleNetwork/bnTile.cpp @@ -1100,9 +1100,6 @@ namespace Battle { taggedAttackers.push_back(attacker->GetID()); } - // Retangible flag takes characters out of passthrough status - retangible = retangible || ((props.flags & Hit::retangible) == Hit::retangible); - // The attacker passed at least one defense check character->DefenseCheck(judge, attacker, DefenseOrder::collisionOnly); @@ -1134,8 +1131,6 @@ namespace Battle { attacker->SetHitboxProperties(props); } - if (retangible) character->SetPassthrough(false); - judge.PrepareForNextAttack(); } // end each spell loop From 1f0020ed0f71097749d9d86fbb3d78eb83c44dff Mon Sep 17 00:00:00 2001 From: Alrysc Date: Fri, 29 Aug 2025 17:21:26 -0700 Subject: [PATCH 077/146] BattleSceneBase can show counter text for other players. NetworkBattleScene subscribes Players to CounterHitListener. --- .../battlescene/bnBattleSceneBase.cpp | 45 ++++++++++++------- .../battlescene/bnNetworkBattleScene.cpp | 6 +++ 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/BattleNetwork/battlescene/bnBattleSceneBase.cpp b/BattleNetwork/battlescene/bnBattleSceneBase.cpp index 7fbfbe085..44914c996 100644 --- a/BattleNetwork/battlescene/bnBattleSceneBase.cpp +++ b/BattleNetwork/battlescene/bnBattleSceneBase.cpp @@ -212,12 +212,16 @@ const bool BattleSceneBase::IsQuitting() const void BattleSceneBase::OnCounter(Entity& victim, Entity& aggressor) { + didCounterHit = true; // This flag allows the counter to display + comboInfoTimer.reset(); // reset display timer + Audio().Play(AudioType::COUNTER, AudioPriority::highest); + + victim.ToggleCounter(false); // disable counter frame for the victim + for (std::shared_ptr p : GetAllPlayers()) { if (&aggressor != p.get()) continue; if (p == localPlayer) { - didCounterHit = true; // This flag allows the counter to display - comboInfoTimer.reset(); // reset display timer totalCounterMoves++; if (victim.IsDeleted()) { @@ -225,11 +229,6 @@ void BattleSceneBase::OnCounter(Entity& victim, Entity& aggressor) } } - Audio().Play(AudioType::COUNTER, AudioPriority::highest); - - victim.ToggleCounter(false); // disable counter frame for the victim - victim.Stun(frames(150)); - PreparePlayerFullSynchro(p); } } @@ -511,16 +510,30 @@ void BattleSceneBase::LoadBlueTeamMob(Mob& mob) void BattleSceneBase::HandleCounterLoss(Entity& subject, bool playsound) { - if (&subject == localPlayer.get()) { - if (field->DoesRevealCounterFrames()) { - localPlayer->RemoveNode(counterReveal); - localPlayer->RemoveDefenseRule(counterCombatRule); - localPlayer->SetEmotion(Emotion::normal); - field->RevealCounterFrames(false); + std::shared_ptr cardUI = subject.GetFirstComponent(); - playsound ? Audio().Play(AudioType::COUNTER_BONUS, AudioPriority::highest) : 0; - } - cardUI->SetMultiplier(1); + // Must not be a Player. No counter to remove. + if (!cardUI) { + return; + } + + std::shared_ptr p = cardUI->GetOwnerAs(); + + // p should never be nullptr. Sanity check. + // There's nothing to do if the Player isn't in the correct emotion. + if (!p || p->GetEmotion() != Emotion::full_synchro) { + return; + } + + p->RemoveNode(counterReveal); + p->RemoveDefenseRule(counterCombatRule); + p->SetEmotion(Emotion::normal); + cardUI->SetMultiplier(1); + + playsound ? Audio().Play(AudioType::COUNTER_BONUS, AudioPriority::highest) : 0; + + if (&subject == localPlayer.get() && field->DoesRevealCounterFrames()) { + field->RevealCounterFrames(false); } } diff --git a/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp b/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp index cbe456432..dea9c37d2 100644 --- a/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp +++ b/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp @@ -137,6 +137,12 @@ NetworkBattleScene::NetworkBattleScene(ActivityController& controller, NetworkBa // Subscribe to player's events combatPtr->Subscribe(*p); timeFreezePtr->Subscribe(*p); + + // TODO: Enemies spawned by the mob or either Player are not subscribed. + // Mob battles do this in the MobIntroBattleState, but that doesn't exist here. + // Check for other listeners which have not been subscribed to that should have, + // and consider subscribing for new spawns through Field::AddEntity. + CounterHitListener::Subscribe(*p); } // Important! State transitions are added in order of priority! From 2a87abb5b13b63523c5510ef6a5bbed1dd5be9ae Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sat, 30 Aug 2025 10:23:01 -0700 Subject: [PATCH 078/146] Changed shader timer to uint8_t, initialized some variables in StatusDirector.h --- BattleNetwork/bnEntity.h | 2 +- BattleNetwork/bnStatusDirector.cpp | 2 -- BattleNetwork/bnStatusDirector.h | 8 ++++---- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/BattleNetwork/bnEntity.h b/BattleNetwork/bnEntity.h index a1faf4ab1..783f1cf41 100644 --- a/BattleNetwork/bnEntity.h +++ b/BattleNetwork/bnEntity.h @@ -881,7 +881,7 @@ class Entity : std::vector> defenses; /* statusQueue; Hit::Drag currentDrag{}; diff --git a/BattleNetwork/bnStatusDirector.cpp b/BattleNetwork/bnStatusDirector.cpp index 00da67207..8d6950d1d 100644 --- a/BattleNetwork/bnStatusDirector.cpp +++ b/BattleNetwork/bnStatusDirector.cpp @@ -1,14 +1,12 @@ #include "bnStatusDirector.h" #include "bnEntity.h" #include "bnHitProperties.h" -#include StatusBehaviorDirector::StatusBehaviorDirector(Entity& owner) : owner(owner), queuedStatuses{ 0 }, currentStatuses{ 0 } { currentStatuses = {}; } void StatusBehaviorDirector::AddStatus(Hit::Flags statusFlag, frame_time_t maxCooldown) { - // Since we might use it twice, create it once. No need to repeat code. AppliedStatus& statusToCheck = GetStatus(statusFlag); statusToCheck.remainingTime = maxCooldown; queuedStatuses = queuedStatuses | statusFlag; diff --git a/BattleNetwork/bnStatusDirector.h b/BattleNetwork/bnStatusDirector.h index b0e8ddd98..279481bc9 100644 --- a/BattleNetwork/bnStatusDirector.h +++ b/BattleNetwork/bnStatusDirector.h @@ -7,8 +7,8 @@ #include struct AppliedStatus { - Hit::Flags statusFlag; - frame_time_t remainingTime; + Hit::Flags statusFlag{}; + frame_time_t remainingTime{}; }; class Entity; @@ -59,8 +59,8 @@ class StatusBehaviorDirector { Entity& owner; std::vector lastFrameStates; std::map statusMap; - Hit::Flags currentStatuses; - Hit::Flags queuedStatuses; + Hit::Flags currentStatuses{}; + Hit::Flags queuedStatuses{}; void ProcessFlags(Hit::Flags attack); From c773ca0d2507c20fced212b1b6bd2875eaee475e Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sat, 30 Aug 2025 16:17:27 -0700 Subject: [PATCH 079/146] Update comment on check for PlayerSelectedCardsUI --- BattleNetwork/battlescene/bnBattleSceneBase.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BattleNetwork/battlescene/bnBattleSceneBase.cpp b/BattleNetwork/battlescene/bnBattleSceneBase.cpp index 44914c996..128d938f8 100644 --- a/BattleNetwork/battlescene/bnBattleSceneBase.cpp +++ b/BattleNetwork/battlescene/bnBattleSceneBase.cpp @@ -512,7 +512,7 @@ void BattleSceneBase::HandleCounterLoss(Entity& subject, bool playsound) { std::shared_ptr cardUI = subject.GetFirstComponent(); - // Must not be a Player. No counter to remove. + // No multipler to remove if (!cardUI) { return; } From f652de39bbb9614aa7d752d02417e1624b8865da Mon Sep 17 00:00:00 2001 From: Alrysc Date: Thu, 11 Sep 2025 19:14:38 -0700 Subject: [PATCH 080/146] More fine-tuned CanAttack return to represent most known outcome, with extra accuracy. Entity public functions for generically checking status. CanAttack used in every place that PlayerControlledState can queue an action. CanAttack is false until the end of the frame that it would become true, to match certain source behavior. --- BattleNetwork/bnCharacter.cpp | 11 +++++- BattleNetwork/bnCharacter.h | 30 ++++++++++++++ BattleNetwork/bnEntity.cpp | 48 +++++++++++++---------- BattleNetwork/bnEntity.h | 16 ++++++++ BattleNetwork/bnPlayer.cpp | 7 +++- BattleNetwork/bnPlayer.h | 2 +- BattleNetwork/bnPlayerControlledState.cpp | 34 +++++++++++++--- 7 files changed, 117 insertions(+), 31 deletions(-) diff --git a/BattleNetwork/bnCharacter.cpp b/BattleNetwork/bnCharacter.cpp index 36cc59548..94bfb8801 100644 --- a/BattleNetwork/bnCharacter.cpp +++ b/BattleNetwork/bnCharacter.cpp @@ -17,6 +17,9 @@ #include "bnCardAction.h" #include "bnCardToActions.h" +// All statuses which should prevent Character from taking actions +constexpr Hit::Flags blockingStatuses = Hit::stun | Hit::freeze | Hit::bubble | Hit::drag; + Character::Character(Rank _rank) : rank(_rank), CardActionUsePublisher(), @@ -124,6 +127,8 @@ void Character::Update(double _elapsed) { actionQueue.Pop(); } } + + actionBlocked = !CanAttackImpl(); } bool Character::CanMoveTo(Battle::Tile * next) @@ -150,7 +155,11 @@ bool Character::CanMoveTo(Battle::Tile * next) const bool Character::CanAttack() const { - return !currCardAction;//&& IsActionable(); + return !actionBlocked && CanAttackImpl();//&& IsActionable(); +} + +const bool Character::CanAttackImpl() const { + return !currCardAction && !(statuses.GetCurrentStatuses() & blockingStatuses); } void Character::MakeActionable() diff --git a/BattleNetwork/bnCharacter.h b/BattleNetwork/bnCharacter.h index 84e540836..0559fc7a3 100644 --- a/BattleNetwork/bnCharacter.h +++ b/BattleNetwork/bnCharacter.h @@ -89,6 +89,16 @@ class Character: */ virtual bool CanMoveTo(Battle::Tile* next) override; + /** + * @brief Whether or not characters are allowed to begin a new action, as + * determined by CanAttackImpl and actionBlocked. + * + * Currently does not indicate that the Character actually can act, but + * expect that some behavior is delayed or skipped while this returns false. + * + * @return false if character is unable to act based on CanAttackImpl, or if + * CanAttackImpl returned false during the previous Update. Otherwise, true. + */ const bool CanAttack() const; /** @@ -104,4 +114,24 @@ class Character: protected: Character::Rank rank; + /* + Cached result of CanAttackImpl. Part of what determines whether or not + characters are free to act. + + This exists in order to enforce the idea that characters cannot act on + the frame that they visibly become actionable, for example on the frame + a CardAction ends or the frame stun ends. + + Set true based on a call to CanAttackImpl done at the end of Update. + Also set true by MakeActionable. + */ + bool actionBlocked = false; + + /** + @brief Called by CanAttack as part of the determination of whether or not + characters are free to act. + @returns true if the Character can act, otherwise false + */ + virtual const bool CanAttackImpl() const; + }; diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index af9614447..54535467b 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -384,8 +384,12 @@ void Entity::Update(double _elapsed) { Hit::Flags newStatuses = queuedStatuses & currentStatuses; // Some statuses clear the action queue. - // TODO: Consider if they should also call FinishMove. + // TODO: Neither FinishMove nor clearing the queue ends the animation initiated + // by PlayerControlled state. This might be smoothly handled if the move + // animation was actually a CardAction. Otherwise, make sure it's safe to enter + // idle right here and do that instead. if ((newStatuses & (Hit::freeze | Hit::stun)) != 0) { + FinishMove(); actionQueue.ClearQueue(ActionQueue::CleanupType::allow_interrupts); } @@ -517,8 +521,11 @@ void Entity::Update(double _elapsed) { setPosition(tile->getPosition().x + offset.x, tile->getPosition().y + offset.y); } - // If drag status is over, reset the flag - if (!IsSliding() && slideFromDrag) slideFromDrag = false; + // If drag slide is over, reset the flag + if (!IsSliding() && slideFromDrag && currentDrag.count == 0) { + slideFromDrag = false; + } + } @@ -1503,9 +1510,6 @@ void Entity::ResolveFrameBattleDamage() if (!IsSliding() && currentDrag.dir != Direction::none) { // enemies and objects on opposing side of field are granted immunity from drag if (Teammate(GetTile()->GetTeam())) { - actionQueue.ClearQueue(ActionQueue::CleanupType::allow_interrupts); - slideFromDrag = true; - if (currentDrag.count > 0u) { currentDrag.count -= 1u; } @@ -1516,26 +1520,20 @@ void Entity::ResolveFrameBattleDamage() Battle::Tile* dest = GetTile() + currentDrag.dir; - // The final drag event applies endlag. - // 22 frames matches the amount of fixed frames applied to player recoil - // This must be applied as move delta time instead of end delay to avoid - // opening new edge cases. When move delta is 0, IsSliding is false and - // statuses are allowed to apply earlier than intended. - // This may be made more clear by making a distinction between voluntary - // and involuntary MoveEvents. - // TODO: Having higher endlag acts badly with ice slide. Ice slide should - // act the same as if the currentDrag.count did not go down when reaching - // that Tile. + // The final drag event should reset currentDrag. + // TODO: Ice slide should act the same as if the currentDrag.count did + // not go down when reaching that Tile. frame_time_t movetime = frames(4); if (currentDrag.dir == Direction::none || !CanMoveTo(dest)) { - movetime = frames(22); - dest = GetTile(); currentDrag.count = 0; currentDrag.dir = Direction::none; } - - // Enqueue a move action at the top of our priorities - actionQueue.Add(MoveEvent{ movetime, frames(0), frames(0), 0, dest, {}, true }, ActionOrder::immediate, ActionDiscardOp::until_resolve); + else { + actionQueue.ClearQueue(ActionQueue::CleanupType::allow_interrupts); + slideFromDrag = true; + // Enqueue a move action at the top of our priorities + actionQueue.Add(MoveEvent{ movetime, frames(0), frames(0), 0, dest, {}, true }, ActionOrder::immediate, ActionDiscardOp::until_resolve); + } } } @@ -1663,6 +1661,14 @@ bool Entity::IsBlind() return statuses.HasStatus(Hit::blind); } +bool Entity::HasStatus(Hit::Flags status) { + return statuses.HasStatus(status); +} + +bool Entity::IsStatusApplied(Hit::Flags status) { + return statuses.IsApplied(status); +} + void Entity::IceFreeze() { diff --git a/BattleNetwork/bnEntity.h b/BattleNetwork/bnEntity.h index 783f1cf41..21948ddfc 100644 --- a/BattleNetwork/bnEntity.h +++ b/BattleNetwork/bnEntity.h @@ -616,6 +616,22 @@ class Entity : */ bool IsBlind(); + /** + * @brief Query if entity has a certain status tracked, whether queued or applied. + * A queued status may not be applied by end of frame, or may be nullified during + * status processing. + * @param status to query + * @return true if entity has status applied OR queued, false otherwise + */ + bool HasStatus(Hit::Flags status); + + /** + * @brief Query if entity is afflicted by a certain status + * @param status to query + * @return true if entity has status applied, false otherwise + */ + bool IsStatusApplied(Hit::Flags status); + /** * @brief Some characters allow others to move on top of them * @param enabled true, characters can share space, false otherwise diff --git a/BattleNetwork/bnPlayer.cpp b/BattleNetwork/bnPlayer.cpp index fa8156249..dab4d5b8e 100644 --- a/BattleNetwork/bnPlayer.cpp +++ b/BattleNetwork/bnPlayer.cpp @@ -138,6 +138,9 @@ void Player::MakeActionable() animationComponent->SetAnimation("PLAYER_IDLE"); animationComponent->SetPlaybackMode(Animator::Mode::Loop); } + + // TODO: Ensure this is safe to set false here, and if Character::MakeActionable should do the same + actionBlocked = false; } bool Player::IsActionable() const @@ -145,9 +148,9 @@ bool Player::IsActionable() const return animationComponent->GetAnimationString() == "PLAYER_IDLE"; } -const bool Player::CanAttack() const +const bool Player::CanAttackImpl() const { - return animationComponent->GetAnimationString() != recoilAnimHash && Character::CanAttack(); + return Character::CanAttackImpl() && animationComponent->GetAnimationString() != recoilAnimHash; } void Player::Attack() { diff --git a/BattleNetwork/bnPlayer.h b/BattleNetwork/bnPlayer.h index d128cbb48..830ba3a08 100644 --- a/BattleNetwork/bnPlayer.h +++ b/BattleNetwork/bnPlayer.h @@ -87,7 +87,6 @@ class Player : public Character, public AI { void MakeActionable() override final; bool IsActionable() const override final; - const bool CanAttack() const; /** * @brief Fires a buster @@ -193,6 +192,7 @@ class Player : public Character, public AI { std::function()> specialOverride{}; std::shared_ptr superArmor{ nullptr }; SyncNodeContainer syncNodeContainer; + const bool CanAttackImpl() const override; }; template diff --git a/BattleNetwork/bnPlayerControlledState.cpp b/BattleNetwork/bnPlayerControlledState.cpp index 2cf440bbd..5b64f74d6 100644 --- a/BattleNetwork/bnPlayerControlledState.cpp +++ b/BattleNetwork/bnPlayerControlledState.cpp @@ -27,7 +27,11 @@ void PlayerControlledState::OnEnter(Player& player) { void PlayerControlledState::OnUpdate(double _elapsed, Player& player) { // Actions with animation lockout controls take priority over movement bool lockout = player.IsLockoutAnimationComplete(); + // Player is idle bool actionable = player.IsActionable(); + bool canAttack = player.CanAttack(); + bool isMoving = player.IsMoving(); + bool isDragged = player.IsStatusApplied(Hit::drag); // One of our ongoing animations is preventing us from charging if (!lockout) { @@ -52,7 +56,7 @@ void PlayerControlledState::OnUpdate(double _elapsed, Player& player) { if (player.InputState().Has(InputEvents::pressed_use_chip)) { std::shared_ptr cardsUI = player.GetFirstComponent(); - if (cardsUI && player.CanAttack() && cardsUI->UseNextCard()) { + if (cardsUI && canAttack && cardsUI->UseNextCard()) { player.chargeEffect->SetCharging(false); isChargeHeld = false; } @@ -60,7 +64,7 @@ void PlayerControlledState::OnUpdate(double _elapsed, Player& player) { } else if (player.InputState().Has(InputEvents::released_special)) { const std::vector> actions = player.AsyncActionList(); - bool canUseSpecial = player.CanAttack(); + bool canUseSpecial = canAttack; // Just make sure one of these actions are not from an ability for (const std::shared_ptr& action : actions) { @@ -72,20 +76,38 @@ void PlayerControlledState::OnUpdate(double _elapsed, Player& player) { } } // queue attack based on input behavior (buster or charge?) else if (player.InputState().Has(InputEvents::released_shoot) || missChargeKey) { - // This routine is responsible for determining the outcome of the attack + // This routine is responsible for determining the outcome of the attack. + // It is not yet determined that the attack will be added to the queue. + // Whether it is or not, charge is reset. isChargeHeld = false; player.chargeEffect->SetCharging(false); - player.Attack(); + + + // TODO: This condition could be somewhat complicated. + // It might make more sense to add a discard filter to ActionQueue that + // discards anything by movement while CanAttack is false. + // It is like this now because you must be able to queue attack while + // moving, but movement could be due to Drag, where you cannot queue. + // You also cannot queue while you cannot act. + if ((isMoving && !isDragged) || canAttack) { + player.Attack(); + } + } else if (player.InputState().Has(InputEvents::held_shoot)) { - if (actionable || player.IsMoving()) { + /* + During Drag, Player may not be moving, but could also not be idle. + This means actionable || IsMoving is false, yet they can charge. + To cover for this case, check for Drag. + */ + if (actionable || isMoving || isDragged) { isChargeHeld = true; player.chargeEffect->SetCharging(true); } } // Movement increments are restricted based on anim speed at this time - if (player.IsMoving()) return; + if (isMoving || !canAttack) return; Direction direction = Direction::none; if (player.InputState().Has(InputEvents::pressed_move_up) || player.InputState().Has(InputEvents::held_move_up)) { From 3cb98388af9921a75ec7940385e09aa8fca33255 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Fri, 12 Sep 2025 12:31:56 -0700 Subject: [PATCH 081/146] Bools in PlayerControlledState::OnUpdate made const --- BattleNetwork/bnPlayerControlledState.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/BattleNetwork/bnPlayerControlledState.cpp b/BattleNetwork/bnPlayerControlledState.cpp index 5b64f74d6..c910ff039 100644 --- a/BattleNetwork/bnPlayerControlledState.cpp +++ b/BattleNetwork/bnPlayerControlledState.cpp @@ -26,12 +26,12 @@ void PlayerControlledState::OnEnter(Player& player) { void PlayerControlledState::OnUpdate(double _elapsed, Player& player) { // Actions with animation lockout controls take priority over movement - bool lockout = player.IsLockoutAnimationComplete(); + const bool lockout = player.IsLockoutAnimationComplete(); // Player is idle - bool actionable = player.IsActionable(); - bool canAttack = player.CanAttack(); - bool isMoving = player.IsMoving(); - bool isDragged = player.IsStatusApplied(Hit::drag); + const bool actionable = player.IsActionable(); + const bool canAttack = player.CanAttack(); + const bool isMoving = player.IsMoving(); + const bool isDragged = player.IsStatusApplied(Hit::drag); // One of our ongoing animations is preventing us from charging if (!lockout) { @@ -50,7 +50,7 @@ void PlayerControlledState::OnUpdate(double _elapsed, Player& player) { */ isChargeHeld = isChargeHeld && player.chargeEffect->GetChargeTime() > frames(0); - bool missChargeKey = isChargeHeld && !player.InputState().Has(InputEvents::held_shoot); + const bool missChargeKey = isChargeHeld && !player.InputState().Has(InputEvents::held_shoot); // Are we creating an action this frame? if (player.InputState().Has(InputEvents::pressed_use_chip)) { @@ -85,7 +85,7 @@ void PlayerControlledState::OnUpdate(double _elapsed, Player& player) { // TODO: This condition could be somewhat complicated. // It might make more sense to add a discard filter to ActionQueue that - // discards anything by movement while CanAttack is false. + // discards anything but movement while CanAttack is false. // It is like this now because you must be able to queue attack while // moving, but movement could be due to Drag, where you cannot queue. // You also cannot queue while you cannot act. From 7a5c85e08c81bcd3a3b0ec732133a89c573fc6c4 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Fri, 12 Sep 2025 23:03:37 -0700 Subject: [PATCH 082/146] Avoid status tick even when Drag expired --- BattleNetwork/bnStatusDirector.cpp | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/BattleNetwork/bnStatusDirector.cpp b/BattleNetwork/bnStatusDirector.cpp index 8d6950d1d..2f4e75d5a 100644 --- a/BattleNetwork/bnStatusDirector.cpp +++ b/BattleNetwork/bnStatusDirector.cpp @@ -128,6 +128,22 @@ void StatusBehaviorDirector::OnUpdate(double elapsed) { if (drag.remainingTime > frames(0)) { return; } + + currentStatuses &= ~Hit::drag; + + /* + Other statuses never tick if Drag was handled during update, even + if Drag ended on this tick. + + This is also safe with regards to Character::CanAttack's goal - even + though Drag ended and a queued blocking status has not become active, + CanAttack will return false this frame because of the cached part of + CanAttack. It will also still return false for all relevant parts of + the Entity::Update routine next frame, because a queued blocking status + would become active near start of update, when StatusBehaviorDirector::OnUpdate + next runs. + */ + return; } auto keyTestThunk = [this](const InputEvent& key) { @@ -201,7 +217,6 @@ const bool StatusBehaviorDirector::HasStatus(Hit::Flags flag) const { return ((currentStatuses | queuedStatuses) & flag) == flag; } - const Hit::Flags StatusBehaviorDirector::GetCurrentStatuses() const { return currentStatuses; } From 2634a1354a5e56db142ecc45c290122de83b263c Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sat, 13 Sep 2025 16:58:08 -0700 Subject: [PATCH 083/146] Correct Lua bindings for InputEvents on Shoot, Left/Right Shoulder --- BattleNetwork/bnScriptResourceManager.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/BattleNetwork/bnScriptResourceManager.cpp b/BattleNetwork/bnScriptResourceManager.cpp index c30c24191..771c64c60 100644 --- a/BattleNetwork/bnScriptResourceManager.cpp +++ b/BattleNetwork/bnScriptResourceManager.cpp @@ -557,9 +557,9 @@ void ScriptResourceManager::ConfigureEnvironment(ScriptPackage& scriptPackage) { "Down", InputEvents::held_move_down, "Use", InputEvents::held_use_chip, "Special", InputEvents::held_special, - "Shoot", InputEvents::pressed_shoot, - "Left_Shoulder", InputEvents::pressed_shoulder_left, - "Right_Shoulder", InputEvents::pressed_shoulder_right + "Shoot", InputEvents::held_shoot, + "Left_Shoulder", InputEvents::held_shoulder_left, + "Right_Shoulder", InputEvents::held_shoulder_right ); input_event_record.new_enum("Released", @@ -569,9 +569,9 @@ void ScriptResourceManager::ConfigureEnvironment(ScriptPackage& scriptPackage) { "Down", InputEvents::released_move_down, "Use", InputEvents::released_use_chip, "Special", InputEvents::released_special, - "Shoot", InputEvents::pressed_shoot, - "Left_Shoulder", InputEvents::pressed_shoulder_left, - "Right_Shoulder", InputEvents::pressed_shoulder_right + "Shoot", InputEvents::released_shoot, + "Left_Shoulder", InputEvents::released_shoulder_left, + "Right_Shoulder", InputEvents::released_shoulder_right ); const auto& character_rank_record = state.new_enum("Rank", From f6af9a47ab8dca051cdae570fdd23dc46215a62c Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sun, 14 Sep 2025 17:45:11 -0700 Subject: [PATCH 084/146] Rename MakeActionable and IsActionable to MakeIdle and IsIdle. FreedomMission checks CanAttack instead of IsIdle when flipping "Actionable" didn't describe what these do. They are used to set idle animation and check for idle animation. CanAttack more reliably checks if the Character can act. --- .../States/bnCharacterTransformBattleState.cpp | 2 +- .../battlescene/bnFreedomMissionMobScene.cpp | 2 +- BattleNetwork/bnCharacter.h | 4 ++-- BattleNetwork/bnPlayer.cpp | 17 +++++++---------- BattleNetwork/bnPlayer.h | 4 ++-- BattleNetwork/bnPlayerControlledState.cpp | 13 ++++++------- 6 files changed, 19 insertions(+), 23 deletions(-) diff --git a/BattleNetwork/battlescene/States/bnCharacterTransformBattleState.cpp b/BattleNetwork/battlescene/States/bnCharacterTransformBattleState.cpp index 310413763..551fb0af2 100644 --- a/BattleNetwork/battlescene/States/bnCharacterTransformBattleState.cpp +++ b/BattleNetwork/battlescene/States/bnCharacterTransformBattleState.cpp @@ -60,7 +60,7 @@ void CharacterTransformBattleState::UpdateAnimation(double elapsed) Audio().Play(AudioType::DEFORM); } else { - player->MakeActionable(); + player->MakeIdle(); if (player == GetScene().GetLocalPlayer()) { // only client player should remove their index information (e.g. PVP battles) diff --git a/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp b/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp index 82608d712..74e5790d3 100644 --- a/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp +++ b/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp @@ -209,7 +209,7 @@ void FreedomMissionMobScene::onUpdate(double elapsed) if (cur == combatPtr && playerCanFlip) { std::shared_ptr localPlayer = GetLocalPlayer(); - if (localPlayer->IsActionable() && localPlayer->InputState().Has(InputEvents::pressed_option)) { + if (localPlayer->CanAttack() && localPlayer->InputState().Has(InputEvents::pressed_option)) { localPlayer->SetFacing(localPlayer->GetFacingAway()); } } diff --git a/BattleNetwork/bnCharacter.h b/BattleNetwork/bnCharacter.h index 0559fc7a3..2cb09145a 100644 --- a/BattleNetwork/bnCharacter.h +++ b/BattleNetwork/bnCharacter.h @@ -72,8 +72,8 @@ class Character: virtual void OnBattleStop() override; - virtual void MakeActionable(); - virtual bool IsActionable() const; + virtual void MakeIdle(); + virtual bool IsIdle() const; const bool IsLockoutAnimationComplete(); diff --git a/BattleNetwork/bnPlayer.cpp b/BattleNetwork/bnPlayer.cpp index dab4d5b8e..0538b09bb 100644 --- a/BattleNetwork/bnPlayer.cpp +++ b/BattleNetwork/bnPlayer.cpp @@ -43,8 +43,8 @@ Player::Player() : ClearActionQueue(); Charge(false); - // At the end of flinch we need to be made actionable if possible - SetAnimation(recoilAnimHash, [this] { MakeActionable();}); + // At the end of flinch we need to be made idle if possible + SetAnimation(recoilAnimHash, [this] { MakeIdle();}); Audio().Play(AudioType::HURT, AudioPriority::lowest); }; @@ -57,9 +57,9 @@ Player::Player() : // When we have no upcoming actions we should be in IDLE state actionQueue.SetIdleCallback([this] { - if (!IsActionable()) { + if (!IsIdle()) { auto finish = [this] { - MakeActionable(); + MakeIdle(); }; animationComponent->OnFinish(finish); @@ -130,20 +130,17 @@ void Player::OnUpdate(double _elapsed) { fullyCharged = chargeEffect->IsFullyCharged(); } -void Player::MakeActionable() +void Player::MakeIdle() { animationComponent->CancelCallbacks(); - if (!IsActionable()) { + if (!IsIdle()) { animationComponent->SetAnimation("PLAYER_IDLE"); animationComponent->SetPlaybackMode(Animator::Mode::Loop); } - - // TODO: Ensure this is safe to set false here, and if Character::MakeActionable should do the same - actionBlocked = false; } -bool Player::IsActionable() const +bool Player::IsIdle() const { return animationComponent->GetAnimationString() == "PLAYER_IDLE"; } diff --git a/BattleNetwork/bnPlayer.h b/BattleNetwork/bnPlayer.h index 830ba3a08..269d024e5 100644 --- a/BattleNetwork/bnPlayer.h +++ b/BattleNetwork/bnPlayer.h @@ -85,8 +85,8 @@ class Player : public Character, public AI { */ virtual void OnUpdate(double _elapsed); - void MakeActionable() override final; - bool IsActionable() const override final; + void MakeIdle() override final; + bool IsIdle() const override final; /** * @brief Fires a buster diff --git a/BattleNetwork/bnPlayerControlledState.cpp b/BattleNetwork/bnPlayerControlledState.cpp index c910ff039..88066cf84 100644 --- a/BattleNetwork/bnPlayerControlledState.cpp +++ b/BattleNetwork/bnPlayerControlledState.cpp @@ -21,14 +21,13 @@ PlayerControlledState::~PlayerControlledState() } void PlayerControlledState::OnEnter(Player& player) { - player.MakeActionable(); + player.MakeIdle(); } void PlayerControlledState::OnUpdate(double _elapsed, Player& player) { // Actions with animation lockout controls take priority over movement const bool lockout = player.IsLockoutAnimationComplete(); - // Player is idle - const bool actionable = player.IsActionable(); + const bool isIdle = player.IsIdle(); const bool canAttack = player.CanAttack(); const bool isMoving = player.IsMoving(); const bool isDragged = player.IsStatusApplied(Hit::drag); @@ -97,10 +96,10 @@ void PlayerControlledState::OnUpdate(double _elapsed, Player& player) { } else if (player.InputState().Has(InputEvents::held_shoot)) { /* During Drag, Player may not be moving, but could also not be idle. - This means actionable || IsMoving is false, yet they can charge. + This means isIdle || IsMoving is false, yet they can charge. To cover for this case, check for Drag. */ - if (actionable || isMoving || isDragged) { + if (isIdle || isMoving || isDragged) { isChargeHeld = true; player.chargeEffect->SetCharging(true); } @@ -123,7 +122,7 @@ void PlayerControlledState::OnUpdate(double _elapsed, Player& player) { direction = player.GetTeam() == Team::red ? Direction::right : Direction::left; } - if(direction != Direction::none && actionable && !player.IsRooted()) { + if(direction != Direction::none && isIdle && !player.IsRooted()) { Battle::Tile* next_tile = player.GetTile() + direction; std::shared_ptr anim = player.GetFirstComponent(); @@ -133,7 +132,7 @@ void PlayerControlledState::OnUpdate(double _elapsed, Player& player) { anim->CancelCallbacks(); auto idle_callback = [player]() { - player->MakeActionable(); + player->MakeIdle(); }; anim->SetAnimation(move_anim, idle_callback); From 3143c1829dd123a321a0c8c53a3dd46389ec63bd Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sun, 14 Sep 2025 17:46:13 -0700 Subject: [PATCH 085/146] Use new name for MakeActionable and IsActionable in bnCharacter.cpp --- BattleNetwork/bnCharacter.cpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/BattleNetwork/bnCharacter.cpp b/BattleNetwork/bnCharacter.cpp index 94bfb8801..5c664bfc0 100644 --- a/BattleNetwork/bnCharacter.cpp +++ b/BattleNetwork/bnCharacter.cpp @@ -95,7 +95,7 @@ void Character::Update(double _elapsed) { if (currCardAction) { // if we have yet to invoke this attack... - if (currCardAction->CanExecute() && IsActionable()) { + if (currCardAction->CanExecute() && IsIdle()) { // reduce the artificial delay cardActionStartDelay -= from_seconds(_elapsed); @@ -105,7 +105,7 @@ void Character::Update(double _elapsed) { for(std::shared_ptr& anim : this->GetComponents()) { anim->CancelCallbacks(); } - MakeActionable(); + MakeIdle(); std::shared_ptr characterPtr = shared_from_base(); currCardAction->Execute(characterPtr); } @@ -155,19 +155,19 @@ bool Character::CanMoveTo(Battle::Tile * next) const bool Character::CanAttack() const { - return !actionBlocked && CanAttackImpl();//&& IsActionable(); + return !actionBlocked && CanAttackImpl(); } const bool Character::CanAttackImpl() const { return !currCardAction && !(statuses.GetCurrentStatuses() & blockingStatuses); } -void Character::MakeActionable() +void Character::MakeIdle() { // impl. defined } -bool Character::IsActionable() const +bool Character::IsIdle() const { return true; // impl. defined } @@ -194,6 +194,7 @@ void Character::AddAction(const PeekCardEvent& event, const ActionOrder& order) void Character::HandleCardEvent(const CardEvent& event, const ActionQueue::ExecutionType& exec) { + if (currCardAction == nullptr) { if (event.action->GetMetaData().GetProps().timeFreeze) { CardActionUsePublisher::Broadcast(event.action, CurrentTime::AsMilli()); @@ -221,8 +222,8 @@ void Character::HandlePeekEvent(const PeekCardEvent& event, const ActionQueue::E // If we have a card via Peeking, then Play it if (publisher->HandlePlayEvent(characterPtr)) { - // prepare for this frame's action animation (we must be actionable) - MakeActionable(); + // prepare for this frame's action animation (we must be idle) + MakeIdle(); } } From 8d9eff04868a640d9de2a539862ccd2eab0ce9d3 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sun, 14 Sep 2025 19:32:35 -0700 Subject: [PATCH 086/146] Fix infinite ice slide and Drag move queue An Entity would continue attempting to slide when the same Tile was moved to, and would follow in the previous move direction of none. Also, because the ActionQueue clear call, in response to a Drag hit, was moved, Drag could queue behind an ice slide. Drag takes priority over this now by clearing an ice slide queued on the previous frame. --- BattleNetwork/bnEntity.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index 54535467b..56ba974af 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -196,8 +196,18 @@ void Entity::UpdateMovement(double elapsed) previousDirection = direction; Battle::Tile* currTile = GetTile(); + /* + Do not check ice slide if the same Tile was moved to. + This prevents a case where sliding to your own Tile would + infinitely slide in place. + + This same check is not used on Sand or Sea, so an Entity would + become rooted when moving to their own Tile in those cases. + */ + const bool sameTile = prevTile == currTile; + // If we slide onto an ice block and we don't have float shoe enabled, slide - if (tile->GetState() == TileState::ice && !HasFloatShoe()) { + if (!sameTile && tile->GetState() == TileState::ice && !HasFloatShoe()) { // calculate our new entity's position UpdateMoveStartPosition(); @@ -385,7 +395,7 @@ void Entity::Update(double _elapsed) { // Some statuses clear the action queue. // TODO: Neither FinishMove nor clearing the queue ends the animation initiated - // by PlayerControlled state. This might be smoothly handled if the move + // by PlayerControlledState. This might be smoothly handled if the move // animation was actually a CardAction. Otherwise, make sure it's safe to enter // idle right here and do that instead. if ((newStatuses & (Hit::freeze | Hit::stun)) != 0) { @@ -1504,6 +1514,7 @@ void Entity::ResolveFrameBattleDamage() if (dragWasReplaced) { FinishMove(); statuses.AddStatus(Hit::drag, frames(22)); + actionQueue.ClearQueue(ActionQueue::CleanupType::allow_interrupts); } // TODO: Drag during wind push will not overwrite? What about other movement, like ice, conveyor? From 703c6dfaf044197e8dc9443df2204d231a63026b Mon Sep 17 00:00:00 2001 From: Alrysc Date: Mon, 15 Sep 2025 16:40:09 -0700 Subject: [PATCH 087/146] Add CanAttack check to HandleCardEvent. --- BattleNetwork/bnCharacter.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/BattleNetwork/bnCharacter.cpp b/BattleNetwork/bnCharacter.cpp index 5c664bfc0..6e9798eb2 100644 --- a/BattleNetwork/bnCharacter.cpp +++ b/BattleNetwork/bnCharacter.cpp @@ -200,7 +200,19 @@ void Character::HandleCardEvent(const CardEvent& event, const ActionQueue::Execu CardActionUsePublisher::Broadcast(event.action, CurrentTime::AsMilli()); actionQueue.Pop(); } - else { + /* + Do not allow card to be used if Character cannot act. + + Scripters are allowed to add actions while they cannot properly execute. + By doing check, they are kept in the queue until it is safe to stage the + acton for use. This especially prevents situations where a CardAction's + animation starts while the actor is, for example, stunned. + + A different way to do this would be to clear the queue each frame while + CanAttack returns false, but this would make it difficult to allow + certain CardActions that should be queued while unable to act. + */ + else if (CanAttack()){ cardActionStartDelay = frames(5); currCardAction = event.action; } From e48ed679a8b0612200d128a53a86c3b146437c05 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Mon, 15 Sep 2025 23:39:20 -0700 Subject: [PATCH 088/146] Change StatusBehaviorDirector::ClearStatus(Hit::Flags flags) to clear multiple flags if given --- BattleNetwork/bnStatusDirector.cpp | 22 +++++++++++++++++----- BattleNetwork/bnStatusDirector.h | 7 ++++++- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/BattleNetwork/bnStatusDirector.cpp b/BattleNetwork/bnStatusDirector.cpp index 2f4e75d5a..35cf4a59c 100644 --- a/BattleNetwork/bnStatusDirector.cpp +++ b/BattleNetwork/bnStatusDirector.cpp @@ -198,11 +198,23 @@ void StatusBehaviorDirector::ClearStatus() { currentStatuses = Hit::none; }; -void StatusBehaviorDirector::ClearStatus(Hit::Flags flag) { - AppliedStatus& status = statusMap[flag]; - status.remainingTime = frames(0); - queuedStatuses &= ~flag; - currentStatuses &= ~flag; +void StatusBehaviorDirector::ClearStatus(Hit::Flags flags) { + + // Start from lowest bit + Hit::Flags curFlag = flags & -flags; + while (flags > 0) { + if (flags & curFlag) { + AppliedStatus& status = statusMap[curFlag]; + status.remainingTime = frames(0); + queuedStatuses &= ~curFlag; + currentStatuses &= ~curFlag; + + flags &= ~curFlag; + } + + curFlag = curFlag << 1; + } + } const Hit::Flags StatusBehaviorDirector::GetQueuedStatuses() const { diff --git a/BattleNetwork/bnStatusDirector.h b/BattleNetwork/bnStatusDirector.h index 279481bc9..25c7eada5 100644 --- a/BattleNetwork/bnStatusDirector.h +++ b/BattleNetwork/bnStatusDirector.h @@ -38,7 +38,12 @@ class StatusBehaviorDirector { const Hit::Flags GetQueuedStatuses() const; const Hit::Flags GetCurrentStatuses() const; void ClearStatus(); - void ClearStatus(Hit::Flags flag); + /* + Clear specific flags from queued and active statuses. + Parameter flags may contain multiple Hit::Flags bits set. Each corresponding + status will be cleared. + */ + void ClearStatus(Hit::Flags flags); /* Process current queuedStatuses. The result of GetQueuedStatuses and GetCurrentStatuses may be different before and after calling this. From 6d9793256f3c470816182b3f3cd272f51b5f70e1 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Mon, 15 Sep 2025 23:46:26 -0700 Subject: [PATCH 089/146] Remove some explicit != 0 checks in expressions involving Hit::Flags --- BattleNetwork/bnEntity.cpp | 8 ++++---- BattleNetwork/bnStatusDirector.cpp | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index 56ba974af..ae098d4ca 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -398,7 +398,7 @@ void Entity::Update(double _elapsed) { // by PlayerControlledState. This might be smoothly handled if the move // animation was actually a CardAction. Otherwise, make sure it's safe to enter // idle right here and do that instead. - if ((newStatuses & (Hit::freeze | Hit::stun)) != 0) { + if (newStatuses & (Hit::freeze | Hit::stun)) { FinishMove(); actionQueue.ClearQueue(ActionQueue::CleanupType::allow_interrupts); } @@ -427,7 +427,7 @@ void Entity::Update(double _elapsed) { /// Run all status callbacks, starting from lowest set bit Hit::Flags checkIdx = statusCheck & -statusCheck; while (statusCheck > 0) { - if ((statusCheck & checkIdx) != 0) { + if (statusCheck & checkIdx) { flagCheckThunk(checkIdx); } @@ -1493,9 +1493,9 @@ void Entity::ResolveFrameBattleDamage() // Add the rest, starting from lowest set bit Hit::Flags curFlag = props.filtered.flags & -props.filtered.flags; while (props.filtered.flags > 0) { - if ((props.filtered.flags & curFlag) != 0) { + if (props.filtered.flags & curFlag) { statuses.AddStatus(curFlag); - props.filtered.flags = props.filtered.flags & ~curFlag; + props.filtered.flags &= ~curFlag; } curFlag = curFlag << 1; diff --git a/BattleNetwork/bnStatusDirector.cpp b/BattleNetwork/bnStatusDirector.cpp index 35cf4a59c..b53ee1991 100644 --- a/BattleNetwork/bnStatusDirector.cpp +++ b/BattleNetwork/bnStatusDirector.cpp @@ -170,7 +170,7 @@ void StatusBehaviorDirector::OnUpdate(double elapsed) { AppliedStatus& status = GetStatus(statusBit); - if (anyKey && (statusBit & (Hit::stun | Hit::freeze | Hit::bubble)) != 0) { + if (anyKey && (statusBit & (Hit::stun | Hit::freeze | Hit::bubble))) { status.remainingTime -= _elapsed; } From a5efe49d2a6b7b029ea4a70e2e135c795496c977 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Mon, 15 Sep 2025 23:50:37 -0700 Subject: [PATCH 090/146] Remove unused timestop check from ProcessFlags --- BattleNetwork/bnStatusDirector.cpp | 7 ------- 1 file changed, 7 deletions(-) diff --git a/BattleNetwork/bnStatusDirector.cpp b/BattleNetwork/bnStatusDirector.cpp index b53ee1991..84c13eb3d 100644 --- a/BattleNetwork/bnStatusDirector.cpp +++ b/BattleNetwork/bnStatusDirector.cpp @@ -70,13 +70,6 @@ void StatusBehaviorDirector::ProcessPendingStatuses() { } void StatusBehaviorDirector::ProcessFlags(Hit::Flags attack) { - // Timestop check. - // TODO: Behavior does not currently happen here, but instead in Entity::Hit. - // Move that here, or remove this check. - if (false && (currentStatuses & Hit::freeze) == Hit::freeze) { - attack &= ~Hit::freeze; - } - // Retangible removes active flash, but not queued. if ((attack & Hit::retangible) == Hit::retangible) { currentStatuses &= ~Hit::flash; From 45069103a35fda0bef84590626b1981cedba856c Mon Sep 17 00:00:00 2001 From: Alrysc Date: Tue, 16 Sep 2025 10:58:01 -0700 Subject: [PATCH 091/146] Set aggressor on Buster Spell --- BattleNetwork/bnBuster.cpp | 4 ++-- BattleNetwork/bnBuster.h | 2 +- BattleNetwork/bnBusterCardAction.cpp | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/BattleNetwork/bnBuster.cpp b/BattleNetwork/bnBuster.cpp index 9bd029cf3..29d7442e9 100644 --- a/BattleNetwork/bnBuster.cpp +++ b/BattleNetwork/bnBuster.cpp @@ -10,8 +10,7 @@ #include "bnAudioResourceManager.h" #include "bnRandom.h" -// TODO: Buster props should have an aggressor set. -Buster::Buster(Team _team, bool _charged, int damage) : isCharged(_charged), Spell(_team) { +Buster::Buster(Team _team, bool _charged, int damage, EntityID_t aggressorId) : isCharged(_charged), Spell(_team) { SetPassthrough(true); SetLayer(-100); @@ -34,6 +33,7 @@ Buster::Buster(Team _team, bool _charged, int damage) : isCharged(_charged), Spe auto props = Hit::DefaultProperties; props.flags = (props.flags | Hit::no_counter) & ~(Hit::flinch | Hit::flash); + props.aggressor = aggressorId; props.damage = damage; SetHitboxProperties(props); diff --git a/BattleNetwork/bnBuster.h b/BattleNetwork/bnBuster.h index 0370895a6..ad7d66232 100644 --- a/BattleNetwork/bnBuster.h +++ b/BattleNetwork/bnBuster.h @@ -15,7 +15,7 @@ class Buster : public Spell { /** * @brief If _charged is true, deals 10 damage */ - Buster(Team _team,bool _charged, int damage); + Buster(Team _team,bool _charged, int damage, EntityID_t aggressorId); ~Buster() override; void Init() override; diff --git a/BattleNetwork/bnBusterCardAction.cpp b/BattleNetwork/bnBusterCardAction.cpp index 16bbef92d..41422e702 100644 --- a/BattleNetwork/bnBusterCardAction.cpp +++ b/BattleNetwork/bnBusterCardAction.cpp @@ -40,7 +40,7 @@ void BusterCardAction::OnExecute(std::shared_ptr user) { // On shoot frame, drop projectile auto onFire = [this, user]() -> void { Team team = user->GetTeam(); - std::shared_ptr b = std::make_shared(team, charged, damage); + std::shared_ptr b = std::make_shared(team, charged, damage, user->GetID()); std::shared_ptr field = user->GetField(); b->SetMoveDirection(user->GetFacing()); From bd315f0a7313957a26cc8f9f81509126d8222de7 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Tue, 16 Sep 2025 21:56:24 -0700 Subject: [PATCH 092/146] Added Entity::HandleNewStatuses, moved some logic from Update. Player overrides to do flinch and charge cancel, flinch handling is no longer a status callback --- BattleNetwork/bnCharacter.cpp | 7 ++- BattleNetwork/bnCharacter.h | 5 ++ BattleNetwork/bnEntity.cpp | 87 ++++++++++++++++++----------------- BattleNetwork/bnEntity.h | 18 ++++++++ BattleNetwork/bnPlayer.cpp | 39 +++++++++++----- BattleNetwork/bnPlayer.h | 2 +- 6 files changed, 104 insertions(+), 54 deletions(-) diff --git a/BattleNetwork/bnCharacter.cpp b/BattleNetwork/bnCharacter.cpp index 6e9798eb2..782836592 100644 --- a/BattleNetwork/bnCharacter.cpp +++ b/BattleNetwork/bnCharacter.cpp @@ -18,7 +18,7 @@ #include "bnCardToActions.h" // All statuses which should prevent Character from taking actions -constexpr Hit::Flags blockingStatuses = Hit::stun | Hit::freeze | Hit::bubble | Hit::drag; +constexpr const Hit::Flags blockingStatuses = Hit::stun | Hit::freeze | Hit::bubble | Hit::drag; Character::Character(Rank _rank) : rank(_rank), @@ -241,3 +241,8 @@ void Character::HandlePeekEvent(const PeekCardEvent& event, const ActionQueue::E actionQueue.Pop(); } + +const Hit::Flags Character::GetBlockingStatuses() const { + return blockingStatuses; +} + diff --git a/BattleNetwork/bnCharacter.h b/BattleNetwork/bnCharacter.h index 2cb09145a..ad49bab4b 100644 --- a/BattleNetwork/bnCharacter.h +++ b/BattleNetwork/bnCharacter.h @@ -111,6 +111,11 @@ class Character: void AddAction(const PeekCardEvent& event, const ActionOrder& order); void HandleCardEvent(const CardEvent& event, const ActionQueue::ExecutionType& exec); void HandlePeekEvent(const PeekCardEvent& event, const ActionQueue::ExecutionType& exec); + /** + * @brief Returns Hit::Flags containing flags which should block actions. + * @return const Hit::Flags representing blocking statuses + */ + const Hit::Flags GetBlockingStatuses() const; protected: Character::Rank rank; diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index ae098d4ca..58659b61a 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -356,66 +356,33 @@ void Entity::Init() { hasInit = true; } -void Entity::Update(double _elapsed) { - ResolveFrameBattleDamage(); - - if (fieldStart && ((maxHealth > 0 && health <= 0) || IsDeleted())) { - // Ensure entity is deleted if health is zero - if (manualDelete == false) { - Delete(); - } - - // Ensure health is zero if marked for immediate deletion - health = 0; - - // Ensure status effects do not play out - statuses.ClearStatus(); - } - - // reset base color - setColor(NoopCompositeColor(GetColorMode())); - - statusShaderTimer++; - - // Used to determine if Sprite should be revealed this frame, - // when flashing was active and became inactive this frame. - bool wasFlashing = statuses.IsApplied(Hit::flash); - - Hit::Flags queuedStatuses = statuses.GetQueuedStatuses(); - - statuses.ProcessPendingStatuses(); - - if (currentDrag.dir == Direction::none) { - // Tick all statuses at once - statuses.OnUpdate(_elapsed); - } - - Hit::Flags currentStatuses = statuses.GetCurrentStatuses(); - Hit::Flags newStatuses = queuedStatuses & currentStatuses; +void Entity::HandleNewStatuses(const Hit::Flags prevStatuses, const Hit::Flags appliedStatuses) { // Some statuses clear the action queue. // TODO: Neither FinishMove nor clearing the queue ends the animation initiated // by PlayerControlledState. This might be smoothly handled if the move // animation was actually a CardAction. Otherwise, make sure it's safe to enter // idle right here and do that instead. - if (newStatuses & (Hit::freeze | Hit::stun)) { + if (appliedStatuses & (Hit::freeze | Hit::stun)) { FinishMove(); actionQueue.ClearQueue(ActionQueue::CleanupType::allow_interrupts); } - if ((newStatuses & Hit::freeze) == Hit::freeze) { + if ((appliedStatuses & Hit::freeze) == Hit::freeze) { IceFreeze(); } - if ((newStatuses & Hit::blind) == Hit::blind) { + if ((appliedStatuses & Hit::blind) == Hit::blind) { Blind(); } - if ((newStatuses & Hit::retangible) == Hit::retangible) { + if ((appliedStatuses & Hit::retangible) == Hit::retangible) { SetPassthrough(false); } + // Now that all other behavior is done, run status callbacks + // a re-usable thunk for custom status effects auto flagCheckThunk = [this](const Hit::Flags& toCheck) { if (Entity::StatusCallback& func = statusCallbackHash[toCheck]) { @@ -423,7 +390,7 @@ void Entity::Update(double _elapsed) { } }; - Hit::Flags statusCheck = newStatuses; + Hit::Flags statusCheck = appliedStatuses; /// Run all status callbacks, starting from lowest set bit Hit::Flags checkIdx = statusCheck & -statusCheck; while (statusCheck > 0) { @@ -434,6 +401,44 @@ void Entity::Update(double _elapsed) { statusCheck = statusCheck & ~checkIdx; checkIdx = checkIdx << 1; } +} + +void Entity::Update(double _elapsed) { + ResolveFrameBattleDamage(); + + if (fieldStart && ((maxHealth > 0 && health <= 0) || IsDeleted())) { + // Ensure entity is deleted if health is zero + if (manualDelete == false) { + Delete(); + } + + // Ensure health is zero if marked for immediate deletion + health = 0; + + // Ensure status effects do not play out + statuses.ClearStatus(); + } + + // reset base color + setColor(NoopCompositeColor(GetColorMode())); + + statusShaderTimer++; + + // Used to determine if Sprite should be revealed this frame, + // when flashing was active and became inactive this frame. + bool wasFlashing = statuses.IsApplied(Hit::flash); + + Hit::Flags prevStatuses = statuses.GetCurrentStatuses(); + Hit::Flags queuedStatuses = statuses.GetQueuedStatuses(); + + statuses.ProcessPendingStatuses(); + + if (currentDrag.dir == Direction::none) { + // Tick all statuses at once + statuses.OnUpdate(_elapsed); + } + + HandleNewStatuses(prevStatuses, queuedStatuses & statuses.GetCurrentStatuses()); RefreshShader(); diff --git a/BattleNetwork/bnEntity.h b/BattleNetwork/bnEntity.h index 21948ddfc..e71f0d503 100644 --- a/BattleNetwork/bnEntity.h +++ b/BattleNetwork/bnEntity.h @@ -553,6 +553,24 @@ class Entity : void ResolveFrameBattleDamage(); + + /** + @brief Runs reactions to statuses that were resolved this frame. This + involves reactions specific to the Entity, such as Hit::freeze playing + a sound effect, as well as running all appropriate status callbacks. + + This does not include behavior related to ongoing statuses, such as + animating blindFx. + + @param prevStatuses, active statuses before new statuses were resolved + @param appliedStatuses, statuses that made it through the queue. This + includes statuses which are now active, or statuses that would have become + active if they were not already active (e.g. if queued and active statuses + included Hit::stun, and Hit::stun passed all filtering, its bit would be + set) + */ + virtual void HandleNewStatuses(const Hit::Flags prevStatuses, const Hit::Flags appliedStatuses); + /** * @brief Get the character's current health * @return diff --git a/BattleNetwork/bnPlayer.cpp b/BattleNetwork/bnPlayer.cpp index 0538b09bb..f1b3d241f 100644 --- a/BattleNetwork/bnPlayer.cpp +++ b/BattleNetwork/bnPlayer.cpp @@ -39,17 +39,6 @@ Player::Player() : activeForm = nullptr; superArmor = std::make_shared(); - auto flinch = [this]() { - ClearActionQueue(); - Charge(false); - - // At the end of flinch we need to be made idle if possible - SetAnimation(recoilAnimHash, [this] { MakeIdle();}); - Audio().Play(AudioType::HURT, AudioPriority::lowest); - }; - - RegisterStatusCallback(Hit::flinch, Callback{ flinch }); - using namespace std::placeholders; auto handler = std::bind(&Player::HandleBusterEvent, this, _1, _2); @@ -99,6 +88,34 @@ void Player::RemoveSyncNode(std::shared_ptr syncNode) { syncNodeContainer.RemoveSyncNode(*this, *animationComponent, syncNode); } + +void Player::HandleNewStatuses(const Hit::Flags prevStatuses, Hit::Flags appliedStatuses) { + // Tracks whether or not charge has already been cancelled, to avoid repeats + bool chargeCancel = false; + + /* + Clear Charge on flinch or any blocking status. + + Action queue should be cleared on blocking status as well, + but Entity::HandleNewStatuses already handles this. + */ + if (appliedStatuses & (Hit::flinch | GetBlockingStatuses())) { + Charge(false); + chargeCancel = true; + } + + if (appliedStatuses & Hit::flinch) { + ClearActionQueue(); + Charge(false); + + // At the end of flinch we need to be made idle if possible + SetAnimation(recoilAnimHash, [this] { MakeIdle(); }); + Audio().Play(AudioType::HURT, AudioPriority::lowest); + } + + Character::HandleNewStatuses(prevStatuses, appliedStatuses); +} + void Player::OnUpdate(double _elapsed) { SetColorMode(ColorMode::additive); diff --git a/BattleNetwork/bnPlayer.h b/BattleNetwork/bnPlayer.h index 269d024e5..133065bba 100644 --- a/BattleNetwork/bnPlayer.h +++ b/BattleNetwork/bnPlayer.h @@ -163,7 +163,7 @@ class Player : public Character, public AI { std::shared_ptr AddSyncNode(const std::string& point); void RemoveSyncNode(std::shared_ptr syncNode); - + virtual void HandleNewStatuses(const Hit::Flags prevStatuses, Hit::Flags appliedStatuses) override; protected: // functions void FinishConstructor(); From 0911337bd27678c469937720f1e00e2b8ded6ed7 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Fri, 19 Sep 2025 20:35:47 -0700 Subject: [PATCH 093/146] Reset statuses and actionability when Player transforms --- .../bnCharacterTransformBattleState.cpp | 40 ++++++++++++++++--- BattleNetwork/bnEntity.cpp | 20 ++++++++-- BattleNetwork/bnEntity.h | 25 ++++++++++++ BattleNetwork/bnPlayer.cpp | 23 +++++++++++ 4 files changed, 100 insertions(+), 8 deletions(-) diff --git a/BattleNetwork/battlescene/States/bnCharacterTransformBattleState.cpp b/BattleNetwork/battlescene/States/bnCharacterTransformBattleState.cpp index 551fb0af2..66cee5263 100644 --- a/BattleNetwork/battlescene/States/bnCharacterTransformBattleState.cpp +++ b/BattleNetwork/battlescene/States/bnCharacterTransformBattleState.cpp @@ -22,6 +22,33 @@ const bool CharacterTransformBattleState::FadeOutBackdrop() return GetScene().FadeOutBackdrop(backdropInc); } +/* + When changing form the following needs to be done: + * Finish movement and end current Drag (both done by Entity::EndDrag) + - TODO: Drag may not end sometimes on deform. Determine how this works. + * Clear ActionQueue (done in Player::ActivateFormAt) + * (If activating form) Clear blocking statuses (done in Player::ActivateFormAt) + - This must be done after a call to ResolveFrameBattleDamage, also done in + Player::ActivateFormAt + * TODO: Hide freezeFx + - This is not important for functionality + * Wait for whiteout + * Make idle (done in Player::ActivateFormAt) + + Player::ActivateFormAt is called when whiteout begins. + + The whiteout is reached much sooner when activating a form. + However, because ONB currently has no actual transform animation beyond + the shine graphic, there is not much difference. In the future, this + function may need to include more animating. + + After transformations finish, supposing there are no blocking statuses + still active, the Player should be completely actionable once the + combat state begins again, no matter their previous state. This is in + contrast to the ordinary behavior of guaranteeing one frame between + being inactionable and being actionable again. See Character::CanAttack. + This special exception is handled in Player::ActivateFormAt. +*/ void CharacterTransformBattleState::UpdateAnimation(double elapsed) { bool allCompleted = true; @@ -45,9 +72,14 @@ void CharacterTransformBattleState::UpdateAnimation(double elapsed) auto onTransform = [=] () { - // The next form has a switch based on health - // This way dying will cancel the form - player->ClearActionQueue(); + player->EndDrag(); + /* + Reset draw position after ending movement. This does not happen + during FinishMove (called by EndDrag) if IsSliding is true, so it's + done here to ensure it happens. + */ + player->setPosition(player->GetTile()->getPosition() + player->GetDrawOffset()); + player->ActivateFormAt(_index); player->SetColorMode(ColorMode::additive); player->setColor(NoopCompositeColor(ColorMode::additive)); @@ -60,8 +92,6 @@ void CharacterTransformBattleState::UpdateAnimation(double elapsed) Audio().Play(AudioType::DEFORM); } else { - player->MakeIdle(); - if (player == GetScene().GetLocalPlayer()) { // only client player should remove their index information (e.g. PVP battles) auto& widget = GetScene().GetCardSelectWidget(); diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index 58659b61a..ac93783d7 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -790,6 +790,14 @@ void Entity::FinishMove() } } +void Entity::EndDrag() { + statuses.ClearStatus(Hit::drag); + currentDrag.count = 0; + currentDrag.dir = Direction::none; + slideFromDrag = false; + FinishMove(); +} + bool Entity::RawMoveEvent(const MoveEvent& event, ActionOrder order) { if (event.dest && CanMoveTo(event.dest)) { @@ -1326,7 +1334,7 @@ const bool Entity::Hit(Hit::Properties props) { // TODO: Likely related to this, breaking clears frozen even when // damage is blocked by defenses. Find out if this can be done, and also how // defenses that trigger actions interact with this, and compare to stun. - statuses.ClearStatus(Hit::freeze); + ClearStatuses(Hit::freeze); iceFx->Hide(); // Remove flinch from breaking attack if it did not have flinch | flash. @@ -1685,6 +1693,13 @@ bool Entity::IsStatusApplied(Hit::Flags status) { return statuses.IsApplied(status); } +void Entity::ClearStatuses(Hit::Flags flags) { + if ((flags & (statuses.GetQueuedStatuses() | statuses.GetCurrentStatuses())) & Hit::drag) { + EndDrag(); + } + + statuses.ClearStatus(flags); +} void Entity::IceFreeze() { @@ -1695,8 +1710,7 @@ void Entity::IceFreeze() // Removing stun here may be redundant with how the StatusBehaviorDirector filters // freeze and stun. SetPassthrough(false); - statuses.ClearStatus(Hit::flash); - statuses.ClearStatus(Hit::stun); + ClearStatuses(Hit::flash | Hit::stun); Audio().Play(freezesfx, AudioPriority::highest); if (height <= 48) { diff --git a/BattleNetwork/bnEntity.h b/BattleNetwork/bnEntity.h index e71f0d503..faef834e6 100644 --- a/BattleNetwork/bnEntity.h +++ b/BattleNetwork/bnEntity.h @@ -197,6 +197,18 @@ class Entity : bool Slide(Battle::Tile* dest, const frame_time_t& slideTime, const frame_time_t& endlag, ActionOrder order = ActionOrder::voluntary, std::function onBegin = [] {}); bool Jump(Battle::Tile* dest, float destHeight, const frame_time_t& jumpTime, const frame_time_t& endlag, ActionOrder order = ActionOrder::voluntary, std::function onBegin = [] {}); void FinishMove(); + /** + * @brief Resets currentDrag, sets slideFromDrag false, clears Drag status, + * and calls FinishMove. + * + * Used by the CharacterTransformBattleState, which must do these things + * before activating the new transformation. + * + * Note: If Drag was cleared on the same frame that a Drag movement was queued + * and before the ActionQueue has processed, the movement from Drag may still + * occur. + */ + void EndDrag(); bool RawMoveEvent(const MoveEvent& event, ActionOrder order = ActionOrder::voluntary); void HandleMoveEvent(MoveEvent& event, const ActionQueue::ExecutionType& exec); void ClearActionQueue(); @@ -650,6 +662,19 @@ class Entity : */ bool IsStatusApplied(Hit::Flags status); + /** + * @brief Clear all statuses in parameter flags, whether queued or applied. + * If Hit::drag is removed as a result of this, calls EndDrag. Because of + * this, prefer calling this function when removing statuses instead of + * directly accessing the underlying StatusBehaviorDirector. + * + * Note: If Drag was cleared on the same frame that a Drag movement was queued + * and before the ActionQueue has processed, the movement from Drag may still + * occur. + * @param flags to clear + */ + void ClearStatuses(Hit::Flags flags); + /** * @brief Some characters allow others to move on top of them * @param enabled true, characters can share space, false otherwise diff --git a/BattleNetwork/bnPlayer.cpp b/BattleNetwork/bnPlayer.cpp index f1b3d241f..1147dac2d 100644 --- a/BattleNetwork/bnPlayer.cpp +++ b/BattleNetwork/bnPlayer.cpp @@ -430,6 +430,20 @@ void Player::ActivateFormAt(int index) activeForm = meta->BuildForm(); if (activeForm) { + /* + Consume pending statuses from attacks to queue them, so they can be + removed. + + This may have also added a MoveEvent for Drag. It's reasonable to + ignore this and allow it to be cleared without processing. If it + was processed, it would bepossible to snap two Tiles at once during + transformation: One if the Player was moving by input, and again if + a Drag was resolved. + */ + ResolveFrameBattleDamage(); + // Additionally clear Flinch, so Player never flinches afterwards + ClearStatuses(GetBlockingStatuses() | Hit::flinch); + SaveStats(); activeForm->OnActivate(shared_from_base()); CreateMoveAnimHash(); @@ -438,10 +452,19 @@ void Player::ActivateFormAt(int index) } } + ClearActionQueue(); + MakeIdle(); + // Cancel charging. This will also refresh charge times // for the new form. Charge(false); + /* + If current state allows, Player can act immediately on the first + combat frame after the transform state finishes. + */ + actionBlocked = CanAttackImpl(); + // Find nodes that do not have tags, those are newly added for (std::shared_ptr& node : GetChildNodes()) { // if untagged and not the charge effect... From e33c5544ffb3a1d99da058f04622f0f4549c3d4d Mon Sep 17 00:00:00 2001 From: Alrysc Date: Fri, 19 Sep 2025 20:53:46 -0700 Subject: [PATCH 094/146] Prevent flinch when stunned --- BattleNetwork/bnStatusDirector.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/BattleNetwork/bnStatusDirector.cpp b/BattleNetwork/bnStatusDirector.cpp index 84c13eb3d..229ed3fa3 100644 --- a/BattleNetwork/bnStatusDirector.cpp +++ b/BattleNetwork/bnStatusDirector.cpp @@ -70,13 +70,15 @@ void StatusBehaviorDirector::ProcessPendingStatuses() { } void StatusBehaviorDirector::ProcessFlags(Hit::Flags attack) { + const Hit::Flags flinch_flash = Hit::flinch | Hit::flash; + // Retangible removes active flash, but not queued. if ((attack & Hit::retangible) == Hit::retangible) { currentStatuses &= ~Hit::flash; } // Flinch|Flash cancels existing Freeze|Stun - if ((attack & (Hit::flinch | Hit::flash)) == (Hit::flinch | Hit::flash)) { + if ((attack & flinch_flash) == flinch_flash) { // If stun is already active, flinch | flash will prevent it from // being added by this attack. That also means a freeze could be // committed. @@ -86,6 +88,14 @@ void StatusBehaviorDirector::ProcessFlags(Hit::Flags attack) { currentStatuses &= ~(Hit::freeze | Hit::stun); } + // If attack did not have Flinch|Flash, some statuses are interested in + // checking one of these flags. + else { + // If stunned is active, prevent flinch + if (((currentStatuses & Hit::stun) == Hit::stun) && (attack & Hit::flinch)) { + attack &= ~Hit::flinch; + } + } // Drag cancels existing and queued Freeze // Additionally cancels current or queued Stun and Freeze if Player has Drag From f603fd08605b509c86c64ca149199bc5e2594ed4 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sun, 21 Sep 2025 14:51:38 -0700 Subject: [PATCH 095/146] Let MoveEvents end when time >= lastFrame instead of > This makes endlag of frames(0) actually have 0 frames of additional time. --- BattleNetwork/bnEntity.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index ac93783d7..cacb53440 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -190,7 +190,8 @@ void Entity::UpdateMovement(double elapsed) // Now that we have finished moving across panels, we must wait out endlag MoveEvent copyMoveEvent = currMoveEvent; frame_time_t lastFrame = currMoveEvent.delayFrames + currMoveEvent.deltaFrames + currMoveEvent.endlagFrames; - if (from_seconds(elapsedMoveTime) > lastFrame) { + + if (from_seconds(elapsedMoveTime) >= lastFrame) { Battle::Tile* prevTile = previous; FinishMove(); // mutates `previous` ptr previousDirection = direction; From f2ef0771fefac984533d211a0e77d9e99f029a89 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sun, 28 Sep 2025 23:17:40 -0700 Subject: [PATCH 096/146] MoveEvent rework. MoveAction is new class representing moves, all movements including Drag use it, Drag movement acts correctly --- BattleNetwork/bindings/bnUserTypeEntity.h | 4 +- BattleNetwork/bnEntity.cpp | 311 +++++++------------ BattleNetwork/bnEntity.h | 110 ++++++- BattleNetwork/bnMoveEvent.cpp | 347 ++++++++++++++++++++++ BattleNetwork/bnMoveEvent.h | 146 +++++++++ BattleNetwork/bnScriptResourceManager.cpp | 19 +- BattleNetwork/bnStatusDirector.cpp | 49 +-- BattleNetwork/bnStatusDirector.h | 4 + BattleNetwork/bnTile.cpp | 6 +- 9 files changed, 755 insertions(+), 241 deletions(-) create mode 100644 BattleNetwork/bnMoveEvent.cpp create mode 100644 BattleNetwork/bnMoveEvent.h diff --git a/BattleNetwork/bindings/bnUserTypeEntity.h b/BattleNetwork/bindings/bnUserTypeEntity.h index 7f9bc5dd7..fc6464c9d 100644 --- a/BattleNetwork/bindings/bnUserTypeEntity.h +++ b/BattleNetwork/bindings/bnUserTypeEntity.h @@ -188,8 +188,8 @@ void DefineEntityFunctionsOn(sol::basic_usertype, sol::basic_refe return entity.Unwrap()->Jump(dest, destHeight, jumpTime, endlag); } ); - entity_table["raw_move_event"] = [](WeakWrapper& entity, const MoveEvent& event, ActionOrder order) -> bool { - return entity.Unwrap()->RawMoveEvent(event, order); + entity_table["raw_move_event"] = [](WeakWrapper& entity, const MoveData& data, ActionOrder order) -> bool { + return entity.Unwrap()->RawMoveEvent(data, order); }; entity_table["is_sliding"] = [](WeakWrapper& entity) -> bool { return entity.Unwrap()->IsSliding(); diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index cacb53440..bc73c0a4d 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -10,6 +10,7 @@ #include "bnAudioResourceManager.h" #include #include +#include "bnMoveEvent.h" long Entity::numOfIDs = 0; @@ -25,7 +26,6 @@ bool EntityComparitor::operator()(Entity* f, Entity* s) const // First entity ID begins at 1 Entity::Entity() : - elapsedMoveTime(0), lastComponentID(0), height(0), moveCount(0), @@ -131,141 +131,15 @@ void Entity::InsertComponentsPendingRegistration() sort ? SortComponents() : void(0); } -void Entity::UpdateMovement(double elapsed) -{ - // Only move if we have a valid next tile pointer - Battle::Tile* next = currMoveEvent.dest; - if (next) { - if (currMoveEvent.onBegin) { - currMoveEvent.onBegin(); - currMoveEvent.onBegin = nullptr; - } - - elapsedMoveTime += elapsed; - - if (from_seconds(elapsedMoveTime) > currMoveEvent.delayFrames) { - // Get a value from 0.0 to 1.0 - float duration = seconds_cast(currMoveEvent.deltaFrames); - float delta = swoosh::ease::linear(static_cast(elapsedMoveTime - currMoveEvent.delayFrames.asSeconds().value), duration, 1.0f); - - sf::Vector2f pos = moveStartPosition; - sf::Vector2f tar = next->getPosition(); - - // Interpolate the sliding position from the start position to the end position - sf::Vector2f interpol = tar * delta + (pos * (1.0f - delta)); - tileOffset = interpol - pos; - - // Once halfway, the mmbn entities switch to the next tile - // and the slide position offset must be readjusted - if (delta >= 0.5f) { - // conditions of the target tile may change, ensure by the time we switch - if (CanMoveTo(next)) { - if (tile != next) { - AdoptNextTile(); - } - - // Adjust for the new current tile, begin halfway approaching the current tile - tileOffset = -tar + pos + tileOffset; - } - else { - // Slide back into the origin tile if we can no longer slide to the next tile - moveStartPosition = next->getPosition(); - currMoveEvent.dest = tile; - - tileOffset = -tar + pos + tileOffset; - } - } - - float heightElapsed = static_cast(elapsedMoveTime - currMoveEvent.delayFrames.asSeconds().value); - float heightDelta = swoosh::ease::wideParabola(heightElapsed, duration, 1.0f); - currJumpHeight = (heightDelta * currMoveEvent.height); - tileOffset.y -= currJumpHeight; - - // When delta is 1.0, the slide duration is complete - if (delta == 1.0f) - { - // Slide or jump is complete, clear the tile offset used in those animations - tileOffset = { 0, 0 }; - - // Now that we have finished moving across panels, we must wait out endlag - MoveEvent copyMoveEvent = currMoveEvent; - frame_time_t lastFrame = currMoveEvent.delayFrames + currMoveEvent.deltaFrames + currMoveEvent.endlagFrames; - - if (from_seconds(elapsedMoveTime) >= lastFrame) { - Battle::Tile* prevTile = previous; - FinishMove(); // mutates `previous` ptr - previousDirection = direction; - Battle::Tile* currTile = GetTile(); - - /* - Do not check ice slide if the same Tile was moved to. - This prevents a case where sliding to your own Tile would - infinitely slide in place. - - This same check is not used on Sand or Sea, so an Entity would - become rooted when moving to their own Tile in those cases. - */ - const bool sameTile = prevTile == currTile; - - // If we slide onto an ice block and we don't have float shoe enabled, slide - if (!sameTile && tile->GetState() == TileState::ice && !HasFloatShoe()) { - // calculate our new entity's position - UpdateMoveStartPosition(); - - if (prevTile->GetX() > currTile->GetX()) { - next = GetField()->GetAt(GetTile()->GetX() - 1, GetTile()->GetY()); - previousDirection = Direction::left; - } - else if (prevTile->GetX() < currTile->GetX()) { - next = GetField()->GetAt(GetTile()->GetX() + 1, GetTile()->GetY()); - previousDirection = Direction::right; - } - else if (prevTile->GetY() < currTile->GetY()) { - next = GetField()->GetAt(GetTile()->GetX(), GetTile()->GetY() + 1); - previousDirection = Direction::down; - } - else if (prevTile->GetY() > currTile->GetY()) { - next = GetField()->GetAt(GetTile()->GetX(), GetTile()->GetY() - 1); - previousDirection = Direction::up; - } - - // If the next tile is not available, not ice, or we are ice element, don't slide - bool notIce = (next && tile->GetState() != TileState::ice); - bool cannotMove = (next && !CanMoveTo(next)); - bool weAreIce = (GetElement() == Element::aqua); - bool cancelSlide = (notIce || cannotMove || weAreIce); - - if (slidesOnTiles && !cancelSlide) { - MoveEvent event = { frames(4), frames(0), frames(0), 0, tile + previousDirection }; - RawMoveEvent(event, ActionOrder::immediate); - copyMoveEvent = {}; - } - } - else if (tile->GetState() == TileState::sea && GetElement() != Element::aqua && !HasFloatShoe()) { - statuses.AddStatus(Hit::root, frames(20)); - auto splash = std::make_shared(); - field.lock()->AddEntity(splash, *tile); - } - else if (tile->GetState() == TileState::sand && !HasFloatShoe()) { - statuses.AddStatus(Hit::root, frames(20)); - } - else { - // Invalidate the next tile pointer - next = nullptr; - } - } - } - } - } - else { - // If we don't have a valid next tile pointer or are not sliding, - // Keep centered in the current tile with no offset - tileOffset = sf::Vector2f(0, 0); - elapsedMoveTime = 0; +void Entity::UpdateMovement(double elapsed) { + if (!currMoveEvent) { + return; } - if (tile) { - setPosition(tile->getPosition() + Entity::tileOffset + drawOffset); + currMoveEvent->Update(); + + if (currMoveEvent->IsFinished()) { + FinishMove(); } } @@ -434,10 +308,8 @@ void Entity::Update(double _elapsed) { statuses.ProcessPendingStatuses(); - if (currentDrag.dir == Direction::none) { - // Tick all statuses at once - statuses.OnUpdate(_elapsed); - } + // Tick all statuses at once + statuses.OnUpdate(_elapsed); HandleNewStatuses(prevStatuses, queuedStatuses & statuses.GetCurrentStatuses()); @@ -536,12 +408,6 @@ void Entity::Update(double _elapsed) { // Add this offset onto our offsets setPosition(tile->getPosition().x + offset.x, tile->getPosition().y + offset.y); } - - // If drag slide is over, reset the flag - if (!IsSliding() && slideFromDrag && currentDrag.count == 0) { - slideFromDrag = false; - } - } @@ -740,7 +606,10 @@ int Entity::GetAlpha() bool Entity::Teleport(Battle::Tile* dest, ActionOrder order, std::function onBegin) { if (dest && CanMoveTo(dest)) { frame_time_t endlagDelay = moveEndlagDelay ? *moveEndlagDelay : frame_time_t{}; - MoveEvent event = { 0, moveStartupDelay, endlagDelay, 0, dest, onBegin }; + + MoveEvent event = { + std::make_shared(weak_from_this(), MoveData{dest, frames(0), moveStartupDelay, endlagDelay, 0.f, onBegin}) + }; actionQueue.Add(event, order, ActionDiscardOp::until_eof); return true; @@ -754,7 +623,10 @@ bool Entity::Slide(Battle::Tile* dest, { if (dest && CanMoveTo(dest)) { frame_time_t endlagDelay = moveEndlagDelay ? *moveEndlagDelay : endlag; - MoveEvent event = { slideTime, moveStartupDelay, endlagDelay, 0, dest, onBegin }; + MoveEvent event = { + + std::make_shared(weak_from_this(), MoveData{dest, slideTime, frames(0), endlagDelay, 0.f, onBegin}) + }; actionQueue.Add(event, order, ActionDiscardOp::until_eof); return true; @@ -770,7 +642,11 @@ bool Entity::Jump(Battle::Tile* dest, float destHeight, if (dest && CanMoveTo(dest)) { frame_time_t endlagDelay = moveEndlagDelay ? *moveEndlagDelay : endlag; - MoveEvent event = { jumpTime, moveStartupDelay, endlagDelay, destHeight, dest, onBegin }; + + + MoveEvent event = { + std::make_shared(weak_from_this(), MoveData{dest, jumpTime, frames(0), endlagDelay, destHeight, onBegin}) + }; actionQueue.Add(event, order, ActionDiscardOp::until_eof); return true; @@ -781,27 +657,31 @@ bool Entity::Jump(Battle::Tile* dest, float destHeight, void Entity::FinishMove() { + slideFromDrag = false; + if (!currMoveEvent) { + return; + } + // completes the move or moves the object back - if (currMoveEvent.dest /*&& !currMoveEvent.immutable*/) { + if (currMoveEvent->data.dest /*&& !currMoveEvent.immutable*/) { AdoptNextTile(); tileOffset = {}; - currMoveEvent = {}; - actionQueue.ClearFilters(); - actionQueue.Pop(); } + + currMoveEvent = nullptr; + actionQueue.ClearFilters(); + actionQueue.Pop(); } void Entity::EndDrag() { statuses.ClearStatus(Hit::drag); - currentDrag.count = 0; - currentDrag.dir = Direction::none; slideFromDrag = false; FinishMove(); } bool Entity::RawMoveEvent(const MoveEvent& event, ActionOrder order) { - if (event.dest && CanMoveTo(event.dest)) { + if (event.move->data.dest && CanMoveTo(event.move->data.dest)) { actionQueue.Add(event, order, ActionDiscardOp::until_eof); return true; @@ -810,6 +690,17 @@ bool Entity::RawMoveEvent(const MoveEvent& event, ActionOrder order) return false; } +bool Entity::RawMoveEvent(const MoveData& data, ActionOrder order) { + if (data.dest) { + const MoveEvent e = { + std::make_shared(weak_from_this(), data) + }; + return RawMoveEvent(e, order); + } + + return false; +} + void Entity::HandleMoveEvent(MoveEvent& event, const ActionQueue::ExecutionType& exec) { if (exec == ActionQueue::ExecutionType::interrupt) { @@ -817,13 +708,12 @@ void Entity::HandleMoveEvent(MoveEvent& event, const ActionQueue::ExecutionType& return; } - if (currMoveEvent.dest == nullptr && !IsRooted()) { + if (!currMoveEvent && !IsRooted()) { UpdateMoveStartPosition(); FilterMoveEvent(event); - currMoveEvent = event; + currMoveEvent = event.move; moveEventFrame = this->frame; previous = tile; - elapsedMoveTime = 0; actionQueue.CreateDiscardFilter(ActionTypes::buster, ActionDiscardOp::until_resolve); actionQueue.CreateDiscardFilter(ActionTypes::peek_card, ActionDiscardOp::until_resolve); } @@ -909,21 +799,21 @@ const sf::Vector2f Entity::GetDrawOffset() const const bool Entity::IsSliding() const { - bool is_moving = currMoveEvent.IsSliding(); + bool is_moving = currMoveEvent && currMoveEvent->IsSliding(); return is_moving; } const bool Entity::IsJumping() const { - bool is_moving = currMoveEvent.IsJumping(); + bool is_moving = currMoveEvent && currMoveEvent->IsJumping(); return is_moving && currJumpHeight > 0.f; } const bool Entity::IsTeleporting() const { - bool is_moving = currMoveEvent.IsTeleporting(); + bool is_moving = currMoveEvent && currMoveEvent->IsTeleporting(); return is_moving; } @@ -1069,7 +959,7 @@ const Element Entity::GetElement() const void Entity::AdoptNextTile() { - Battle::Tile* next = currMoveEvent.dest; + Battle::Tile* next = currMoveEvent->data.dest; if (next == nullptr) { return; } @@ -1189,7 +1079,7 @@ void Entity::ClearActionQueue() const float Entity::GetJumpHeight() const { - return currMoveEvent.height; + return currMoveEvent ? currMoveEvent->GetHeight() : 0.f; } void Entity::ShowShadow(bool enabled) @@ -1436,7 +1326,10 @@ void Entity::ResolveFrameBattleDamage() std::queue append; - bool dragWasReplaced = false; + // Adding drag creates a MmoveAction. Wait until statusQueue is done, + // then create the MoveAction if this is true. + bool addDrag = false; + Hit::Drag currentDrag{}; while (!statusQueue.empty()) { CombatHitProps props = statusQueue.front(); @@ -1461,7 +1354,7 @@ void Entity::ResolveFrameBattleDamage() // Drag replaces current Drag effects. // Do not consider Drag if it has no direction if ((props.filtered.flags & Hit::drag) == Hit::drag && props.filtered.drag.dir != Direction::none) { - dragWasReplaced = true; + addDrag = true; currentDrag = props.filtered.drag; } @@ -1517,7 +1410,6 @@ void Entity::ResolveFrameBattleDamage() if (GetHealth() == 0) { currentDrag.dir = Direction::none; // Cancel slide post-status if blowing up - dragWasReplaced = true; } } } // end while-loop @@ -1525,41 +1417,33 @@ void Entity::ResolveFrameBattleDamage() // A new Drag should immediately end current movement // TODO: Drag forcibly ends the movement. Find out if that counts as a movement, because FinishMove // calls AdoptTile, which increases moveCount. - if (dragWasReplaced) { + if (addDrag) { + bool activeDrag = slideFromDrag; + // TODO: Drag during wind push will not overwrite? What about other movement, like ice, conveyor? FinishMove(); - statuses.AddStatus(Hit::drag, frames(22)); + // Preserve slideFromDrag. FinishMove sets false, but it must remain true + // if Drag was already in effect, for status processing purposes. + // Otherwise, when this Hit::drag processes, it will process as if there was + // not already an active Drag. + slideFromDrag = activeDrag; + statuses.AddStatus(Hit::drag); + + actionQueue.ClearQueue(ActionQueue::CleanupType::allow_interrupts); - } - - // TODO: Drag during wind push will not overwrite? What about other movement, like ice, conveyor? - if (!IsSliding() && currentDrag.dir != Direction::none) { - // enemies and objects on opposing side of field are granted immunity from drag - if (Teammate(GetTile()->GetTeam())) { - if (currentDrag.count > 0u) { - currentDrag.count -= 1u; - } - else { - currentDrag.dir = Direction::none; - } - - Battle::Tile* dest = GetTile() + currentDrag.dir; + /* + Do not set slideFromDrag true here. This could interfere with status + processing after ResolveFrameBattleDamage. This will be set true + by the StatusBehaviorDirector instead. + ------ slideFromDrag = true; + */ + actionQueue.Add( + MoveEvent{ + std::make_shared(weak_from_this(), currentDrag) + }, + ActionOrder::immediate, ActionDiscardOp::until_resolve + ); - // The final drag event should reset currentDrag. - // TODO: Ice slide should act the same as if the currentDrag.count did - // not go down when reaching that Tile. - frame_time_t movetime = frames(4); - if (currentDrag.dir == Direction::none || !CanMoveTo(dest)) { - currentDrag.count = 0; - currentDrag.dir = Direction::none; - } - else { - actionQueue.ClearQueue(ActionQueue::CleanupType::allow_interrupts); - slideFromDrag = true; - // Enqueue a move action at the top of our priorities - actionQueue.Add(MoveEvent{ movetime, frames(0), frames(0), 0, dest, {}, true }, ActionOrder::immediate, ActionDiscardOp::until_resolve); - } - } } if (GetHealth() == 0) { @@ -1686,12 +1570,39 @@ bool Entity::IsBlind() return statuses.HasStatus(Hit::blind); } -bool Entity::HasStatus(Hit::Flags status) { - return statuses.HasStatus(status); +void Entity::AddStatus(Hit::Flags status) { + statuses.AddStatus(status); +} + +void Entity::AddStatus(Hit::Flags status, frame_time_t duration) { + statuses.AddStatus(status, duration); } -bool Entity::IsStatusApplied(Hit::Flags status) { - return statuses.IsApplied(status); +const bool Entity::HasStatus(Hit::Flags status) const { + bool dragCheck = true; + if (status == Hit::drag) { + dragCheck = slideFromDrag; + status &= ~Hit::drag; + } + + return dragCheck && statuses.HasStatus(status); +} + +const bool Entity::HasAnyStatusFrom(Hit::Flags status) const { + if ((status & Hit::drag) == Hit::drag && slideFromDrag) { + return true; + } + + return statuses.HasAnyStatusFrom(status); +} + +const bool Entity::IsStatusApplied(Hit::Flags status) const { + bool dragCheck = true; + if (status == Hit::drag) { + dragCheck = slideFromDrag; + status &= ~Hit::drag; + } + return dragCheck && statuses.IsApplied(status); } void Entity::ClearStatuses(Hit::Flags flags) { diff --git a/BattleNetwork/bnEntity.h b/BattleNetwork/bnEntity.h index faef834e6..8b85ab683 100644 --- a/BattleNetwork/bnEntity.h +++ b/BattleNetwork/bnEntity.h @@ -38,6 +38,7 @@ using std::string; #include "bnHitProperties.h" #include "stx/memory.h" #include "bnStatusDirector.h" +#include "bnMoveEvent.h" namespace Battle { class Tile; @@ -45,8 +46,14 @@ namespace Battle { } class Field; -class BattleSceneBase; // forward decl +class BattleSceneBase; +// Defined in bnMoveEvent.h +class MoveAction; +struct MoveEvent; +struct MoveData; + +/* struct MoveEvent { frame_time_t deltaFrames{}; //!< Frames between tile A and B. If 0, teleport. Else, we could be sliding frame_time_t delayFrames{}; //!< Startup lag to be used with animations @@ -54,8 +61,11 @@ struct MoveEvent { float height{}; //!< If this is non-zero with delta frames, the character will effectively jump Battle::Tile* dest{ nullptr }; std::function onBegin = []{}; + bool immutable{ false }; //!< Some move events cannot be cancelled or interupted + std::function onFinish = [](const bool didReachDest, const bool willIceSlide) {}; + //!< helper function true if jumping inline bool IsJumping() const { return dest && height > 0.f && deltaFrames > frames(0); @@ -71,6 +81,75 @@ struct MoveEvent { return dest && deltaFrames == frames(0) && (+height) == 0.0f; } }; +*/ + +/* +typedef std::function VoidCallback; +typedef std::function MoveFinishCallback; + +class Entity; + +class MoveBehavior { +public: + MoveBehavior() {} + virtual ~MoveBehavior() {} + virtual MoveFinishCallback apply() = 0; +}; + +class DragMoveBehavior : public MoveBehavior { + Entity* ent; + Hit::Drag drag{}; + bool firstMovement{ true }; + +public: + DragMoveBehavior(Entity* ent, Hit::Drag drag) : ent(ent), drag(drag), MoveBehavior() {} + DragMoveBehavior(Entity* ent, Hit::Drag drag, bool firstMovement) : ent(ent), drag(drag), firstMovement(firstMovement), MoveBehavior() {} + + ~DragMoveBehavior() {} + + MoveFinishCallback apply() override { + auto impl = [ent = ent, oldDrag = drag, firstMovement = firstMovement](bool didReachDest, bool iceSliding){ + + // Clear queue to wipe a queued ice slide or other actions that are ahead of the Drag move. + // Only Drag will resolve. + ent->ClearActionQueue(); + + + Hit::Drag newDrag = {oldDrag.dir, oldDrag.count }; + // Entity must stop moving if the movement failed + if (!didReachDest) { + newDrag.count = 0; + } + + frame_time_t moveTime = frames(4); + + const bool noDir = newDrag.dir == Direction::none; + Battle::Tile* dest = noDir ? ent->GetTile() : ent->GetTile(newDrag.dir, 1); + const bool canReachDest = dest && ent->Teammate(ent->GetTile()->GetTeam()) && ent->CanMoveTo(dest); + + const bool finalMove = noDir || !canReachDest || (newDrag.count == 0 && !iceSliding); + if (finalMove) { + dest = ent->GetTile(); + moveTime = frames(firstMovement ? 24 : 20); + } + + if (newDrag.count > 0) { + newDrag.count--; + } + + const MoveEvent move = MoveEvent{ + moveTime, frames(0), frames(0), 0, dest, {}, true, + !finalMove ? DragMoveBehavior(ent, newDrag, false).apply() : [ent = ent](bool _, bool __) {ent->ClearStatuses(Hit::drag); } + }; + + + ent->actionQueue.Add(move, ActionOrder::immediate, ActionDiscardOp::until_resolve); + }; + + return impl; + } +}; +*/ struct CombatHitProps { Hit::Properties hitbox; // original hitbox data @@ -110,6 +189,8 @@ class Entity : friend class Field; friend class Component; friend class BattleSceneBase; + friend class StatusBehaviorDirector; + friend class MoveAction; enum class Shadow : char { none = 0, @@ -131,7 +212,7 @@ class Entity : float currJumpHeight{}; float height{}; /*!< Height of the entity relative to tile floor. Used for visual effects like projectiles or for hitbox detection */ EventBus::Channel channel; /*!< Our event bus channel to emit events */ - MoveEvent currMoveEvent{}; + std::shared_ptr currMoveEvent; VirtualInputState inputState; std::shared_ptr shadow{ nullptr }; std::shared_ptr iceFx{ nullptr }; @@ -198,8 +279,7 @@ class Entity : bool Jump(Battle::Tile* dest, float destHeight, const frame_time_t& jumpTime, const frame_time_t& endlag, ActionOrder order = ActionOrder::voluntary, std::function onBegin = [] {}); void FinishMove(); /** - * @brief Resets currentDrag, sets slideFromDrag false, clears Drag status, - * and calls FinishMove. + * @brief Sets slideFromDrag false, clears Drag status, and calls FinishMove. * * Used by the CharacterTransformBattleState, which must do these things * before activating the new transformation. @@ -210,6 +290,8 @@ class Entity : */ void EndDrag(); bool RawMoveEvent(const MoveEvent& event, ActionOrder order = ActionOrder::voluntary); + bool RawMoveEvent(const MoveData& data, ActionOrder order = ActionOrder::voluntary); + void HandleMoveEvent(MoveEvent& event, const ActionQueue::ExecutionType& exec); void ClearActionQueue(); const float GetJumpHeight() const; @@ -646,6 +728,10 @@ class Entity : */ bool IsBlind(); + + void AddStatus(Hit::Flags status, frame_time_t duration); + void AddStatus(Hit::Flags status); + /** * @brief Query if entity has a certain status tracked, whether queued or applied. * A queued status may not be applied by end of frame, or may be nullified during @@ -653,14 +739,22 @@ class Entity : * @param status to query * @return true if entity has status applied OR queued, false otherwise */ - bool HasStatus(Hit::Flags status); - + const bool HasStatus(Hit::Flags status) const; + /** + * @brief Query if entity has at least one of certain statuses tracked, whether queued + * or applied. + * A queued status may not be applied by end of frame, or may be nullified during + * status processing. + * @param statuses to query + * @return true if entity has any status in statuses + */ + const bool HasAnyStatusFrom(Hit::Flags statuses) const; /** * @brief Query if entity is afflicted by a certain status * @param status to query * @return true if entity has status applied, false otherwise */ - bool IsStatusApplied(Hit::Flags status); + const bool IsStatusApplied(Hit::Flags status) const; /** * @brief Clear all statuses in parameter flags, whether queued or applied. @@ -930,7 +1024,6 @@ class Entity : int maxHealth{}; float elevation{}; // vector away from grid float counterSlideDelta{}; - double elapsedMoveTime{}; /*!< delta time since recent move event began */ Battle::TileHighlight mode; /*!< Highlight occupying tile */ Hit::Properties hitboxProperties; /*!< Hitbox properties used when an entity is hit by this attack */ Direction direction{}; @@ -943,7 +1036,6 @@ class Entity : uint8_t statusShaderTimer{ 0 }; std::queue statusQueue; - Hit::Drag currentDrag{}; sf::Shader* whiteout{ nullptr }; /*!< Flash white when hit */ sf::Shader* stun{ nullptr }; /*!< Flicker yellow with luminance values when stun */ diff --git a/BattleNetwork/bnMoveEvent.cpp b/BattleNetwork/bnMoveEvent.cpp new file mode 100644 index 000000000..3f589ff75 --- /dev/null +++ b/BattleNetwork/bnMoveEvent.cpp @@ -0,0 +1,347 @@ +#include "bnMoveEvent.h" +#include "bnTile.h" +#include "bnWaterSplash.h" +#include "bnField.h" +#include + +/// class MoveAction /// + +MoveAction::MoveAction(std::weak_ptr owner, const MoveData& data) + : owner(owner), data(data) +{ +} + +void MoveAction::Begin() +{ + if (!data.onBegin) + return; + + data.onBegin(); + data.onBegin = nullptr; +} + + +bool MoveAction::IsFinished() const +{ + return completed; +} + +float MoveAction::GetHeight() const +{ + return data.height; +} + +//!< helper function true if jumping +bool MoveAction::IsJumping() const +{ + return data.dest && data.height > 0.f && data.deltaFrames > frames(0); +} + +//!< helper function true if sliding +bool MoveAction::IsSliding() const +{ + return data.dest && data.deltaFrames > frames(0) && data.height <= 0.0f; +} + +//!< helper function true if normal moving +bool MoveAction::IsTeleporting() const +{ + return data.dest && data.deltaFrames == frames(0) && (+data.height) == 0.0f; +} + +sf::Vector2f MoveAction::GetOwnerStartPosition() +{ + return owner.lock()->moveStartPosition; +} + +void MoveAction::SetOwnerStartPosition(sf::Vector2f offset) +{ + owner.lock()->moveStartPosition = offset; +} + +void MoveAction::SetOwnerJumpHeight(float height) +{ + owner.lock()->currJumpHeight = height; +} + +void MoveAction::SetOwnerPreviousDirection(Direction dir) +{ + owner.lock()->previousDirection = dir; +} + +Battle::Tile* MoveAction::GetOwnerPreviousTile() +{ + return owner.lock()->previous; +} + +void MoveAction::UpdateMoveStartPosition() { + owner.lock()->UpdateMoveStartPosition(); +} + +void MoveAction::OnUpdate(frame_time_t elapsed) { + if (completed) { + return; + } + + // Some MoveActions may determine Tile in Begin. Let them do so + // before terminating for nullptr dest. + Begin(); + + auto owner = this->owner.lock(); + + // Only move if we have a valid next tile pointer. + // Move is marked complete if there is no destination. + if (!data.dest) + { + if (owner->GetTile()) + { + owner->RefreshPosition(); + } + + completed = true; + return; + } + + // Only move if we have a valid next tile pointer + + Battle::Tile* next = data.dest; + Battle::Tile* currTile = owner->GetTile(); + + elapsedFrames += elapsed; + + if (elapsedFrames > data.delayFrames) { + // Get a value from 0.0 to 1.0 + float duration = seconds_cast(data.deltaFrames); + float delta = swoosh::ease::linear(static_cast((elapsedFrames - data.delayFrames).asSeconds().value), duration, 1.0f); + + sf::Vector2f pos = GetOwnerStartPosition(); + sf::Vector2f tar = next->getPosition(); + + sf::Vector2f tileOffset = owner->GetTileOffset(); + // Interpolate the sliding position from the start position to the end position + sf::Vector2f interpol = tar * delta + (pos * (1.0f - delta)); + tileOffset = interpol - pos; + + // Once halfway, entities switch to the next tile + // and the slide position offset must be readjusted + if (delta >= 0.5f) { + // conditions of the target tile may change, ensure by the time we switch + if (owner->CanMoveTo(next)) { + reachedDest = true; + if (currTile != next) { + owner->AdoptNextTile(); + } + + // Adjust for the new current tile, begin halfway approaching the current tile + tileOffset = -tar + pos + tileOffset; + } + else { + // Slide back into the origin tile if we can no longer slide to the next tile + SetOwnerStartPosition(next->getPosition()); + data.dest = currTile; + + tileOffset = -tar + pos + tileOffset; + } + } + + + + float heightElapsed = static_cast((elapsedFrames - data.delayFrames).asSeconds().value); + float heightDelta = swoosh::ease::wideParabola(heightElapsed, duration, 1.0f); + + SetOwnerJumpHeight(heightDelta * data.height); + tileOffset.y -= owner->GetCurrJumpHeight(); + owner->SetTileOffset(tileOffset); + + // When delta is 1.0, the slide duration is complete + if (delta == 1.0f) + { + // Slide or jump is complete, clear the tile offset used in those animations + tileOffset = { 0, 0 }; + owner->SetTileOffset(tileOffset); + + if (IsPendingFinish()) { + OnPostMove(); + } + } + } + + + if (owner->GetTile()) { + owner->RefreshPosition(); + } +} + +bool MoveAction::IsPendingFinish() const { + return elapsedFrames >= (data.delayFrames + data.deltaFrames + data.endlagFrames); +} + +void MoveAction::OnPostMove() { + auto owner = this->owner.lock(); + + Battle::Tile* prevTile = GetOwnerPreviousTile(); + Direction previousDirection = owner->GetMoveDirection(); + Battle::Tile* currTile = owner->GetTile(); + + /* + Do not check ice slide if the same Tile was moved to. + This prevents a case where sliding to your own Tile would + infinitely slide in place. + + This same check is not used on Sand or Sea, so an Entity would + become rooted when moving to their own Tile in those cases. + */ + const bool sameTile = prevTile == currTile; + bool willIceSlide = false; + + std::shared_ptr field = owner->GetField(); + + if (!sameTile && currTile->GetState() == TileState::ice && !owner->HasFloatShoe()) { + const int tileX = currTile->GetX(); + const int tileY = currTile->GetY(); + + Battle::Tile* next = nullptr; + if (prevTile->GetX() > tileX) { + next = field->GetAt(tileX - 1, tileY); + previousDirection = Direction::left; + } + else if (prevTile->GetX() < tileX) { + next = field->GetAt(tileX + 1, tileY); + previousDirection = Direction::right; + } + else if (prevTile->GetY() < tileY) { + next = field->GetAt(tileX, tileY + 1); + previousDirection = Direction::down; + } + else if (prevTile->GetY() > tileY) { + next = field->GetAt(tileX, tileY - 1); + previousDirection = Direction::up; + } + + // If the next tile is not available, not ice, or we are ice element, don't slide + bool notIce = (next && currTile->GetState() != TileState::ice); + bool cannotMove = (next && !owner->CanMoveTo(next)); + bool weAreIce = (owner->GetElement() == Element::aqua); + bool cancelSlide = (notIce || cannotMove || weAreIce); + + willIceSlide = owner->WillSlideOnTiles() && !cancelSlide; + } + + SetOwnerPreviousDirection(previousDirection); + + // If we slide onto an ice block and we don't have float shoe enabled, slide + if (willIceSlide) { + ResetWith({ currTile + previousDirection, frames(4), frames(0), frames(0), 0.f, nullptr }); + return; + } + + completed = true; + + // TODO: Determine if these really should wait for endlag to be finished. + // It's possible OnPostMove should run before endlag is considered, which + // could overwrite endlag for ice slide. + if (currTile->GetState() == TileState::sea && owner->GetElement() != Element::aqua && !owner->HasFloatShoe()) { + owner->AddStatus(Hit::root, frames(20)); + auto splash = std::make_shared(); + field->AddEntity(splash, *currTile); + } + else if (currTile->GetState() == TileState::sand && !owner->HasFloatShoe()) { + owner->AddStatus(Hit::root, frames(20)); + } +} + +void MoveAction::UpdatePreviousTile() { + auto owner = this->owner.lock(); + owner->previous = data.dest ? data.dest : owner->GetTile(); +} + +void MoveAction::ResetWith(const MoveData& newData) +{ + elapsedFrames = frames(0); + reachedDest = false; + UpdatePreviousTile(); + + data = newData; + // Calculate our new Entity's position + // Without this, Entity will appear offset and then slingshot back as ice + // move starts. + UpdateMoveStartPosition(); +} + + + +// dest is nullptr until Begin +DragAction::DragAction(std::weak_ptr owner, Hit::Drag drag) : drag(drag), MoveAction(owner, {}) +{ +} + +void DragAction::Begin() { + if (!firstMove) { + return; + } + PrepareMovement(); + MoveAction::Begin(); + firstMove = false; +} + +void DragAction::PrepareFinalMove() { + startedFinalMove = true; + auto owner = this->owner.lock(); + /* + On timing: + + Character::CanAttack should return false 26 times (25 while moving, +1 for the cached time) + if firstMove is true. Otherwise, or 26 times (25 while moving +1 for the cached time) otherwise. + These timings achieve this. + + Otherwise, the total inactionable time is 27 frames if pushed one Tile, 31 if two, etc. + A move time of 23 achieves this, since each move is 4 frames. + + Note that, because + */ + ResetWith(MoveData{ owner->GetTile(), frames(firstMove ? 26 : 23), frames(0), frames(0), 0.f, nullptr}); +} + +void DragAction::PrepareMovement() { + if (completed) { + return; + } + + auto owner = this->owner.lock(); + Battle::Tile* currTile = owner->GetTile(); + const bool noDir = drag.dir == Direction::none; + Battle::Tile* dest = noDir ? currTile : owner->GetTile(drag.dir, 1); + + if (!(dest && currTile)) { + PrepareFinalMove(); + return; + } + + + const bool canReachDest = owner->Teammate(currTile->GetTeam()) && owner->CanMoveTo(dest); + // False if firstMove true, because reachedDest is always false on the first + // movement. + const bool canIceSlide = this->reachedDest && owner->WillSlideOnTiles() && currTile->GetState() == TileState::ice && owner->GetElement() != Element::aqua; + + + // Ice slide allows 0 count Drag to continue moving. + if (noDir || !canReachDest || (drag.count == 0 && !canIceSlide)) { + PrepareFinalMove(); + return; + } + + if (drag.count > 0) { + drag.count--; + } + + ResetWith(MoveData{ dest, frames(4), frames(0), frames(0), 0.f, nullptr }); +} +void DragAction::OnPostMove() { + // The final movement has finished. The action is done. + if (startedFinalMove) { + completed = true; + return; + } + + PrepareMovement(); +} diff --git a/BattleNetwork/bnMoveEvent.h b/BattleNetwork/bnMoveEvent.h new file mode 100644 index 000000000..62390d9df --- /dev/null +++ b/BattleNetwork/bnMoveEvent.h @@ -0,0 +1,146 @@ +#pragma once +#include +#include "bnEntity.h" +#include "bnFrameTimeUtils.h" + + +class Entity; +class Battle::Tile; +class MoveAction; + +typedef std::function VoidCallback; + +struct MoveEvent { + std::shared_ptr move; +}; + +/* + Contains data used to create a generic MoveEventClass. + Useful as shorthand for creating a new MoveEventClass through + RawMoveEvent, or as an entry point to creating a C++ MoveEventClass + from scripting. +*/ +struct MoveData { + Battle::Tile* dest{ nullptr }; + frame_time_t deltaFrames{}; //!< Frames between tile A and B. If 0, teleport. Else, we could be sliding + frame_time_t delayFrames{}; //!< Startup lag to be used with animations + frame_time_t endlagFrames{}; //!< Wait period before action is complete + float height{}; //!< If this is non-zero with delta frames, the character will effectively jump + VoidCallback onBegin = [] {}; + + bool immutable{ false }; //!< Some move events cannot be cancelled or interupted +}; + +class MoveAction { +public: + MoveData data; + + MoveAction(std::weak_ptr owner, const MoveData& data); + + // The underlining move event data may have completed, but the move action + // as a whole may queue additional move events (e.g. DragAction). + // If [IsPendingFinish] is true, then this action is also a candidate, + // but not necessarily going to return true. + // This will only return true when the MoveAction is fully completed. + bool IsFinished() const; + + float GetHeight() const; + + bool IsJumping() const; + bool IsSliding() const; + bool IsTeleporting() const; + void OnUpdate(frame_time_t elapsed); + + // Explicitly calls one frame of [OnUpdate]. + virtual void Update() { OnUpdate(frames(1)); } +protected: + std::weak_ptr owner; + frame_time_t elapsedFrames{}; + /* + Whether or not the MoveEvent is complete. + When true, [OnUpdate] is a no-op. + */ + bool completed{ false }; + // Whether or not the destination was reached. Only false during OnPostMove + // if OnUpdate determined the dest could not be reached. + bool reachedDest{ false }; + + virtual void Begin(); + + /* + Called during [OnUpdate] to determine whether or not the current + movement action animation is finished. If true, [OnUpdate] will call [OnPostMove]. + Note, while the move action's animation may be finished, this does not + necessarily indicate that this is the last move. + */ + virtual bool IsPendingFinish() const; + + /* + Run by OnUpdate when IsFinishedMoving returns true. + However this may not be the last move. + It will perform final events for the movement, such as marking completed + or making calls for the additional movement. + */ + virtual void OnPostMove(); + + /* + Terminates the MoveEvent + + virtual void Interrupt() = 0; + */ + + /* + Prepares the MoveEvent for a new movement using given + parameters. + */ + virtual void ResetWith(const MoveData& newData); + + // Helpers for accessing protected members on Entity + // through MoveAction friendship + sf::Vector2f GetOwnerStartPosition(); + void SetOwnerStartPosition(sf::Vector2f offset); + void SetOwnerJumpHeight(float height); + void SetOwnerPreviousDirection(Direction dir); + Battle::Tile* GetOwnerPreviousTile(); + void UpdateMoveStartPosition(); + // Sets Entity::previous to data.dest (or Entity's current Tile if nullptr). + // Used as part of ResetWith to record previous Tile, which allows AdoptTile + // to work without removing the Entity on multiple Tiles. + void UpdatePreviousTile(); +}; + +class DragAction : public MoveAction { +private: + /* + Whether or not the last movement is in progress. + After this movement finished, completed is set true. + */ + bool startedFinalMove{ false }; + /* + Whether or not the first movement is in progress. + Used to determine movement timing. + Set false during PostMove. + */ + bool firstMove{ true }; + Hit::Drag drag{}; + + /* + Determines destination and move time based on drag. + Sets dest modifies drag, and may change firstMove and startedFinalMove. + */ + void PrepareMovement(); + /* + Resets movement with parameters for the last movement + of Drag. Targets the current Tile as the destination, + and uses a high move time based on firstMove to keep + the Entity in place. + */ + void PrepareFinalMove(); +protected: + void Begin() override; + void OnPostMove() override; + +public: + DragAction(std::weak_ptr owner, Hit::Drag drag); + +}; \ No newline at end of file diff --git a/BattleNetwork/bnScriptResourceManager.cpp b/BattleNetwork/bnScriptResourceManager.cpp index 771c64c60..d0e04f7d2 100644 --- a/BattleNetwork/bnScriptResourceManager.cpp +++ b/BattleNetwork/bnScriptResourceManager.cpp @@ -655,17 +655,20 @@ void ScriptResourceManager::ConfigureEnvironment(ScriptPackage& scriptPackage) { "Yellow", PlayerCustScene::Piece::Types::yellow ); - const auto& move_event_record = state.new_usertype("MoveEvent", + // "MoveEvent", as Lua knows it, was renamed to "MoveData". + // Lua does not have access to the new MoveEvent, so it can continue to + // use the old name to avoid breaking scripts from v2.0. + const auto& move_event_record = state.new_usertype("MoveEvent", sol::factories([] { - return MoveEvent{}; + return MoveData{}; }), - "delta_frames", &MoveEvent::deltaFrames, - "delay_frames", &MoveEvent::delayFrames, - "endlag_frames",&MoveEvent::endlagFrames, - "height", &MoveEvent::height, - "dest_tile", &MoveEvent::dest, + "delta_frames", &MoveData::deltaFrames, + "delay_frames", &MoveData::delayFrames, + "endlag_frames",&MoveData::endlagFrames, + "height", &MoveData::height, + "dest_tile", &MoveData::dest, "on_begin_func", sol::property( - [](MoveEvent& event, sol::object onBeginObject) { + [](MoveData& event, sol::object onBeginObject) { ExpectLuaFunction(onBeginObject); event.onBegin = [onBeginObject] { diff --git a/BattleNetwork/bnStatusDirector.cpp b/BattleNetwork/bnStatusDirector.cpp index 229ed3fa3..ec8f7902a 100644 --- a/BattleNetwork/bnStatusDirector.cpp +++ b/BattleNetwork/bnStatusDirector.cpp @@ -51,12 +51,17 @@ void StatusBehaviorDirector::ProcessPendingStatuses() { // Process only Drag if it's queued if ((queuedStatuses & Hit::drag) == Hit::drag) { ProcessFlags(Hit::drag); + // Drag is a special case where most behavior is handled + // based on this bool. Set true after processing. + owner.slideFromDrag = true; queuedStatuses &= ~Hit::drag; return; } - // Do not process other flags if Drag is a current status - if((currentStatuses & Hit::drag) == Hit::drag) { + // Do not process other flags if Drag is a current status. + // Base this on Entity::slideFromDrag, as it more accurately + // represents the special case of Drag. + if(owner.slideFromDrag) { return; } @@ -92,6 +97,11 @@ void StatusBehaviorDirector::ProcessFlags(Hit::Flags attack) { // checking one of these flags. else { // If stunned is active, prevent flinch + /* + This should mean that an attack which ends stun and flinches will not flinch. + The only way for that to be the case when the attack doesn't also flash is + with Drag | Flinch. TODO: Does an attack like that exist to test? + */ if (((currentStatuses & Hit::stun) == Hit::stun) && (attack & Hit::flinch)) { attack &= ~Hit::flinch; } @@ -107,9 +117,12 @@ void StatusBehaviorDirector::ProcessFlags(Hit::Flags attack) { // cleaner. queuedStatuses &= ~Hit::freeze; - if ((currentStatuses & Hit::drag) == Hit::drag) { - currentStatuses &= ~(Hit::stun | Hit::freeze); - queuedStatuses &= ~(Hit::stun | Hit::freeze); + // Cancel current or queued Stun and Freeze if Player has Drag. + // Base this on Entity::slideFromDrag, as it more accurately + // represents the special case of Drag. + if (owner.slideFromDrag) { + currentStatuses &= ~(Hit::stun | Hit::freeze); + queuedStatuses &= ~(Hit::stun | Hit::freeze); } } @@ -123,9 +136,17 @@ void StatusBehaviorDirector::ProcessFlags(Hit::Flags attack) { void StatusBehaviorDirector::OnUpdate(double elapsed) { frame_time_t _elapsed = from_seconds(elapsed); - if ((currentStatuses & Hit::drag) == Hit::drag) { + // Update only Drag if Entity is under Drag. + // Base this on Entity::slideFromDrag, as it more accurately + // represents the special case of Drag. + // TODO: Because this is set false after move ends, Drag ends at EoF instead of start of next frame after movement. Good, bad? + if (owner.slideFromDrag) { AppliedStatus& drag = GetStatus(Hit::drag); + // Tick time down and remove even though Drag status is handled + // more through Entity::slideFromDrag. The Hit::drag tracked on + // this is still used to trigger status callbacks on hit, so tick + // it down and remove as normal. drag.remainingTime -= _elapsed; if (drag.remainingTime > frames(0)) { @@ -134,18 +155,6 @@ void StatusBehaviorDirector::OnUpdate(double elapsed) { currentStatuses &= ~Hit::drag; - /* - Other statuses never tick if Drag was handled during update, even - if Drag ended on this tick. - - This is also safe with regards to Character::CanAttack's goal - even - though Drag ended and a queued blocking status has not become active, - CanAttack will return false this frame because of the cached part of - CanAttack. It will also still return false for all relevant parts of - the Entity::Update routine next frame, because a queued blocking status - would become active near start of update, when StatusBehaviorDirector::OnUpdate - next runs. - */ return; } @@ -232,6 +241,10 @@ const bool StatusBehaviorDirector::HasStatus(Hit::Flags flag) const { return ((currentStatuses | queuedStatuses) & flag) == flag; } +const bool StatusBehaviorDirector::HasAnyStatusFrom(Hit::Flags flags) const { + return ((currentStatuses | queuedStatuses) & flags); +} + const Hit::Flags StatusBehaviorDirector::GetCurrentStatuses() const { return currentStatuses; } diff --git a/BattleNetwork/bnStatusDirector.h b/BattleNetwork/bnStatusDirector.h index 25c7eada5..3282e3d61 100644 --- a/BattleNetwork/bnStatusDirector.h +++ b/BattleNetwork/bnStatusDirector.h @@ -56,6 +56,10 @@ class StatusBehaviorDirector { Returns true if flag is uncommitted or applied. */ const bool HasStatus(Hit::Flags flag) const; + /* + Returns true if any flag in [flags] is uncommitted or applied. + */ + const bool HasAnyStatusFrom(Hit::Flags flags) const; /* Returns true only if flag isapplied. */ diff --git a/BattleNetwork/bnTile.cpp b/BattleNetwork/bnTile.cpp index 23f9c6160..c4e90a700 100644 --- a/BattleNetwork/bnTile.cpp +++ b/BattleNetwork/bnTile.cpp @@ -686,8 +686,7 @@ namespace Battle { if (obst.WillSlideOnTiles()) { if (!obst.HasAirShoe() && !obst.HasFloatShoe()) { if (!obst.IsSliding() && notMoving) { - MoveEvent event{ frames(3), frames(0), frames(0), 0, obst.GetTile() + directional }; - obst.Entity::RawMoveEvent(event, ActionOrder::involuntary); + obst.Entity::RawMoveEvent(MoveData{obst.GetTile() + directional, frames(3), frames(0), frames(0), 0.f, nullptr}, ActionOrder::involuntary); } } } @@ -762,8 +761,7 @@ namespace Battle { if (character.WillSlideOnTiles()) { if (!character.HasAirShoe() && !character.HasFloatShoe()) { if (notMoving && !character.IsSliding()) { - MoveEvent event{ frames(3), frames(0), frames(0), 0, character.GetTile() + directional }; - character.RawMoveEvent(event, ActionOrder::involuntary); + character.RawMoveEvent(MoveData{ character.GetTile() + directional, frames(3), frames(0), frames(0), 0.f, nullptr }, ActionOrder::involuntary); } } } From 7907ec3659aadfe7decbc7db233813836049eebc Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sun, 28 Sep 2025 23:18:29 -0700 Subject: [PATCH 097/146] Fix backwards actionBlocked assignment, update Character::CanAttackImpl to check Drag correctly --- BattleNetwork/bnCharacter.cpp | 2 +- BattleNetwork/bnPlayer.cpp | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/BattleNetwork/bnCharacter.cpp b/BattleNetwork/bnCharacter.cpp index 782836592..6793e84e5 100644 --- a/BattleNetwork/bnCharacter.cpp +++ b/BattleNetwork/bnCharacter.cpp @@ -159,7 +159,7 @@ const bool Character::CanAttack() const } const bool Character::CanAttackImpl() const { - return !currCardAction && !(statuses.GetCurrentStatuses() & blockingStatuses); + return !currCardAction && !HasAnyStatusFrom(blockingStatuses); } void Character::MakeIdle() diff --git a/BattleNetwork/bnPlayer.cpp b/BattleNetwork/bnPlayer.cpp index 1147dac2d..f3bd4979e 100644 --- a/BattleNetwork/bnPlayer.cpp +++ b/BattleNetwork/bnPlayer.cpp @@ -460,10 +460,10 @@ void Player::ActivateFormAt(int index) Charge(false); /* - If current state allows, Player can act immediately on the first - combat frame after the transform state finishes. + If current state allows, Player can act immediately on the first + combat frame after the transform state finishes. */ - actionBlocked = CanAttackImpl(); + actionBlocked = !CanAttackImpl(); // Find nodes that do not have tags, those are newly added for (std::shared_ptr& node : GetChildNodes()) { From 335c2eb673e3b43d68a31f938f61c3234e51f8e9 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Tue, 30 Sep 2025 01:05:00 -0700 Subject: [PATCH 098/146] MoveAction no longer refreshes position. Entity refreshes position during UpdateMovement. No longer calls MoveAction.Update so that Update(0) properly does not elapse time --- BattleNetwork/bnEntity.cpp | 9 ++++++++- BattleNetwork/bnMoveEvent.cpp | 5 ----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index bc73c0a4d..d96423d6b 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -133,14 +133,21 @@ void Entity::InsertComponentsPendingRegistration() void Entity::UpdateMovement(double elapsed) { if (!currMoveEvent) { + if (tile) { + RefreshPosition(); + } return; } - currMoveEvent->Update(); + currMoveEvent->OnUpdate(from_seconds(elapsed)); if (currMoveEvent->IsFinished()) { FinishMove(); } + + if (tile) { + RefreshPosition(); + } } void Entity::SetFrame(unsigned frame) diff --git a/BattleNetwork/bnMoveEvent.cpp b/BattleNetwork/bnMoveEvent.cpp index 3f589ff75..c3ba84db4 100644 --- a/BattleNetwork/bnMoveEvent.cpp +++ b/BattleNetwork/bnMoveEvent.cpp @@ -165,11 +165,6 @@ void MoveAction::OnUpdate(frame_time_t elapsed) { } } } - - - if (owner->GetTile()) { - owner->RefreshPosition(); - } } bool MoveAction::IsPendingFinish() const { From fef7495387d0df43b8d86252a897d7193e3435d5 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Tue, 30 Sep 2025 19:05:50 -0700 Subject: [PATCH 099/146] Remove MoveEvent::Update, use OnUpdate instead, fixes issue related to Entity::Update(0) --- BattleNetwork/bnMoveEvent.h | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/BattleNetwork/bnMoveEvent.h b/BattleNetwork/bnMoveEvent.h index 62390d9df..2c2453bbc 100644 --- a/BattleNetwork/bnMoveEvent.h +++ b/BattleNetwork/bnMoveEvent.h @@ -49,10 +49,7 @@ class MoveAction { bool IsJumping() const; bool IsSliding() const; bool IsTeleporting() const; - void OnUpdate(frame_time_t elapsed); - - // Explicitly calls one frame of [OnUpdate]. - virtual void Update() { OnUpdate(frames(1)); } + virtual void OnUpdate(frame_time_t elapsed); protected: std::weak_ptr owner; frame_time_t elapsedFrames{}; From 5d18f6ab4cbe8b2ba2e76672800e3b82a3afb179 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Tue, 30 Sep 2025 19:06:12 -0700 Subject: [PATCH 100/146] HandleNewStatuses call made closer to actual applied statuses --- BattleNetwork/bnEntity.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index d96423d6b..8e13e0823 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -318,7 +318,7 @@ void Entity::Update(double _elapsed) { // Tick all statuses at once statuses.OnUpdate(_elapsed); - HandleNewStatuses(prevStatuses, queuedStatuses & statuses.GetCurrentStatuses()); + HandleNewStatuses(prevStatuses, queuedStatuses & ~statuses.GetQueuedStatuses() & statuses.GetCurrentStatuses()); RefreshShader(); From a55a236864ceea81aa67aa9c1bc53f3926d97114 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Tue, 30 Sep 2025 19:24:57 -0700 Subject: [PATCH 101/146] Comment cleanup --- BattleNetwork/bnEntity.cpp | 3 +- BattleNetwork/bnEntity.h | 98 ------------------------------ BattleNetwork/bnMoveEvent.cpp | 2 - BattleNetwork/bnMoveEvent.h | 12 +--- BattleNetwork/bnPlayer.cpp | 2 +- BattleNetwork/bnStatusDirector.cpp | 13 ++-- 6 files changed, 12 insertions(+), 118 deletions(-) diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index 8e13e0823..6720f8149 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -670,7 +670,7 @@ void Entity::FinishMove() } // completes the move or moves the object back - if (currMoveEvent->data.dest /*&& !currMoveEvent.immutable*/) { + if (currMoveEvent->data.dest) { AdoptNextTile(); tileOffset = {}; } @@ -1441,7 +1441,6 @@ void Entity::ResolveFrameBattleDamage() Do not set slideFromDrag true here. This could interfere with status processing after ResolveFrameBattleDamage. This will be set true by the StatusBehaviorDirector instead. - ------ slideFromDrag = true; */ actionQueue.Add( diff --git a/BattleNetwork/bnEntity.h b/BattleNetwork/bnEntity.h index 8b85ab683..e4b01ce23 100644 --- a/BattleNetwork/bnEntity.h +++ b/BattleNetwork/bnEntity.h @@ -53,104 +53,6 @@ class MoveAction; struct MoveEvent; struct MoveData; -/* -struct MoveEvent { - frame_time_t deltaFrames{}; //!< Frames between tile A and B. If 0, teleport. Else, we could be sliding - frame_time_t delayFrames{}; //!< Startup lag to be used with animations - frame_time_t endlagFrames{}; //!< Wait period before action is complete - float height{}; //!< If this is non-zero with delta frames, the character will effectively jump - Battle::Tile* dest{ nullptr }; - std::function onBegin = []{}; - - bool immutable{ false }; //!< Some move events cannot be cancelled or interupted - - std::function onFinish = [](const bool didReachDest, const bool willIceSlide) {}; - - //!< helper function true if jumping - inline bool IsJumping() const { - return dest && height > 0.f && deltaFrames > frames(0); - } - - //!< helper function true if sliding - inline bool IsSliding() const { - return dest && deltaFrames > frames(0) && height <= 0.0f; - } - - //!< helper function true if normal moving - inline bool IsTeleporting() const { - return dest && deltaFrames == frames(0) && (+height) == 0.0f; - } -}; -*/ - -/* -typedef std::function VoidCallback; -typedef std::function MoveFinishCallback; - -class Entity; - -class MoveBehavior { -public: - MoveBehavior() {} - virtual ~MoveBehavior() {} - virtual MoveFinishCallback apply() = 0; -}; - -class DragMoveBehavior : public MoveBehavior { - Entity* ent; - Hit::Drag drag{}; - bool firstMovement{ true }; - -public: - DragMoveBehavior(Entity* ent, Hit::Drag drag) : ent(ent), drag(drag), MoveBehavior() {} - DragMoveBehavior(Entity* ent, Hit::Drag drag, bool firstMovement) : ent(ent), drag(drag), firstMovement(firstMovement), MoveBehavior() {} - - ~DragMoveBehavior() {} - - MoveFinishCallback apply() override { - auto impl = [ent = ent, oldDrag = drag, firstMovement = firstMovement](bool didReachDest, bool iceSliding){ - - // Clear queue to wipe a queued ice slide or other actions that are ahead of the Drag move. - // Only Drag will resolve. - ent->ClearActionQueue(); - - - Hit::Drag newDrag = {oldDrag.dir, oldDrag.count }; - // Entity must stop moving if the movement failed - if (!didReachDest) { - newDrag.count = 0; - } - - frame_time_t moveTime = frames(4); - - const bool noDir = newDrag.dir == Direction::none; - Battle::Tile* dest = noDir ? ent->GetTile() : ent->GetTile(newDrag.dir, 1); - const bool canReachDest = dest && ent->Teammate(ent->GetTile()->GetTeam()) && ent->CanMoveTo(dest); - - const bool finalMove = noDir || !canReachDest || (newDrag.count == 0 && !iceSliding); - if (finalMove) { - dest = ent->GetTile(); - moveTime = frames(firstMovement ? 24 : 20); - } - - if (newDrag.count > 0) { - newDrag.count--; - } - - const MoveEvent move = MoveEvent{ - moveTime, frames(0), frames(0), 0, dest, {}, true, - !finalMove ? DragMoveBehavior(ent, newDrag, false).apply() : [ent = ent](bool _, bool __) {ent->ClearStatuses(Hit::drag); } - }; - - - ent->actionQueue.Add(move, ActionOrder::immediate, ActionDiscardOp::until_resolve); - }; - - return impl; - } -}; -*/ - struct CombatHitProps { Hit::Properties hitbox; // original hitbox data Hit::Properties filtered; // statuses after defense rules pass diff --git a/BattleNetwork/bnMoveEvent.cpp b/BattleNetwork/bnMoveEvent.cpp index c3ba84db4..8418f5253 100644 --- a/BattleNetwork/bnMoveEvent.cpp +++ b/BattleNetwork/bnMoveEvent.cpp @@ -263,8 +263,6 @@ void MoveAction::ResetWith(const MoveData& newData) UpdateMoveStartPosition(); } - - // dest is nullptr until Begin DragAction::DragAction(std::weak_ptr owner, Hit::Drag drag) : drag(drag), MoveAction(owner, {}) { diff --git a/BattleNetwork/bnMoveEvent.h b/BattleNetwork/bnMoveEvent.h index 2c2453bbc..ce9126e0d 100644 --- a/BattleNetwork/bnMoveEvent.h +++ b/BattleNetwork/bnMoveEvent.h @@ -15,9 +15,9 @@ struct MoveEvent { }; /* - Contains data used to create a generic MoveEventClass. - Useful as shorthand for creating a new MoveEventClass through - RawMoveEvent, or as an entry point to creating a C++ MoveEventClass + Contains data used to create a generic MoveAction. + Useful as shorthand for creating a new MoveAction through + RawMoveEvent, or as an entry point to creating a C++ MoveAction from scripting. */ struct MoveData { @@ -80,12 +80,6 @@ class MoveAction { */ virtual void OnPostMove(); - /* - Terminates the MoveEvent - - virtual void Interrupt() = 0; - */ - /* Prepares the MoveEvent for a new movement using given parameters. diff --git a/BattleNetwork/bnPlayer.cpp b/BattleNetwork/bnPlayer.cpp index f3bd4979e..298329b67 100644 --- a/BattleNetwork/bnPlayer.cpp +++ b/BattleNetwork/bnPlayer.cpp @@ -436,7 +436,7 @@ void Player::ActivateFormAt(int index) This may have also added a MoveEvent for Drag. It's reasonable to ignore this and allow it to be cleared without processing. If it - was processed, it would bepossible to snap two Tiles at once during + was processed, it would be possible to snap two Tiles at once during transformation: One if the Player was moving by input, and again if a Drag was resolved. */ diff --git a/BattleNetwork/bnStatusDirector.cpp b/BattleNetwork/bnStatusDirector.cpp index ec8f7902a..3ac96e1a4 100644 --- a/BattleNetwork/bnStatusDirector.cpp +++ b/BattleNetwork/bnStatusDirector.cpp @@ -65,7 +65,6 @@ void StatusBehaviorDirector::ProcessPendingStatuses() { return; } - if (queuedStatuses == 0) { return; } @@ -139,14 +138,16 @@ void StatusBehaviorDirector::OnUpdate(double elapsed) { // Update only Drag if Entity is under Drag. // Base this on Entity::slideFromDrag, as it more accurately // represents the special case of Drag. - // TODO: Because this is set false after move ends, Drag ends at EoF instead of start of next frame after movement. Good, bad? + // Because this is set false after move ends, Drag ends at EoF + // instead of start of next frame after movement, contrary to + // other statuses. if (owner.slideFromDrag) { AppliedStatus& drag = GetStatus(Hit::drag); - // Tick time down and remove even though Drag status is handled - // more through Entity::slideFromDrag. The Hit::drag tracked on - // this is still used to trigger status callbacks on hit, so tick - // it down and remove as normal. + // Tick time down and remove even though Drag effects are handled + // using Entity::slideFromDrag instead of the Hit::drag status. + // The Hit::drag tracked on this is still used to trigger status + // callbacks on hit, so tick it down and remove as with other statuses. drag.remainingTime -= _elapsed; if (drag.remainingTime > frames(0)) { From 74bcc5fb95aa9571df71c8753b472aa1351c45d0 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Tue, 30 Sep 2025 20:43:01 -0700 Subject: [PATCH 102/146] Rename ClearStatus to ClearStatuses and ClearAllStatuses --- BattleNetwork/bnEntity.cpp | 23 +++++------------------ BattleNetwork/bnStatusDirector.cpp | 4 ++-- BattleNetwork/bnStatusDirector.h | 4 ++-- 3 files changed, 9 insertions(+), 22 deletions(-) diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index 6720f8149..7cad6aef8 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -298,7 +298,7 @@ void Entity::Update(double _elapsed) { health = 0; // Ensure status effects do not play out - statuses.ClearStatus(); + statuses.ClearAllStatuses(); } // reset base color @@ -330,14 +330,6 @@ void Entity::Update(double _elapsed) { // The answer is likely yes. bool canUpdateThisFrame = !(frozen || stunned); - /* - TODO: Using Hide and Reveal here may conflict with user mods that - attempt to hide an Entity. Find some way to play nice. - - Example: AntiDamage mod, which hides the Character using it for - some time. If triggered while flashing, they will unhide before - they become vulnerable again. - */ if (!hit) { AppliedStatus& flash = statuses.GetStatus(Hit::flash); @@ -681,7 +673,7 @@ void Entity::FinishMove() } void Entity::EndDrag() { - statuses.ClearStatus(Hit::drag); + statuses.ClearStatuses(Hit::drag); slideFromDrag = false; FinishMove(); } @@ -935,7 +927,7 @@ void Entity::Delete() deleted = true; - statuses.ClearStatus(); + statuses.ClearAllStatuses(); OnDelete(); } @@ -1426,7 +1418,6 @@ void Entity::ResolveFrameBattleDamage() // calls AdoptTile, which increases moveCount. if (addDrag) { bool activeDrag = slideFromDrag; - // TODO: Drag during wind push will not overwrite? What about other movement, like ice, conveyor? FinishMove(); // Preserve slideFromDrag. FinishMove sets false, but it must remain true // if Drag was already in effect, for status processing purposes. @@ -1454,7 +1445,7 @@ void Entity::ResolveFrameBattleDamage() if (GetHealth() == 0) { // We are dying. Prevent special fx and status animations from triggering. - statuses.ClearStatus(); + statuses.ClearAllStatuses(); while(statusQueue.size() > 0) { statusQueue.pop(); @@ -1612,11 +1603,7 @@ const bool Entity::IsStatusApplied(Hit::Flags status) const { } void Entity::ClearStatuses(Hit::Flags flags) { - if ((flags & (statuses.GetQueuedStatuses() | statuses.GetCurrentStatuses())) & Hit::drag) { - EndDrag(); - } - - statuses.ClearStatus(flags); + statuses.ClearStatuses(flags); } void Entity::IceFreeze() diff --git a/BattleNetwork/bnStatusDirector.cpp b/BattleNetwork/bnStatusDirector.cpp index 3ac96e1a4..5be7f54f4 100644 --- a/BattleNetwork/bnStatusDirector.cpp +++ b/BattleNetwork/bnStatusDirector.cpp @@ -202,7 +202,7 @@ AppliedStatus& StatusBehaviorDirector::GetStatus(Hit::Flags flag) { return status; }; -void StatusBehaviorDirector::ClearStatus() { +void StatusBehaviorDirector::ClearAllStatuses() { for (auto& [_, status] : statusMap) { status.remainingTime = frames(0); } @@ -211,7 +211,7 @@ void StatusBehaviorDirector::ClearStatus() { currentStatuses = Hit::none; }; -void StatusBehaviorDirector::ClearStatus(Hit::Flags flags) { +void StatusBehaviorDirector::ClearStatuses(Hit::Flags flags) { // Start from lowest bit Hit::Flags curFlag = flags & -flags; diff --git a/BattleNetwork/bnStatusDirector.h b/BattleNetwork/bnStatusDirector.h index 3282e3d61..919dade17 100644 --- a/BattleNetwork/bnStatusDirector.h +++ b/BattleNetwork/bnStatusDirector.h @@ -37,13 +37,13 @@ class StatusBehaviorDirector { AppliedStatus& GetStatus(Hit::Flags flag); const Hit::Flags GetQueuedStatuses() const; const Hit::Flags GetCurrentStatuses() const; - void ClearStatus(); + void ClearAllStatuses(); /* Clear specific flags from queued and active statuses. Parameter flags may contain multiple Hit::Flags bits set. Each corresponding status will be cleared. */ - void ClearStatus(Hit::Flags flags); + void ClearStatuses(Hit::Flags flags); /* Process current queuedStatuses. The result of GetQueuedStatuses and GetCurrentStatuses may be different before and after calling this. From 9bdc79e528d5c12501386a922c62b6e06bf45bdb Mon Sep 17 00:00:00 2001 From: Alrysc Date: Tue, 30 Sep 2025 21:48:16 -0700 Subject: [PATCH 103/146] Character blockingStatuses moved to static member of Character --- BattleNetwork/bnCharacter.cpp | 10 +--------- BattleNetwork/bnCharacter.h | 7 ++----- BattleNetwork/bnPlayer.cpp | 4 ++-- 3 files changed, 5 insertions(+), 16 deletions(-) diff --git a/BattleNetwork/bnCharacter.cpp b/BattleNetwork/bnCharacter.cpp index 6793e84e5..d3f0796ea 100644 --- a/BattleNetwork/bnCharacter.cpp +++ b/BattleNetwork/bnCharacter.cpp @@ -17,9 +17,6 @@ #include "bnCardAction.h" #include "bnCardToActions.h" -// All statuses which should prevent Character from taking actions -constexpr const Hit::Flags blockingStatuses = Hit::stun | Hit::freeze | Hit::bubble | Hit::drag; - Character::Character(Rank _rank) : rank(_rank), CardActionUsePublisher(), @@ -159,7 +156,7 @@ const bool Character::CanAttack() const } const bool Character::CanAttackImpl() const { - return !currCardAction && !HasAnyStatusFrom(blockingStatuses); + return !currCardAction && Character::blockingStatuses; } void Character::MakeIdle() @@ -241,8 +238,3 @@ void Character::HandlePeekEvent(const PeekCardEvent& event, const ActionQueue::E actionQueue.Pop(); } - -const Hit::Flags Character::GetBlockingStatuses() const { - return blockingStatuses; -} - diff --git a/BattleNetwork/bnCharacter.h b/BattleNetwork/bnCharacter.h index ad49bab4b..e957335d7 100644 --- a/BattleNetwork/bnCharacter.h +++ b/BattleNetwork/bnCharacter.h @@ -49,6 +49,8 @@ class Character: std::shared_ptr currCardAction{ nullptr }; frame_time_t cardActionStartDelay{0}; public: + // Flags which should should block actions for Characters + static const Hit::Flags blockingStatuses = Hit::stun | Hit::freeze | Hit::bubble | Hit::drag; /** * @class Rank @@ -111,11 +113,6 @@ class Character: void AddAction(const PeekCardEvent& event, const ActionOrder& order); void HandleCardEvent(const CardEvent& event, const ActionQueue::ExecutionType& exec); void HandlePeekEvent(const PeekCardEvent& event, const ActionQueue::ExecutionType& exec); - /** - * @brief Returns Hit::Flags containing flags which should block actions. - * @return const Hit::Flags representing blocking statuses - */ - const Hit::Flags GetBlockingStatuses() const; protected: Character::Rank rank; diff --git a/BattleNetwork/bnPlayer.cpp b/BattleNetwork/bnPlayer.cpp index 298329b67..6a1f0bf3c 100644 --- a/BattleNetwork/bnPlayer.cpp +++ b/BattleNetwork/bnPlayer.cpp @@ -99,7 +99,7 @@ void Player::HandleNewStatuses(const Hit::Flags prevStatuses, Hit::Flags applied Action queue should be cleared on blocking status as well, but Entity::HandleNewStatuses already handles this. */ - if (appliedStatuses & (Hit::flinch | GetBlockingStatuses())) { + if (appliedStatuses & (Hit::flinch | Character::blockingStatuses)) { Charge(false); chargeCancel = true; } @@ -442,7 +442,7 @@ void Player::ActivateFormAt(int index) */ ResolveFrameBattleDamage(); // Additionally clear Flinch, so Player never flinches afterwards - ClearStatuses(GetBlockingStatuses() | Hit::flinch); + ClearStatuses(Character::blockingStatuses | Hit::flinch); SaveStats(); activeForm->OnActivate(shared_from_base()); From e76752b9baf4261d095725b8af5c6be8e1f7cc10 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Tue, 30 Sep 2025 22:49:01 -0700 Subject: [PATCH 104/146] Change MoveAction's owner to Entity& --- BattleNetwork/bnEntity.cpp | 11 +++-- BattleNetwork/bnMoveEvent.cpp | 75 ++++++++++++++++------------------- BattleNetwork/bnMoveEvent.h | 6 +-- 3 files changed, 42 insertions(+), 50 deletions(-) diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index 7cad6aef8..26e672bd2 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -607,7 +607,7 @@ bool Entity::Teleport(Battle::Tile* dest, ActionOrder order, std::function(weak_from_this(), MoveData{dest, frames(0), moveStartupDelay, endlagDelay, 0.f, onBegin}) + std::make_shared(*this, MoveData{dest, frames(0), moveStartupDelay, endlagDelay, 0.f, onBegin}) }; actionQueue.Add(event, order, ActionDiscardOp::until_eof); @@ -623,8 +623,7 @@ bool Entity::Slide(Battle::Tile* dest, if (dest && CanMoveTo(dest)) { frame_time_t endlagDelay = moveEndlagDelay ? *moveEndlagDelay : endlag; MoveEvent event = { - - std::make_shared(weak_from_this(), MoveData{dest, slideTime, frames(0), endlagDelay, 0.f, onBegin}) + std::make_shared(*this, MoveData{dest, slideTime, frames(0), endlagDelay, 0.f, onBegin}) }; actionQueue.Add(event, order, ActionDiscardOp::until_eof); @@ -644,7 +643,7 @@ bool Entity::Jump(Battle::Tile* dest, float destHeight, MoveEvent event = { - std::make_shared(weak_from_this(), MoveData{dest, jumpTime, frames(0), endlagDelay, destHeight, onBegin}) + std::make_shared(*this, MoveData{dest, jumpTime, frames(0), endlagDelay, destHeight, onBegin}) }; actionQueue.Add(event, order, ActionDiscardOp::until_eof); @@ -692,7 +691,7 @@ bool Entity::RawMoveEvent(const MoveEvent& event, ActionOrder order) bool Entity::RawMoveEvent(const MoveData& data, ActionOrder order) { if (data.dest) { const MoveEvent e = { - std::make_shared(weak_from_this(), data) + std::make_shared(*this, data) }; return RawMoveEvent(e, order); } @@ -1436,7 +1435,7 @@ void Entity::ResolveFrameBattleDamage() actionQueue.Add( MoveEvent{ - std::make_shared(weak_from_this(), currentDrag) + std::make_shared(*this, currentDrag) }, ActionOrder::immediate, ActionDiscardOp::until_resolve ); diff --git a/BattleNetwork/bnMoveEvent.cpp b/BattleNetwork/bnMoveEvent.cpp index 8418f5253..a3de240d5 100644 --- a/BattleNetwork/bnMoveEvent.cpp +++ b/BattleNetwork/bnMoveEvent.cpp @@ -6,7 +6,7 @@ /// class MoveAction /// -MoveAction::MoveAction(std::weak_ptr owner, const MoveData& data) +MoveAction::MoveAction(Entity& owner, const MoveData& data) : owner(owner), data(data) { } @@ -51,31 +51,31 @@ bool MoveAction::IsTeleporting() const sf::Vector2f MoveAction::GetOwnerStartPosition() { - return owner.lock()->moveStartPosition; + return owner.moveStartPosition; } void MoveAction::SetOwnerStartPosition(sf::Vector2f offset) { - owner.lock()->moveStartPosition = offset; + owner.moveStartPosition = offset; } void MoveAction::SetOwnerJumpHeight(float height) { - owner.lock()->currJumpHeight = height; + owner.currJumpHeight = height; } void MoveAction::SetOwnerPreviousDirection(Direction dir) { - owner.lock()->previousDirection = dir; + owner.previousDirection = dir; } Battle::Tile* MoveAction::GetOwnerPreviousTile() { - return owner.lock()->previous; + return owner.previous; } void MoveAction::UpdateMoveStartPosition() { - owner.lock()->UpdateMoveStartPosition(); + owner.UpdateMoveStartPosition(); } void MoveAction::OnUpdate(frame_time_t elapsed) { @@ -87,15 +87,13 @@ void MoveAction::OnUpdate(frame_time_t elapsed) { // before terminating for nullptr dest. Begin(); - auto owner = this->owner.lock(); - // Only move if we have a valid next tile pointer. // Move is marked complete if there is no destination. if (!data.dest) { - if (owner->GetTile()) + if (owner.GetTile()) { - owner->RefreshPosition(); + owner.RefreshPosition(); } completed = true; @@ -105,7 +103,7 @@ void MoveAction::OnUpdate(frame_time_t elapsed) { // Only move if we have a valid next tile pointer Battle::Tile* next = data.dest; - Battle::Tile* currTile = owner->GetTile(); + Battle::Tile* currTile = owner.GetTile(); elapsedFrames += elapsed; @@ -117,7 +115,7 @@ void MoveAction::OnUpdate(frame_time_t elapsed) { sf::Vector2f pos = GetOwnerStartPosition(); sf::Vector2f tar = next->getPosition(); - sf::Vector2f tileOffset = owner->GetTileOffset(); + sf::Vector2f tileOffset = owner.GetTileOffset(); // Interpolate the sliding position from the start position to the end position sf::Vector2f interpol = tar * delta + (pos * (1.0f - delta)); tileOffset = interpol - pos; @@ -126,10 +124,10 @@ void MoveAction::OnUpdate(frame_time_t elapsed) { // and the slide position offset must be readjusted if (delta >= 0.5f) { // conditions of the target tile may change, ensure by the time we switch - if (owner->CanMoveTo(next)) { + if (owner.CanMoveTo(next)) { reachedDest = true; if (currTile != next) { - owner->AdoptNextTile(); + owner.AdoptNextTile(); } // Adjust for the new current tile, begin halfway approaching the current tile @@ -150,15 +148,15 @@ void MoveAction::OnUpdate(frame_time_t elapsed) { float heightDelta = swoosh::ease::wideParabola(heightElapsed, duration, 1.0f); SetOwnerJumpHeight(heightDelta * data.height); - tileOffset.y -= owner->GetCurrJumpHeight(); - owner->SetTileOffset(tileOffset); + tileOffset.y -= owner.GetCurrJumpHeight(); + owner.SetTileOffset(tileOffset); // When delta is 1.0, the slide duration is complete if (delta == 1.0f) { // Slide or jump is complete, clear the tile offset used in those animations tileOffset = { 0, 0 }; - owner->SetTileOffset(tileOffset); + owner.SetTileOffset(tileOffset); if (IsPendingFinish()) { OnPostMove(); @@ -172,11 +170,9 @@ bool MoveAction::IsPendingFinish() const { } void MoveAction::OnPostMove() { - auto owner = this->owner.lock(); - Battle::Tile* prevTile = GetOwnerPreviousTile(); - Direction previousDirection = owner->GetMoveDirection(); - Battle::Tile* currTile = owner->GetTile(); + Direction previousDirection = owner.GetMoveDirection(); + Battle::Tile* currTile = owner.GetTile(); /* Do not check ice slide if the same Tile was moved to. @@ -189,9 +185,9 @@ void MoveAction::OnPostMove() { const bool sameTile = prevTile == currTile; bool willIceSlide = false; - std::shared_ptr field = owner->GetField(); + std::shared_ptr field = owner.GetField(); - if (!sameTile && currTile->GetState() == TileState::ice && !owner->HasFloatShoe()) { + if (!sameTile && currTile->GetState() == TileState::ice && !owner.HasFloatShoe()) { const int tileX = currTile->GetX(); const int tileY = currTile->GetY(); @@ -215,11 +211,11 @@ void MoveAction::OnPostMove() { // If the next tile is not available, not ice, or we are ice element, don't slide bool notIce = (next && currTile->GetState() != TileState::ice); - bool cannotMove = (next && !owner->CanMoveTo(next)); - bool weAreIce = (owner->GetElement() == Element::aqua); + bool cannotMove = (next && !owner.CanMoveTo(next)); + bool weAreIce = (owner.GetElement() == Element::aqua); bool cancelSlide = (notIce || cannotMove || weAreIce); - willIceSlide = owner->WillSlideOnTiles() && !cancelSlide; + willIceSlide = owner.WillSlideOnTiles() && !cancelSlide; } SetOwnerPreviousDirection(previousDirection); @@ -235,19 +231,18 @@ void MoveAction::OnPostMove() { // TODO: Determine if these really should wait for endlag to be finished. // It's possible OnPostMove should run before endlag is considered, which // could overwrite endlag for ice slide. - if (currTile->GetState() == TileState::sea && owner->GetElement() != Element::aqua && !owner->HasFloatShoe()) { - owner->AddStatus(Hit::root, frames(20)); + if (currTile->GetState() == TileState::sea && owner.GetElement() != Element::aqua && !owner.HasFloatShoe()) { + owner.AddStatus(Hit::root, frames(20)); auto splash = std::make_shared(); field->AddEntity(splash, *currTile); } - else if (currTile->GetState() == TileState::sand && !owner->HasFloatShoe()) { - owner->AddStatus(Hit::root, frames(20)); + else if (currTile->GetState() == TileState::sand && !owner.HasFloatShoe()) { + owner.AddStatus(Hit::root, frames(20)); } } void MoveAction::UpdatePreviousTile() { - auto owner = this->owner.lock(); - owner->previous = data.dest ? data.dest : owner->GetTile(); + owner.previous = data.dest ? data.dest : owner.GetTile(); } void MoveAction::ResetWith(const MoveData& newData) @@ -264,7 +259,7 @@ void MoveAction::ResetWith(const MoveData& newData) } // dest is nullptr until Begin -DragAction::DragAction(std::weak_ptr owner, Hit::Drag drag) : drag(drag), MoveAction(owner, {}) +DragAction::DragAction(Entity& owner, Hit::Drag drag) : drag(drag), MoveAction(owner, {}) { } @@ -279,7 +274,6 @@ void DragAction::Begin() { void DragAction::PrepareFinalMove() { startedFinalMove = true; - auto owner = this->owner.lock(); /* On timing: @@ -292,7 +286,7 @@ void DragAction::PrepareFinalMove() { Note that, because */ - ResetWith(MoveData{ owner->GetTile(), frames(firstMove ? 26 : 23), frames(0), frames(0), 0.f, nullptr}); + ResetWith(MoveData{ owner.GetTile(), frames(firstMove ? 26 : 23), frames(0), frames(0), 0.f, nullptr}); } void DragAction::PrepareMovement() { @@ -300,10 +294,9 @@ void DragAction::PrepareMovement() { return; } - auto owner = this->owner.lock(); - Battle::Tile* currTile = owner->GetTile(); + Battle::Tile* currTile = owner.GetTile(); const bool noDir = drag.dir == Direction::none; - Battle::Tile* dest = noDir ? currTile : owner->GetTile(drag.dir, 1); + Battle::Tile* dest = noDir ? currTile : owner.GetTile(drag.dir, 1); if (!(dest && currTile)) { PrepareFinalMove(); @@ -311,10 +304,10 @@ void DragAction::PrepareMovement() { } - const bool canReachDest = owner->Teammate(currTile->GetTeam()) && owner->CanMoveTo(dest); + const bool canReachDest = owner.Teammate(currTile->GetTeam()) && owner.CanMoveTo(dest); // False if firstMove true, because reachedDest is always false on the first // movement. - const bool canIceSlide = this->reachedDest && owner->WillSlideOnTiles() && currTile->GetState() == TileState::ice && owner->GetElement() != Element::aqua; + const bool canIceSlide = this->reachedDest && owner.WillSlideOnTiles() && currTile->GetState() == TileState::ice && owner.GetElement() != Element::aqua; // Ice slide allows 0 count Drag to continue moving. diff --git a/BattleNetwork/bnMoveEvent.h b/BattleNetwork/bnMoveEvent.h index ce9126e0d..eaa46ba67 100644 --- a/BattleNetwork/bnMoveEvent.h +++ b/BattleNetwork/bnMoveEvent.h @@ -35,7 +35,7 @@ class MoveAction { public: MoveData data; - MoveAction(std::weak_ptr owner, const MoveData& data); + MoveAction(Entity& owner, const MoveData& data); // The underlining move event data may have completed, but the move action // as a whole may queue additional move events (e.g. DragAction). @@ -51,7 +51,7 @@ class MoveAction { bool IsTeleporting() const; virtual void OnUpdate(frame_time_t elapsed); protected: - std::weak_ptr owner; + Entity& owner; frame_time_t elapsedFrames{}; /* Whether or not the MoveEvent is complete. @@ -132,6 +132,6 @@ class DragAction : public MoveAction { void OnPostMove() override; public: - DragAction(std::weak_ptr owner, Hit::Drag drag); + DragAction(Entity& owner, Hit::Drag drag); }; \ No newline at end of file From 985a8e70fe43bfdb80d11c37095fec380062aba4 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Tue, 30 Sep 2025 22:50:34 -0700 Subject: [PATCH 105/146] MoveAction::IsSliding checks positive height --- BattleNetwork/bnMoveEvent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BattleNetwork/bnMoveEvent.cpp b/BattleNetwork/bnMoveEvent.cpp index a3de240d5..0f9ab868a 100644 --- a/BattleNetwork/bnMoveEvent.cpp +++ b/BattleNetwork/bnMoveEvent.cpp @@ -40,7 +40,7 @@ bool MoveAction::IsJumping() const //!< helper function true if sliding bool MoveAction::IsSliding() const { - return data.dest && data.deltaFrames > frames(0) && data.height <= 0.0f; + return data.dest && data.deltaFrames > frames(0) && (+data.height) <= 0.0f; } //!< helper function true if normal moving From 032165467edbc25c7a5a1448aee7a2327c5847ea Mon Sep 17 00:00:00 2001 From: Alrysc Date: Tue, 30 Sep 2025 22:51:47 -0700 Subject: [PATCH 106/146] Change unnecessary <= to == --- BattleNetwork/bnMoveEvent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BattleNetwork/bnMoveEvent.cpp b/BattleNetwork/bnMoveEvent.cpp index 0f9ab868a..ceda0b56d 100644 --- a/BattleNetwork/bnMoveEvent.cpp +++ b/BattleNetwork/bnMoveEvent.cpp @@ -40,7 +40,7 @@ bool MoveAction::IsJumping() const //!< helper function true if sliding bool MoveAction::IsSliding() const { - return data.dest && data.deltaFrames > frames(0) && (+data.height) <= 0.0f; + return data.dest && data.deltaFrames > frames(0) && (+data.height) == 0.0f; } //!< helper function true if normal moving From 54f1692d345702d94e3037d30abf1da5a67273a3 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Wed, 1 Oct 2025 11:02:15 -0700 Subject: [PATCH 107/146] Clarify comment on Hit::drag tick --- BattleNetwork/bnStatusDirector.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/BattleNetwork/bnStatusDirector.cpp b/BattleNetwork/bnStatusDirector.cpp index 5be7f54f4..49c18b9d9 100644 --- a/BattleNetwork/bnStatusDirector.cpp +++ b/BattleNetwork/bnStatusDirector.cpp @@ -144,10 +144,18 @@ void StatusBehaviorDirector::OnUpdate(double elapsed) { if (owner.slideFromDrag) { AppliedStatus& drag = GetStatus(Hit::drag); - // Tick time down and remove even though Drag effects are handled - // using Entity::slideFromDrag instead of the Hit::drag status. - // The Hit::drag tracked on this is still used to trigger status - // callbacks on hit, so tick it down and remove as with other statuses. + /* + Tick time down and remove Drag even though Drag effects may continue. + + Drag is a special case where the status is active for an + indeterminable amount of time, so its effects are based on + Entity::sildeFromDrag instead of this tracked Hit::drag. This means the + status can safely be removed long before its effects are over. + + To trigger status callbacks on hit, Hit::drag still passes through the + StatusBehaviorDirector, so tick it down and remove as with other statuses. + */ + drag.remainingTime -= _elapsed; if (drag.remainingTime > frames(0)) { From efaf01d857cf4de60928997c6979381ee594fa20 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Mon, 6 Oct 2025 12:58:23 -0700 Subject: [PATCH 108/146] Player is no longer marked for decross unless they were in a form This fixes an issue where the battle start state could be skipped if a Player was hit by weakness and then transformed when exiting a future card select. --- BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp | 2 +- BattleNetwork/battlescene/bnMobBattleScene.cpp | 2 +- BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp b/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp index 74e5790d3..5924bb39d 100644 --- a/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp +++ b/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp @@ -185,7 +185,7 @@ void FreedomMissionMobScene::OnHit(Entity& victim, const Hit::Properties& props) GetSelectedCardsUI().SetMultiplier(2); } - if (player->IsSuperEffective(props.element)) { + if (player->IsInForm() && player->IsSuperEffective(props.element)) { playerDecross = true; } } diff --git a/BattleNetwork/battlescene/bnMobBattleScene.cpp b/BattleNetwork/battlescene/bnMobBattleScene.cpp index 1592553e8..26f47cf0b 100644 --- a/BattleNetwork/battlescene/bnMobBattleScene.cpp +++ b/BattleNetwork/battlescene/bnMobBattleScene.cpp @@ -192,7 +192,7 @@ void MobBattleScene::OnHit(Entity& victim, const Hit::Properties& props) GetSelectedCardsUI().SetMultiplier(2); } - if (player->IsSuperEffective(props.element)) { + if (player->IsInForm() && player->IsSuperEffective(props.element)) { playerDecross = true; } } diff --git a/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp b/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp index dea9c37d2..d6718860f 100644 --- a/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp +++ b/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp @@ -243,7 +243,7 @@ void NetworkBattleScene::OnHit(Entity& victim, const Hit::Properties& props) { } } - if (player->IsSuperEffective(props.element)) { + if (player->IsInForm() && player->IsSuperEffective(props.element)) { // animate the transformation back to default form TrackedFormData& formData = GetPlayerFormData(player); @@ -252,6 +252,8 @@ void NetworkBattleScene::OnHit(Entity& victim, const Hit::Properties& props) { formData.selectedForm = -1; } + // TODO: Do we set this flag even if we weren't in a form? + // Get hit by weakness, then transform. if (player == GetLocalPlayer()) { // Local player needs to update their form selections in the card gui cardStatePtr->ResetSelectedForm(); From 5a88175694723846ee9fde5e5dd4b49f7cff1aa7 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Mon, 6 Oct 2025 13:01:42 -0700 Subject: [PATCH 109/146] Correctly handle stun and freeze cancelling each other. Allow Flinch to process with Drag so Flinch animation plays --- BattleNetwork/bnEntity.cpp | 20 ++++++--- BattleNetwork/bnEntity.h | 4 +- BattleNetwork/bnPlayer.cpp | 13 ++++-- BattleNetwork/bnPlayer.h | 2 +- BattleNetwork/bnStatusDirector.cpp | 71 ++++++++++++++++++------------ 5 files changed, 71 insertions(+), 39 deletions(-) diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index 26e672bd2..df2372965 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -238,7 +238,7 @@ void Entity::Init() { hasInit = true; } -void Entity::HandleNewStatuses(const Hit::Flags prevStatuses, const Hit::Flags appliedStatuses) { +void Entity::HandleNewStatuses(const Hit::Flags prevStatuses, Hit::Flags& appliedStatuses) { // Some statuses clear the action queue. // TODO: Neither FinishMove nor clearing the queue ends the animation initiated @@ -252,6 +252,8 @@ void Entity::HandleNewStatuses(const Hit::Flags prevStatuses, const Hit::Flags a if ((appliedStatuses & Hit::freeze) == Hit::freeze) { IceFreeze(); + // IceFreeze removes flash + appliedStatuses &= ~Hit::flash; } @@ -318,7 +320,8 @@ void Entity::Update(double _elapsed) { // Tick all statuses at once statuses.OnUpdate(_elapsed); - HandleNewStatuses(prevStatuses, queuedStatuses & ~statuses.GetQueuedStatuses() & statuses.GetCurrentStatuses()); + Hit::Flags applied = (queuedStatuses & ~statuses.GetQueuedStatuses() & statuses.GetCurrentStatuses()); + HandleNewStatuses(prevStatuses, applied); RefreshShader(); @@ -706,7 +709,9 @@ void Entity::HandleMoveEvent(MoveEvent& event, const ActionQueue::ExecutionType& return; } - if (!currMoveEvent && !IsRooted()) { + // TODO: Hack. Root blocks Drag from being added, which means slideFromDrag is never set false + // if move was + if (!currMoveEvent && (!IsRooted() || dynamic_cast(event.move.get()))) { UpdateMoveStartPosition(); FilterMoveEvent(event); currMoveEvent = event.move; @@ -1611,10 +1616,13 @@ void Entity::IceFreeze() static std::shared_ptr freezesfx = Audio().LoadFromFile(SoundPaths::ICE_FX); // Becoming frozen instantly ends flashing, which includes removing its passthrough effect. - // Removing stun here may be redundant with how the StatusBehaviorDirector filters - // freeze and stun. + // Removing flash here is redundant only if IceFreeze was called because Hit::freeze was added + // by the StatusBehaviorDirector. + // This is considered a reaction to becoming frozen, based on the interaction where qeueuing + // a freeze during timestop where a card activated some flashing effect results in the effect + // being cancelled. SetPassthrough(false); - ClearStatuses(Hit::flash | Hit::stun); + ClearStatuses(Hit::flash); Audio().Play(freezesfx, AudioPriority::highest); if (height <= 48) { diff --git a/BattleNetwork/bnEntity.h b/BattleNetwork/bnEntity.h index e4b01ce23..917adbdf2 100644 --- a/BattleNetwork/bnEntity.h +++ b/BattleNetwork/bnEntity.h @@ -555,6 +555,8 @@ class Entity : involves reactions specific to the Entity, such as Hit::freeze playing a sound effect, as well as running all appropriate status callbacks. + Some reactions may remove statuses in [appliedStatuses]. + This does not include behavior related to ongoing statuses, such as animating blindFx. @@ -565,7 +567,7 @@ class Entity : included Hit::stun, and Hit::stun passed all filtering, its bit would be set) */ - virtual void HandleNewStatuses(const Hit::Flags prevStatuses, const Hit::Flags appliedStatuses); + virtual void HandleNewStatuses(const Hit::Flags prevStatuses, Hit::Flags& appliedStatuses); /** * @brief Get the character's current health diff --git a/BattleNetwork/bnPlayer.cpp b/BattleNetwork/bnPlayer.cpp index 6a1f0bf3c..bfa3633ea 100644 --- a/BattleNetwork/bnPlayer.cpp +++ b/BattleNetwork/bnPlayer.cpp @@ -89,7 +89,7 @@ void Player::RemoveSyncNode(std::shared_ptr syncNode) { } -void Player::HandleNewStatuses(const Hit::Flags prevStatuses, Hit::Flags appliedStatuses) { +void Player::HandleNewStatuses(const Hit::Flags prevStatuses, Hit::Flags& appliedStatuses) { // Tracks whether or not charge has already been cancelled, to avoid repeats bool chargeCancel = false; @@ -104,9 +104,16 @@ void Player::HandleNewStatuses(const Hit::Flags prevStatuses, Hit::Flags applied chargeCancel = true; } + // Clear action queue if flinched, but not if Dragged, since Flinch is allowed + // to process with Drag. If it was cleared during Drag, the Drag movement would + // be incorrectly removed. if (appliedStatuses & Hit::flinch) { - ClearActionQueue(); - Charge(false); + if (!(appliedStatuses & Hit::drag)) { + ClearActionQueue(); + } + if (!chargeCancel) { + Charge(false); + } // At the end of flinch we need to be made idle if possible SetAnimation(recoilAnimHash, [this] { MakeIdle(); }); diff --git a/BattleNetwork/bnPlayer.h b/BattleNetwork/bnPlayer.h index 133065bba..f2c100f4e 100644 --- a/BattleNetwork/bnPlayer.h +++ b/BattleNetwork/bnPlayer.h @@ -163,7 +163,7 @@ class Player : public Character, public AI { std::shared_ptr AddSyncNode(const std::string& point); void RemoveSyncNode(std::shared_ptr syncNode); - virtual void HandleNewStatuses(const Hit::Flags prevStatuses, Hit::Flags appliedStatuses) override; + virtual void HandleNewStatuses(const Hit::Flags prevStatuses, Hit::Flags& appliedStatuses) override; protected: // functions void FinishConstructor(); diff --git a/BattleNetwork/bnStatusDirector.cpp b/BattleNetwork/bnStatusDirector.cpp index 49c18b9d9..97defdf5c 100644 --- a/BattleNetwork/bnStatusDirector.cpp +++ b/BattleNetwork/bnStatusDirector.cpp @@ -48,13 +48,15 @@ Hit::Flags StatusBehaviorDirector::GetAppliedFlags(Hit::Flags flags) { } void StatusBehaviorDirector::ProcessPendingStatuses() { - // Process only Drag if it's queued + // Process only Drag and Flinch if Drag is queued. if ((queuedStatuses & Hit::drag) == Hit::drag) { - ProcessFlags(Hit::drag); + ProcessFlags(queuedStatuses & (Hit::drag | Hit::flinch)); + // Drag is a special case where most behavior is handled // based on this bool. Set true after processing. owner.slideFromDrag = true; - queuedStatuses &= ~Hit::drag; + + queuedStatuses &= ~(Hit::drag | Hit::flinch); return; } @@ -81,33 +83,10 @@ void StatusBehaviorDirector::ProcessFlags(Hit::Flags attack) { currentStatuses &= ~Hit::flash; } - // Flinch|Flash cancels existing Freeze|Stun - if ((attack & flinch_flash) == flinch_flash) { - // If stun is already active, flinch | flash will prevent it from - // being added by this attack. That also means a freeze could be - // committed. - if ((currentStatuses & Hit::stun) == Hit::stun) { - attack &= ~Hit::stun; - } - - currentStatuses &= ~(Hit::freeze | Hit::stun); - } - // If attack did not have Flinch|Flash, some statuses are interested in - // checking one of these flags. - else { - // If stunned is active, prevent flinch - /* - This should mean that an attack which ends stun and flinches will not flinch. - The only way for that to be the case when the attack doesn't also flash is - with Drag | Flinch. TODO: Does an attack like that exist to test? - */ - if (((currentStatuses & Hit::stun) == Hit::stun) && (attack & Hit::flinch)) { - attack &= ~Hit::flinch; - } - } - // Drag cancels existing and queued Freeze // Additionally cancels current or queued Stun and Freeze if Player has Drag + // Does not take freeze out of the attack, since that will be handled by + // GetAppliedFlags. if ((attack & Hit::drag) == Hit::drag) { currentStatuses &= ~Hit::freeze; // TODO: It would be more correct to handle this in GetAppliedFlags, and @@ -125,7 +104,40 @@ void StatusBehaviorDirector::ProcessFlags(Hit::Flags attack) { } } + // Flinch|Flash cancels existing Freeze|Stun + if ((attack & flinch_flash) == flinch_flash) { + // If stun is already active, flinch | flash will prevent it from + // being added by this attack. That also means a freeze could be + // committed, which is correct. Removing Freeze and Stun after this + // makes no difference in the end result if Freeze is finally + // applied. + if ((currentStatuses & Hit::stun) == Hit::stun) { + attack &= ~Hit::stun; + } + + currentStatuses &= ~(Hit::freeze | Hit::stun); + } + // If attack did not have Flinch|Flash, some statuses are interested in + // checking one of these flags. + else { + // If stunned is active, prevent flinch. + // Note that this correctly does not happen if Drag removed the active + // Hit::stun, as that check ran before this one. + if ((currentStatuses & Hit::stun) == Hit::stun) { + attack &= ~Hit::flinch; + } + } + Hit::Flags toApply = GetAppliedFlags(attack); + + // At this point, toApply & (Stun | Freeze) cannot be true, but one of these + // flags can be present. If Freeze is there, remove active Stun, and vice versa. + if (toApply & Hit::stun) { + currentStatuses &= ~Hit::freeze; + } else if (toApply & Hit::freeze) { + currentStatuses &= ~Hit::stun; + } + currentStatuses |= toApply; } @@ -154,6 +166,9 @@ void StatusBehaviorDirector::OnUpdate(double elapsed) { To trigger status callbacks on hit, Hit::drag still passes through the StatusBehaviorDirector, so tick it down and remove as with other statuses. + + Note that Flinch and Flash are allowed to process with Flinch, but do not + count down here. Flinch's remaining time is inconsequential to the Entity. */ drag.remainingTime -= _elapsed; From 5079f49870af357112b747568131d83896eb2a43 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Mon, 6 Oct 2025 13:02:07 -0700 Subject: [PATCH 110/146] Fix possible softlock when countering timefreeze chips on the last possible frame --- .../battlescene/States/bnTimeFreezeBattleState.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp b/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp index 7ab43b13d..020433a1e 100644 --- a/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp +++ b/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp @@ -158,6 +158,9 @@ void TimeFreezeBattleState::onUpdate(double elapsed) summonTick = frames(0); } + // Uses same comparison as CanCounter. + // If they were different, a counter could happen after + // ExecuteTimeFreeze was already called. if (summonTick >= summonTextLength) { scene.HighlightTiles(true); // re-enable tile highlighting for new entities currState = state::animate; // animate this attack @@ -473,7 +476,11 @@ void TimeFreezeBattleState::OnCardActionUsed(std::shared_ptr action, const bool TimeFreezeBattleState::CanCounter(std::shared_ptr user) { // tfc window ended - if (summonTick > summonTextLength) return false; + // Uses same comparison as the display_name state for checking if the action + // should execute. If they were different, a counter could happen after + // ExecuteTimeFreeze was already called, which leads to a softlock. + // With this, an actor cannot counter once the text has disappeared. + if (summonTick >= summonTextLength) return false; // bool addEvent = true; From ae710d9dc0eb856afd9491d52d60c1edf3eee6bc Mon Sep 17 00:00:00 2001 From: Alrysc Date: Mon, 6 Oct 2025 13:03:06 -0700 Subject: [PATCH 111/146] Rename ScriptedPlayer charged_time_table_func and ScriptedPlayerForm calculate_charge_time_func to match each other, both now pass in ScriptedPlayer to Lua --- BattleNetwork/bindings/bnScriptedPlayer.cpp | 4 ++-- BattleNetwork/bindings/bnScriptedPlayer.h | 2 +- BattleNetwork/bindings/bnScriptedPlayerForm.cpp | 6 +++--- BattleNetwork/bindings/bnScriptedPlayerForm.h | 4 ++-- BattleNetwork/bindings/bnUserTypeScriptedPlayer.cpp | 12 ++++++------ 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/BattleNetwork/bindings/bnScriptedPlayer.cpp b/BattleNetwork/bindings/bnScriptedPlayer.cpp index b5aa3933a..d20b152f5 100644 --- a/BattleNetwork/bindings/bnScriptedPlayer.cpp +++ b/BattleNetwork/bindings/bnScriptedPlayer.cpp @@ -187,9 +187,9 @@ void ScriptedPlayer::OnBattleStop() { frame_time_t ScriptedPlayer::CalculateChargeTime(const unsigned chargeLevel) { - if (charge_time_table_func.valid()) + if (charge_time_func.valid()) { - stx::result_t result = CallLuaCallbackExpectingValue(charge_time_table_func, chargeLevel); + stx::result_t result = CallLuaCallbackExpectingValue(charge_time_func, chargeLevel); if (!result.is_error()) { return result.value(); diff --git a/BattleNetwork/bindings/bnScriptedPlayer.h b/BattleNetwork/bindings/bnScriptedPlayer.h index 2f7a4f500..c4a5143b4 100644 --- a/BattleNetwork/bindings/bnScriptedPlayer.h +++ b/BattleNetwork/bindings/bnScriptedPlayer.h @@ -61,7 +61,7 @@ class ScriptedPlayer : public Player, public dynamic_object { sol::object charged_attack_func; sol::object special_attack_func; sol::object on_spawn_func; - sol::object charge_time_table_func; + sol::object charge_time_func; }; #endif \ No newline at end of file diff --git a/BattleNetwork/bindings/bnScriptedPlayerForm.cpp b/BattleNetwork/bindings/bnScriptedPlayerForm.cpp index a7b94ac1f..f761dd95f 100644 --- a/BattleNetwork/bindings/bnScriptedPlayerForm.cpp +++ b/BattleNetwork/bindings/bnScriptedPlayerForm.cpp @@ -110,11 +110,11 @@ std::shared_ptr ScriptedPlayerForm::OnSpecialAction(std::shared_ptr< frame_time_t ScriptedPlayerForm::CalculateChargeTime(unsigned chargeLevel) { - if (!calculate_charge_time_func.valid()) { + if (!charge_time_func.valid()) { return frames(60); } - auto result = CallLuaCallbackExpectingValue(calculate_charge_time_func, chargeLevel); + auto result = CallLuaCallbackExpectingValue(charge_time_func, WeakWrapper(playerWeak), chargeLevel); if (result.is_error()) { Logger::Log(LogLevel::critical, result.error_cstr()); @@ -134,7 +134,7 @@ PlayerForm* ScriptedPlayerFormMeta::BuildForm() ScriptedPlayerForm* form = static_cast(PlayerFormMeta::BuildForm()); form->playerWeak = this->playerWeak; - form->calculate_charge_time_func = this->calculate_charge_time_func; + form->charge_time_func = this->charge_time_func; form->on_activate_func = this->on_activate_func; form->on_deactivate_func = this->on_deactivate_func; form->update_func = this->update_func; diff --git a/BattleNetwork/bindings/bnScriptedPlayerForm.h b/BattleNetwork/bindings/bnScriptedPlayerForm.h index ba84c7526..5e4765c36 100644 --- a/BattleNetwork/bindings/bnScriptedPlayerForm.h +++ b/BattleNetwork/bindings/bnScriptedPlayerForm.h @@ -22,7 +22,7 @@ class ScriptedPlayerForm final : public PlayerForm, public dynamic_object { frame_time_t CalculateChargeTime(unsigned chargeLevel) override; std::weak_ptr playerWeak; - sol::object calculate_charge_time_func; + sol::object charge_time_func; sol::object on_activate_func; sol::object on_deactivate_func; sol::object update_func; @@ -37,7 +37,7 @@ class ScriptedPlayerFormMeta : public PlayerFormMeta { PlayerForm* BuildForm() override; std::weak_ptr playerWeak; - sol::object calculate_charge_time_func; + sol::object charge_time_func; sol::object on_activate_func; sol::object on_deactivate_func; sol::object update_func; diff --git a/BattleNetwork/bindings/bnUserTypeScriptedPlayer.cpp b/BattleNetwork/bindings/bnUserTypeScriptedPlayer.cpp index 0ec665fec..e560f802d 100644 --- a/BattleNetwork/bindings/bnUserTypeScriptedPlayer.cpp +++ b/BattleNetwork/bindings/bnUserTypeScriptedPlayer.cpp @@ -135,10 +135,10 @@ void DefineScriptedPlayerUserType(sol::state& state, sol::table& battle_namespac player.Unwrap()->charged_attack_func = VerifyLuaCallback(value); } ), - "charged_time_table_func", sol::property( - [](WeakWrapper& player) { return player.Unwrap()->charge_time_table_func; }, + "charge_time_func", sol::property( + [](WeakWrapper& player) { return player.Unwrap()->charge_time_func; }, [](WeakWrapper& player, sol::stack_object value) { - player.Unwrap()->charge_time_table_func = VerifyLuaCallback(value); + player.Unwrap()->charge_time_func = VerifyLuaCallback(value); } ), "special_attack_func", sol::property( @@ -162,10 +162,10 @@ void DefineScriptedPlayerUserType(sol::state& state, sol::table& battle_namespac "set_mugshot_texture_path", [] (WeakWrapperChild& form, const std::string& path) { form.Unwrap().SetUIPath(path); }, - "calculate_charge_time_func", sol::property( - [](WeakWrapperChild& form) { return form.Unwrap().calculate_charge_time_func; }, + "charge_time_func", sol::property( + [](WeakWrapperChild& form) { return form.Unwrap().charge_time_func; }, [](WeakWrapperChild& form, sol::stack_object value) { - form.Unwrap().calculate_charge_time_func = VerifyLuaCallback(value); + form.Unwrap().charge_time_func = VerifyLuaCallback(value); } ), "on_activate_func", sol::property( From c8754051064208adac93d5d8847591a2d636d62c Mon Sep 17 00:00:00 2001 From: Alrysc Date: Mon, 6 Oct 2025 13:47:46 -0700 Subject: [PATCH 112/146] Fix grass heal using incorrect timers --- BattleNetwork/bnTile.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/BattleNetwork/bnTile.cpp b/BattleNetwork/bnTile.cpp index c4e90a700..196957c14 100644 --- a/BattleNetwork/bnTile.cpp +++ b/BattleNetwork/bnTile.cpp @@ -613,7 +613,7 @@ namespace Battle { } if (grassHealCooldown1 <= frames(0)) grassHealCooldown1 = frames(20); - if (grassHealCooldown2 <= frames(0)) grassHealCooldown1 = frames(180); + if (grassHealCooldown2 <= frames(0)) grassHealCooldown2 = frames(180); } void Tile::ToggleTimeFreeze(bool state) @@ -774,11 +774,12 @@ namespace Battle { charElement == Element::wood && state == TileState::grass; + const bool heal = doGrassCheck && ( - (grassHealCooldown1 == frames(0) && health <= 9) + (grassHealCooldown1 == frames(0) && health > 9) || - (grassHealCooldown2 == frames(0) && health > 9) + (grassHealCooldown2 == frames(0) && health <= 9) ); if (heal) { From 05941cd255f9a82c632511353d51a134392e073e Mon Sep 17 00:00:00 2001 From: Alrysc Date: Mon, 6 Oct 2025 15:52:59 -0700 Subject: [PATCH 113/146] Add Lua callback for Entity.is_dragged --- BattleNetwork/bindings/bnUserTypeEntity.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/BattleNetwork/bindings/bnUserTypeEntity.h b/BattleNetwork/bindings/bnUserTypeEntity.h index fc6464c9d..26c4b679e 100644 --- a/BattleNetwork/bindings/bnUserTypeEntity.h +++ b/BattleNetwork/bindings/bnUserTypeEntity.h @@ -372,6 +372,9 @@ void DefineEntityFunctionsOn(sol::basic_usertype, sol::basic_refe entity_table["is_blind"] = [](WeakWrapper& entity) -> bool { return entity.Unwrap()->IsBlind(); }; + entity_table["is_dragged"] = [](WeakWrapper& entity) -> bool { + return entity.Unwrap()->HasStatus(Hit::drag); + }; } #endif From 28d9aebf067adf9eaf62e8c731e1a2e6e944b425 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Mon, 6 Oct 2025 15:53:24 -0700 Subject: [PATCH 114/146] Pass weakWrap to Lua charge_time_func --- BattleNetwork/bindings/bnScriptedPlayer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BattleNetwork/bindings/bnScriptedPlayer.cpp b/BattleNetwork/bindings/bnScriptedPlayer.cpp index d20b152f5..2addc6650 100644 --- a/BattleNetwork/bindings/bnScriptedPlayer.cpp +++ b/BattleNetwork/bindings/bnScriptedPlayer.cpp @@ -189,7 +189,7 @@ frame_time_t ScriptedPlayer::CalculateChargeTime(const unsigned chargeLevel) { if (charge_time_func.valid()) { - stx::result_t result = CallLuaCallbackExpectingValue(charge_time_func, chargeLevel); + stx::result_t result = CallLuaCallbackExpectingValue(charge_time_func, weakWrap, chargeLevel); if (!result.is_error()) { return result.value(); From 7e6e162194fae43bace965a53b412cf8e183b381 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Mon, 6 Oct 2025 20:22:00 -0700 Subject: [PATCH 115/146] Character::CanAttackImpl checks blocking statuses --- BattleNetwork/bnCharacter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BattleNetwork/bnCharacter.cpp b/BattleNetwork/bnCharacter.cpp index d3f0796ea..4913640d7 100644 --- a/BattleNetwork/bnCharacter.cpp +++ b/BattleNetwork/bnCharacter.cpp @@ -156,7 +156,7 @@ const bool Character::CanAttack() const } const bool Character::CanAttackImpl() const { - return !currCardAction && Character::blockingStatuses; + return !currCardAction && !HasAnyStatusFrom(Character::blockingStatuses); } void Character::MakeIdle() From ac1e67246b01f8c7dcf13371e556b13c70379ba3 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Mon, 6 Oct 2025 20:23:48 -0700 Subject: [PATCH 116/146] DragAction uses endlag for final move --- BattleNetwork/bnMoveEvent.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/BattleNetwork/bnMoveEvent.cpp b/BattleNetwork/bnMoveEvent.cpp index ceda0b56d..af3037503 100644 --- a/BattleNetwork/bnMoveEvent.cpp +++ b/BattleNetwork/bnMoveEvent.cpp @@ -284,9 +284,13 @@ void DragAction::PrepareFinalMove() { Otherwise, the total inactionable time is 27 frames if pushed one Tile, 31 if two, etc. A move time of 23 achieves this, since each move is 4 frames. - Note that, because + Endlag is used for this time. This means IsMoving/IsSliding will return false during this + movement, which appropriately reflects visuals. + + Note that, because of the timing of movement and slideFromDrag being set, these times + cover for this. */ - ResetWith(MoveData{ owner.GetTile(), frames(firstMove ? 26 : 23), frames(0), frames(0), 0.f, nullptr}); + ResetWith(MoveData{ owner.GetTile(), frames(0), frames(0), frames(firstMove ? 26 : 23), 0.f, nullptr}); } void DragAction::PrepareMovement() { From 231ce7187afbf7d88875fd7fbd3dfb208062f79c Mon Sep 17 00:00:00 2001 From: Alrysc Date: Wed, 8 Oct 2025 12:19:31 -0700 Subject: [PATCH 117/146] Pull Confuse from 8d47940 and c749c3a, slightly modified for StatusBehaviorDirector --- BattleNetwork/bindings/bnUserTypeEntity.h | 3 + BattleNetwork/bindings/bnUserTypeHitbox.cpp | 3 +- BattleNetwork/bnEntity.cpp | 63 ++++++++++++++++-- BattleNetwork/bnEntity.h | 17 +++-- BattleNetwork/bnHitProperties.h | 13 ++-- BattleNetwork/bnPlayerControlledState.cpp | 4 ++ BattleNetwork/bnResourcePaths.h | 5 +- BattleNetwork/bnStatusDirector.cpp | 10 ++- .../scenes/battle/spells/confused.animation | 11 +++ .../scenes/battle/spells/confused.png | Bin 0 -> 1395 bytes BattleNetwork/resources/sfx/confused.ogg | Bin 0 -> 5754 bytes 11 files changed, 107 insertions(+), 22 deletions(-) create mode 100644 BattleNetwork/resources/scenes/battle/spells/confused.animation create mode 100644 BattleNetwork/resources/scenes/battle/spells/confused.png create mode 100644 BattleNetwork/resources/sfx/confused.ogg diff --git a/BattleNetwork/bindings/bnUserTypeEntity.h b/BattleNetwork/bindings/bnUserTypeEntity.h index 26c4b679e..0686d7810 100644 --- a/BattleNetwork/bindings/bnUserTypeEntity.h +++ b/BattleNetwork/bindings/bnUserTypeEntity.h @@ -372,6 +372,9 @@ void DefineEntityFunctionsOn(sol::basic_usertype, sol::basic_refe entity_table["is_blind"] = [](WeakWrapper& entity) -> bool { return entity.Unwrap()->IsBlind(); }; + entity_table["is_confused"] = [](WeakWrapper& entity) -> bool { + return entity.Unwrap()->HasStatus(Hit::confuse); + }; entity_table["is_dragged"] = [](WeakWrapper& entity) -> bool { return entity.Unwrap()->HasStatus(Hit::drag); }; diff --git a/BattleNetwork/bindings/bnUserTypeHitbox.cpp b/BattleNetwork/bindings/bnUserTypeHitbox.cpp index 088610fe5..f53c92e00 100644 --- a/BattleNetwork/bindings/bnUserTypeHitbox.cpp +++ b/BattleNetwork/bindings/bnUserTypeHitbox.cpp @@ -141,7 +141,8 @@ void DefineHitboxUserTypes(sol::state& state, sol::table& battle_namespace) { "Freeze", Hit::freeze, "Drag", Hit::drag, "Blind", Hit::blind, - "NoCounter", Hit::no_counter + "NoCounter", Hit::no_counter, + "Confuse", Hit::confuse ); state.new_usertype("Drag", diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index df2372965..a247eaac8 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -75,8 +75,15 @@ Entity::Entity() : blindFx->Hide(); // default: hidden AddNode(blindFx); + confusedFx = std::make_shared(); + confusedFx->setTexture(Textures().LoadFromFile(TexturePaths::CONFUSED_FX)); + confusedFx->SetLayer(-2); + confusedFx->Hide(); // default: hidden + AddNode(confusedFx); + iceFxAnimation = Animation(AnimationPaths::ICE_FX); blindFxAnimation = Animation(AnimationPaths::BLIND_FX); + confusedFxAnimation = Animation(AnimationPaths::CONFUSED_FX); } Entity::~Entity() { @@ -261,6 +268,10 @@ void Entity::HandleNewStatuses(const Hit::Flags prevStatuses, Hit::Flags& applie Blind(); } + if ((appliedStatuses & Hit::confuse) == Hit::confuse) { + Confuse(); + } + if ((appliedStatuses & Hit::retangible) == Hit::retangible) { SetPassthrough(false); } @@ -328,6 +339,7 @@ void Entity::Update(double _elapsed) { bool stunned = statuses.IsApplied(Hit::stun); bool frozen = statuses.IsApplied(Hit::freeze); bool blind = statuses.IsApplied(Hit::blind); + bool confused = statuses.IsApplied(Hit::confuse); // TODO: Determine if Drag should also be checked here. // The answer is likely yes. @@ -364,6 +376,24 @@ void Entity::Update(double _elapsed) { blindFxAnimation.Update(_elapsed, blindFx->getSprite()); blindFx->Reveal(); } + + // assume this is hidden, will flip to visible if not + confusedFx->Hide(); + if (confused) { + confusedFxAnimation.Update(_elapsed, confusedFx->getSprite()); + confusedFx->Reveal(); + confuseSfxCooldown -= from_seconds(_elapsed); + // Unclear if 55f is the correct timing: this seems to be the one used in source, though, as the confusion SFX only plays twice during a 110f confusion period. + constexpr frame_time_t CONFUSED_SFX_INTERVAL{ 55 }; + if (confuseSfxCooldown <= frames(0)) { + static std::shared_ptr confusedsfx = Audio().LoadFromFile(SoundPaths::CONFUSED_FX); + Audio().Play(confusedsfx, AudioPriority::highest); + confuseSfxCooldown = CONFUSED_SFX_INTERVAL; + } + } + else { + confuseSfxCooldown = frames(0); + } if(canUpdateThisFrame) { OnUpdate(_elapsed); @@ -1180,7 +1210,7 @@ const bool Entity::Hit(Hit::Properties props) { const Hit::Properties original = props; - // If in time freeze, shake immediate on any contact + // If in time freeze, shake immediately on any contact if ((props.flags & Hit::shake) == Hit::shake && IsTimeFrozen()) { CreateComponent(weak_from_this()); } @@ -1370,36 +1400,42 @@ void Entity::ResolveFrameBattleDamage() props.filtered.flags = props.filtered.flags & ~Hit::flash; - if ((props.filtered.flags & Hit::freeze) == Hit::freeze) { + if ((props.filtered.flags & Hit::freeze)) { statuses.AddStatus(Hit::freeze, frames(150)); } props.filtered.flags = props.filtered.flags & ~Hit::freeze; - if ((props.filtered.flags & Hit::stun) == Hit::stun) { + if ((props.filtered.flags & Hit::stun)) { statuses.AddStatus(Hit::stun, frames(120)); } props.filtered.flags = props.filtered.flags & ~Hit::stun; - if ((props.filtered.flags & Hit::bubble) == Hit::bubble) { + if ((props.filtered.flags & Hit::bubble)) { statuses.AddStatus(Hit::bubble, frames(150)); } props.filtered.flags = props.filtered.flags & ~Hit::bubble; - if ((props.filtered.flags & Hit::root) == Hit::root) { + if ((props.filtered.flags & Hit::root)) { statuses.AddStatus(Hit::root, frames(120)); } props.filtered.flags = props.filtered.flags & ~Hit::root; - if ((props.filtered.flags & Hit::blind) == Hit::blind) { + if ((props.filtered.flags & Hit::blind)) { statuses.AddStatus(Hit::blind, frames(300)); } props.filtered.flags = props.filtered.flags & ~Hit::blind; + if ((props.filtered.flags & Hit::confuse)) { + statuses.AddStatus(Hit::confuse, frames(110)); + } + + props.filtered.flags = props.filtered.flags & ~Hit::confuse; + // Add the rest, starting from lowest set bit Hit::Flags curFlag = props.filtered.flags & -props.filtered.flags; while (props.filtered.flags > 0) { @@ -1655,6 +1691,21 @@ void Entity::Blind() blindFxAnimation.Refresh(blindFx->getSprite()); } +void Entity::Confuse() { + constexpr float OFFSET_Y = 10.f; + + float height = -GetHeight() - OFFSET_Y; + std::shared_ptr anim = GetFirstComponent(); + + if (anim && anim->HasPoint("head")) { + height = (anim->GetPoint("head") - anim->GetPoint("origin")).y - OFFSET_Y; + } + + confusedFx->setPosition(0, height); + confusedFxAnimation << "default" << Animator::Mode::Loop; + confusedFxAnimation.Refresh(confusedFx->getSprite()); +} + bool Entity::IsCountered() { return (counterable && !statuses.IsApplied(Hit::stun)); diff --git a/BattleNetwork/bnEntity.h b/BattleNetwork/bnEntity.h index 917adbdf2..05ef77535 100644 --- a/BattleNetwork/bnEntity.h +++ b/BattleNetwork/bnEntity.h @@ -119,7 +119,8 @@ class Entity : std::shared_ptr shadow{ nullptr }; std::shared_ptr iceFx{ nullptr }; std::shared_ptr blindFx{ nullptr }; - Animation iceFxAnimation, blindFxAnimation; + std::shared_ptr confusedFx{ nullptr }; + Animation iceFxAnimation, blindFxAnimation, confusedFxAnimation; /** * @brief Frees one component with the same ID * @param ID ID of the component to remove @@ -829,7 +830,6 @@ class Entity : ActionQueue actionQueue; frame_time_t moveStartupDelay{}; std::optional moveEndlagDelay; - frame_time_t grassHealCooldown{ 0 }; /*!< Timer until next healing is allowed */ StatusBehaviorDirector statuses; bool counterable{}; @@ -854,21 +854,25 @@ class Entity : const int GetMoveCount() const; /*!< Total intended movements made. Used to calculate rank*/ /** - * @brief Stop a character from moving for maxCooldown seconds - * @param maxCooldown + * @brief Handle setup for freeze graphics and SFX * Used internally by class * */ void IceFreeze(); /** - * @brief This entity should not see opponents for maxCooldown seconds - * @param maxCooldown + * @brief Handle setup for blind graphics * Used internally by class * */ void Blind(); + /* + * @brief Handle setup for confuse graphics + * Used internally by class + */ + void Confuse(); + /** * @brief Query if an attack successfully countered a Character * @return true if character is currently countered, false otherwise @@ -938,6 +942,7 @@ class Entity : std::string name; /*!< Name of the entity */ // Controls shader active timing for statuses. Increments every Update and will overflow. uint8_t statusShaderTimer{ 0 }; + frame_time_t confuseSfxCooldown{}; std::queue statusQueue; diff --git a/BattleNetwork/bnHitProperties.h b/BattleNetwork/bnHitProperties.h index 189f9ab02..9e9d8eb5b 100644 --- a/BattleNetwork/bnHitProperties.h +++ b/BattleNetwork/bnHitProperties.h @@ -23,6 +23,7 @@ namespace Hit { const Flags no_counter = 0x00000800; const Flags root = 0x00001000; const Flags blind = 0x00002000; + const Flags confuse = 0x00004000; struct Drag { Direction dir{ Direction::none }; @@ -52,12 +53,12 @@ namespace Hit { Context context{}; }; - const constexpr Hit::Properties DefaultProperties = { - 0, - Flags(Hit::flinch | Hit::impact), - Element::none, - 0, + const constexpr Hit::Properties DefaultProperties = { + 0, + Flags(Hit::flinch | Hit::impact), + Element::none, + 0, Direction::none, true }; -} \ No newline at end of file +} diff --git a/BattleNetwork/bnPlayerControlledState.cpp b/BattleNetwork/bnPlayerControlledState.cpp index 88066cf84..ecace78c3 100644 --- a/BattleNetwork/bnPlayerControlledState.cpp +++ b/BattleNetwork/bnPlayerControlledState.cpp @@ -122,6 +122,10 @@ void PlayerControlledState::OnUpdate(double _elapsed, Player& player) { direction = player.GetTeam() == Team::red ? Direction::right : Direction::left; } + if (player.HasStatus(Hit::confuse)) { + direction = Reverse(direction); + } + if(direction != Direction::none && isIdle && !player.IsRooted()) { Battle::Tile* next_tile = player.GetTile() + direction; std::shared_ptr anim = player.GetFirstComponent(); diff --git a/BattleNetwork/bnResourcePaths.h b/BattleNetwork/bnResourcePaths.h index cd66032cf..38e8f9b53 100644 --- a/BattleNetwork/bnResourcePaths.h +++ b/BattleNetwork/bnResourcePaths.h @@ -30,6 +30,7 @@ namespace TexturePaths { path SPELL_POOF = "resources/scenes/battle/spells/poof.png"; path ICE_FX = "resources/scenes/battle/spells/ice_fx.png"; path BLIND_FX = "resources/scenes/battle/blind.png"; + path CONFUSED_FX = "resources/scenes/battle/spells/confused.png"; //Card Select path CHIP_SELECT_MENU = "resources/ui/card_select.png"; @@ -145,10 +146,12 @@ namespace TexturePaths { namespace AnimationPaths { path ICE_FX = "resources/scenes/battle/spells/ice_fx.animation"; path BLIND_FX = "resources/scenes/battle/blind.animation"; + path CONFUSED_FX = "resources/scenes/battle/spells/confused.animation"; path MISC_COUNTER_REVEAL = "resources/scenes/battle/counter_reveal.animation"; } namespace SoundPaths { path ICE_FX = "resources/sfx/freeze.ogg"; + path CONFUSED_FX = "resources/sfx/confused.ogg"; } -#undef path \ No newline at end of file +#undef path diff --git a/BattleNetwork/bnStatusDirector.cpp b/BattleNetwork/bnStatusDirector.cpp index 97defdf5c..a34ccbf3b 100644 --- a/BattleNetwork/bnStatusDirector.cpp +++ b/BattleNetwork/bnStatusDirector.cpp @@ -25,8 +25,8 @@ Hit::Flags StatusBehaviorDirector::GetAppliedFlags(Hit::Flags flags) { Hit::Flags i = flags & -flags; Hit::Flags m = ~0; - static const Hit::Flags stunMask = ~(Hit::flinch | Hit::freeze); - static const Hit::Flags freezeMask = ~(Hit::flinch | Hit::flash); + static const Hit::Flags stunMask = ~(Hit::flinch | Hit::freeze | Hit::confuse); + static const Hit::Flags freezeMask = ~(Hit::flinch | Hit::flash | Hit::confuse); // NOTE: Flag order is important. stun < freeze < drag while (i != 0) { @@ -138,6 +138,12 @@ void StatusBehaviorDirector::ProcessFlags(Hit::Flags attack) { currentStatuses &= ~Hit::stun; } + // If Confuse is still here after GetAppliedFlags, it must not have been filtered + // off by Stun or Freeze. It will remove an active Stun or Freeze. + if (toApply & Hit::confuse) { + currentStatuses &= ~(Hit::stun | Hit::freeze); + } + currentStatuses |= toApply; } diff --git a/BattleNetwork/resources/scenes/battle/spells/confused.animation b/BattleNetwork/resources/scenes/battle/spells/confused.animation new file mode 100644 index 000000000..7b0f7c283 --- /dev/null +++ b/BattleNetwork/resources/scenes/battle/spells/confused.animation @@ -0,0 +1,11 @@ +imagePath="confused.png" + +animation state="DEFAULT" +frame duration="0.05" x="0" y="0" w="49" h="28" originx="24" originy="14" +frame duration="0.05" x="0" y="28" w="49" h="28" originx="24" originy="14" +frame duration="0.05" x="0" y="56" w="49" h="28" originx="24" originy="14" +frame duration="0.05" x="0" y="84" w="49" h="28" originx="24" originy="14" +frame duration="0.05" x="0" y="112" w="49" h="28" originx="24" originy="14" +frame duration="0.05" x="0" y="140" w="49" h="28" originx="24" originy="14" +frame duration="0.05" x="0" y="168" w="49" h="28" originx="24" originy="14" +frame duration="0.05" x="0" y="196" w="49" h="28" originx="24" originy="14" diff --git a/BattleNetwork/resources/scenes/battle/spells/confused.png b/BattleNetwork/resources/scenes/battle/spells/confused.png new file mode 100644 index 0000000000000000000000000000000000000000..ae1c0a6181a432c6100dcab7ea31c8cec830764a GIT binary patch literal 1395 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 B_gw%0 literal 0 HcmV?d00001 From e467f0ddfba6b90ecc9f46a95470a57d91fb98bb Mon Sep 17 00:00:00 2001 From: Alrysc Date: Thu, 9 Oct 2025 20:14:20 -0700 Subject: [PATCH 118/146] End confusion when hit by stun or freeze --- BattleNetwork/bnStatusDirector.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/BattleNetwork/bnStatusDirector.cpp b/BattleNetwork/bnStatusDirector.cpp index a34ccbf3b..766ae0aca 100644 --- a/BattleNetwork/bnStatusDirector.cpp +++ b/BattleNetwork/bnStatusDirector.cpp @@ -123,6 +123,9 @@ void StatusBehaviorDirector::ProcessFlags(Hit::Flags attack) { // If stunned is active, prevent flinch. // Note that this correctly does not happen if Drag removed the active // Hit::stun, as that check ran before this one. + // + // This correctly implies that an attack that confuses and flinches, but does + // not flash, will cancel stun and still will not flinch. if ((currentStatuses & Hit::stun) == Hit::stun) { attack &= ~Hit::flinch; } @@ -130,12 +133,13 @@ void StatusBehaviorDirector::ProcessFlags(Hit::Flags attack) { Hit::Flags toApply = GetAppliedFlags(attack); - // At this point, toApply & (Stun | Freeze) cannot be true, but one of these - // flags can be present. If Freeze is there, remove active Stun, and vice versa. + // At this point, toApply can have Stun OR Freeze, but not both. + // If Freeze is there, remove active Stun, and vice versa. + // Both remove active Confuse. if (toApply & Hit::stun) { - currentStatuses &= ~Hit::freeze; + currentStatuses &= ~(Hit::freeze | Hit::confuse); } else if (toApply & Hit::freeze) { - currentStatuses &= ~Hit::stun; + currentStatuses &= ~(Hit::stun | Hit::confuse); } // If Confuse is still here after GetAppliedFlags, it must not have been filtered From 2736b6ee4751effa8a95c3aaebf9dee5373b136c Mon Sep 17 00:00:00 2001 From: Alrysc Date: Fri, 10 Oct 2025 12:04:39 -0700 Subject: [PATCH 119/146] Pull Cust read/write and changes from 22cafc542a71c7b4c662e9c89bc3357edab2dc88, a9aadba58f9906b78dbc17b3d577313b982852ee, d60c4ba6f0733b2d38c5f5f274fb4dc46af0b718, and 993de6e60c2105c176f46da144b5a8e533cc0216, extra changes to use frame_time_t, fix graphical error when setting max time, and made setting max time also reset progress to match previous percentage --- .../States/bnCardSelectBattleState.cpp | 2 +- .../States/bnCombatBattleState.cpp | 3 +- .../battlescene/bnBattleSceneBase.cpp | 64 ++++++++++++++----- BattleNetwork/battlescene/bnBattleSceneBase.h | 15 +++-- BattleNetwork/bnScriptResourceManager.cpp | 48 ++++++++++++++ BattleNetwork/bnScriptResourceManager.h | 6 ++ 6 files changed, 114 insertions(+), 24 deletions(-) diff --git a/BattleNetwork/battlescene/States/bnCardSelectBattleState.cpp b/BattleNetwork/battlescene/States/bnCardSelectBattleState.cpp index c016e5e73..52ca63d77 100644 --- a/BattleNetwork/battlescene/States/bnCardSelectBattleState.cpp +++ b/BattleNetwork/battlescene/States/bnCardSelectBattleState.cpp @@ -69,7 +69,7 @@ void CardSelectBattleState::onStart(const BattleSceneState*) Audio().Play(AudioType::CUSTOM_SCREEN_OPEN); // Reset bar and related flags - scene.SetCustomBarProgress(0.0); + scene.SetCustomBarProgress(frames(0)); // Load the next cards cardCust.ResetState(); diff --git a/BattleNetwork/battlescene/States/bnCombatBattleState.cpp b/BattleNetwork/battlescene/States/bnCombatBattleState.cpp index 4912847a8..6ff1309c2 100644 --- a/BattleNetwork/battlescene/States/bnCombatBattleState.cpp +++ b/BattleNetwork/battlescene/States/bnCombatBattleState.cpp @@ -161,8 +161,7 @@ void CombatBattleState::onUpdate(double elapsed) } if (isPaused) return; // do not update anything else - - scene.SetCustomBarProgress(scene.GetCustomBarProgress() + elapsed); + scene.SetCustomBarProgress(scene.GetCustomBarProgress() + from_seconds(elapsed)); // Update the field. This includes the player. // After this function, the player may have used a card. diff --git a/BattleNetwork/battlescene/bnBattleSceneBase.cpp b/BattleNetwork/battlescene/bnBattleSceneBase.cpp index 128d938f8..8fb07de7a 100644 --- a/BattleNetwork/battlescene/bnBattleSceneBase.cpp +++ b/BattleNetwork/battlescene/bnBattleSceneBase.cpp @@ -44,8 +44,9 @@ BattleSceneBase::BattleSceneBase(ActivityController& controller, BattleSceneBase comboDeleteCounter(0), totalCounterMoves(0), totalCounterDeletions(0), - customProgress(0), - customDuration(10), + customProgress(frames(0)), + customDuration(frames(512)), + customDefaultDuration(frames(512)), whiteShader(Shaders().GetShader(ShaderType::WHITE_FADE)), backdropShader(Shaders().GetShader(ShaderType::BLACK_FADE)), yellowShader(Shaders().GetShader(ShaderType::YELLOW)), @@ -166,13 +167,21 @@ BattleSceneBase::BattleSceneBase(ActivityController& controller, BattleSceneBase setView(sf::Vector2u(480, 320)); - // add the camera to our event bus - channel.Register(&camera); + // Camera and scripts can be triggered by scene events + channel.Register(&camera, &Scripts(), this); + + // create bi-directional communication + Scripts().SetEventChannel(channel); + + Scripts().SetKeyValue("cust_gauge_default_max_time", std::to_string(customDefaultDuration.count())); + channel.Emit(&ScriptResourceManager::SetKeyValue, "cust_gauge_max_time", std::to_string(customDuration.count())); } BattleSceneBase::~BattleSceneBase() { - // drop the camera from our event bus - channel.Drop(&camera); + // drop the registered items from the bus + channel.Drop(&camera, &Scripts(), this); + + Scripts().DropEventChannel(); for (BattleSceneState* statePtr : states) { delete statePtr; @@ -305,12 +314,12 @@ void BattleSceneBase::HighlightTiles(bool enable) this->highlightTiles = enable; } -const double BattleSceneBase::GetCustomBarProgress() const +const frame_time_t BattleSceneBase::GetCustomBarProgress() const { return this->customProgress; } -const double BattleSceneBase::GetCustomBarDuration() const +const frame_time_t BattleSceneBase::GetCustomBarDuration() const { return this->customDuration; } @@ -320,7 +329,7 @@ const bool BattleSceneBase::IsCustGaugeFull() const return isGaugeFull; } -void BattleSceneBase::SetCustomBarProgress(double value) +void BattleSceneBase::SetCustomBarProgress(frame_time_t value) { this->customProgress = value; @@ -328,14 +337,36 @@ void BattleSceneBase::SetCustomBarProgress(double value) isGaugeFull = false; } + float percentage = (float)customProgress.count() / (float)customDuration.count(); + if (customBarShader) { - customBarShader->setUniform("factor", std::min(1.0f, (float)(customProgress/customDuration))); + customBarShader->setUniform("factor", std::min(1.0f, percentage)); } + + if (percentage >= 1.0) { + percentage = 0.0; + } + + channel.Emit(&ScriptResourceManager::SetKeyValue, "cust_gauge_time", std::to_string(customProgress.count())); + channel.Emit(&ScriptResourceManager::SetKeyValue, "cust_gauge_value", std::to_string(percentage)); } -void BattleSceneBase::SetCustomBarDuration(double maxTimeSeconds) +void BattleSceneBase::SetCustomBarDuration(frame_time_t maxTimeFrames) { - this->customDuration = maxTimeSeconds; + const float percentage = (float)customProgress.count() / (float)customDuration.count(); + + customDuration = std::max(frames(1), maxTimeFrames); + + // Recalculate progress so that percentage stays the same. + const frame_time_t newProgress = from_seconds(customDuration.asSeconds().value * percentage); + + // Update progress and percentage, which may be slightly different now + SetCustomBarProgress(newProgress); + channel.Emit(&ScriptResourceManager::SetKeyValue, "cust_gauge_max_time", std::to_string(maxTimeFrames.count())); +} + +void BattleSceneBase::ResetCustomBarDuration() { + SetCustomBarDuration(customDefaultDuration); } void BattleSceneBase::SubscribeToCardActions(CardActionUsePublisher& publisher) @@ -419,6 +450,9 @@ sf::Vector2f BattleSceneBase::PerspectiveOrigin(const sf::Vector2f& origin, cons void BattleSceneBase::SpawnLocalPlayer(int x, int y) { if (hasPlayerSpawned) return; + + localPlayerSpawnIndex = otherPlayers.size(); + hasPlayerSpawned = true; Team team = field->GetAt(x, y)->GetTeam(); @@ -731,7 +765,7 @@ void BattleSceneBase::onUpdate(double elapsed) { current->onUpdate(elapsed); - if (customProgress / customDuration >= 1.0 && !isGaugeFull) { + if ((float)customProgress.count() / (float)customDuration.count() >= 1.0 && !isGaugeFull) { isGaugeFull = true; Audio().Play(AudioType::CUSTOM_BAR_FULL); } @@ -843,7 +877,7 @@ void BattleSceneBase::onUpdate(double elapsed) { // custom bar continues to animate when it is already full if (isGaugeFull) { - customFullAnimDelta += elapsed/customDuration; + customFullAnimDelta += elapsed / customDuration.asSeconds().value; customBarShader->setUniform("factor", (float)(1.0 + customFullAnimDelta)); } @@ -1146,7 +1180,7 @@ std::vector> BattleSceneBase::GetOtherPlayers() std::vector> BattleSceneBase::GetAllPlayers() { std::vector> result = otherPlayers; - result.insert(result.begin(), localPlayer); + result.insert(result.begin() + localPlayerSpawnIndex, localPlayer); return result; } diff --git a/BattleNetwork/battlescene/bnBattleSceneBase.h b/BattleNetwork/battlescene/bnBattleSceneBase.h index 36bb4f014..f6bfc2dbd 100644 --- a/BattleNetwork/battlescene/bnBattleSceneBase.h +++ b/BattleNetwork/battlescene/bnBattleSceneBase.h @@ -102,8 +102,9 @@ class BattleSceneBase : int newRedTeamMobSize{ 0 }, newBlueTeamMobSize{ 0 }; frame_time_t frameNumber{ 0 }; double elapsed{ 0 }; /*!< total time elapsed in battle */ - double customProgress{ 0 }; /*!< Cust bar progress in seconds */ - double customDuration{ 10.0 }; /*!< Cust bar max time in seconds */ + frame_time_t customProgress{}; /*!< Cust bar progress in seconds */ + frame_time_t customDuration{}; /*!< Cust bar max time in seconds */ + frame_time_t customDefaultDuration{}; /*!< Default value */ double customFullAnimDelta{ 0 }; /*!< For animating a complete cust bar*/ double backdropOpacity{ 1.0 }; double backdropFadeIncrements{ 125 }; /*!< x/255 per tick */ @@ -119,6 +120,7 @@ class BattleSceneBase : std::shared_ptr localPlayer; /*!< Local player */ std::vector deletingRedMobs, deletingBlueMobs; /*!< mobs untrack enemies but we need to know when they fully finish deleting*/ std::vector> otherPlayers; /*!< Player array supports multiplayer */ + size_t localPlayerSpawnIndex{}; /*!< The index in the `otherPlayers` hash to respect spawn order relative to the local player*/ std::map allPlayerFormsHash; std::map allPlayerTeamHash; /*!< Check previous frames teams for traitors */ Mob* redTeamMob{ nullptr }; /*!< Mob and mob data opposing team are fighting against */ @@ -310,11 +312,12 @@ class BattleSceneBase : void HandleCounterLoss(Entity& subject, bool playsound); void HighlightTiles(bool enable); - const double GetCustomBarProgress() const; - const double GetCustomBarDuration() const; - void SetCustomBarProgress(double value); - void SetCustomBarDuration(double maxTimeSeconds); + const frame_time_t GetCustomBarProgress() const; + const frame_time_t GetCustomBarDuration() const; + void SetCustomBarProgress(frame_time_t value); + void SetCustomBarDuration(frame_time_t maxTimeFrames); + void ResetCustomBarDuration(); void DrawCustGauage(sf::RenderTexture& surface); void SubscribeToCardActions(CardActionUsePublisher& publisher); const std::vector>& GetCardActionSubscriptions() const; diff --git a/BattleNetwork/bnScriptResourceManager.cpp b/BattleNetwork/bnScriptResourceManager.cpp index d0e04f7d2..900ee39eb 100644 --- a/BattleNetwork/bnScriptResourceManager.cpp +++ b/BattleNetwork/bnScriptResourceManager.cpp @@ -187,6 +187,18 @@ sol::object ScriptResourceManager::PrintInvalidAssignMessage( sol::table table, return sol::lua_nil; } +void ScriptResourceManager::SetKeyValue(const std::string& key, const std::string& value) { + keys[key] = value; +} + +void ScriptResourceManager::SetEventChannel(EventBus::Channel& channel) { + eventChannel = &channel; +} + +void ScriptResourceManager::DropEventChannel() { + eventChannel = nullptr; +} + stx::result_t ScriptResourceManager::GetCurrentFile(lua_State* L) { lua_Debug ar; @@ -499,6 +511,42 @@ void ScriptResourceManager::ConfigureEnvironment(ScriptPackage& scriptPackage) { } ); + battle_namespace.set_function("get_cust_gauge_value", + [this] { + return std::atof(keys["cust_gauge_value"].c_str()); + }); + + battle_namespace.set_function("get_cust_gauge_time", + [this]() { + return std::atof(keys["cust_gauge_time"].c_str()); + }); + + battle_namespace.set_function("get_cust_gauge_max_time", + [this]() { + return std::atof(keys["cust_gauge_max_time"].c_str()); + }); + + battle_namespace.set_function("get_default_cust_gauge_max_time", + [this]() { + return std::atof(keys["cust_gauge_default_max_time"].c_str()); + }); + + battle_namespace.set_function("set_cust_gauge_time", + [this](frame_time_t frames) { + if (eventChannel == nullptr) return; + eventChannel->Emit(&BattleSceneBase::SetCustomBarProgress, frames); + }); + + battle_namespace.set_function("set_cust_gauge_max_time", + [this](frame_time_t frames) { + eventChannel->Emit(&BattleSceneBase::SetCustomBarDuration, frames); + }); + + battle_namespace.set_function("reset_cust_gauge_to_default", + [this]() { + eventChannel->Emit(&BattleSceneBase::ResetCustomBarDuration); + }); + const auto& elements_table = state.new_enum("Element", "Fire", Element::fire, "Aqua", Element::aqua, diff --git a/BattleNetwork/bnScriptResourceManager.h b/BattleNetwork/bnScriptResourceManager.h index 7104672d5..b95b52caf 100644 --- a/BattleNetwork/bnScriptResourceManager.h +++ b/BattleNetwork/bnScriptResourceManager.h @@ -19,6 +19,7 @@ #include #include "bnPackageAddress.h" +#include "bnEventBus.h" class CardPackagePartitioner; @@ -55,7 +56,12 @@ class ScriptResourceManager { static sol::object PrintInvalidAccessMessage(sol::table table, const std::string typeName, const std::string key ); static sol::object PrintInvalidAssignMessage(sol::table table, const std::string typeName, const std::string key ); + void SetKeyValue(const std::string& key, const std::string& value); + void SetEventChannel(EventBus::Channel& channel); + void DropEventChannel(); private: + EventBus::Channel* eventChannel{ nullptr }; + std::map keys; std::map state2package; /*!< lua state pointer to script package */ std::map address2package; /*!< PackageAddress to script package */ CardPackagePartitioner* cardPartition{ nullptr }; From 781008fd50c24de395b4ab9a8cf906295782f02d Mon Sep 17 00:00:00 2001 From: Alrysc Date: Fri, 10 Oct 2025 12:11:30 -0700 Subject: [PATCH 120/146] Fix Custom bar animation speed being based on current duration instead of default --- BattleNetwork/battlescene/bnBattleSceneBase.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BattleNetwork/battlescene/bnBattleSceneBase.cpp b/BattleNetwork/battlescene/bnBattleSceneBase.cpp index 8fb07de7a..30eecf52b 100644 --- a/BattleNetwork/battlescene/bnBattleSceneBase.cpp +++ b/BattleNetwork/battlescene/bnBattleSceneBase.cpp @@ -877,7 +877,7 @@ void BattleSceneBase::onUpdate(double elapsed) { // custom bar continues to animate when it is already full if (isGaugeFull) { - customFullAnimDelta += elapsed / customDuration.asSeconds().value; + customFullAnimDelta += elapsed / customDefaultDuration.asSeconds().value; customBarShader->setUniform("factor", (float)(1.0 + customFullAnimDelta)); } From f1897b555798d266a3edba11965877d1deb2c4ba Mon Sep 17 00:00:00 2001 From: Alrysc Date: Fri, 10 Oct 2025 12:21:43 -0700 Subject: [PATCH 121/146] Add Battle.get_turn_count --- BattleNetwork/battlescene/bnBattleSceneBase.cpp | 2 ++ BattleNetwork/bnScriptResourceManager.cpp | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/BattleNetwork/battlescene/bnBattleSceneBase.cpp b/BattleNetwork/battlescene/bnBattleSceneBase.cpp index 30eecf52b..f2058f211 100644 --- a/BattleNetwork/battlescene/bnBattleSceneBase.cpp +++ b/BattleNetwork/battlescene/bnBattleSceneBase.cpp @@ -175,6 +175,7 @@ BattleSceneBase::BattleSceneBase(ActivityController& controller, BattleSceneBase Scripts().SetKeyValue("cust_gauge_default_max_time", std::to_string(customDefaultDuration.count())); channel.Emit(&ScriptResourceManager::SetKeyValue, "cust_gauge_max_time", std::to_string(customDuration.count())); + channel.Emit(&ScriptResourceManager::SetKeyValue, "turn_count", std::to_string(turn)); } BattleSceneBase::~BattleSceneBase() { @@ -1278,6 +1279,7 @@ void BattleSceneBase::BroadcastBattleStop() void BattleSceneBase::IncrementTurnCount() { turn++; + channel.Emit(&ScriptResourceManager::SetKeyValue, "turn_count", std::to_string(turn)); } void BattleSceneBase::IncrementRoundCount() diff --git a/BattleNetwork/bnScriptResourceManager.cpp b/BattleNetwork/bnScriptResourceManager.cpp index 900ee39eb..f34b7f7f9 100644 --- a/BattleNetwork/bnScriptResourceManager.cpp +++ b/BattleNetwork/bnScriptResourceManager.cpp @@ -511,6 +511,11 @@ void ScriptResourceManager::ConfigureEnvironment(ScriptPackage& scriptPackage) { } ); + battle_namespace.set_function("get_turn_count", + [this] { + return std::atof(keys["turn_count"].c_str()); + }); + battle_namespace.set_function("get_cust_gauge_value", [this] { return std::atof(keys["cust_gauge_value"].c_str()); From 017ef0479d66727ac2e5f8f6faa0804ab95de395 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Fri, 10 Oct 2025 20:00:26 -0700 Subject: [PATCH 122/146] Added secondaryElement to HitProps, some durations for certain statuses, extra functions for Lua to add status durations --- .../battlescene/bnFreedomMissionMobScene.cpp | 6 +- .../battlescene/bnMobBattleScene.cpp | 7 +- BattleNetwork/bindings/bnUserTypeHitbox.cpp | 114 ++++++++++++++++-- BattleNetwork/bnEntity.cpp | 25 ++-- BattleNetwork/bnHitProperties.h | 9 ++ BattleNetwork/bnTile.cpp | 12 +- .../battlescene/bnNetworkBattleScene.cpp | 6 +- 7 files changed, 145 insertions(+), 34 deletions(-) diff --git a/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp b/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp index 5924bb39d..6fe5d8303 100644 --- a/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp +++ b/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp @@ -177,6 +177,8 @@ void FreedomMissionMobScene::Init() void FreedomMissionMobScene::OnHit(Entity& victim, const Hit::Properties& props) { std::shared_ptr player = GetLocalPlayer(); + + const bool superEffective = props.damage > 0 && (victim.IsSuperEffective(props.element) || victim.IsSuperEffective(props.secondaryElement)); if (player.get() == &victim && props.damage > 0) { playerHitCount++; @@ -185,12 +187,12 @@ void FreedomMissionMobScene::OnHit(Entity& victim, const Hit::Properties& props) GetSelectedCardsUI().SetMultiplier(2); } - if (player->IsInForm() && player->IsSuperEffective(props.element)) { + if (player->IsInForm() && superEffective) { playerDecross = true; } } - if (victim.IsSuperEffective(props.element) && props.damage > 0) { + if (superEffective) { std::shared_ptr seSymbol = std::make_shared(); seSymbol->SetLayer(-100); seSymbol->SetHeight(victim.GetHeight()+(victim.getLocalBounds().height*0.5f)); // place it at sprite height diff --git a/BattleNetwork/battlescene/bnMobBattleScene.cpp b/BattleNetwork/battlescene/bnMobBattleScene.cpp index 26f47cf0b..8566cda4f 100644 --- a/BattleNetwork/battlescene/bnMobBattleScene.cpp +++ b/BattleNetwork/battlescene/bnMobBattleScene.cpp @@ -184,6 +184,8 @@ void MobBattleScene::Init() void MobBattleScene::OnHit(Entity& victim, const Hit::Properties& props) { std::shared_ptr player = GetLocalPlayer(); + + const bool superEffective = props.damage > 0 && (victim.IsSuperEffective(props.element) || victim.IsSuperEffective(props.secondaryElement)); if (player.get() == &victim && props.damage > 0) { playerHitCount++; @@ -192,13 +194,12 @@ void MobBattleScene::OnHit(Entity& victim, const Hit::Properties& props) GetSelectedCardsUI().SetMultiplier(2); } - if (player->IsInForm() && player->IsSuperEffective(props.element)) { + if (player->IsInForm() && superEffective) { playerDecross = true; } } - bool freezeBreak = victim.IsIceFrozen() && ((props.flags & Hit::breaking) == Hit::breaking); - bool superEffective = victim.IsSuperEffective(props.element) && props.damage > 0; + const bool freezeBreak = victim.IsIceFrozen() && ((props.flags & Hit::breaking) == Hit::breaking); if (freezeBreak || superEffective) { std::shared_ptr seSymbol = std::make_shared(); diff --git a/BattleNetwork/bindings/bnUserTypeHitbox.cpp b/BattleNetwork/bindings/bnUserTypeHitbox.cpp index f53c92e00..16c65d439 100644 --- a/BattleNetwork/bindings/bnUserTypeHitbox.cpp +++ b/BattleNetwork/bindings/bnUserTypeHitbox.cpp @@ -107,23 +107,117 @@ void DefineHitboxUserTypes(sol::state& state, sol::table& battle_namespace) { ) ); + auto createHitProps = + [](int damage, + Hit::Flags flags, + Element element, + Element secondaryElement, + std::optional optCtx, + Hit::Drag drag) { + Hit::Properties props = { static_cast(damage), flags, element, secondaryElement, 0, drag }; - state.new_usertype("HitProps", - sol::factories([](int damage, Hit::Flags flags, Element element, std::optional contextOptional, Hit::Drag drag) { - Hit::Properties props = { damage, flags, element, 0, drag }; + if (optCtx) { + props.context = *optCtx; + props.aggressor = props.context.aggressor; + } - if (contextOptional) { - props.context = *contextOptional; - props.aggressor = props.context.aggressor; - } + return props; + }; - return props; - }), + state.new_usertype("HitProps", + sol::factories( + // deprecated API in v2.5 + createHitProps, + [createHitProps](int damage, Hit::Flags flags, Element element, std::optional optCtx, Hit::Drag drag) { + Logger::Log(LogLevel::warning, + return createHitProps(damage, flags, element, Element::none, optCtx, drag); + }, + // Cover for scripters who passed in Entity ID, which did nothing but is considered an + // error now without this constructor + [createHitProps](int damage, Hit::Flags flags, Element element, EntityID_t id, Hit::Drag drag) { + Logger::Log(LogLevel::warning, + return createHitProps(damage, flags, element, Element::none, std::nullopt, drag); + }, + [createHitProps](std::optional optCtx) -> Hit::Properties { + return createHitProps(0, Hit::none, Element::none, Element::none, optCtx, Hit::Drag{}); + } + ), + // deprecated API in v2.5 "aggressor", &Hit::Properties::aggressor, "damage", &Hit::Properties::damage, "drag", &Hit::Properties::drag, "element", &Hit::Properties::element, - "flags", &Hit::Properties::flags + "element2", &Hit::Properties::secondaryElement, + "flags", &Hit::Properties::flags, + + // New API in v2.5 + "from", [](Hit::Properties& self, Hit::Context ctx) -> Hit::Properties& { self.aggressor = ctx.aggressor; self.context = ctx; return self; }, + "dmg", [](Hit::Properties& self, int damage) -> Hit::Properties& { self.damage = static_cast(damage); return self; }, + "drg", [](Hit::Properties& self, Hit::Drag drag) -> Hit::Properties& { self.drag = drag; return self; }, + "elem", [](Hit::Properties& self, Element element) -> Hit::Properties& { self.element = element; return self; }, + "elem2", [](Hit::Properties& self, Element element) -> Hit::Properties& { self.secondaryElement = element; return self; }, + + // Add specific flags, some with duration + "retangible", [](Hit::Properties& self) -> Hit::Properties& { self.flags = self.flags | Hit::retangible; return self; }, + "stun", sol::overload( + [](Hit::Properties& self, frame_time_t duration) -> Hit::Properties& { + self.flags = self.flags | Hit::stun; + self.stun_duration = duration; + return self; + }, + [](Hit::Properties& self) -> Hit::Properties& { self.flags = self.flags | Hit::stun; return self; } + ), + "pierce", [](Hit::Properties& self) -> Hit::Properties& { self.flags = self.flags | Hit::pierce; return self; }, + "flinch", [](Hit::Properties& self) -> Hit::Properties& { self.flags = self.flags | Hit::flinch; return self; }, + "shake", [](Hit::Properties& self) -> Hit::Properties& { self.flags = self.flags | Hit::shake; return self; }, + "freeze", sol::overload( + [](Hit::Properties& self, frame_time_t duration) -> Hit::Properties& { + self.flags = self.flags | Hit::freeze; + self.freeze_duration = duration; + return self; + }, + [](Hit::Properties& self) -> Hit::Properties& { self.flags = self.flags | Hit::freeze; return self; } + ), + "flash", sol::overload( + [](Hit::Properties& self, frame_time_t duration) -> Hit::Properties& { + self.flags = self.flags | Hit::flash; + self.flash_duration = duration; + return self; + }, + [](Hit::Properties& self) -> Hit::Properties& { self.flags = self.flags | Hit::flash; return self; } + ), + "breaking", [](Hit::Properties& self) -> Hit::Properties& { self.flags = self.flags | Hit::breaking; return self; }, + "impact", [](Hit::Properties& self) -> Hit::Properties& { self.flags = self.flags | Hit::impact; return self; }, + "drag", [](Hit::Properties& self, Hit::Drag drag) -> Hit::Properties& { + self.flags = self.flags | Hit::drag; + self.drag = drag; + return self; + }, + "no_counter", [](Hit::Properties& self) -> Hit::Properties& { self.flags = self.flags | Hit::no_counter; return self; }, + "root", sol::overload( + [](Hit::Properties& self, frame_time_t duration) -> Hit::Properties& { + self.flags = self.flags | Hit::root; + self.root_duration = duration; + return self; + }, + [](Hit::Properties& self) -> Hit::Properties& { self.flags = self.flags | Hit::root; return self; } + ), + "blind", sol::overload( + [](Hit::Properties& self, frame_time_t duration) -> Hit::Properties& { + self.flags = self.flags | Hit::blind; + self.blind_duration = duration; + return self; + }, + [](Hit::Properties& self) -> Hit::Properties& { self.flags = self.flags | Hit::blind; return self; } + ), + "confuse", sol::overload( + [](Hit::Properties& self, frame_time_t duration) -> Hit::Properties& { + self.flags = self.flags | Hit::confuse; + self.confuse_duration = duration; + return self; + }, + [](Hit::Properties& self) -> Hit::Properties& { self.flags = self.flags | Hit::pierce; return self; } + ) ); state.new_enum("Hit", diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index a247eaac8..5222f8ab1 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -1221,7 +1221,10 @@ const bool Entity::Hit(Hit::Properties props) { // If the character itself is also super-effective, // double the damage independently from tile damage - bool isSuperEffective = IsSuperEffective(props.element); + const bool isSuperEffective = IsSuperEffective(props.element) || IsSuperEffective(props.secondaryElement); + const bool isFire = props.element == Element::fire || props.secondaryElement == Element::fire; + const bool isElec = props.element == Element::elec || props.secondaryElement == Element::elec; + const bool isAqua = props.element == Element::aqua || props.secondaryElement == Element::aqua; // super effective damage is x2 if (isSuperEffective) { @@ -1232,20 +1235,20 @@ const bool Entity::Hit(Hit::Properties props) { int extraDamage = 0; // Calculate elemental damage if the tile the character is on is super effective to it - if (props.element == Element::fire + if (isFire && GetTile()->GetState() == TileState::grass) { tileDamage = props.damage; GetTile()->SetState(TileState::normal); } - if (props.element == Element::elec + if (isElec && GetTile()->GetState() == TileState::sea) { tileDamage = props.damage; } - if (props.element == Element::aqua + if (isAqua && GetTile()->GetState() == TileState::ice) { props.flags |= Hit::freeze; GetTile()->SetState(TileState::normal); @@ -1379,6 +1382,8 @@ void Entity::ResolveFrameBattleDamage() if (countered) { // Only consider a counter if there was an aggressor if (frameCounterAggressor = GetField()->GetCharacter(props.filtered.aggressor)) { + // Counter stun takes priority over the attack's Stun duration + props.filtered.flags = props.filtered.flags & ~Hit::stun; statuses.AddStatus(Hit::stun, frames(150)); OnCountered(); } @@ -1395,19 +1400,19 @@ void Entity::ResolveFrameBattleDamage() const bool hasFlash = ((props.filtered.flags & Hit::flash) == Hit::flash); if (hasFlash) { - statuses.AddStatus(Hit::flash, frames(120)); + statuses.AddStatus(Hit::flash, props.filtered.flash_duration); } props.filtered.flags = props.filtered.flags & ~Hit::flash; if ((props.filtered.flags & Hit::freeze)) { - statuses.AddStatus(Hit::freeze, frames(150)); + statuses.AddStatus(Hit::freeze, props.filtered.freeze_duration); } props.filtered.flags = props.filtered.flags & ~Hit::freeze; if ((props.filtered.flags & Hit::stun)) { - statuses.AddStatus(Hit::stun, frames(120)); + statuses.AddStatus(Hit::stun, props.filtered.stun_duration); } props.filtered.flags = props.filtered.flags & ~Hit::stun; @@ -1419,19 +1424,19 @@ void Entity::ResolveFrameBattleDamage() props.filtered.flags = props.filtered.flags & ~Hit::bubble; if ((props.filtered.flags & Hit::root)) { - statuses.AddStatus(Hit::root, frames(120)); + statuses.AddStatus(Hit::root, props.filtered.root_duration); } props.filtered.flags = props.filtered.flags & ~Hit::root; if ((props.filtered.flags & Hit::blind)) { - statuses.AddStatus(Hit::blind, frames(300)); + statuses.AddStatus(Hit::blind, props.filtered.blind_duration); } props.filtered.flags = props.filtered.flags & ~Hit::blind; if ((props.filtered.flags & Hit::confuse)) { - statuses.AddStatus(Hit::confuse, frames(110)); + statuses.AddStatus(Hit::confuse, props.filtered.confuse_duration); } props.filtered.flags = props.filtered.flags & ~Hit::confuse; diff --git a/BattleNetwork/bnHitProperties.h b/BattleNetwork/bnHitProperties.h index 9e9d8eb5b..0ef9f8dc8 100644 --- a/BattleNetwork/bnHitProperties.h +++ b/BattleNetwork/bnHitProperties.h @@ -1,6 +1,7 @@ #pragma once #include "bnElements.h" #include "bnDirection.h" +#include "bnFrameTimeUtils.h" // forward declare using EntityID_t = long; @@ -48,15 +49,23 @@ namespace Hit { int damage{}; Flags flags{ Hit::none }; Element element{ Element::none }; + Element secondaryElement{ Element::none }; EntityID_t aggressor{}; Drag drag{ }; // Used by Hit::drag flag Context context{}; + frame_time_t stun_duration{ 120 }; + frame_time_t freeze_duration{ 150 }; + frame_time_t flash_duration{ 120 }; + frame_time_t root_duration{ 120 }; + frame_time_t blind_duration{ 300 }; + frame_time_t confuse_duration{ 110 }; }; const constexpr Hit::Properties DefaultProperties = { 0, Flags(Hit::flinch | Hit::impact), Element::none, + Element::none, 0, Direction::none, true diff --git a/BattleNetwork/bnTile.cpp b/BattleNetwork/bnTile.cpp index 196957c14..ac008afd8 100644 --- a/BattleNetwork/bnTile.cpp +++ b/BattleNetwork/bnTile.cpp @@ -711,7 +711,7 @@ namespace Battle { if (!character.HasFloatShoe()) { if (GetState() == TileState::poison) { if (elapsedBurnTime <= frames(0)) { - if (character.Hit(Hit::Properties({ 1, Hit::pierce, Element::none, 0, Direction::none }))) { + if (character.Hit(Hit::Properties({ 1, Hit::pierce, Element::none, Element::none, 0, Direction::none }))) { elapsedBurnTime = burncycle; } } @@ -721,7 +721,7 @@ namespace Battle { } if (GetState() == TileState::lava && character.GetElement() != Element::fire) { - Hit::Properties props = { 50, Hit::flash | Hit::flinch | Hit::impact, Element::none, 0, Direction::none }; + Hit::Properties props = { 50, Hit::flash | Hit::flinch | Hit::impact, Element::none, Element::none, 0, Direction::none }; if (character.HasCollision(props)) { character.Hit(props); field.AddEntity(std::make_shared(), GetX(), GetY()); @@ -731,7 +731,7 @@ namespace Battle { if (GetState() == TileState::sea && character.GetElement() == Element::fire) { if (seaDamageCooldown <= frames(0)) { - if (character.Hit(Hit::Properties({ 1, Hit::pierce, Element::none, 0, Direction::none }))) { + if (character.Hit(Hit::Properties({ 1, Hit::pierce, Element::none, Element::none, 0, Direction::none }))) { seaDamageCooldown = seaDamageCooldownLength; } } @@ -1043,9 +1043,9 @@ namespace Battle { Hit::Properties props = attacker->GetHitboxProperties(); - hitByWind = hitByWind || props.element == Element::wind; - hitByFire = hitByFire || props.element == Element::fire; - hitByAqua = hitByAqua || props.element == Element::aqua; + hitByWind = hitByWind || props.element == Element::wind || props.secondaryElement == Element::wind; + hitByFire = hitByFire || props.element == Element::fire || props.secondaryElement == Element::fire; + hitByAqua = hitByAqua || props.element == Element::aqua || props.secondaryElement == Element::aqua; bool retangible = false; DefenseFrameStateJudge judge; // judge for this character's defenses diff --git a/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp b/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp index d6718860f..395ace2b4 100644 --- a/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp +++ b/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp @@ -218,8 +218,8 @@ NetworkBattleScene::~NetworkBattleScene() { } void NetworkBattleScene::OnHit(Entity& victim, const Hit::Properties& props) { - bool freezeBreak = victim.IsIceFrozen() && ((props.flags & Hit::breaking) == Hit::breaking); - bool superEffective = victim.IsSuperEffective(props.element) && props.damage > 0; + const bool freezeBreak = victim.IsIceFrozen() && ((props.flags & Hit::breaking) == Hit::breaking); + const bool superEffective = props.damage > 0 && (victim.IsSuperEffective(props.element) || victim.IsSuperEffective(props.secondaryElement)); if (freezeBreak || superEffective) { std::shared_ptr seSymbol = std::make_shared(); @@ -243,7 +243,7 @@ void NetworkBattleScene::OnHit(Entity& victim, const Hit::Properties& props) { } } - if (player->IsInForm() && player->IsSuperEffective(props.element)) { + if (player->IsInForm() && superEffective) { // animate the transformation back to default form TrackedFormData& formData = GetPlayerFormData(player); From d8742cf00bddfc6867014a66e3a635bdcbc86f2f Mon Sep 17 00:00:00 2001 From: Alrysc Date: Fri, 10 Oct 2025 20:30:42 -0700 Subject: [PATCH 123/146] Player::SetEmotion handles setting or removing multiplier, removed Logs for deprecated HitProperties constructor, transformation state now sets Emotion::normal --- .../States/bnCharacterTransformBattleState.cpp | 4 ++-- BattleNetwork/battlescene/bnBattleSceneBase.cpp | 16 ++-------------- .../battlescene/bnFreedomMissionMobScene.cpp | 2 +- BattleNetwork/battlescene/bnMobBattleScene.cpp | 2 +- BattleNetwork/bindings/bnUserTypeHitbox.cpp | 2 -- BattleNetwork/bnPlayer.cpp | 17 +++++++++++++++++ .../battlescene/bnNetworkBattleScene.cpp | 7 +------ 7 files changed, 24 insertions(+), 26 deletions(-) diff --git a/BattleNetwork/battlescene/States/bnCharacterTransformBattleState.cpp b/BattleNetwork/battlescene/States/bnCharacterTransformBattleState.cpp index 66cee5263..62bec40ec 100644 --- a/BattleNetwork/battlescene/States/bnCharacterTransformBattleState.cpp +++ b/BattleNetwork/battlescene/States/bnCharacterTransformBattleState.cpp @@ -25,7 +25,6 @@ const bool CharacterTransformBattleState::FadeOutBackdrop() /* When changing form the following needs to be done: * Finish movement and end current Drag (both done by Entity::EndDrag) - - TODO: Drag may not end sometimes on deform. Determine how this works. * Clear ActionQueue (done in Player::ActivateFormAt) * (If activating form) Clear blocking statuses (done in Player::ActivateFormAt) - This must be done after a call to ResolveFrameBattleDamage, also done in @@ -78,7 +77,7 @@ void CharacterTransformBattleState::UpdateAnimation(double elapsed) during FinishMove (called by EndDrag) if IsSliding is true, so it's done here to ensure it happens. */ - player->setPosition(player->GetTile()->getPosition() + player->GetDrawOffset()); + player->RefreshPosition(); player->ActivateFormAt(_index); player->SetColorMode(ColorMode::additive); @@ -100,6 +99,7 @@ void CharacterTransformBattleState::UpdateAnimation(double elapsed) } GetScene().HandleCounterLoss(*player, false); + player->SetEmotion(Emotion::normal); Audio().Play(AudioType::SHINE); } diff --git a/BattleNetwork/battlescene/bnBattleSceneBase.cpp b/BattleNetwork/battlescene/bnBattleSceneBase.cpp index f2058f211..8befcdc80 100644 --- a/BattleNetwork/battlescene/bnBattleSceneBase.cpp +++ b/BattleNetwork/battlescene/bnBattleSceneBase.cpp @@ -545,13 +545,6 @@ void BattleSceneBase::LoadBlueTeamMob(Mob& mob) void BattleSceneBase::HandleCounterLoss(Entity& subject, bool playsound) { - std::shared_ptr cardUI = subject.GetFirstComponent(); - - // No multipler to remove - if (!cardUI) { - return; - } - std::shared_ptr p = cardUI->GetOwnerAs(); // p should never be nullptr. Sanity check. @@ -562,8 +555,8 @@ void BattleSceneBase::HandleCounterLoss(Entity& subject, bool playsound) p->RemoveNode(counterReveal); p->RemoveDefenseRule(counterCombatRule); + // Removes the multiplier p->SetEmotion(Emotion::normal); - cardUI->SetMultiplier(1); playsound ? Audio().Play(AudioType::COUNTER_BONUS, AudioPriority::highest) : 0; @@ -1107,12 +1100,7 @@ void BattleSceneBase::PreparePlayerFullSynchro(const std::shared_ptr& pl counterReveal->setPosition(0, -bounds.height / 4.0f); player->AddNode(counterReveal); - std::shared_ptr cardUI = player->GetFirstComponent(); - - if (cardUI) { - cardUI->SetMultiplier(2); - } - + // Adds the multiplier player->SetEmotion(Emotion::full_synchro); // when players get hit by impact, battle scene takes back counter blessings diff --git a/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp b/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp index 6fe5d8303..6e199b19a 100644 --- a/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp +++ b/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp @@ -183,8 +183,8 @@ void FreedomMissionMobScene::OnHit(Entity& victim, const Hit::Properties& props) playerHitCount++; if (props.damage >= 300) { + // Sets the multiplier player->SetEmotion(Emotion::angry); - GetSelectedCardsUI().SetMultiplier(2); } if (player->IsInForm() && superEffective) { diff --git a/BattleNetwork/battlescene/bnMobBattleScene.cpp b/BattleNetwork/battlescene/bnMobBattleScene.cpp index 8566cda4f..372bfce66 100644 --- a/BattleNetwork/battlescene/bnMobBattleScene.cpp +++ b/BattleNetwork/battlescene/bnMobBattleScene.cpp @@ -190,8 +190,8 @@ void MobBattleScene::OnHit(Entity& victim, const Hit::Properties& props) playerHitCount++; if (props.damage >= 300) { + // Sets the multiplier player->SetEmotion(Emotion::angry); - GetSelectedCardsUI().SetMultiplier(2); } if (player->IsInForm() && superEffective) { diff --git a/BattleNetwork/bindings/bnUserTypeHitbox.cpp b/BattleNetwork/bindings/bnUserTypeHitbox.cpp index 16c65d439..efcf95e4a 100644 --- a/BattleNetwork/bindings/bnUserTypeHitbox.cpp +++ b/BattleNetwork/bindings/bnUserTypeHitbox.cpp @@ -129,13 +129,11 @@ void DefineHitboxUserTypes(sol::state& state, sol::table& battle_namespace) { // deprecated API in v2.5 createHitProps, [createHitProps](int damage, Hit::Flags flags, Element element, std::optional optCtx, Hit::Drag drag) { - Logger::Log(LogLevel::warning, return createHitProps(damage, flags, element, Element::none, optCtx, drag); }, // Cover for scripters who passed in Entity ID, which did nothing but is considered an // error now without this constructor [createHitProps](int damage, Hit::Flags flags, Element element, EntityID_t id, Hit::Drag drag) { - Logger::Log(LogLevel::warning, return createHitProps(damage, flags, element, Element::none, std::nullopt, drag); }, [createHitProps](std::optional optCtx) -> Hit::Properties { diff --git a/BattleNetwork/bnPlayer.cpp b/BattleNetwork/bnPlayer.cpp index bfa3633ea..abead1b78 100644 --- a/BattleNetwork/bnPlayer.cpp +++ b/BattleNetwork/bnPlayer.cpp @@ -9,6 +9,7 @@ #include "bnBubbleTrap.h" #include "bnBubbleState.h" +#include "bnPlayerSelectedCardsUI.h" #define RESOURCE_PATH "resources/navis/megaman/megaman.animation" @@ -329,8 +330,24 @@ const int Player::GetMaxHealthMod() void Player::SetEmotion(Emotion emotion) { + // Forms do not use emotions aside from normal + if (IsInForm() && emotion != Emotion::normal) { + return; + } + this->emotion = emotion; + std::shared_ptr cardUI = GetFirstComponent(); + + if (cardUI) { + if (emotion == Emotion::angry || emotion == Emotion::full_synchro) { + cardUI->SetMultiplier(2); + } + else { + cardUI->SetMultiplier(1); + } + } + if (this->emotion == Emotion::angry) { AddDefenseRule(superArmor); } diff --git a/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp b/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp index 395ace2b4..9d0b2848d 100644 --- a/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp +++ b/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp @@ -234,13 +234,8 @@ void NetworkBattleScene::OnHit(Entity& victim, const Hit::Properties& props) { if (props.damage > 0) { if (props.damage >= 300) { + // Sets the multiplier player->SetEmotion(Emotion::angry); - - std::shared_ptr ui = player->GetFirstComponent(); - - if (ui) { - ui->SetMultiplier(2); - } } if (player->IsInForm() && superEffective) { From 8dc4d6df49ba7bb939b3d5dc5c0989c649d57223 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Fri, 10 Oct 2025 20:40:32 -0700 Subject: [PATCH 124/146] Check filtered flags for no_counter, so TimeFreeze properly does not counter --- BattleNetwork/bnEntity.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index 5222f8ab1..34c71c4b7 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -1375,7 +1375,7 @@ void Entity::ResolveFrameBattleDamage() { bool countered = IsCountered() - && (props.hitbox.flags & Hit::no_counter) == 0 // This is the original instead of filtered + && (props.filtered.flags & Hit::no_counter) == 0 && (props.filtered.flags & Hit::impact) == Hit::impact && !frameCounterAggressor && props.filtered.aggressor; From ceab5b769426976b6a06334d6107b251219888fe Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sat, 11 Oct 2025 14:18:03 -0700 Subject: [PATCH 125/146] Ensure localPlayer is not added past vector end in GetAllPlayers --- BattleNetwork/battlescene/bnBattleSceneBase.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/BattleNetwork/battlescene/bnBattleSceneBase.cpp b/BattleNetwork/battlescene/bnBattleSceneBase.cpp index 8befcdc80..0f04ab48f 100644 --- a/BattleNetwork/battlescene/bnBattleSceneBase.cpp +++ b/BattleNetwork/battlescene/bnBattleSceneBase.cpp @@ -1169,7 +1169,14 @@ std::vector> BattleSceneBase::GetOtherPlayers() std::vector> BattleSceneBase::GetAllPlayers() { std::vector> result = otherPlayers; - result.insert(result.begin() + localPlayerSpawnIndex, localPlayer); + // Add the local player to the correct spot. Do not insert past the end. + if (result.size() < localPlayerSpawnIndex) { + result.insert(result.end(), localPlayer); + } + else { + result.insert(result.begin() + localPlayerSpawnIndex, localPlayer); + } + return result; } From 60d9c3c7b73acea3e658930a7af632a5c85374cb Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sat, 11 Oct 2025 15:07:47 -0700 Subject: [PATCH 126/146] ObstacleBody blocks Confuse and Blind --- BattleNetwork/bnDefenseObstacleBody.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/BattleNetwork/bnDefenseObstacleBody.cpp b/BattleNetwork/bnDefenseObstacleBody.cpp index 9d99395fc..02270425b 100644 --- a/BattleNetwork/bnDefenseObstacleBody.cpp +++ b/BattleNetwork/bnDefenseObstacleBody.cpp @@ -13,7 +13,8 @@ Hit::Properties& DefenseObstacleBody::FilterStatuses(Hit::Properties& statuses) statuses.flags &= ~Hit::stun; statuses.flags &= ~Hit::freeze; statuses.flags &= ~Hit::root; - + statuses.flags &= ~Hit::blind; + statuses.flags &= ~Hit::confuse; return statuses; } From acd748dd0f43c49a18d1d7b7682c8916f807c77c Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sat, 11 Oct 2025 15:08:21 -0700 Subject: [PATCH 127/146] Confuse and blind no longer reset anims when applied while already applied, counterhit now removes flash from the attack --- BattleNetwork/bnEntity.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index 34c71c4b7..531beff7e 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -264,11 +264,13 @@ void Entity::HandleNewStatuses(const Hit::Flags prevStatuses, Hit::Flags& applie } - if ((appliedStatuses & Hit::blind) == Hit::blind) { + // Avoid resetting the animation + if ((appliedStatuses & Hit::blind) == Hit::blind && !(prevStatuses & Hit::blind)) { Blind(); } - if ((appliedStatuses & Hit::confuse) == Hit::confuse) { + // Avoid resetting the animation + if ((appliedStatuses & Hit::confuse) == Hit::confuse && !(prevStatuses & Hit::confuse)) { Confuse(); } @@ -1382,8 +1384,9 @@ void Entity::ResolveFrameBattleDamage() if (countered) { // Only consider a counter if there was an aggressor if (frameCounterAggressor = GetField()->GetCharacter(props.filtered.aggressor)) { - // Counter stun takes priority over the attack's Stun duration - props.filtered.flags = props.filtered.flags & ~Hit::stun; + // Counter stun takes priority over the attack's Stun duration. + // Additionally, remove flashing from the countering hit. + props.filtered.flags = props.filtered.flags & ~(Hit::stun | Hit::flash); statuses.AddStatus(Hit::stun, frames(150)); OnCountered(); } From 9f2991145969226c60fefe753ca697ddade05c3e Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sat, 11 Oct 2025 15:08:43 -0700 Subject: [PATCH 128/146] Remove duplicate Lua HitProps:drag, renamed to drg to avoid duplicate name --- BattleNetwork/bindings/bnUserTypeHitbox.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/BattleNetwork/bindings/bnUserTypeHitbox.cpp b/BattleNetwork/bindings/bnUserTypeHitbox.cpp index efcf95e4a..b8c3a919e 100644 --- a/BattleNetwork/bindings/bnUserTypeHitbox.cpp +++ b/BattleNetwork/bindings/bnUserTypeHitbox.cpp @@ -151,7 +151,6 @@ void DefineHitboxUserTypes(sol::state& state, sol::table& battle_namespace) { // New API in v2.5 "from", [](Hit::Properties& self, Hit::Context ctx) -> Hit::Properties& { self.aggressor = ctx.aggressor; self.context = ctx; return self; }, "dmg", [](Hit::Properties& self, int damage) -> Hit::Properties& { self.damage = static_cast(damage); return self; }, - "drg", [](Hit::Properties& self, Hit::Drag drag) -> Hit::Properties& { self.drag = drag; return self; }, "elem", [](Hit::Properties& self, Element element) -> Hit::Properties& { self.element = element; return self; }, "elem2", [](Hit::Properties& self, Element element) -> Hit::Properties& { self.secondaryElement = element; return self; }, @@ -186,7 +185,7 @@ void DefineHitboxUserTypes(sol::state& state, sol::table& battle_namespace) { ), "breaking", [](Hit::Properties& self) -> Hit::Properties& { self.flags = self.flags | Hit::breaking; return self; }, "impact", [](Hit::Properties& self) -> Hit::Properties& { self.flags = self.flags | Hit::impact; return self; }, - "drag", [](Hit::Properties& self, Hit::Drag drag) -> Hit::Properties& { + "drg", [](Hit::Properties& self, Hit::Drag drag) -> Hit::Properties& { self.flags = self.flags | Hit::drag; self.drag = drag; return self; From 4578022c703f39a7708493a8bd2e813b9611e31b Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sun, 12 Oct 2025 14:45:39 -0700 Subject: [PATCH 129/146] Rename package partitioner getters to not conflict with class name --- .../States/bnRewardBattleState.cpp | 2 +- .../States/bnTimeFreezeBattleState.cpp | 2 +- .../battlescene/bnBattleSceneBase.cpp | 10 +++---- .../battlescene/bnFreedomMissionMobScene.cpp | 2 +- .../battlescene/bnMobBattleScene.cpp | 2 +- BattleNetwork/bnFolderChangeNameScene.cpp | 2 +- BattleNetwork/bnFolderEditScene.cpp | 10 +++---- BattleNetwork/bnFolderScene.cpp | 4 +-- BattleNetwork/bnGame.cpp | 10 +++---- BattleNetwork/bnGame.h | 10 +++---- BattleNetwork/bnLibraryScene.cpp | 2 +- BattleNetwork/bnSelectMobScene.cpp | 14 +++++----- BattleNetwork/bnSelectNaviScene.cpp | 8 +++--- BattleNetwork/bnTitleScene.cpp | 2 +- BattleNetwork/main.cpp | 18 ++++++------- .../battlescene/bnNetworkBattleScene.cpp | 6 ++--- BattleNetwork/netplay/bnDownloadScene.cpp | 20 +++++++------- BattleNetwork/netplay/bnMatchMakingScene.cpp | 14 +++++----- .../overworld/bnOverworldHomepage.cpp | 2 +- .../overworld/bnOverworldOnlineArea.cpp | 26 +++++++++---------- .../overworld/bnOverworldSceneBase.cpp | 6 ++--- 21 files changed, 86 insertions(+), 86 deletions(-) diff --git a/BattleNetwork/battlescene/States/bnRewardBattleState.cpp b/BattleNetwork/battlescene/States/bnRewardBattleState.cpp index 47817d271..21057298e 100644 --- a/BattleNetwork/battlescene/States/bnRewardBattleState.cpp +++ b/BattleNetwork/battlescene/States/bnRewardBattleState.cpp @@ -50,7 +50,7 @@ void RewardBattleState::onStart(const BattleSceneState*) battleResultsWidget = new BattleResultsWidget( BattleResults::CalculateScore(results, mob), mob, - scene.getController().CardPackagePartitioner().GetPartition(Game::LocalPartition) + scene.getController().GetCardPackagePartitioner().GetPartition(Game::LocalPartition) ); } diff --git a/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp b/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp index 020433a1e..8ffc7904b 100644 --- a/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp +++ b/BattleNetwork/battlescene/States/bnTimeFreezeBattleState.cpp @@ -79,7 +79,7 @@ void TimeFreezeBattleState::ProcessInputs() const Battle::Card& card = *maybe_card; if (card.IsTimeFreeze() && CanCounter(p)) { - if (std::shared_ptr action = CardToAction(card, p, &GetScene().getController().CardPackagePartitioner(), card.GetProps())) { + if (std::shared_ptr action = CardToAction(card, p, &GetScene().getController().GetCardPackagePartitioner(), card.GetProps())) { OnCardActionUsed(action, CurrentTime::AsMilli()); cardsUI->DropNextCard(); } diff --git a/BattleNetwork/battlescene/bnBattleSceneBase.cpp b/BattleNetwork/battlescene/bnBattleSceneBase.cpp index 0f04ab48f..88bcfc86c 100644 --- a/BattleNetwork/battlescene/bnBattleSceneBase.cpp +++ b/BattleNetwork/battlescene/bnBattleSceneBase.cpp @@ -38,7 +38,7 @@ using swoosh::ActivityController; BattleSceneBase::BattleSceneBase(ActivityController& controller, BattleSceneBaseProps& props, BattleResultsFunc onEnd) : Scene(controller), - cardActionListener(this->getController().CardPackagePartitioner()), + cardActionListener(this->getController().GetCardPackagePartitioner()), localPlayer(props.player), programAdvance(props.programAdvance), comboDeleteCounter(0), @@ -54,7 +54,7 @@ BattleSceneBase::BattleSceneBase(ActivityController& controller, BattleSceneBase iceShader(Shaders().GetShader(ShaderType::SPOT_REFLECTION)), customBarShader(Shaders().GetShader(ShaderType::CUSTOM_BAR)), // cap of 8 cards, 8 cards drawn per turn - cardCustGUI(CardSelectionCust::Props{ std::move(props.folder), &getController().CardPackagePartitioner().GetPartition(Game::LocalPartition), 8, 8 }), + cardCustGUI(CardSelectionCust::Props{ std::move(props.folder), &getController().GetCardPackagePartitioner().GetPartition(Game::LocalPartition), 8, 8 }), mobFont(Font::Style::thick), camera(sf::View{ sf::Vector2f(240, 160), sf::Vector2f(480, 320) }), onEndCallback(onEnd), @@ -464,7 +464,7 @@ void BattleSceneBase::SpawnLocalPlayer(int x, int y) field->AddEntity(localPlayer, x, y); // Player UI - cardUI = localPlayer->CreateComponent(localPlayer, &getController().CardPackagePartitioner()); + cardUI = localPlayer->CreateComponent(localPlayer, &getController().GetCardPackagePartitioner()); this->SubscribeToCardActions(*localPlayer); this->SubscribeToCardActions(*cardUI); @@ -507,7 +507,7 @@ void BattleSceneBase::SpawnOtherPlayer(std::shared_ptr player, int x, in field->AddEntity(player, x, y); // Other Player UI - std::shared_ptr cardUI = player->CreateComponent(player, &getController().CardPackagePartitioner()); + std::shared_ptr cardUI = player->CreateComponent(player, &getController().GetCardPackagePartitioner()); cardUI->Hide(); this->SubscribeToCardActions(*player); SubscribeToCardActions(*cardUI); @@ -566,7 +566,7 @@ void BattleSceneBase::HandleCounterLoss(Entity& subject, bool playsound) } void BattleSceneBase::FilterSupportCards(const std::shared_ptr& player, std::vector& cards) { - CardPackagePartitioner& partitions = getController().CardPackagePartitioner(); + CardPackagePartitioner& partitions = getController().GetCardPackagePartitioner(); for (size_t i = 0; i < cards.size(); i++) { std::string uuid = cards[i].GetUUID(); diff --git a/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp b/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp index 6e199b19a..b9d431dd3 100644 --- a/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp +++ b/BattleNetwork/battlescene/bnFreedomMissionMobScene.cpp @@ -156,7 +156,7 @@ void FreedomMissionMobScene::Init() // Run block programs on the local player now that they are spawned - BlockPackageManager& blockPackages = getController().BlockPackagePartitioner().GetPartition(Game::LocalPartition); + BlockPackageManager& blockPackages = getController().GetBlockPackagePartitioner().GetPartition(Game::LocalPartition); for (const std::string& blockID : props.blocks) { if (!blockPackages.HasPackage(blockID)) continue; diff --git a/BattleNetwork/battlescene/bnMobBattleScene.cpp b/BattleNetwork/battlescene/bnMobBattleScene.cpp index 372bfce66..9a658a0f5 100644 --- a/BattleNetwork/battlescene/bnMobBattleScene.cpp +++ b/BattleNetwork/battlescene/bnMobBattleScene.cpp @@ -166,7 +166,7 @@ void MobBattleScene::Init() } // Run block programs on the local player now that they are spawned - BlockPackageManager& blockPackages = getController().BlockPackagePartitioner().GetPartition(Game::LocalPartition); + BlockPackageManager& blockPackages = getController().GetBlockPackagePartitioner().GetPartition(Game::LocalPartition); for (const std::string& blockID : props.blocks) { if (!blockPackages.HasPackage(blockID)) continue; diff --git a/BattleNetwork/bnFolderChangeNameScene.cpp b/BattleNetwork/bnFolderChangeNameScene.cpp index 8c3a5a911..237c92413 100644 --- a/BattleNetwork/bnFolderChangeNameScene.cpp +++ b/BattleNetwork/bnFolderChangeNameScene.cpp @@ -137,7 +137,7 @@ void FolderChangeNameScene::DoOK() // We must have a key for the selected navi auto naviSelectedStr = getController().Session().GetKeyValue("SelectedNavi"); if (naviSelectedStr.empty()) - naviSelectedStr = getController().PlayerPackagePartitioner().GetPartition(Game::LocalPartition).FirstValidPackage(); + naviSelectedStr = getController().GetPlayerPackagePartitioner().GetPartition(Game::LocalPartition).FirstValidPackage(); // Save this session data new folder name getController().Session().SetKeyValue("FolderFor:" + naviSelectedStr, folderName); diff --git a/BattleNetwork/bnFolderEditScene.cpp b/BattleNetwork/bnFolderEditScene.cpp index 2e904a3f7..4206f7cfe 100644 --- a/BattleNetwork/bnFolderEditScene.cpp +++ b/BattleNetwork/bnFolderEditScene.cpp @@ -235,7 +235,7 @@ void FolderEditScene::onUpdate(double elapsed) { if (Input().HasSystemCopyEvent()) { std::string buffer; const std::string& nickname = getController().Session().GetNick(); - const CardPackageManager& manager = getController().CardPackagePartitioner().GetPartition(Game::LocalPartition); + const CardPackageManager& manager = getController().GetCardPackagePartitioner().GetPartition(Game::LocalPartition); buffer += "```\n"; buffer += "# Folder by " + nickname + "\n"; @@ -811,7 +811,7 @@ void FolderEditScene::DrawFolder(sf::RenderTarget& surface) { for (int i = 0; i < folderView.maxCardsOnScreen && folderView.firstCardOnScreen + i < folderView.numOfCards; i++) { if (!iter->IsEmpty()) { const Battle::Card& copy = iter->ViewCard(); - bool hasID = getController().CardPackagePartitioner().GetPartition(Game::LocalPartition).HasPackage(copy.GetUUID()); + bool hasID = getController().GetCardPackagePartitioner().GetPartition(Game::LocalPartition).HasPackage(copy.GetUUID()); cardLabel.SetColor(sf::Color::White); @@ -1216,7 +1216,7 @@ void FolderEditScene::PlaceFolderDataIntoCardSlots() { } void FolderEditScene::PlaceLibraryDataIntoBuckets() { - auto& packageManager = getController().CardPackagePartitioner().GetPartition(Game::LocalPartition); + auto& packageManager = getController().GetCardPackagePartitioner().GetPartition(Game::LocalPartition); std::string packageId = packageManager.FirstValidPackage(); if (packageId.empty()) return; @@ -1247,7 +1247,7 @@ void FolderEditScene::WriteNewFolderData() { } std::shared_ptr FolderEditScene::GetIconForCard(const std::string& uuid) { - auto& packageManager = getController().CardPackagePartitioner().GetPartition(Game::LocalPartition); + auto& packageManager = getController().GetCardPackagePartitioner().GetPartition(Game::LocalPartition); if (!packageManager.HasPackage(uuid)) return noIcon; @@ -1256,7 +1256,7 @@ std::shared_ptr FolderEditScene::GetIconForCard(const std::string& return meta.GetIconTexture(); } std::shared_ptr FolderEditScene::GetPreviewForCard(const std::string& uuid) { - auto& packageManager = getController().CardPackagePartitioner().GetPartition(Game::LocalPartition); + auto& packageManager = getController().GetCardPackagePartitioner().GetPartition(Game::LocalPartition); if (!packageManager.HasPackage(uuid)) return noPreview; diff --git a/BattleNetwork/bnFolderScene.cpp b/BattleNetwork/bnFolderScene.cpp index 81b232733..e799aa80a 100644 --- a/BattleNetwork/bnFolderScene.cpp +++ b/BattleNetwork/bnFolderScene.cpp @@ -386,7 +386,7 @@ void FolderScene::onUpdate(double elapsed) { std::string naviSelectedStr = session.GetKeyValue("SelectedNavi"); if (naviSelectedStr.empty()) { - naviSelectedStr = getController().PlayerPackagePartitioner().GetPartition(Game::LocalPartition).FirstValidPackage(); + naviSelectedStr = getController().GetPlayerPackagePartitioner().GetPartition(Game::LocalPartition).FirstValidPackage(); } session.SetKeyValue("FolderFor:" + naviSelectedStr, folderStr); @@ -502,7 +502,7 @@ void FolderScene::onResume() { } void FolderScene::onDraw(sf::RenderTexture& surface) { - CardPackageManager& packageManager = getController().CardPackagePartitioner().GetPartition(Game::LocalPartition); + CardPackageManager& packageManager = getController().GetCardPackagePartitioner().GetPartition(Game::LocalPartition); surface.draw(bg); surface.draw(menuLabel); diff --git a/BattleNetwork/bnGame.cpp b/BattleNetwork/bnGame.cpp index d2b5d57b3..661732c3b 100644 --- a/BattleNetwork/bnGame.cpp +++ b/BattleNetwork/bnGame.cpp @@ -571,27 +571,27 @@ const std::filesystem::path Game::SaveGamesPath() return std::filesystem::u8path(sago::getSaveGamesFolder1()); } -CardPackagePartitioner& Game::CardPackagePartitioner() +CardPackagePartitioner& Game::GetCardPackagePartitioner() { return *cardPackagePartitioner; } -PlayerPackagePartitioner& Game::PlayerPackagePartitioner() +PlayerPackagePartitioner& Game::GetPlayerPackagePartitioner() { return *playerPackagePartitioner; } -MobPackagePartitioner& Game::MobPackagePartitioner() +MobPackagePartitioner& Game::GetMobPackagePartitioner() { return *mobPackagePartitioner; } -BlockPackagePartitioner& Game::BlockPackagePartitioner() +BlockPackagePartitioner& Game::GetBlockPackagePartitioner() { return *blockPackagePartitioner; } -LuaLibraryPackagePartitioner& Game::LuaLibraryPackagePartitioner() +LuaLibraryPackagePartitioner& Game::GetLuaLibraryPackagePartitioner() { return *luaLibraryPackagePartitioner; } diff --git a/BattleNetwork/bnGame.h b/BattleNetwork/bnGame.h index affbddd0e..4e07a6939 100644 --- a/BattleNetwork/bnGame.h +++ b/BattleNetwork/bnGame.h @@ -159,11 +159,11 @@ class Game final : public ActivityController { const std::filesystem::path PicturesPath(); const std::filesystem::path SaveGamesPath(); - CardPackagePartitioner& CardPackagePartitioner(); - PlayerPackagePartitioner& PlayerPackagePartitioner(); - MobPackagePartitioner& MobPackagePartitioner(); - BlockPackagePartitioner& BlockPackagePartitioner(); - LuaLibraryPackagePartitioner& LuaLibraryPackagePartitioner(); + CardPackagePartitioner& GetCardPackagePartitioner(); + PlayerPackagePartitioner& GetPlayerPackagePartitioner(); + MobPackagePartitioner& GetMobPackagePartitioner(); + BlockPackagePartitioner& GetBlockPackagePartitioner(); + LuaLibraryPackagePartitioner& GetLuaLibraryPackagePartitioner(); static char* LocalPartition; static char* RemotePartition; diff --git a/BattleNetwork/bnLibraryScene.cpp b/BattleNetwork/bnLibraryScene.cpp index 7c58fdb94..dac8f12e3 100644 --- a/BattleNetwork/bnLibraryScene.cpp +++ b/BattleNetwork/bnLibraryScene.cpp @@ -328,7 +328,7 @@ void LibraryScene::onResume() { } void LibraryScene::onDraw(sf::RenderTexture& surface) { - auto& packageManager = getController().CardPackagePartitioner().GetPartition(Game::LocalPartition); + auto& packageManager = getController().GetCardPackagePartitioner().GetPartition(Game::LocalPartition); surface.draw(bg); surface.draw(menuLabel); diff --git a/BattleNetwork/bnSelectMobScene.cpp b/BattleNetwork/bnSelectMobScene.cpp index 3dfdde89d..25d1a44be 100644 --- a/BattleNetwork/bnSelectMobScene.cpp +++ b/BattleNetwork/bnSelectMobScene.cpp @@ -69,7 +69,7 @@ SelectMobScene::SelectMobScene(swoosh::ActivityController& controller, SelectMob shader = Shaders().GetShader(ShaderType::TEXEL_PIXEL_BLUR); // Current selection index - mobSelectionId = getController().MobPackagePartitioner().GetPartition(Game::LocalPartition).FirstValidPackage(); + mobSelectionId = getController().GetMobPackagePartitioner().GetPartition(Game::LocalPartition).FirstValidPackage(); // Text box navigator textbox.Stop(); @@ -105,7 +105,7 @@ void SelectMobScene::onUpdate(double elapsed) { if (selectInputCooldown <= 0) { // Go to previous mob selectInputCooldown = maxSelectInputCooldown; - mobSelectionId = getController().MobPackagePartitioner().GetPartition(Game::LocalPartition).GetPackageBefore(mobSelectionId); + mobSelectionId = getController().GetMobPackagePartitioner().GetPartition(Game::LocalPartition).GetPackageBefore(mobSelectionId); // Number scramble effect numberCooldown = maxNumberCooldown; @@ -117,7 +117,7 @@ void SelectMobScene::onUpdate(double elapsed) { if (selectInputCooldown <= 0) { // Go to next mob selectInputCooldown = maxSelectInputCooldown; - mobSelectionId = getController().MobPackagePartitioner().GetPartition(Game::LocalPartition).GetPackageAfter(mobSelectionId); + mobSelectionId = getController().GetMobPackagePartitioner().GetPartition(Game::LocalPartition).GetPackageAfter(mobSelectionId); // Number scramble effect numberCooldown = maxNumberCooldown; @@ -171,7 +171,7 @@ void SelectMobScene::onUpdate(double elapsed) { #endif // Grab the mob info object from this index - auto& mobinfo = getController().MobPackagePartitioner().GetPartition(Game::LocalPartition).FindPackageByID(mobSelectionId); + auto& mobinfo = getController().GetMobPackagePartitioner().GetPartition(Game::LocalPartition).FindPackageByID(mobSelectionId); mobLabel.SetString(mobinfo.GetName()); hpLabel.SetString(mobinfo.GetHPString()); @@ -335,7 +335,7 @@ void SelectMobScene::onUpdate(double elapsed) { if (Input().Has(InputEvents::pressed_confirm) && !gotoNextScene) { Mob* mob = nullptr; - MobPackageManager& packageManager = getController().MobPackagePartitioner().GetPartition(Game::LocalPartition); + MobPackageManager& packageManager = getController().GetMobPackagePartitioner().GetPartition(Game::LocalPartition); if (packageManager.Size() != 0) { try { auto mobFactory = packageManager.FindPackageByID(mobSelectionId).GetData(); @@ -365,7 +365,7 @@ void SelectMobScene::onUpdate(double elapsed) { // Get the navi we selected - auto& meta = getController().PlayerPackagePartitioner().GetPartition(Game::LocalPartition).FindPackageByID(selectedNaviId); + auto& meta = getController().GetPlayerPackagePartitioner().GetPartition(Game::LocalPartition).FindPackageByID(selectedNaviId); const std::string& image = meta.GetMugshotTexturePath(); const std::string& mugshotAnim = meta.GetMugshotAnimationPath(); const std::string& emotionsTexture = meta.GetEmotionsTexturePath(); @@ -373,7 +373,7 @@ void SelectMobScene::onUpdate(double elapsed) { auto emotions = Textures().LoadFromFile(emotionsTexture); auto player = std::shared_ptr(meta.GetData()); - BlockPackageManager& blockPackages = getController().BlockPackagePartitioner().GetPartition(Game::LocalPartition); + BlockPackageManager& blockPackages = getController().GetBlockPackagePartitioner().GetPartition(Game::LocalPartition); GameSession& session = getController().Session(); // Get the package ID from the address since we know we're only using local packages diff --git a/BattleNetwork/bnSelectNaviScene.cpp b/BattleNetwork/bnSelectNaviScene.cpp index 0d73822a6..dbbad1551 100644 --- a/BattleNetwork/bnSelectNaviScene.cpp +++ b/BattleNetwork/bnSelectNaviScene.cpp @@ -12,7 +12,7 @@ using namespace swoosh::types; bool SelectNaviScene::IsNaviAllowed() { - PlayerPackagePartitioner& partitioner = getController().PlayerPackagePartitioner(); + PlayerPackagePartitioner& partitioner = getController().GetPlayerPackagePartitioner(); PackageAddress addr = { Game::LocalPartition, naviSelectionId }; PackageHash hash = { addr.packageId, partitioner.FindPackageByAddress(addr).GetPackageFingerprint() }; @@ -92,7 +92,7 @@ SelectNaviScene::SelectNaviScene(swoosh::ActivityController& controller, std::st navi.setOrigin(navi.getLocalBounds().width / 2.f, navi.getLocalBounds().height / 2.f); navi.setPosition(100.f, 150.f); - auto& playerPkg = getController().PlayerPackagePartitioner().GetPartition(Game::LocalPartition).FindPackageByID(currentChosenId); + auto& playerPkg = getController().GetPlayerPackagePartitioner().GetPartition(Game::LocalPartition).FindPackageByID(currentChosenId); if (auto tex = playerPkg.GetPreviewTexture()) { navi.setTexture(tex); } @@ -285,7 +285,7 @@ void SelectNaviScene::GotoPlayerCust() std::vector blocks; - auto& blockManager = getController().BlockPackagePartitioner().GetPartition(Game::LocalPartition); + auto& blockManager = getController().GetBlockPackagePartitioner().GetPartition(Game::LocalPartition); std::string package = blockManager.FirstValidPackage(); do { @@ -323,7 +323,7 @@ void SelectNaviScene::onUpdate(double elapsed) { bg->Update((float)elapsed); std::string prevSelectId = currentChosenId; - PlayerPackageManager& packageManager = getController().PlayerPackagePartitioner().GetPartition(Game::LocalPartition); + PlayerPackageManager& packageManager = getController().GetPlayerPackagePartitioner().GetPartition(Game::LocalPartition); bool openTextbox = owTextbox.IsOpen(); diff --git a/BattleNetwork/bnTitleScene.cpp b/BattleNetwork/bnTitleScene.cpp index de3803a32..a5d5069ce 100644 --- a/BattleNetwork/bnTitleScene.cpp +++ b/BattleNetwork/bnTitleScene.cpp @@ -117,7 +117,7 @@ void TitleScene::onUpdate(double elapsed) if (!checkMods) { checkMods = true; - PlayerPackageManager& pm = getController().PlayerPackagePartitioner().GetPartition(Game::LocalPartition); + PlayerPackageManager& pm = getController().GetPlayerPackagePartitioner().GetPartition(Game::LocalPartition); if (pm.Size() == 0) { std::string path = "resources/ow/prog/"; diff --git a/BattleNetwork/main.cpp b/BattleNetwork/main.cpp index 82e9cf76f..46ad0169d 100644 --- a/BattleNetwork/main.cpp +++ b/BattleNetwork/main.cpp @@ -236,7 +236,7 @@ int HandleBattleOnly(Game& g, TaskGroup tasks, const std::string& playerpath, co std::string mobid = mobpath; if (isURL) { - auto result = DownloadPackageFromURL(mobpath, g.MobPackagePartitioner().GetPartition(Game::LocalPartition)); + auto result = DownloadPackageFromURL(mobpath, g.GetMobPackagePartitioner().GetPartition(Game::LocalPartition)); if (result.is_error()) { Logger::Log(LogLevel::critical, result.error_cstr()); return EXIT_FAILURE; @@ -265,7 +265,7 @@ int HandleBattleOnly(Game& g, TaskGroup tasks, const std::string& playerpath, co auto field = std::make_shared(6, 3); // Get the navi we selected - auto& playermeta = g.PlayerPackagePartitioner().GetPartition(Game::LocalPartition).FindPackageByID(playerpath); + auto& playermeta = g.GetPlayerPackagePartitioner().GetPartition(Game::LocalPartition).FindPackageByID(playerpath); const std::string& image = playermeta.GetMugshotTexturePath(); Animation mugshotAnim = Animation() << playermeta.GetMugshotAnimationPath(); const std::string& emotionsTexture = playermeta.GetEmotionsTexturePath(); @@ -273,11 +273,11 @@ int HandleBattleOnly(Game& g, TaskGroup tasks, const std::string& playerpath, co auto emotions = handle.Textures().LoadFromFile(emotionsTexture); auto player = std::shared_ptr(playermeta.GetData()); - auto& mobmeta = g.MobPackagePartitioner().GetPartition(Game::LocalPartition).FindPackageByID(mobid); + auto& mobmeta = g.GetMobPackagePartitioner().GetPartition(Game::LocalPartition).FindPackageByID(mobid); Mob* mob = mobmeta.GetData()->Build(field); // Shuffle our new folder - std::unique_ptr folder = LoadFolderFromFile(folderPath, g.CardPackagePartitioner().GetPartition(Game::LocalPartition)); + std::unique_ptr folder = LoadFolderFromFile(folderPath, g.GetCardPackagePartitioner().GetPartition(Game::LocalPartition)); // Queue screen transition to Battle Scene with a white fade effect // just like the game @@ -435,11 +435,11 @@ void PrintPackageHash(Game& g, TaskGroup tasks) { tasks.DoNextTask(); } - BlockPackageManager& blocks = g.BlockPackagePartitioner().GetPartition(Game::LocalPartition); - PlayerPackageManager& players = g.PlayerPackagePartitioner().GetPartition(Game::LocalPartition); - CardPackageManager& cards = g.CardPackagePartitioner().GetPartition(Game::LocalPartition); - MobPackageManager& mobs = g.MobPackagePartitioner().GetPartition(Game::LocalPartition); - LuaLibraryPackageManager& libs = g.LuaLibraryPackagePartitioner().GetPartition(Game::LocalPartition); + BlockPackageManager& blocks = g.GetBlockPackagePartitioner().GetPartition(Game::LocalPartition); + PlayerPackageManager& players = g.GetPlayerPackagePartitioner().GetPartition(Game::LocalPartition); + CardPackageManager& cards = g.GetCardPackagePartitioner().GetPartition(Game::LocalPartition); + MobPackageManager& mobs = g.GetMobPackagePartitioner().GetPartition(Game::LocalPartition); + LuaLibraryPackageManager& libs = g.GetLuaLibraryPackagePartitioner().GetPartition(Game::LocalPartition); size_t lineLen{}; std::string blockStr, playerStr, cardStr, mobStr, libStr; diff --git a/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp b/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp index 9d0b2848d..ccd611ff3 100644 --- a/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp +++ b/BattleNetwork/netplay/battlescene/bnNetworkBattleScene.cpp @@ -404,7 +404,7 @@ bool NetworkBattleScene::IsRemoteBehind() { } void NetworkBattleScene::Init() { - BlockPackagePartitioner& partition = getController().BlockPackagePartitioner(); + BlockPackagePartitioner& partition = getController().GetBlockPackagePartitioner(); size_t idx = 0; for (auto& [blocks, p, x, y] : spawnOrder) { @@ -474,7 +474,7 @@ void NetworkBattleScene::SendHandshakeSignal(uint8_t syncIndex) { writer.Write(buffer, (int32_t)form); writer.Write(buffer, (uint8_t)len); - CardPackagePartitioner& partitioner = getController().CardPackagePartitioner(); + CardPackagePartitioner& partitioner = getController().GetCardPackagePartitioner(); CardPackageManager& localPackages = partitioner.GetPartition(Game::LocalPartition); CardPackageManager& remotePackages = partitioner.GetPartition(Game::RemotePartition); for (std::string& id : prefilteredCardSelection) { @@ -592,7 +592,7 @@ void NetworkBattleScene::ReceiveHandshakeSignal(const Poco::Buffer& buffer size_t handSize = remoteUUIDs.size(); int len = (int)handSize; - CardPackagePartitioner& partition = getController().CardPackagePartitioner(); + CardPackagePartitioner& partition = getController().GetCardPackagePartitioner(); CardPackageManager& localPackageManager = partition.GetPartition(Game::LocalPartition); if (handSize) { for (size_t i = 0; i < handSize; i++) { diff --git a/BattleNetwork/netplay/bnDownloadScene.cpp b/BattleNetwork/netplay/bnDownloadScene.cpp index 394f65255..e628d79b2 100644 --- a/BattleNetwork/netplay/bnDownloadScene.cpp +++ b/BattleNetwork/netplay/bnDownloadScene.cpp @@ -144,52 +144,52 @@ void DownloadScene::SendCoinFlip() { void DownloadScene::ResetRemotePartitions() { - CardPackagePartitioner& cardPartitioner = getController().CardPackagePartitioner(); + CardPackagePartitioner& cardPartitioner = getController().GetCardPackagePartitioner(); cardPartitioner.CreateNamespace(Game::RemotePartition); cardPartitioner.GetPartition(Game::RemotePartition).ErasePackages(); - PlayerPackagePartitioner& playerPartitioner = getController().PlayerPackagePartitioner(); + PlayerPackagePartitioner& playerPartitioner = getController().GetPlayerPackagePartitioner(); playerPartitioner.CreateNamespace(Game::RemotePartition); playerPartitioner.GetPartition(Game::RemotePartition).ErasePackages(); - BlockPackagePartitioner& blockPartitioner = getController().BlockPackagePartitioner(); + BlockPackagePartitioner& blockPartitioner = getController().GetBlockPackagePartitioner(); blockPartitioner.CreateNamespace(Game::RemotePartition); blockPartitioner.GetPartition(Game::RemotePartition).ErasePackages(); - LuaLibraryPackagePartitioner& libPartitioner = getController().LuaLibraryPackagePartitioner(); + LuaLibraryPackagePartitioner& libPartitioner = getController().GetLuaLibraryPackagePartitioner(); libPartitioner.CreateNamespace(Game::RemotePartition); libPartitioner.GetPartition(Game::RemotePartition).ErasePackages(); } CardPackageManager& DownloadScene::RemoteCardPartition() { - CardPackagePartitioner& partitioner = getController().CardPackagePartitioner(); + CardPackagePartitioner& partitioner = getController().GetCardPackagePartitioner(); return partitioner.GetPartition(Game::RemotePartition); } CardPackageManager& DownloadScene::LocalCardPartition() { - return getController().CardPackagePartitioner().GetPartition(Game::LocalPartition); + return getController().GetCardPackagePartitioner().GetPartition(Game::LocalPartition); } BlockPackageManager& DownloadScene::RemoteBlockPartition() { - return getController().BlockPackagePartitioner().GetPartition(Game::RemotePartition); + return getController().GetBlockPackagePartitioner().GetPartition(Game::RemotePartition); } BlockPackageManager& DownloadScene::LocalBlockPartition() { - return getController().BlockPackagePartitioner().GetPartition(Game::LocalPartition); + return getController().GetBlockPackagePartitioner().GetPartition(Game::LocalPartition); } PlayerPackageManager& DownloadScene::RemotePlayerPartition() { - return getController().PlayerPackagePartitioner().GetPartition(Game::RemotePartition); + return getController().GetPlayerPackagePartitioner().GetPartition(Game::RemotePartition); } PlayerPackageManager& DownloadScene::LocalPlayerPartition() { - return getController().PlayerPackagePartitioner().GetPartition(Game::LocalPartition); + return getController().GetPlayerPackagePartitioner().GetPartition(Game::LocalPartition); } void DownloadScene::RemoveFromDownloadList(const std::string& id) diff --git a/BattleNetwork/netplay/bnMatchMakingScene.cpp b/BattleNetwork/netplay/bnMatchMakingScene.cpp index 8b93842fe..f9216ce56 100644 --- a/BattleNetwork/netplay/bnMatchMakingScene.cpp +++ b/BattleNetwork/netplay/bnMatchMakingScene.cpp @@ -52,7 +52,7 @@ MatchMakingScene::MatchMakingScene(swoosh::ActivityController& controller, const this->gridBG = new GridBackground(); gridBG->SetColor(sf::Color(0)); // hide until it is ready - auto& playerPkg = getController().PlayerPackagePartitioner().GetPartition(Game::LocalPartition).FindPackageByID(selectedNaviId); + auto& playerPkg = getController().GetPlayerPackagePartitioner().GetPartition(Game::LocalPartition).FindPackageByID(selectedNaviId); clientPreview.setTexture(playerPkg.GetPreviewTexture()); clientPreview.setScale(2.f, 2.f); clientPreview.setOrigin(clientPreview.getLocalBounds().width, clientPreview.getLocalBounds().height); @@ -423,7 +423,7 @@ void MatchMakingScene::onResume() { Reset(); } else if(remoteNaviPackage.HasID()) { - PlayerMeta& playerPkg = getController().PlayerPackagePartitioner().FindPackageByAddress(remoteNaviPackage); + PlayerMeta& playerPkg = getController().GetPlayerPackagePartitioner().FindPackageByAddress(remoteNaviPackage); this->remotePreview.setTexture(playerPkg.GetPreviewTexture()); auto height = remotePreview.getSprite().getLocalBounds().height; remotePreview.setOrigin(sf::Vector2f(0, height)); @@ -461,9 +461,9 @@ void MatchMakingScene::onUpdate(double elapsed) { std::vector cardHashes, selectedNaviBlocks; - BlockPackageManager& blockPackages = getController().BlockPackagePartitioner().GetPartition(Game::LocalPartition); - CardPackageManager& cardPackages = getController().CardPackagePartitioner().GetPartition(Game::LocalPartition); - PlayerPackageManager& playerPackages = getController().PlayerPackagePartitioner().GetPartition(Game::LocalPartition); + BlockPackageManager& blockPackages = getController().GetBlockPackagePartitioner().GetPartition(Game::LocalPartition); + CardPackageManager& cardPackages = getController().GetCardPackagePartitioner().GetPartition(Game::LocalPartition); + PlayerPackageManager& playerPackages = getController().GetPlayerPackagePartitioner().GetPartition(Game::LocalPartition); GameSession& session = getController().Session(); for (const PackageAddress& blockAddr : PlayerCustScene::GetInstalledBlocks(selectedNaviId, session)) { @@ -580,8 +580,8 @@ void MatchMakingScene::onUpdate(double elapsed) { Audio().StopStream(); // Configure the session - PlayerPackagePartitioner& playerPartitioner = getController().PlayerPackagePartitioner(); - BlockPackagePartitioner& blockPartitioner = getController().BlockPackagePartitioner(); + PlayerPackagePartitioner& playerPartitioner = getController().GetPlayerPackagePartitioner(); + BlockPackagePartitioner& blockPartitioner = getController().GetBlockPackagePartitioner(); PlayerMeta& meta = playerPartitioner.FindPackageByAddress({ Game::LocalPartition, selectedNaviId }); const std::string& image = meta.GetMugshotTexturePath(); diff --git a/BattleNetwork/overworld/bnOverworldHomepage.cpp b/BattleNetwork/overworld/bnOverworldHomepage.cpp index 620954836..f3b6753be 100644 --- a/BattleNetwork/overworld/bnOverworldHomepage.cpp +++ b/BattleNetwork/overworld/bnOverworldHomepage.cpp @@ -326,7 +326,7 @@ void Overworld::Homepage::onUpdate(double elapsed) SceneBase::onUpdate(elapsed); if (Input().Has(InputEvents::pressed_shoulder_right) && !IsInputLocked()) { - PlayerMeta& meta = getController().PlayerPackagePartitioner().GetPartition(Game::LocalPartition).FindPackageByID(GetCurrentNaviID()); + PlayerMeta& meta = getController().GetPlayerPackagePartitioner().GetPartition(Game::LocalPartition).FindPackageByID(GetCurrentNaviID()); const std::string& image = meta.GetMugshotTexturePath(); const std::string& anim = meta.GetMugshotAnimationPath(); auto mugshot = Textures().LoadFromFile(image); diff --git a/BattleNetwork/overworld/bnOverworldOnlineArea.cpp b/BattleNetwork/overworld/bnOverworldOnlineArea.cpp index 5307711d2..b85cfe887 100644 --- a/BattleNetwork/overworld/bnOverworldOnlineArea.cpp +++ b/BattleNetwork/overworld/bnOverworldOnlineArea.cpp @@ -107,7 +107,7 @@ Overworld::OnlineArea::OnlineArea( player->AddNode(emoteNode); // ensure the existence of these package partitions - getController().MobPackagePartitioner().CreateNamespace(Game::ServerPartition); + getController().GetMobPackagePartitioner().CreateNamespace(Game::ServerPartition); } Overworld::OnlineArea::~OnlineArea() @@ -150,7 +150,7 @@ void Overworld::OnlineArea::AddSceneChangeTask(const std::function& task } void Overworld::OnlineArea::SetAvatarAsSpeaker() { - PlayerMeta& meta = getController().PlayerPackagePartitioner().GetPartition(Game::LocalPartition).FindPackageByID(GetCurrentNaviID()); + PlayerMeta& meta = getController().GetPlayerPackagePartitioner().GetPartition(Game::LocalPartition).FindPackageByID(GetCurrentNaviID()); const std::string& image = meta.GetMugshotTexturePath(); const std::string& anim = meta.GetMugshotAnimationPath(); std::shared_ptr mugshot = Textures().LoadFromFile(image); @@ -242,7 +242,7 @@ void Overworld::OnlineArea::ResetPVPStep(bool failed) void Overworld::OnlineArea::RemovePackages() { Logger::Log(LogLevel::debug, "Removing server packages"); - getController().MobPackagePartitioner().GetPartition(Game::ServerPartition).ClearPackages(); + getController().GetMobPackagePartitioner().GetPartition(Game::ServerPartition).ClearPackages(); } void Overworld::OnlineArea::updateOtherPlayers(double elapsed) { @@ -1005,7 +1005,7 @@ void Overworld::OnlineArea::processPacketBody(const Poco::Buffer& data) void Overworld::OnlineArea::CheckPlayerAgainstWhitelist() { // Check if the current navi is compatible - PlayerPackagePartitioner& partitioner = getController().PlayerPackagePartitioner(); + PlayerPackagePartitioner& partitioner = getController().GetPlayerPackagePartitioner(); PlayerPackageManager& packages = partitioner.GetPartition(Game::LocalPartition); std::string& id = GetCurrentNaviID(); PackageAddress addr = { Game::LocalPartition, id }; @@ -1178,7 +1178,7 @@ void Overworld::OnlineArea::sendAvatarChangeSignal() { sendAvatarAssetStream(); - auto& naviMeta = getController().PlayerPackagePartitioner().GetPartition(Game::LocalPartition).FindPackageByID(GetCurrentNaviID()); + auto& naviMeta = getController().GetPlayerPackagePartitioner().GetPartition(Game::LocalPartition).FindPackageByID(GetCurrentNaviID()); auto naviName = naviMeta.GetName(); auto maxHP = naviMeta.GetHP(); auto element = GetStrFromElement(naviMeta.GetElement()); @@ -1227,7 +1227,7 @@ void Overworld::OnlineArea::sendAvatarAssetStream() { // + reliability type + id + packet type auto packetHeaderSize = 1 + 8 + 2; - auto& naviMeta = getController().PlayerPackagePartitioner().GetPartition(Game::LocalPartition).FindPackageByID(GetCurrentNaviID()); + auto& naviMeta = getController().GetPlayerPackagePartitioner().GetPartition(Game::LocalPartition).FindPackageByID(GetCurrentNaviID()); auto texturePath = naviMeta.GetOverworldTexturePath(); auto textureData = readBytes(texturePath); @@ -2218,9 +2218,9 @@ void Overworld::OnlineArea::receivePVPSignal(BufferReader& reader, const Poco::B std::string remoteAddress = reader.ReadString(buffer); Poco::Net::SocketAddress remote = Poco::Net::SocketAddress(remoteAddress); - BlockPackagePartitioner& blockPartition = getController().BlockPackagePartitioner(); - CardPackagePartitioner& cardPartition = getController().CardPackagePartitioner(); - PlayerPackagePartitioner& playerPartition = getController().PlayerPackagePartitioner(); + BlockPackagePartitioner& blockPartition = getController().GetBlockPackagePartitioner(); + CardPackagePartitioner& cardPartition = getController().GetCardPackagePartitioner(); + PlayerPackagePartitioner& playerPartition = getController().GetPlayerPackagePartitioner(); try { netBattleProcessor = std::make_shared(remote, Net().GetMaxPayloadSize()); @@ -2238,7 +2238,7 @@ void Overworld::OnlineArea::receivePVPSignal(BufferReader& reader, const Poco::B }); AddSceneChangeTask([=, &blockPartition, &playerPartition] { - CardPackagePartitioner& cardPartition = getController().CardPackagePartitioner(); + CardPackagePartitioner& cardPartition = getController().GetCardPackagePartitioner(); std::vector cards, selectedNaviBlocks; const std::string& selectedNaviId = GetCurrentNaviID(); std::optional selectedFolder = GetSelectedFolder(); @@ -2404,7 +2404,7 @@ void Overworld::OnlineArea::receiveLoadPackageSignal(BufferReader& reader, const } // loading everything as an encounter for now - LoadPackage(getController().MobPackagePartitioner(), file_path); + LoadPackage(getController().GetMobPackagePartitioner(), file_path); } void Overworld::OnlineArea::receiveModWhitelistSignal(BufferReader& reader, const Poco::Buffer& buffer) @@ -2473,8 +2473,8 @@ void Overworld::OnlineArea::receiveMobSignal(BufferReader& reader, const Poco::B return; } - MobPackageManager& mobPackages = getController().MobPackagePartitioner().GetPartition(Game::ServerPartition); - PlayerPackageManager& playerPackages = getController().PlayerPackagePartitioner().GetPartition(Game::LocalPartition); + MobPackageManager& mobPackages = getController().GetMobPackagePartitioner().GetPartition(Game::ServerPartition); + PlayerPackageManager& playerPackages = getController().GetPlayerPackagePartitioner().GetPartition(Game::LocalPartition); std::string packageId = mobPackages.FilepathToPackageID(file_path); diff --git a/BattleNetwork/overworld/bnOverworldSceneBase.cpp b/BattleNetwork/overworld/bnOverworldSceneBase.cpp index e6db10159..b3451a09b 100644 --- a/BattleNetwork/overworld/bnOverworldSceneBase.cpp +++ b/BattleNetwork/overworld/bnOverworldSceneBase.cpp @@ -526,7 +526,7 @@ void Overworld::SceneBase::RefreshNaviSprite() // Only refresh all data and graphics if this is a new navi if (lastSelectedNaviId == currentNaviId && !lastSelectedNaviId.empty()) return; - PlayerPackageManager& packageManager = getController().PlayerPackagePartitioner().GetPartition(Game::LocalPartition); + PlayerPackageManager& packageManager = getController().GetPlayerPackagePartitioner().GetPartition(Game::LocalPartition); if (!packageManager.HasPackage(currentNaviId)) { currentNaviId = packageManager.FirstValidPackage(); } @@ -579,7 +579,7 @@ void Overworld::SceneBase::NaviEquipSelectedFolder() } } else { - currentNaviId = getController().PlayerPackagePartitioner().GetPartition(Game::LocalPartition).FirstValidPackage(); + currentNaviId = getController().GetPlayerPackagePartitioner().GetPartition(Game::LocalPartition).FirstValidPackage(); session.SetKeyValue("SelectedNavi", currentNaviId); } } @@ -826,7 +826,7 @@ void Overworld::SceneBase::GotoConfig() void Overworld::SceneBase::GotoMobSelect() { - MobPackageManager& pm = getController().MobPackagePartitioner().GetPartition(Game::LocalPartition); + MobPackageManager& pm = getController().GetMobPackagePartitioner().GetPartition(Game::LocalPartition); if (pm.Size() == 0) { personalMenu->Close(); menuSystem.EnqueueMessage("No enemy mods installed."); From d76b2078a763b3a3de0b0714ada6cb4e8a2878f8 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sun, 12 Oct 2025 14:50:12 -0700 Subject: [PATCH 130/146] Rename ConfigSetting getter in Game --- BattleNetwork/bnConfigScene.cpp | 2 +- BattleNetwork/bnGame.cpp | 2 +- BattleNetwork/bnGame.h | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/BattleNetwork/bnConfigScene.cpp b/BattleNetwork/bnConfigScene.cpp index 5f36032ef..4f6f0e317 100644 --- a/BattleNetwork/bnConfigScene.cpp +++ b/BattleNetwork/bnConfigScene.cpp @@ -190,7 +190,7 @@ ConfigScene::ConfigScene(swoosh::ActivityController& controller) : textbox(sf::Vector2f(4, 250)), Scene(controller) { - configSettings = getController().ConfigSettings(); + configSettings = getController().GetConfigSettings(); gamepadWasActive = Input().IsUsingGamepadControls(); textbox.SetTextSpeed(2.0); isSelectingTopMenu = false; diff --git a/BattleNetwork/bnGame.cpp b/BattleNetwork/bnGame.cpp index 661732c3b..f83538057 100644 --- a/BattleNetwork/bnGame.cpp +++ b/BattleNetwork/bnGame.cpp @@ -596,7 +596,7 @@ LuaLibraryPackagePartitioner& Game::GetLuaLibraryPackagePartitioner() return *luaLibraryPackagePartitioner; } -ConfigSettings& Game::ConfigSettings() +ConfigSettings& Game::GetConfigSettings() { return configSettings; } diff --git a/BattleNetwork/bnGame.h b/BattleNetwork/bnGame.h index 4e07a6939..7cfaab244 100644 --- a/BattleNetwork/bnGame.h +++ b/BattleNetwork/bnGame.h @@ -170,7 +170,7 @@ class Game final : public ActivityController { static char* ServerPartition; static char* Version; - ConfigSettings& ConfigSettings(); + ConfigSettings& GetConfigSettings(); GameSession& Session(); /** From 07afc4ba8e0d91431a5e22b16ec62c87e8699e83 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sun, 12 Oct 2025 14:54:58 -0700 Subject: [PATCH 131/146] Include cstdint in stx/string --- BattleNetwork/stx/string.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/BattleNetwork/stx/string.cpp b/BattleNetwork/stx/string.cpp index 76c17fa05..61d081b49 100644 --- a/BattleNetwork/stx/string.cpp +++ b/BattleNetwork/stx/string.cpp @@ -3,6 +3,7 @@ #include #include #include +#include // NOTE: the following code was from http://burtleburtle.net/bob/c/lookup3.c // References: http://burtleburtle.net/bob/hash/index.html From a1ec1310e8d38800639cb2250e442ef5fd1973d8 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sun, 12 Oct 2025 14:56:14 -0700 Subject: [PATCH 132/146] Move stx/string cstdint include to header --- BattleNetwork/stx/string.cpp | 1 - BattleNetwork/stx/string.h | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/BattleNetwork/stx/string.cpp b/BattleNetwork/stx/string.cpp index 61d081b49..76c17fa05 100644 --- a/BattleNetwork/stx/string.cpp +++ b/BattleNetwork/stx/string.cpp @@ -3,7 +3,6 @@ #include #include #include -#include // NOTE: the following code was from http://burtleburtle.net/bob/c/lookup3.c // References: http://burtleburtle.net/bob/hash/index.html diff --git a/BattleNetwork/stx/string.h b/BattleNetwork/stx/string.h index 09edc5097..b09a15c2d 100644 --- a/BattleNetwork/stx/string.h +++ b/BattleNetwork/stx/string.h @@ -3,6 +3,7 @@ #include #include #include +#include #include "../stx/result.h" namespace stx { From 6062a223517b807b15fd4a8c04033f2abe117b27 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sun, 12 Oct 2025 15:20:57 -0700 Subject: [PATCH 133/146] Use this->pivotPred --- BattleNetwork/bnFolderEditScene.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BattleNetwork/bnFolderEditScene.h b/BattleNetwork/bnFolderEditScene.h index d956dbaf8..da8787ac0 100644 --- a/BattleNetwork/bnFolderEditScene.h +++ b/BattleNetwork/bnFolderEditScene.h @@ -207,8 +207,8 @@ class FolderEditScene : public Scene { this->lastIndex = index; } - if (pivotPred) { - auto pivot = std::partition(this->container.begin(), this->container.end(), pivotPred); + if (this->pivotPred) { + auto pivot = std::partition(this->container.begin(), this->container.end(), this->pivotPred); size_t pivotDist = std::distance(this->container.begin(), pivot); std::vector copy = std::vector(this->container.begin(), pivot); From 9ca8fa8df702bb733ae27e4ff9039d63c67d2a63 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sun, 12 Oct 2025 15:47:34 -0700 Subject: [PATCH 134/146] Peek now returns const reference, as comment says. Stored return values in vars to satisfy C26815 --- BattleNetwork/bnSelectedCardsUI.cpp | 15 +++++++++------ BattleNetwork/bnSelectedCardsUI.h | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/BattleNetwork/bnSelectedCardsUI.cpp b/BattleNetwork/bnSelectedCardsUI.cpp index ab34b2cce..5d670f217 100644 --- a/BattleNetwork/bnSelectedCardsUI.cpp +++ b/BattleNetwork/bnSelectedCardsUI.cpp @@ -113,7 +113,7 @@ void SelectedCardsUI::OnUpdate(double _elapsed) { Battle::Tile* tile = character->GetTile(); if (tile == nullptr) return; - MaybeCard& maybeCard = Peek(); + const MaybeCard& maybeCard = Peek(); if (!maybeCard.has_value()) return; Battle::Card& data = maybeCard.value().get(); @@ -159,6 +159,8 @@ bool SelectedCardsUI::UseNextCard() { Battle::Tile* tile = owner->GetTile(); + // TODO: This happens before the card is removed from the hand if moving. It should not. + // Reset tile when card is boosted by Sea. // It could be worth checking this under the CanBoost() check below, // but for now, hfacing the modded damage should mean the modded damage @@ -186,18 +188,19 @@ void SelectedCardsUI::Broadcast(std::shared_ptr action) CardActionUsePublisher::Broadcast(action, CurrentTime::AsMilli()); } -SelectedCardsUI::MaybeCard SelectedCardsUI::Peek() +const SelectedCardsUI::MaybeCard& SelectedCardsUI::Peek() { if (curr < selectedCards->size()) { - return MaybeCard(std::ref((*selectedCards)[curr])); + const MaybeCard& ret = MaybeCard(std::ref((*selectedCards)[curr])); + return ret; } - - return {}; + const MaybeCard& ret = {}; + return ret; } bool SelectedCardsUI::HandlePlayEvent(std::shared_ptr from) { - auto maybe_card = Peek(); + const MaybeCard& maybe_card = Peek(); if (maybe_card.has_value()) { // convert meta data into a useable action object diff --git a/BattleNetwork/bnSelectedCardsUI.h b/BattleNetwork/bnSelectedCardsUI.h index dbfc299df..3d781bb27 100644 --- a/BattleNetwork/bnSelectedCardsUI.h +++ b/BattleNetwork/bnSelectedCardsUI.h @@ -75,7 +75,7 @@ class SelectedCardsUI : public CardActionUsePublisher, public UIComponent { * @brief Return a const reference to the next card, if valid * @preconditions Assumes the card can be used and currCard < cardCount! */ - MaybeCard Peek(); + const MaybeCard& Peek(); //!< Returns true if there was a card to play, false if empty bool HandlePlayEvent(std::shared_ptr from); From b9132802f1190415c476f6da3631ff2dfeb1edd4 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sun, 12 Oct 2025 16:05:47 -0700 Subject: [PATCH 135/146] Include cstdint for uint16_t --- BattleNetwork/overworld/bnIdentityManager.h | 1 + 1 file changed, 1 insertion(+) diff --git a/BattleNetwork/overworld/bnIdentityManager.h b/BattleNetwork/overworld/bnIdentityManager.h index 7aeb7f13d..4907085d4 100644 --- a/BattleNetwork/overworld/bnIdentityManager.h +++ b/BattleNetwork/overworld/bnIdentityManager.h @@ -1,4 +1,5 @@ #include +#include namespace Overworld { /** From ea6faff0abb432d72a094e682b9f8fe23faaf590 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Mon, 13 Oct 2025 11:18:55 -0700 Subject: [PATCH 136/146] SelectedCardsUI::Peek now returns const non-reference --- BattleNetwork/bnSelectedCardsUI.cpp | 10 +++++----- BattleNetwork/bnSelectedCardsUI.h | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/BattleNetwork/bnSelectedCardsUI.cpp b/BattleNetwork/bnSelectedCardsUI.cpp index 5d670f217..e2b6ac57e 100644 --- a/BattleNetwork/bnSelectedCardsUI.cpp +++ b/BattleNetwork/bnSelectedCardsUI.cpp @@ -188,14 +188,14 @@ void SelectedCardsUI::Broadcast(std::shared_ptr action) CardActionUsePublisher::Broadcast(action, CurrentTime::AsMilli()); } -const SelectedCardsUI::MaybeCard& SelectedCardsUI::Peek() +const SelectedCardsUI::MaybeCard SelectedCardsUI::Peek() { if (curr < selectedCards->size()) { - const MaybeCard& ret = MaybeCard(std::ref((*selectedCards)[curr])); - return ret; + + return MaybeCard(std::ref((*selectedCards)[curr]));; } - const MaybeCard& ret = {}; - return ret; + + return {}; } bool SelectedCardsUI::HandlePlayEvent(std::shared_ptr from) diff --git a/BattleNetwork/bnSelectedCardsUI.h b/BattleNetwork/bnSelectedCardsUI.h index 3d781bb27..9eede484d 100644 --- a/BattleNetwork/bnSelectedCardsUI.h +++ b/BattleNetwork/bnSelectedCardsUI.h @@ -75,7 +75,7 @@ class SelectedCardsUI : public CardActionUsePublisher, public UIComponent { * @brief Return a const reference to the next card, if valid * @preconditions Assumes the card can be used and currCard < cardCount! */ - const MaybeCard& Peek(); + const MaybeCard Peek(); //!< Returns true if there was a card to play, false if empty bool HandlePlayEvent(std::shared_ptr from); From 2aa6f7db0493c9a00dfeb425aca6a40d73b4102e Mon Sep 17 00:00:00 2001 From: TheMaverickProgrammer <91709+TheMaverickProgrammer@users.noreply.github.com> Date: Tue, 21 Oct 2025 00:24:03 -0500 Subject: [PATCH 137/146] Backport collab with Alrysc. --- BattleNetwork/bnInbox.cpp | 23 +++- BattleNetwork/bnInbox.h | 14 +- BattleNetwork/bnMailScene.cpp | 4 +- BattleNetwork/bnResourcePaths.h | 3 + .../overworld/bnOverworldOnlineArea.cpp | 123 +++++++++++++++++- .../overworld/bnOverworldOnlineArea.h | 19 ++- .../overworld/bnOverworldPacketHeaders.h | 15 ++- .../overworld/bnOverworldPersonalMenu.cpp | 28 +++- .../overworld/bnOverworldPersonalMenu.h | 16 ++- .../overworld/bnOverworldPlayerSession.h | 1 + .../overworld/bnOverworldSceneBase.cpp | 11 -- 11 files changed, 224 insertions(+), 33 deletions(-) diff --git a/BattleNetwork/bnInbox.cpp b/BattleNetwork/bnInbox.cpp index 843aadd3f..44f0fb5ac 100644 --- a/BattleNetwork/bnInbox.cpp +++ b/BattleNetwork/bnInbox.cpp @@ -2,14 +2,29 @@ void Inbox::AddMail(const Inbox::Mail& mail) { + auto& iter = mailList.begin(); + while (iter != mailList.end()) { + if (iter->id == mail.id) { + *iter = mail; // overwrite contents + return; + } + iter = std::next(iter); + } + + // New id, insert the mail mailList.push_back(mail); } -void Inbox::RemoveMail(size_t index) +void Inbox::RemoveMail(const std::string& id) { - if (index >= mailList.size()) return; - - mailList.erase(mailList.begin() + index); + auto& iter = mailList.begin(); + while (iter != mailList.end()) { + if (iter->id == id) { + mailList.erase(iter); + return; + } + iter = std::next(iter); + } } void Inbox::ReadMail(size_t index, std::function onRead) diff --git a/BattleNetwork/bnInbox.h b/BattleNetwork/bnInbox.h index 1347514f4..bedfb3ba7 100644 --- a/BattleNetwork/bnInbox.h +++ b/BattleNetwork/bnInbox.h @@ -3,10 +3,13 @@ #include #include #include +#include +#include "bnAnimation.h" +#include "bnCallback.h" class Inbox { public: - enum class Icons : char { + enum class Icons : uint8_t { announcement = 0, dm, dm_w_attachment, @@ -15,17 +18,22 @@ class Inbox { size // For counting only! }; + struct Mail; + using OnMailReadCallback = Callback; struct Mail { + std::string id; Icons icon{}; std::string title; std::string from; std::string body; - sf::Texture mugshot; + std::shared_ptr mugshot; + Animation mugshotAnim; + OnMailReadCallback onReadCallback; bool read{}; }; void AddMail(const Mail& msg); - void RemoveMail(size_t index); + void RemoveMail(const std::string& id); void ReadMail(size_t index, std::function onRead); const Mail& GetAt(size_t index) const; void Clear(); diff --git a/BattleNetwork/bnMailScene.cpp b/BattleNetwork/bnMailScene.cpp index 7601430ba..36f5dc02b 100644 --- a/BattleNetwork/bnMailScene.cpp +++ b/BattleNetwork/bnMailScene.cpp @@ -301,8 +301,8 @@ void MailScene::onDraw(sf::RenderTexture& surface) if (isReading) { auto& msg = inbox.GetAt(this->reading); - if (msg.mugshot.getNativeHandle()) { - sf::Sprite mug(msg.mugshot, sf::IntRect(0, 0, 40, 48)); + if (msg.mugshot->getNativeHandle()) { + sf::Sprite mug(*msg.mugshot.get(), sf::IntRect(0, 0, 40, 48)); mug.setScale(2.f, 2.f); mug.setPosition(12.f, 208.f); sf::RenderStates states; diff --git a/BattleNetwork/bnResourcePaths.h b/BattleNetwork/bnResourcePaths.h index 38e8f9b53..3b896ef5f 100644 --- a/BattleNetwork/bnResourcePaths.h +++ b/BattleNetwork/bnResourcePaths.h @@ -123,6 +123,7 @@ namespace TexturePaths { path GAMEPAD_SUPPORT_ICON = "resources/ui/gamepad_support_icon.png"; path MAIN_MENU_UI = "resources/ui/main_menu_ui.png"; path ELEMENT_ICON = "resources/ui/elements.png"; + path HUD_RING = "resources/ui/hud_ring.png"; // SHADER TEXTURE MAPS path DISTORTION_TEXTURE = "resources/shaders/textures/distortion.png"; @@ -148,10 +149,12 @@ namespace AnimationPaths { path BLIND_FX = "resources/scenes/battle/blind.animation"; path CONFUSED_FX = "resources/scenes/battle/spells/confused.animation"; path MISC_COUNTER_REVEAL = "resources/scenes/battle/counter_reveal.animation"; + path HUD_RING = "resources/ui/hud_ring.anim"; } namespace SoundPaths { path ICE_FX = "resources/sfx/freeze.ogg"; path CONFUSED_FX = "resources/sfx/confused.ogg"; + path PET_RINGTONE = "resources/sfx/ringtone.ogg"; } #undef path diff --git a/BattleNetwork/overworld/bnOverworldOnlineArea.cpp b/BattleNetwork/overworld/bnOverworldOnlineArea.cpp index b85cfe887..3afcdeef4 100644 --- a/BattleNetwork/overworld/bnOverworldOnlineArea.cpp +++ b/BattleNetwork/overworld/bnOverworldOnlineArea.cpp @@ -1381,6 +1381,14 @@ void Overworld::OnlineArea::sendBattleResultsSignal(const BattleResults& battleR packetProcessor->SendPacket(Reliability::ReliableOrdered, buffer); } +void Overworld::OnlineArea::sendEmailReadSignal(const std::string& id) +{ + BufferWriter writer; + Poco::Buffer buffer{ 0 }; + writer.Write(buffer, ClientEvents::read_email); + writer.WriteString(buffer, id); +} + void Overworld::OnlineArea::receiveAuthorizeSignal(BufferReader& reader, const Poco::Buffer& buffer) { auto authAddress = reader.ReadString(buffer); @@ -1705,7 +1713,7 @@ void Overworld::OnlineArea::receiveEmotionSignal(BufferReader& reader, const Poc void Overworld::OnlineArea::receiveMoneySignal(BufferReader& reader, const Poco::Buffer& buffer) { - auto balance = reader.Read(buffer); + int balance = reader.Read(buffer); GetPlayerSession()->money = balance; } @@ -2976,6 +2984,119 @@ void Overworld::OnlineArea::receiveActorMinimapColorSignal(BufferReader& reader, } } +void Overworld::OnlineArea::receiveFragmentSignal(BufferReader& reader, const Poco::Buffer& buffer) +{ + int balance = reader.Read(buffer); + GetPlayerSession()->money = balance; +} + +void Overworld::OnlineArea::receiveHudVisibleSignal(BufferReader& reader, const Poco::Buffer& buffer) +{ + int balance = reader.Read(buffer); + GetPlayerSession()->fragments = balance; +} + +void Overworld::OnlineArea::receiveHudSetModeSignal(BufferReader& reader, const Poco::Buffer& buffer) +{ + PlayerDisplayMode mode = static_cast(reader.Read(buffer)); + GetPersonalMenu().SetPlayerDisplayMode(mode); +} + +void Overworld::OnlineArea::receiveBattleRewardItemSignal(BufferReader& reader, const Poco::Buffer& buffer) +{ +} + +void Overworld::OnlineArea::receiveSendMailSignal(BufferReader& reader, const Poco::Buffer& buffer) +{ + Inbox& inbox = GetPlayerSession()->inbox; + + std::string id = reader.ReadString(buffer); + Inbox::Icons icon = reader.Read(buffer); + std::string title = reader.ReadString(buffer); + std::string from = reader.ReadString(buffer); + std::string body = reader.ReadString(buffer); + std::string mugshotAssetName = reader.ReadString(buffer); + std::string animationAssetName = reader.ReadString(buffer); + bool readAlready = reader.Read(buffer); + + std::shared_ptr mugshot = serverAssetManager.GetTexture(mugshotAssetName); + Animation mugshotAnim = serverAssetManager.GetPath(animationAssetName); + + Inbox::OnMailReadCallback onReadCallback; + + onReadCallback.Slot([this](Inbox::Mail& data) { + sendEmailReadSignal(data.id); + }); + + inbox.AddMail({ id, icon, title, from, body, mugshot, mugshotAnim, onReadCallback, readAlready }); +} + +void Overworld::OnlineArea::receiveRingtoneSignal(BufferReader& reader, const Poco::Buffer& buffer) +{ + GetPersonalMenu().Ringtone(); +} + +void Overworld::OnlineArea::receiveSpriteCreateSignal(BufferReader& reader, const Poco::Buffer& buffer) +{ + const std::string& sprite_id = reader.ReadString(buffer); + auto iter = remoteSprites.find(sprite_id); + if (iter != remoteSprites.end()) return; + + std::shared_ptr node = std::make_shared(); + + const std::string& texture_path = reader.ReadString(buffer); + auto tex = serverAssetManager.GetTexture(texture_path); + + if (tex) { + node->setTexture(tex, true); + } + + // Add to table + RemoteScreenSprite& spr = + remoteSprites[sprite_id] = RemoteScreenSprite{ {}, node }; + + const std::string anim_path = reader.ReadString(buffer); + const std::string& anim_state = reader.ReadString(buffer); + + // TODO: sprite _should_ allow changing the state + if (anim_path.empty() || anim_state.empty()) return; + + const std::vector& dataBuff = serverAssetManager.GetData(anim_path); + const std::string& data = std::string(dataBuff.begin(), dataBuff.end()); + spr.anim.LoadWithData(data); + spr.anim.SetAnimation(anim_state); +} + +void Overworld::OnlineArea::receiveSpriteUpdateSignal(BufferReader& reader, const Poco::Buffer& buffer) +{ + const std::string& sprite_id = reader.ReadString(buffer); + auto iter = remoteSprites.find(sprite_id); + if (iter == remoteSprites.end()) return; + + std::shared_ptr spr = iter->second.node; + + // Translate + float tx = static_cast(reader.Read(buffer)); + float ty = static_cast(reader.Read(buffer)); + + // Scale + float sx = reader.Read(buffer); + float sy = reader.Read(buffer); + + // Rotate + float rot = reader.Read(buffer); + + // Apply + spr->setPosition(tx, ty); + spr->setScale(sx, sy); + spr->setRotation(rot); +} + +void Overworld::OnlineArea::receiveSpriteRemoveSignal(BufferReader& reader, const Poco::Buffer& buffer) +{ + remoteSprites.erase(reader.ReadString(buffer)); +} + void Overworld::OnlineArea::leave() { if (packetProcessor) { Net().DropProcessor(packetProcessor); diff --git a/BattleNetwork/overworld/bnOverworldOnlineArea.h b/BattleNetwork/overworld/bnOverworldOnlineArea.h index 5a66ca09c..7b7e7f5b3 100644 --- a/BattleNetwork/overworld/bnOverworldOnlineArea.h +++ b/BattleNetwork/overworld/bnOverworldOnlineArea.h @@ -68,6 +68,11 @@ namespace Overworld { Poco::Buffer buffer{ 0 }; }; + struct RemoteScreenSprite { + Animation anim{}; + std::shared_ptr node{ nullptr }; + }; + std::string host; uint16_t port; std::shared_ptr emoteNode; @@ -94,6 +99,7 @@ namespace Overworld { ServerAssetManager serverAssetManager; IdentityManager identityManager; AssetMeta incomingAsset; + std::map remoteSprites; std::map onlinePlayers; std::map excludedObjects; std::unordered_set excludedActors; @@ -151,6 +157,7 @@ namespace Overworld { void sendShopCloseSignal(); void sendShopPurchaseSignal(const std::string& itemName); void sendBattleResultsSignal(const BattleResults& results); + void sendEmailReadSignal(const std::string& id); void receiveAuthorizeSignal(BufferReader& reader, const Poco::Buffer&); void receiveLoginSignal(BufferReader& reader, const Poco::Buffer&); @@ -205,8 +212,18 @@ namespace Overworld { void receiveActorAnimateSignal(BufferReader& reader, const Poco::Buffer&); void receiveActorKeyFramesSignal(BufferReader& reader, const Poco::Buffer&); void receiveActorMinimapColorSignal(BufferReader& reader, const Poco::Buffer&); + void receiveFragmentSignal(BufferReader& reader, const Poco::Buffer&); + void receiveHudVisibleSignal(BufferReader& reader, const Poco::Buffer&); + void receiveHudSetModeSignal(BufferReader& reader, const Poco::Buffer&); + void receiveBattleRewardItemSignal(BufferReader& reader, const Poco::Buffer&); + void receiveSendMailSignal(BufferReader& reader, const Poco::Buffer&); + void receiveRingtoneSignal(BufferReader& reader, const Poco::Buffer&); + void receiveSpriteCreateSignal(BufferReader& reader, const Poco::Buffer&); + void receiveSpriteUpdateSignal(BufferReader& reader, const Poco::Buffer&); + void receiveSpriteRemoveSignal(BufferReader& reader, const Poco::Buffer&); void leave(); - protected: + +protected: virtual std::string GetPath(const std::string& path); virtual std::string GetText(const std::string& path); virtual std::shared_ptr GetTexture(const std::string& path); diff --git a/BattleNetwork/overworld/bnOverworldPacketHeaders.h b/BattleNetwork/overworld/bnOverworldPacketHeaders.h index 78104d716..844d84ae4 100644 --- a/BattleNetwork/overworld/bnOverworldPacketHeaders.h +++ b/BattleNetwork/overworld/bnOverworldPacketHeaders.h @@ -5,8 +5,8 @@ namespace Overworld { - constexpr std::string_view VERSION_ID = "https://github.com/ArthurCose/Scriptable-OpenNetBattle-Server"; - const uint64_t VERSION_ITERATION = 42; + constexpr std::string_view VERSION_ID = "https://github.com/OpenNetBattle/Server@Backport/2.1"; + const uint64_t VERSION_ITERATION = 43; constexpr double PACKET_RESEND_RATE = 1.0 / 20.0; @@ -41,6 +41,7 @@ namespace Overworld shop_close, shop_purchase, battle_results, + read_email, size, unknown = size }; @@ -115,6 +116,16 @@ namespace Overworld actor_animate, actor_keyframes, actor_minimap_color, + // 2.1 Backport features from 2.5 + fragments, + hud_visible, + battle_reward_item, + send_mail, + hud_set_mode, + ringtone, + sprite_create, + sprite_update, + sprite_remove, size, unknown = size }; diff --git a/BattleNetwork/overworld/bnOverworldPersonalMenu.cpp b/BattleNetwork/overworld/bnOverworldPersonalMenu.cpp index 7268afcfb..70f5618f4 100644 --- a/BattleNetwork/overworld/bnOverworldPersonalMenu.cpp +++ b/BattleNetwork/overworld/bnOverworldPersonalMenu.cpp @@ -15,12 +15,14 @@ namespace Overworld { // Load resources areaLabel.setPosition(127, 119); infoText = areaLabel; + ringSound = Audio().LoadFromFile(SoundPaths::PET_RINGTONE); // clock time.setPosition(480 - 4.f, 6.f); time.setScale(2.f, 2.f); widgetTexture = Textures().LoadFromFile("resources/ui/main_menu_ui.png"); + ringTexture = Textures().LoadFromFile(TexturePaths::HUD_RING); banner = std::make_shared(); symbol = std::make_shared(); @@ -29,6 +31,7 @@ namespace Overworld { infoBox = std::make_shared(); selectTextSpr = std::make_shared(); placeTextSpr = std::make_shared(); + ringSpr = std::make_shared(); banner->setTexture(Textures().LoadFromFile("resources/ui/menu_overlay.png")); symbol->setTexture(widgetTexture); @@ -37,6 +40,7 @@ namespace Overworld { infoBox->setTexture(widgetTexture); selectTextSpr->setTexture(widgetTexture); placeTextSpr->setTexture(widgetTexture); + ringSpr->setTexture(ringTexture); AddNode(banner); @@ -71,12 +75,19 @@ namespace Overworld { // Device / HP top-left position optionAnim << "PET"; optionAnim.SetFrame(1, icon->getSprite()); + + ResetIconTexture(); + SetPlayerDisplayMode(PlayerDisplayMode::health); icon->setPosition(2, 3); + ringAnim = Animation(AnimationPaths::HUD_RING) << "RING"; + icon->AddNode(ringSpr); + const sf::FloatRect& iconBounds = icon->getLocalBounds(); + ringSpr->setPosition(iconBounds.width, iconBounds.height * 0.5f); + ringSpr->SetLayer(-1); + healthUI.setPosition(2, 3); healthUI.setScale(0.5, 0.5); - SetPlayerDisplay(PlayerDisplay::PlayerHealth); - exitAnim = Animation("resources/ui/main_menu_ui.animation") << Animator::Mode::Loop; // @@ -301,6 +312,7 @@ namespace Overworld { frameTick = frames(0); } + ringAnim.Update(0, ringSpr->getSprite()); easeInTimer.update(sf::seconds(static_cast(elapsed))); elapsedThisFrame = elapsed; healthUI.SetHP(session->health); @@ -507,15 +519,15 @@ namespace Overworld { DrawTime(target); } - void PersonalMenu::SetPlayerDisplay(PlayerDisplay mode) { + void PersonalMenu::SetPlayerDisplayMode(PlayerDisplayMode mode) { switch (mode) { - case PlayerDisplay::PlayerHealth: + case PlayerDisplayMode::health: { healthUI.Reveal(); icon->Hide(); } break; - case PlayerDisplay::PlayerIcon: + case PlayerDisplayMode::icon: { healthUI.Hide(); icon->Reveal(); @@ -681,4 +693,10 @@ namespace Overworld { easeInTimer.start(); } } + + void PersonalMenu::Ringtone() + { + ringAnim << "RING"; + Audio().Play(ringSound, AudioPriority::highest); + } } diff --git a/BattleNetwork/overworld/bnOverworldPersonalMenu.h b/BattleNetwork/overworld/bnOverworldPersonalMenu.h index 5184a3e38..c5d7b694c 100644 --- a/BattleNetwork/overworld/bnOverworldPersonalMenu.h +++ b/BattleNetwork/overworld/bnOverworldPersonalMenu.h @@ -11,11 +11,12 @@ #include "../bnPlayerHealthUI.h" #include #include +#include namespace Overworld { - enum class PlayerDisplay : char { - PlayerIcon = 0, - PlayerHealth + enum class PlayerDisplayMode : uint16_t { + health = 0, + icon }; /** @@ -50,8 +51,10 @@ namespace Overworld { bool extendedHold{ false }; //!< If player holds the arrow keys down state currState{}; //!< Track all open/close states. Default is closed std::string areaName; //!< Area name typed out + std::shared_ptr ringTexture; std::shared_ptr iconTexture; //!< If supplying an icon, use this one std::shared_ptr widgetTexture; //!< texture used by widget + std::shared_ptr ringSound; Text areaLabel; //!< Area name displayed by widget mutable Text areaLabelThick; //!< Thick area name displayed outside of widget mutable Text infoText; //!< Text obj used for all other info @@ -64,6 +67,8 @@ namespace Overworld { std::shared_ptr infoBox; std::shared_ptr selectTextSpr; std::shared_ptr placeTextSpr; + std::shared_ptr ringSpr; + PlayerHealthUI healthUI; OptionsList optionsList; std::vector> options; @@ -71,6 +76,7 @@ namespace Overworld { Animation infoBoxAnim; Animation optionAnim; Animation exitAnim; + Animation ringAnim; // Selection input delays double maxSelectInputCooldown{}; /*!< Maximum delay */ @@ -112,7 +118,7 @@ namespace Overworld { /// Set data - void SetPlayerDisplay(PlayerDisplay mode); + void SetPlayerDisplayMode(PlayerDisplayMode mode); /** * @brief Set the area name to display @@ -167,5 +173,7 @@ namespace Overworld { * @brief Close the widget and begin the close animations */ virtual void Close(); + + void Ringtone(); }; } diff --git a/BattleNetwork/overworld/bnOverworldPlayerSession.h b/BattleNetwork/overworld/bnOverworldPlayerSession.h index 11873e18c..e3d1434bc 100644 --- a/BattleNetwork/overworld/bnOverworldPlayerSession.h +++ b/BattleNetwork/overworld/bnOverworldPlayerSession.h @@ -8,6 +8,7 @@ namespace Overworld { int health{}; int maxHealth{}; int money{}; + int fragments{}; Emotion emotion{}; Inbox inbox; }; diff --git a/BattleNetwork/overworld/bnOverworldSceneBase.cpp b/BattleNetwork/overworld/bnOverworldSceneBase.cpp index b3451a09b..a529dc98a 100644 --- a/BattleNetwork/overworld/bnOverworldSceneBase.cpp +++ b/BattleNetwork/overworld/bnOverworldSceneBase.cpp @@ -118,17 +118,6 @@ void Overworld::SceneBase::onStart() { #ifdef __ANDROID__ StartupTouchControls(); #endif - - // TODO: Take out after endpoints are added to server @Konst - Inbox& inbox = playerSession->inbox; - - sf::Texture mugshot = *Textures().LoadFromFile("resources/ow/prog/prog_mug.png"); - inbox.AddMail(Inbox::Mail{ Inbox::Icons::announcement, "Welcome", "NO-TITLE", "This is your first email!", mugshot }); - inbox.AddMail(Inbox::Mail{ Inbox::Icons::dm, "HELLO", "KERISTERO", "try gravy" }); - inbox.AddMail(Inbox::Mail{ Inbox::Icons::dm_w_attachment, "ELLO", "DESTROYED", "ello govna" }); - inbox.AddMail(Inbox::Mail{ Inbox::Icons::important, "FIRE", "NO-TITLE", "There's a fire in the undernet!", mugshot }); - inbox.AddMail(Inbox::Mail{ Inbox::Icons::mission, "MISSING", "ANON", "Can you find my missing data? It would really help me out right now... Or don't if it's too hard, I understand..." }); - inbox.AddMail(Inbox::Mail{ Inbox::Icons::dm, "Test", "NO-TITLE", "Just another test.", mugshot }); } void Overworld::SceneBase::onUpdate(double elapsed) { From c6260f2bb27372fb83a6115af708469520a2863d Mon Sep 17 00:00:00 2001 From: TheMaverickProgrammer <91709+TheMaverickProgrammer@users.noreply.github.com> Date: Tue, 21 Oct 2025 19:23:00 -0500 Subject: [PATCH 138/146] results callback moved to the start of BattleResults state. Additional results callback moved to Freedom mission onStart. --- .../battlescene/States/bnFreedomMissionOverState.cpp | 8 +++++--- .../battlescene/States/bnRewardBattleState.cpp | 3 +++ BattleNetwork/battlescene/bnBattleSceneBase.cpp | 11 ++++++++--- BattleNetwork/battlescene/bnBattleSceneBase.h | 1 + BattleNetwork/overworld/bnOverworldOnlineArea.h | 12 +++++++----- 5 files changed, 24 insertions(+), 11 deletions(-) diff --git a/BattleNetwork/battlescene/States/bnFreedomMissionOverState.cpp b/BattleNetwork/battlescene/States/bnFreedomMissionOverState.cpp index bb4472a94..59b1eb33d 100644 --- a/BattleNetwork/battlescene/States/bnFreedomMissionOverState.cpp +++ b/BattleNetwork/battlescene/States/bnFreedomMissionOverState.cpp @@ -82,10 +82,10 @@ void FreedomMissionOverState::onStart(const BattleSceneState* _) // NOTE: paranoid cleanup codes ALWAYS cleans up! // some attacks use nodes that would be cleanup with End() but overwriting the animation prevents this - auto ourNodes = p->GetChildNodesWithTag({ Player::BASE_NODE_TAG,Player::FORM_NODE_TAG }); - auto allNodes = p->GetChildNodes(); + auto& ourNodes = p->GetChildNodesWithTag({ Player::BASE_NODE_TAG,Player::FORM_NODE_TAG }); + auto& allNodes = p->GetChildNodes(); - for (auto node : allNodes) { + for (auto& node : allNodes) { auto iter = ourNodes.find(node); if (iter == ourNodes.end()) { p->RemoveNode(node); @@ -102,6 +102,8 @@ void FreedomMissionOverState::onStart(const BattleSceneState* _) } GetScene().GetField()->RequestBattleStop(); + + scene.InvokeEndCallback(results); } diff --git a/BattleNetwork/battlescene/States/bnRewardBattleState.cpp b/BattleNetwork/battlescene/States/bnRewardBattleState.cpp index 21057298e..1def2dceb 100644 --- a/BattleNetwork/battlescene/States/bnRewardBattleState.cpp +++ b/BattleNetwork/battlescene/States/bnRewardBattleState.cpp @@ -33,6 +33,7 @@ RewardBattleState::~RewardBattleState() void RewardBattleState::onStart(const BattleSceneState*) { BattleSceneBase& scene = GetScene(); + Player& player = *scene.GetLocalPlayer(); player.ChangeState(); scene.GetField()->RequestBattleStop(); @@ -46,6 +47,8 @@ void RewardBattleState::onStart(const BattleSceneState*) results.doubleDelete = scene.DoubleDelete(); results.tripleDelete = scene.TripleDelete(); results.finalEmotion = player.GetEmotion(); + + scene.InvokeEndCallback(results); battleResultsWidget = new BattleResultsWidget( BattleResults::CalculateScore(results, mob), diff --git a/BattleNetwork/battlescene/bnBattleSceneBase.cpp b/BattleNetwork/battlescene/bnBattleSceneBase.cpp index 88bcfc86c..f5181a9bc 100644 --- a/BattleNetwork/battlescene/bnBattleSceneBase.cpp +++ b/BattleNetwork/battlescene/bnBattleSceneBase.cpp @@ -295,6 +295,14 @@ void BattleSceneBase::OnDeleteEvent(Character& pending) } } +void BattleSceneBase::InvokeEndCallback(const BattleResults& results) +{ + if (onEndCallback) { + onEndCallback(results); + onEndCallback = nullptr; + } +} + const BattleSceneState* BattleSceneBase::GetCurrentState() const { return current; @@ -1037,9 +1045,6 @@ void BattleSceneBase::onDraw(sf::RenderTexture& surface) { void BattleSceneBase::onEnd() { - if (onEndCallback) { - onEndCallback(battleResults); - } } bool BattleSceneBase::TrackOtherPlayer(std::shared_ptr& other) { diff --git a/BattleNetwork/battlescene/bnBattleSceneBase.h b/BattleNetwork/battlescene/bnBattleSceneBase.h index f6bfc2dbd..d1324268b 100644 --- a/BattleNetwork/battlescene/bnBattleSceneBase.h +++ b/BattleNetwork/battlescene/bnBattleSceneBase.h @@ -423,6 +423,7 @@ class BattleSceneBase : const bool FadeInBackdrop(double amount, double to, bool affectBackground); const bool FadeOutBackdrop(double amount); + void InvokeEndCallback(const BattleResults& results); std::vector> RedTeamMobList(); std::vector> BlueTeamMobList(); diff --git a/BattleNetwork/overworld/bnOverworldOnlineArea.h b/BattleNetwork/overworld/bnOverworldOnlineArea.h index 7b7e7f5b3..2cf53c486 100644 --- a/BattleNetwork/overworld/bnOverworldOnlineArea.h +++ b/BattleNetwork/overworld/bnOverworldOnlineArea.h @@ -21,13 +21,15 @@ namespace Overworld { struct OnlinePlayer { - OnlinePlayer(std::string name) : actor(std::make_shared(name)) {} + OnlinePlayer(std::string name) + : actor(std::make_shared(name)) {} + std::shared_ptr marker; std::shared_ptr actor; std::shared_ptr emoteNode; Overworld::TeleportController teleportController{}; bool disconnecting{ false }; - Direction idleDirection; + Direction idleDirection{ Direction::none }; sf::Vector3f startBroadcastPos{}; sf::Vector3f endBroadcastPos{}; long long timestamp{}; @@ -54,8 +56,8 @@ namespace Overworld { }; struct ExcludedObjectData { - bool visible; - bool solid; + bool visible {}; + bool solid {}; }; struct AssetMeta { @@ -74,7 +76,7 @@ namespace Overworld { }; std::string host; - uint16_t port; + uint16_t port{}; std::shared_ptr emoteNode; std::shared_ptr customEmotesTexture; std::string ticket; //!< How we are represented on the server From dbc9fe349ada02b4904fba1c8fc783c9986e0c2d Mon Sep 17 00:00:00 2001 From: TheMaverickProgrammer <91709+TheMaverickProgrammer@users.noreply.github.com> Date: Wed, 22 Oct 2025 01:01:47 -0500 Subject: [PATCH 139/146] new features seem stable --- .../overworld/bnOverworldOnlineArea.cpp | 187 +++++++++++++++--- .../overworld/bnOverworldOnlineArea.h | 8 +- .../overworld/bnOverworldPacketHeaders.h | 8 +- 3 files changed, 169 insertions(+), 34 deletions(-) diff --git a/BattleNetwork/overworld/bnOverworldOnlineArea.cpp b/BattleNetwork/overworld/bnOverworldOnlineArea.cpp index 3afcdeef4..92393a7ab 100644 --- a/BattleNetwork/overworld/bnOverworldOnlineArea.cpp +++ b/BattleNetwork/overworld/bnOverworldOnlineArea.cpp @@ -217,6 +217,11 @@ void Overworld::OnlineArea::onUpdate(double elapsed) SceneBase::onUpdate(elapsed); + for (auto& p : remoteSpriteObjects) { + auto& data = p.second; + data.anim.Update(elapsed, data.node->getSprite()); + } + auto& camera = GetCamera(); warpCameraController.UpdateCamera(float(elapsed), camera); serverCameraController.UpdateCamera(float(elapsed), camera); @@ -519,6 +524,14 @@ void Overworld::OnlineArea::onDraw(sf::RenderTexture& surface) copyScreen = false; } + for (auto& p : remoteSpriteObjects) { + auto& data = p.second; + const sf::Vector2f prev = data.node->getPosition(); + //data.node->setPosition(GetCamera().GetView().getCenter()); + data.node->draw(surface); + //data.node->setPosition(prev); + } + if (GetMenuSystem().IsFullscreen()) { return; } @@ -567,7 +580,7 @@ void Overworld::OnlineArea::onDraw(sf::RenderTexture& surface) }; for (auto& pair : onlinePlayers) { - auto id = pair.first; + auto& id = pair.first; if (excludedActors.find(id) != excludedActors.end()) { // actor is excluded, do not display on hover @@ -993,6 +1006,22 @@ void Overworld::OnlineArea::processPacketBody(const Poco::Buffer& data) break; case ServerEvents::actor_minimap_color: receiveActorMinimapColorSignal(reader, data); + break; + case ServerEvents::sprite_alloc: + receiveSpriteAllocSignal(reader, data); + break; + case ServerEvents::sprite_dealloc: + receiveSpriteDeallocSignal(reader, data); + break; + case ServerEvents::sprite_draw: + receiveSpriteDrawSignal(reader, data); + break; + case ServerEvents::sprite_erase: + receiveSpriteEraseSignal(reader, data); + break; + case ServerEvents::hud_visible: + receiveHudVisibleSignal(reader, data); + break; } } catch (Poco::IOException& e) { @@ -2992,8 +3021,13 @@ void Overworld::OnlineArea::receiveFragmentSignal(BufferReader& reader, const Po void Overworld::OnlineArea::receiveHudVisibleSignal(BufferReader& reader, const Poco::Buffer& buffer) { - int balance = reader.Read(buffer); - GetPlayerSession()->fragments = balance; + const bool visible = GetPersonalMenu().IsVisible(); + if (visible) { + GetPersonalMenu().Hide(); + } + else { + GetPersonalMenu().Reveal(); + } } void Overworld::OnlineArea::receiveHudSetModeSignal(BufferReader& reader, const Poco::Buffer& buffer) @@ -3036,17 +3070,18 @@ void Overworld::OnlineArea::receiveRingtoneSignal(BufferReader& reader, const Po GetPersonalMenu().Ringtone(); } -void Overworld::OnlineArea::receiveSpriteCreateSignal(BufferReader& reader, const Poco::Buffer& buffer) +void Overworld::OnlineArea::receiveSpriteAllocSignal(BufferReader& reader, const Poco::Buffer& buffer) { - const std::string& sprite_id = reader.ReadString(buffer); + const std::string& sprite_id = reader.ReadString(buffer); + const std::string& texture_path = reader.ReadString(buffer); + auto iter = remoteSprites.find(sprite_id); if (iter != remoteSprites.end()) return; - std::shared_ptr node = std::make_shared(); + std::shared_ptr node = + std::make_shared(); - const std::string& texture_path = reader.ReadString(buffer); auto tex = serverAssetManager.GetTexture(texture_path); - if (tex) { node->setTexture(tex, true); } @@ -3058,43 +3093,139 @@ void Overworld::OnlineArea::receiveSpriteCreateSignal(BufferReader& reader, cons const std::string anim_path = reader.ReadString(buffer); const std::string& anim_state = reader.ReadString(buffer); - // TODO: sprite _should_ allow changing the state if (anim_path.empty() || anim_state.empty()) return; - const std::vector& dataBuff = serverAssetManager.GetData(anim_path); - const std::string& data = std::string(dataBuff.begin(), dataBuff.end()); - spr.anim.LoadWithData(data); + const std::string anim_data = serverAssetManager.GetText(anim_path); + spr.anim.LoadWithData(anim_data); spr.anim.SetAnimation(anim_state); } -void Overworld::OnlineArea::receiveSpriteUpdateSignal(BufferReader& reader, const Poco::Buffer& buffer) +void Overworld::OnlineArea::receiveSpriteDrawSignal(BufferReader& reader, const Poco::Buffer& buffer) { - const std::string& sprite_id = reader.ReadString(buffer); + const std::string& sprite_id = reader.ReadString(buffer); auto iter = remoteSprites.find(sprite_id); if (iter == remoteSprites.end()) return; - std::shared_ptr spr = iter->second.node; + std::shared_ptr node = iter->second.node; + + // Unique instance object + const std::string& obj_id = reader.ReadString(buffer); + + auto iter2 = remoteSpriteObjects.find(obj_id); + if (iter2 == remoteSpriteObjects.end()) { + // Add to table + remoteSpriteObjects[obj_id] = RemoteScreenSprite{ + iter->second.anim, + std::make_shared(node->getSprite()), + }; + iter2 = remoteSpriteObjects.find(obj_id); + } + + // Fetch + RemoteScreenSprite& obj = iter2->second; + + // Read mask + const uint16_t mask = reader.Read(buffer); + + sf::Vector2f prevPos = obj.node->getPosition(); + sf::Vector2f prevScale = obj.node->getScale(); + + // Translation X + if ((mask & 0x01) == 0x01) { + const float tx = + static_cast(reader.Read(buffer)); + obj.node->setPosition(tx, prevPos.y); + prevPos.x = tx; + } + + // Translation Y + if ((mask & 0x02) == 0x02) { + const float ty = + static_cast(reader.Read(buffer)); + obj.node->setPosition(prevPos.x, ty); + } - // Translate - float tx = static_cast(reader.Read(buffer)); - float ty = static_cast(reader.Read(buffer)); + // Scale X + if ((mask & 0x04) == 0x04) { + const float sx = reader.Read(buffer); + obj.node->setScale(sx, prevScale.y); + prevScale.x = sx; + } - // Scale - float sx = reader.Read(buffer); - float sy = reader.Read(buffer); + // Scale Y + if ((mask & 0x08) == 0x08) { + const float sy = reader.Read(buffer); + obj.node->setScale(prevScale.x, sy); + } // Rotate - float rot = reader.Read(buffer); + if ((mask & 0x10) == 0x10) { + const float rot = reader.Read(buffer); + obj.node->setRotation(rot); + } + + // Opacity + if ((mask & 0x20) == 0x20) { + const uint8_t opacity = reader.Read(buffer); + sf::Color color = obj.node->getColor(); + color.a = opacity; + obj.node->setColor(color); + } + + // Texture + if ((mask & 0x40) == 0x40) { + const std::string texture_path = + reader.ReadString(buffer); - // Apply - spr->setPosition(tx, ty); - spr->setScale(sx, sy); - spr->setRotation(rot); + auto tex = serverAssetManager.GetTexture(texture_path); + + if (tex) { + node->setTexture(tex, true); + obj.anim.Refresh(obj.node->getSprite()); + } + } + + // Anim Path + if ((mask & 0x80) == 0x80) { + const std::string anim_path = + reader.ReadString(buffer); + + auto anim_data = serverAssetManager.GetText(anim_path); + + if (!anim_data.empty()) { + obj.anim = Animation(anim_data); + } + } + + // Anim State + if ((mask & 0x100) == 0x100) { + const std::string anim_state = + reader.ReadString(buffer); + obj.anim.SetAnimation(anim_state); + obj.anim.Refresh(obj.node->getSprite()); + } } -void Overworld::OnlineArea::receiveSpriteRemoveSignal(BufferReader& reader, const Poco::Buffer& buffer) +void Overworld::OnlineArea::receiveSpriteEraseSignal(BufferReader& reader, const Poco::Buffer& buffer) { - remoteSprites.erase(reader.ReadString(buffer)); + remoteSpriteObjects.erase(reader.ReadString(buffer)); +} + +void Overworld::OnlineArea::receiveSpriteDeallocSignal(BufferReader& reader, const Poco::Buffer& buffer) +{ + auto iter = remoteSprites.find(reader.ReadString(buffer)); + if (iter == remoteSprites.end()) return; + + for (auto& iter2 = remoteSpriteObjects.begin(); iter2 != remoteSpriteObjects.end(); /*manual*/) { + if (iter2->second.node == iter->second.node) { + iter2 = remoteSpriteObjects.erase(iter2); + } + else { + iter2 = std::next(iter2); + } + } + + remoteSprites.erase(iter); } void Overworld::OnlineArea::leave() { diff --git a/BattleNetwork/overworld/bnOverworldOnlineArea.h b/BattleNetwork/overworld/bnOverworldOnlineArea.h index 2cf53c486..1d196d11f 100644 --- a/BattleNetwork/overworld/bnOverworldOnlineArea.h +++ b/BattleNetwork/overworld/bnOverworldOnlineArea.h @@ -102,6 +102,7 @@ namespace Overworld { IdentityManager identityManager; AssetMeta incomingAsset; std::map remoteSprites; + std::map remoteSpriteObjects; std::map onlinePlayers; std::map excludedObjects; std::unordered_set excludedActors; @@ -220,9 +221,10 @@ namespace Overworld { void receiveBattleRewardItemSignal(BufferReader& reader, const Poco::Buffer&); void receiveSendMailSignal(BufferReader& reader, const Poco::Buffer&); void receiveRingtoneSignal(BufferReader& reader, const Poco::Buffer&); - void receiveSpriteCreateSignal(BufferReader& reader, const Poco::Buffer&); - void receiveSpriteUpdateSignal(BufferReader& reader, const Poco::Buffer&); - void receiveSpriteRemoveSignal(BufferReader& reader, const Poco::Buffer&); + void receiveSpriteAllocSignal(BufferReader& reader, const Poco::Buffer&); + void receiveSpriteDrawSignal(BufferReader& reader, const Poco::Buffer&); + void receiveSpriteEraseSignal(BufferReader& reader, const Poco::Buffer&); + void receiveSpriteDeallocSignal(BufferReader& reader, const Poco::Buffer&); void leave(); protected: diff --git a/BattleNetwork/overworld/bnOverworldPacketHeaders.h b/BattleNetwork/overworld/bnOverworldPacketHeaders.h index 844d84ae4..f56471a52 100644 --- a/BattleNetwork/overworld/bnOverworldPacketHeaders.h +++ b/BattleNetwork/overworld/bnOverworldPacketHeaders.h @@ -117,15 +117,17 @@ namespace Overworld actor_keyframes, actor_minimap_color, // 2.1 Backport features from 2.5 + UNUSED_offer_package, // Unused packet from 2.0 server. fragments, hud_visible, battle_reward_item, send_mail, hud_set_mode, ringtone, - sprite_create, - sprite_update, - sprite_remove, + sprite_alloc, + sprite_dealloc, + sprite_draw, + sprite_erase, size, unknown = size }; From 57c79c5fe76ba4bca6c9820de32a687150c4fd25 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Wed, 22 Oct 2025 11:54:28 -0700 Subject: [PATCH 140/146] Backport online area cleanup TODO: onEnd called without cleaning up when exiting via home warp --- .../overworld/bnOverworldOnlineArea.cpp | 108 +++++++++++++----- .../overworld/bnOverworldOnlineArea.h | 2 + 2 files changed, 79 insertions(+), 31 deletions(-) diff --git a/BattleNetwork/overworld/bnOverworldOnlineArea.cpp b/BattleNetwork/overworld/bnOverworldOnlineArea.cpp index 92393a7ab..ccd6c7540 100644 --- a/BattleNetwork/overworld/bnOverworldOnlineArea.cpp +++ b/BattleNetwork/overworld/bnOverworldOnlineArea.cpp @@ -23,6 +23,7 @@ #include "../bnMobPackageManager.h" #include "../bnPlayerPackageManager.h" #include "../bnBlockPackageManager.h" +#include "../bnLuaLibraryPackageManager.h" #include "../bnMessageQuestion.h" #include "../bnPlayerCustScene.h" #include "../bnSelectNaviScene.h" @@ -108,6 +109,10 @@ Overworld::OnlineArea::OnlineArea( // ensure the existence of these package partitions getController().GetMobPackagePartitioner().CreateNamespace(Game::ServerPartition); + getController().GetCardPackagePartitioner().CreateNamespace(Game::ServerPartition); + getController().GetBlockPackagePartitioner().CreateNamespace(Game::ServerPartition); + getController().GetLuaLibraryPackagePartitioner().CreateNamespace(Game::ServerPartition); + getController().GetPlayerPackagePartitioner().CreateNamespace(Game::ServerPartition); } Overworld::OnlineArea::~OnlineArea() @@ -246,8 +251,33 @@ void Overworld::OnlineArea::ResetPVPStep(bool failed) } void Overworld::OnlineArea::RemovePackages() { - Logger::Log(LogLevel::debug, "Removing server packages"); - getController().GetMobPackagePartitioner().GetPartition(Game::ServerPartition).ClearPackages(); + Logger::Log(LogLevel::debug, + "Removing server packages"); + + getController() + .GetBlockPackagePartitioner() + .GetPartition(Game::ServerPartition) + .ClearPackages(); + + getController() + .GetCardPackagePartitioner() + .GetPartition(Game::ServerPartition) + .ClearPackages(); + + getController() + .GetMobPackagePartitioner() + .GetPartition(Game::ServerPartition) + .ClearPackages(); + + getController() + .GetLuaLibraryPackagePartitioner() + .GetPartition(Game::ServerPartition) + .ClearPackages(); + + getController() + .GetPlayerPackagePartitioner() + .GetPartition(Game::ServerPartition) + .ClearPackages(); } void Overworld::OnlineArea::updateOtherPlayers(double elapsed) { @@ -617,25 +647,32 @@ void Overworld::OnlineArea::onStart() movementTimer.start(); } -void Overworld::OnlineArea::onEnd() -{ - if (packetProcessor) { - sendLogoutSignal(); - Net().DropProcessor(packetProcessor); - packetProcessor = nullptr; +void Overworld::OnlineArea::onEnd() { + if (!cleanedUp) { + Logger::Log(LogLevel::critical, "OverworldOnlineArea::cleanup() call missed, may have unintended effects from transition period"); + cleanup(); } +} +void Overworld::OnlineArea::cleanup() { for (auto& [key, processor] : authorizationProcessors) { Net().DropProcessor(processor); } + authorizationProcessors.clear(); - getController().Session().SetWhitelist({}); // clear the whitelist - - if (!transferringServers) { - // clear packages when completing the return to the homepage - // we already clear packages when transferring to a new server - RemovePackages(); + if (packetProcessor) { + sendLogoutSignal(); + Net().DropProcessor(packetProcessor); + packetProcessor = nullptr; } + + RemovePackages(); + GameSession& session = getController().Session(); + session.SetWhitelist({}); // clear the whitelist + // TODO: Add blacklist support + //session.SetBlacklist({}); // clear the blacklist + + cleanedUp = true; } void Overworld::OnlineArea::onLeave() @@ -754,7 +791,11 @@ Overworld::TeleportController::Command& Overworld::OnlineArea::teleportIn(sf::Ve return GetTeleportController().TeleportIn(actor, position, direction); } -void Overworld::OnlineArea::transferServer(const std::string& host, uint16_t port, std::string data, bool warpOut) { +void Overworld::OnlineArea::transferServer( + const std::string& host, + uint16_t port, + std::string data, + bool warpOut) { auto reportFailure = [=] { SetAvatarAsSpeaker(); GetMenuSystem().EnqueueMessage("Looks like the next area is offline..."); @@ -762,9 +803,10 @@ void Overworld::OnlineArea::transferServer(const std::string& host, uint16_t por auto handleFail = [=] { if (warpOut) { - auto player = GetPlayer(); - auto position = player->Get3DPosition(); - auto direction = Reverse(player->GetHeading()); + std::shared_ptr player = GetPlayer(); + sf::Vector3f position = player->Get3DPosition(); + Direction direction = Reverse(player->GetHeading()); + GetPlayerController().ReleaseActor(); auto& command = GetTeleportController().TeleportIn(player, position, direction); warpCameraController.UnlockCamera(); @@ -788,17 +830,19 @@ void Overworld::OnlineArea::transferServer(const std::string& host, uint16_t por return; } - auto packetProcessor = std::make_shared( - remoteAddress, - Net().GetMaxPayloadSize() - ); + std::shared_ptr packetProcessor = + std::make_shared( + remoteAddress, + Net().GetMaxPayloadSize()); - packetProcessor->SetStatusHandler([this, host, port, data, handleFail, packetProcessor = packetProcessor.get()](auto status, auto maxPayloadSize) { + packetProcessor->SetStatusHandler( + [this, host, port, data, handleFail, packetProcessor = packetProcessor.get()] + (auto status, auto maxPayloadSize) { if (status == ServerStatus::online) { - AddSceneChangeTask([=] { - RemovePackages(); - getController().replace::to>(host, port, data, maxPayloadSize); - }); + cleanup(); + getController() + .replace + ::to>(host, port, data, maxPayloadSize); } else { handleFail(); @@ -811,6 +855,7 @@ void Overworld::OnlineArea::transferServer(const std::string& host, uint16_t por }; if (warpOut) { + GetPlayerController().ReleaseActor(); auto& command = GetTeleportController().TeleportOut(GetPlayer()); command.onFinish.Slot(attemptTransfer); } @@ -3229,11 +3274,12 @@ void Overworld::OnlineArea::receiveSpriteDeallocSignal(BufferReader& reader, con } void Overworld::OnlineArea::leave() { - if (packetProcessor) { - Net().DropProcessor(packetProcessor); - packetProcessor = nullptr; + try { + cleanup(); + } + catch (std::exception& e) { + Logger::Logf(LogLevel::critical, e.what()); } - tryPopScene = true; } diff --git a/BattleNetwork/overworld/bnOverworldOnlineArea.h b/BattleNetwork/overworld/bnOverworldOnlineArea.h index 1d196d11f..f53eb1546 100644 --- a/BattleNetwork/overworld/bnOverworldOnlineArea.h +++ b/BattleNetwork/overworld/bnOverworldOnlineArea.h @@ -96,6 +96,7 @@ namespace Overworld { bool tryPopScene{ false }; bool canProceedToBattle{ false }; bool copyScreen{ false }; + bool cleanedUp{ false }; ReturningScene returningFrom{ ReturningScene::Null }; ActorPropertyAnimator propertyAnimator; ServerAssetManager serverAssetManager; @@ -256,6 +257,7 @@ namespace Overworld { void onStart() override; void onEnd() override; void onLeave() override; + void cleanup(); void onResume() override; void OnTileCollision() override; From bc69e41248fca145ae1ab68b5d29e78f708ab038 Mon Sep 17 00:00:00 2001 From: TheMaverickProgrammer <91709+TheMaverickProgrammer@users.noreply.github.com> Date: Wed, 22 Oct 2025 14:23:14 -0500 Subject: [PATCH 141/146] packets for sprite api --- .../overworld/bnOverworldOnlineArea.cpp | 93 ++++++++++++------- .../overworld/bnOverworldOnlineArea.h | 1 + 2 files changed, 60 insertions(+), 34 deletions(-) diff --git a/BattleNetwork/overworld/bnOverworldOnlineArea.cpp b/BattleNetwork/overworld/bnOverworldOnlineArea.cpp index 92393a7ab..4d71bfc24 100644 --- a/BattleNetwork/overworld/bnOverworldOnlineArea.cpp +++ b/BattleNetwork/overworld/bnOverworldOnlineArea.cpp @@ -217,8 +217,23 @@ void Overworld::OnlineArea::onUpdate(double elapsed) SceneBase::onUpdate(elapsed); - for (auto& p : remoteSpriteObjects) { - auto& data = p.second; + auto z_order = + [this] + (const std::string& a, const std::string& b) -> bool + { + auto& aNode = remoteSpriteObjects[a].node; + auto& bNode = remoteSpriteObjects[b].node; + return aNode->GetLayer() < bNode->GetLayer(); + }; + + std::sort( + remoteSpriteObjectOrder.begin(), + remoteSpriteObjectOrder.end(), + z_order + ); + + for (auto& p : remoteSpriteObjectOrder) { + auto& data = remoteSpriteObjects[p]; data.anim.Update(elapsed, data.node->getSprite()); } @@ -524,18 +539,15 @@ void Overworld::OnlineArea::onDraw(sf::RenderTexture& surface) copyScreen = false; } - for (auto& p : remoteSpriteObjects) { - auto& data = p.second; - const sf::Vector2f prev = data.node->getPosition(); - //data.node->setPosition(GetCamera().GetView().getCenter()); - data.node->draw(surface); - //data.node->setPosition(prev); - } - if (GetMenuSystem().IsFullscreen()) { return; } + for (auto& p : remoteSpriteObjectOrder) { + auto& data = remoteSpriteObjects[p]; + data.node->draw(surface); + } + auto& window = getController().getWindow(); auto mousei = sf::Mouse::getPosition(window); auto mousef = window.mapPixelToCoords(mousei); @@ -3119,6 +3131,9 @@ void Overworld::OnlineArea::receiveSpriteDrawSignal(BufferReader& reader, const std::make_shared(node->getSprite()), }; iter2 = remoteSpriteObjects.find(obj_id); + + // Add index entry to the order list + remoteSpriteObjectOrder.push_back(obj_id); } // Fetch @@ -3129,6 +3144,7 @@ void Overworld::OnlineArea::receiveSpriteDrawSignal(BufferReader& reader, const sf::Vector2f prevPos = obj.node->getPosition(); sf::Vector2f prevScale = obj.node->getScale(); + sf::Vector2f prevOrigin = obj.node->getOrigin(); // Translation X if ((mask & 0x01) == 0x01) { @@ -3172,43 +3188,52 @@ void Overworld::OnlineArea::receiveSpriteDrawSignal(BufferReader& reader, const obj.node->setColor(color); } - // Texture + // Anim State if ((mask & 0x40) == 0x40) { - const std::string texture_path = + const std::string anim_state = reader.ReadString(buffer); - - auto tex = serverAssetManager.GetTexture(texture_path); - - if (tex) { - node->setTexture(tex, true); - obj.anim.Refresh(obj.node->getSprite()); - } + obj.anim.SetAnimation(anim_state); + obj.anim.Refresh(obj.node->getSprite()); } - // Anim Path + // Layer (Sorting) if ((mask & 0x80) == 0x80) { - const std::string anim_path = - reader.ReadString(buffer); - - auto anim_data = serverAssetManager.GetText(anim_path); - - if (!anim_data.empty()) { - obj.anim = Animation(anim_data); - } + const int16_t layer = reader.Read(buffer); + obj.node->SetLayer(layer); } - // Anim State + // Origin X (in pixels) if ((mask & 0x100) == 0x100) { - const std::string anim_state = - reader.ReadString(buffer); - obj.anim.SetAnimation(anim_state); - obj.anim.Refresh(obj.node->getSprite()); + const float ox = static_cast(reader.Read(buffer)); + obj.node->setOrigin(ox, prevOrigin.y); + prevOrigin.x = ox; + } + + // Origin Y (in pixels) + if ((mask & 0x200) == 0x200) { + const float oy = static_cast(reader.Read(buffer)); + obj.node->setOrigin(prevOrigin.x, oy); } } void Overworld::OnlineArea::receiveSpriteEraseSignal(BufferReader& reader, const Poco::Buffer& buffer) { - remoteSpriteObjects.erase(reader.ReadString(buffer)); + const std::string& obj_id = reader.ReadString(buffer); + if (remoteSpriteObjects.find(obj_id) == remoteSpriteObjects.end()) { + return; + } + + remoteSpriteObjects.erase(obj_id); + + auto iter = std::find( + remoteSpriteObjectOrder.begin(), + remoteSpriteObjectOrder.end(), + obj_id + ); + + // This should always exist b/c its lifetime is bound to the + // corresponding remoteSpriteObjects entry. + remoteSpriteObjectOrder.erase(iter); } void Overworld::OnlineArea::receiveSpriteDeallocSignal(BufferReader& reader, const Poco::Buffer& buffer) diff --git a/BattleNetwork/overworld/bnOverworldOnlineArea.h b/BattleNetwork/overworld/bnOverworldOnlineArea.h index 1d196d11f..d4128f494 100644 --- a/BattleNetwork/overworld/bnOverworldOnlineArea.h +++ b/BattleNetwork/overworld/bnOverworldOnlineArea.h @@ -101,6 +101,7 @@ namespace Overworld { ServerAssetManager serverAssetManager; IdentityManager identityManager; AssetMeta incomingAsset; + std::vector remoteSpriteObjectOrder; std::map remoteSprites; std::map remoteSpriteObjects; std::map onlinePlayers; From bb7f850b9b9b15ea6d89b02bb63b18c0bc3a9012 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Wed, 22 Oct 2025 14:49:16 -0700 Subject: [PATCH 142/146] Pull 9724d395aa1b25bc15f1f15766ae3693aee4a249, 5ad50cb2fb1eb1e5e1873eda04ba280f43ae4f1a, ceb393bc025286036d9e0ca44b50563ed1e285a9, and 0c0e5506a4d052517d370f2d1b2cdaf37fa9a791 for package_offer packet and server mod blacklist --- BattleNetwork/bnPackageManager.h | 35 ++++ .../overworld/bnOverworldOnlineArea.cpp | 184 ++++++++++++++++-- .../overworld/bnOverworldOnlineArea.h | 9 + .../overworld/bnOverworldPacketHeaders.h | 11 +- 4 files changed, 227 insertions(+), 12 deletions(-) diff --git a/BattleNetwork/bnPackageManager.h b/BattleNetwork/bnPackageManager.h index 52ebcc2a1..599352856 100644 --- a/BattleNetwork/bnPackageManager.h +++ b/BattleNetwork/bnPackageManager.h @@ -181,6 +181,11 @@ class PackageManager { */ void ClearPackages(); + /** + * @brief Erase files associated with packages, file2PackageId, and zipFile2PackageId hashes. + */ + void ErasePackage(const std::string& packageId); + /** * @brief Erase files associated with packages, file2PackageId, and zipFile2PackageId hashes. * @warning Does not clear the assigned namespaceId @@ -590,6 +595,36 @@ inline void PackageManager::ClearPackages() zipFilepathToPackageId.clear(); } +template +inline void PackageManager::ErasePackage(const std::string& packageId) { + auto packageIt = packages.find(packageId); + + if (packageIt == packages.end()) { + Logger::Logf(LogLevel::debug, "Could not find package %s to erase", packageId.c_str()); + return; + } + + MetaClass* package = packageIt->second; + + ResourceHandle handle; + + PackageAddress addr = { GetNamespace(), packageId }; + handle.Scripts().DropPackageData(addr); + + auto path = package->GetFilePath(); + auto zipPath = path + ".zip"; + + std::filesystem::path absolute = std::filesystem::absolute(path); + std::filesystem::path absoluteZip = std::filesystem::absolute(zipPath); + + std::filesystem::remove_all(absolute); + std::filesystem::remove_all(absoluteZip); + + packages.erase(packageId); + filepathToPackageId.erase(path); + zipFilepathToPackageId.erase(path); +} + template inline void PackageManager::ErasePackages() { diff --git a/BattleNetwork/overworld/bnOverworldOnlineArea.cpp b/BattleNetwork/overworld/bnOverworldOnlineArea.cpp index ccd6c7540..8832107ad 100644 --- a/BattleNetwork/overworld/bnOverworldOnlineArea.cpp +++ b/BattleNetwork/overworld/bnOverworldOnlineArea.cpp @@ -24,6 +24,10 @@ #include "../bnPlayerPackageManager.h" #include "../bnBlockPackageManager.h" #include "../bnLuaLibraryPackageManager.h" +#include "../bindings/bnScriptedCard.h" +#include "../bindings/bnLuaLibrary.h" +#include "../bindings/bnScriptedPlayer.h" +#include "../bindings/bnScriptedBlock.h" #include "../bnMessageQuestion.h" #include "../bnPlayerCustScene.h" #include "../bnSelectNaviScene.h" @@ -1016,6 +1020,9 @@ void Overworld::OnlineArea::processPacketBody(const Poco::Buffer& data) case ServerEvents::load_package: receiveLoadPackageSignal(reader, data); break; + case ServerEvents::offer_package: + receivePackageOfferSignal(reader, data); + break; case ServerEvents::mod_whitelist: receiveModWhitelistSignal(reader, data); break; @@ -2477,6 +2484,7 @@ void Overworld::OnlineArea::receiveLoadPackageSignal(BufferReader& reader, const } std::string asset_path = reader.ReadString(buffer); + PackageType package_type = reader.Read(buffer); std::string file_path = serverAssetManager.GetPath(asset_path); @@ -2485,28 +2493,163 @@ void Overworld::OnlineArea::receiveLoadPackageSignal(BufferReader& reader, const return; } - // loading everything as an encounter for now - LoadPackage(getController().GetMobPackagePartitioner(), file_path); + switch (package_type) { + case PackageType::blocks: + LoadPackage(getController().GetBlockPackagePartitioner(), file_path); + break; + case PackageType::card: + LoadPackage(getController().GetCardPackagePartitioner(), file_path); + break; + case PackageType::library: + LoadPackage(getController().GetLuaLibraryPackagePartitioner(), file_path); + break; + case PackageType::player: + LoadPackage(getController().GetPlayerPackagePartitioner(), file_path); + break; + default: + LoadPackage(getController().GetMobPackagePartitioner(), file_path); + } +} + +template +void Overworld::OnlineArea::InstallPackage(Manager& manager, const std::string& modFolder, const std::string& packageName, const std::string& packageId, const std::string& filePath) { + auto& menuSystem = GetMenuSystem(); + + SetAvatarAsSpeaker(); + menuSystem.EnqueueMessage("Installing\x01..."); + + manager.ErasePackage(packageId); + + std::string installPath = modFolder + "/package-" + URIEncode(packageId) + ".zip"; + + try { + std::filesystem::copy_file(filePath, installPath); + } + catch (std::exception& e) { + Logger::Logf(LogLevel::critical, "Failed to copy package %s to %s. Reason: %s", packageId.c_str(), installPath.c_str(), e.what()); + menuSystem.EnqueueMessage("Installation failed."); + return; + } + + auto res = manager.template LoadPackageFromZip(installPath); + + if (res.is_error()) { + Logger::Logf(LogLevel::critical, "%s", res.error_cstr()); + menuSystem.EnqueueMessage("Installation failed."); + return; + } + + menuSystem.EnqueueMessage(packageName + " successfully installed!"); } -void Overworld::OnlineArea::receiveModWhitelistSignal(BufferReader& reader, const Poco::Buffer& buffer) +template +void Overworld::OnlineArea::RunPackageWizard(Partitioner& partitioner, const std::string& modFolder, const std::string& packageName, const std::string& packageId, const std::string& filePath) { - std::string assetPath = reader.ReadString(buffer); - std::string whitelistString = GetText(assetPath); - std::string_view whitelistView = whitelistString; + auto& localManager = partitioner.GetPartition(Game::LocalPartition); + + bool hasPackage = localManager.HasPackage(packageId); + + // check if the package is already installed + if (localManager.HasPackage(packageId)) { + stx::result_t md5Result = stx::generate_md5_from_file(filePath); + + if (md5Result.is_error()) { + Logger::Logf(LogLevel::critical, "Failed to create md5 for %s. Reason: %s", filePath.c_str(), md5Result.error_cstr()); + return; + } + + std::string md5 = md5Result.value(); + + if (localManager.FindPackageByID(packageId).fingerprint == md5) { + // package already installed + return; + } + } + + // request permission from the player + SetAvatarAsSpeaker(); + + GetMenuSystem().EnqueueMessage("Receiving data\x01...", [this]() { + GetPlayer()->Face(Direction::down_right); + }); + + GetMenuSystem().EnqueueQuestion( + "Received data for " + packageName + " install?", + [this, &localManager, hasPackage, modFolder, packageName, packageId, filePath](bool yes) { + if (!yes) { + return; + } + + if (!hasPackage) { + InstallPackage(localManager, modFolder, packageName, packageId, filePath); + return; + } + + + SetAvatarAsSpeaker(); + GetMenuSystem().EnqueueQuestion( + packageName + " conflicts with an existing package, overwrite?", + [this, &localManager, modFolder, packageName, packageId, filePath](bool yes) { + if (!yes) { + return; + } + + InstallPackage(localManager, modFolder, packageName, packageId, filePath); + } + ); + } + ); +} + +void Overworld::OnlineArea::RunPackageWizard(PackageType packageType, const std::string& packageName, std::string& packageId, const std::string& filePath) { + // todo: define mod folders in a single location? + + switch (packageType) { + case PackageType::blocks: + RunPackageWizard(getController().GetBlockPackagePartitioner(), "resources/mods/blocks", packageName, packageId, filePath); + break; + case PackageType::card: + RunPackageWizard(getController().GetCardPackagePartitioner(), "resources/mods/cards", packageName, packageId, filePath); + break; + case PackageType::library: + RunPackageWizard(getController().GetLuaLibraryPackagePartitioner(), "resources/mods/libs", packageName, packageId, filePath); + break; + case PackageType::player: + RunPackageWizard(getController().GetPlayerPackagePartitioner(), "resources/mods/players", packageName, packageId, filePath); + break; + default: + RunPackageWizard(getController().GetMobPackagePartitioner(), "resources/mods/enemies", packageName, packageId, filePath); + } +} + +void Overworld::OnlineArea::receivePackageOfferSignal(BufferReader& reader, const Poco::Buffer& buffer) { + PackageType packageType = reader.Read(buffer); + std::string packageId = GetPath(reader.ReadString(buffer)); + std::string packageName = GetPath(reader.ReadString(buffer)); + std::string filePath = GetPath(reader.ReadString(buffer)); + + if (packageName.empty()) { + packageName = "Dependency"; + } + + RunPackageWizard(packageType, packageName, packageId, filePath); +} + + +static std::vector ParsePackageList(std::string_view packageListView) { std::vector packageHashes; size_t endLine = 0; do { size_t startLine = endLine; - endLine = whitelistView.find("\n", startLine); + endLine = packageListView.find("\n", startLine); if (endLine == string::npos) { - endLine = whitelistView.size(); + endLine = packageListView.size(); } - std::string_view lineView = whitelistView.substr(startLine, endLine - startLine); + std::string_view lineView = packageListView.substr(startLine, endLine - startLine); endLine += 1; // skip past the \n if (lineView[lineView.size() - 1] == '\r') { @@ -2519,7 +2662,7 @@ void Overworld::OnlineArea::receiveModWhitelistSignal(BufferReader& reader, cons } size_t spaceIndex = lineView.find(' '); - + if (spaceIndex == string::npos) { // missing space continue; @@ -2530,13 +2673,32 @@ void Overworld::OnlineArea::receiveModWhitelistSignal(BufferReader& reader, cons packageHash.packageId = lineView.substr(33); packageHashes.push_back(packageHash); - } while(endLine < whitelistView.size()); + } while (endLine < packageListView.size()); + + return packageHashes; +} + + +void Overworld::OnlineArea::receiveModWhitelistSignal(BufferReader& reader, const Poco::Buffer& buffer) { + std::string assetPath = reader.ReadString(buffer); + std::string whitelistString = assetPath.empty() ? "" : GetText(assetPath); + std::vector packageHashes = ParsePackageList(whitelistString); getController().Session().SetWhitelist(packageHashes); AddSceneChangeTask([this] { CheckPlayerAgainstWhitelist(); }); } +void Overworld::OnlineArea::receiveModBlacklistSignal(BufferReader& reader, const Poco::Buffer& buffer) { + std::string assetPath = reader.ReadString(buffer); + std::string blacklistString = assetPath.empty() ? "" : GetText(assetPath); + std::vector packageHashes = ParsePackageList(blacklistString); + + // getController().Session().SetBlacklist(packageHashes); + + // AddSceneChangeTask([this] { CheckPlayerAgainstWhitelist(); }); +} + void Overworld::OnlineArea::receiveMobSignal(BufferReader& reader, const Poco::Buffer& buffer) { if (transferringServers) { diff --git a/BattleNetwork/overworld/bnOverworldOnlineArea.h b/BattleNetwork/overworld/bnOverworldOnlineArea.h index f53eb1546..c3d5a5eaa 100644 --- a/BattleNetwork/overworld/bnOverworldOnlineArea.h +++ b/BattleNetwork/overworld/bnOverworldOnlineArea.h @@ -134,6 +134,13 @@ namespace Overworld { Overworld::TeleportController::Command& teleportIn(sf::Vector3f position, Direction direction); void transferServer(const std::string& host, uint16_t port, std::string data, bool warpOut); void processPacketBody(const Poco::Buffer& data); + + template + void InstallPackage(Partition& partition, const std::string& modFolder, const std::string& packageName, const std::string& packageId, const std::string& filePath); + template + void RunPackageWizard(Partitioner& partitioner, const std::string& modFolder, const std::string& packageName, const std::string& packageId, const std::string& filePath); + void RunPackageWizard(PackageType packageType, const std::string& packageName, std::string& packageId, const std::string& filePath); + void CheckPlayerAgainstWhitelist(); void sendAssetFoundSignal(const std::string& path, uint64_t lastModified); @@ -205,7 +212,9 @@ namespace Overworld { void receiveOpenShopSignal(BufferReader& reader, const Poco::Buffer&); void receivePVPSignal(BufferReader& reader, const Poco::Buffer&); void receiveLoadPackageSignal(BufferReader& reader, const Poco::Buffer&); + void receivePackageOfferSignal(BufferReader& reader, const Poco::Buffer&); void receiveModWhitelistSignal(BufferReader& reader, const Poco::Buffer& buffer); + void receiveModBlacklistSignal(BufferReader& reader, const Poco::Buffer& buffer); void receiveMobSignal(BufferReader& reader, const Poco::Buffer&); void receiveActorConnectedSignal(BufferReader& reader, const Poco::Buffer&); void receiveActorDisconnectedSignal(BufferReader& reader, const Poco::Buffer&); diff --git a/BattleNetwork/overworld/bnOverworldPacketHeaders.h b/BattleNetwork/overworld/bnOverworldPacketHeaders.h index f56471a52..f2b6e6202 100644 --- a/BattleNetwork/overworld/bnOverworldPacketHeaders.h +++ b/BattleNetwork/overworld/bnOverworldPacketHeaders.h @@ -117,7 +117,7 @@ namespace Overworld actor_keyframes, actor_minimap_color, // 2.1 Backport features from 2.5 - UNUSED_offer_package, // Unused packet from 2.0 server. + offer_package, // Unused packet from 2.0 server. fragments, hud_visible, battle_reward_item, @@ -138,4 +138,13 @@ namespace Overworld audio, data }; + + enum class PackageType : char { + blocks, + card, + encounter, + character, + library, + player, + }; } // namespace Overworld From 7a3f5bed4fee92b0895ce952ae095d8b1f86da72 Mon Sep 17 00:00:00 2001 From: TheMaverickProgrammer <91709+TheMaverickProgrammer@users.noreply.github.com> Date: Wed, 22 Oct 2025 17:03:02 -0500 Subject: [PATCH 143/146] smarter resource management with sprites. smarter tracking. --- .../overworld/bnOverworldOnlineArea.cpp | 88 +++++++++++------ .../overworld/bnOverworldOnlineArea.h | 99 ++++++++++++++++++- 2 files changed, 157 insertions(+), 30 deletions(-) diff --git a/BattleNetwork/overworld/bnOverworldOnlineArea.cpp b/BattleNetwork/overworld/bnOverworldOnlineArea.cpp index 4d71bfc24..1dec401a5 100644 --- a/BattleNetwork/overworld/bnOverworldOnlineArea.cpp +++ b/BattleNetwork/overworld/bnOverworldOnlineArea.cpp @@ -221,9 +221,9 @@ void Overworld::OnlineArea::onUpdate(double elapsed) [this] (const std::string& a, const std::string& b) -> bool { - auto& aNode = remoteSpriteObjects[a].node; - auto& bNode = remoteSpriteObjects[b].node; - return aNode->GetLayer() < bNode->GetLayer(); + auto& objA = remoteSpriteObjects[a]; + auto& objB = remoteSpriteObjects[b]; + return objA.GetLayer() < objB.GetLayer(); }; std::sort( @@ -233,8 +233,7 @@ void Overworld::OnlineArea::onUpdate(double elapsed) ); for (auto& p : remoteSpriteObjectOrder) { - auto& data = remoteSpriteObjects[p]; - data.anim.Update(elapsed, data.node->getSprite()); + remoteSpriteObjects[p].Update(elapsed); } auto& camera = GetCamera(); @@ -545,7 +544,23 @@ void Overworld::OnlineArea::onDraw(sf::RenderTexture& surface) for (auto& p : remoteSpriteObjectOrder) { auto& data = remoteSpriteObjects[p]; - data.node->draw(surface); + auto& sprite = remoteSprites[data.sprite_id]; + + const std::string& state = data.GetAnimation(); + Animation& anim = sprite.anim; + if (anim.GetAnimationString() != state) { + anim.SetAnimation(state); + } + anim.SyncTime(from_seconds(data.elapsed)); + anim.Refresh(sprite.node->getSprite()); + + std::shared_ptr& node = sprite.node; + node->setPosition(data.GetPosition()); + node->setScale(data.GetScale()); + node->setOrigin(data.GetOrigin()); + node->setRotation(data.GetRotation()); + node->setColor(data.GetColor()); + node->draw(surface); } auto& window = getController().getWindow(); @@ -2585,7 +2600,9 @@ void Overworld::OnlineArea::receiveMobSignal(BufferReader& reader, const Poco::B mob->SetBackground(GetBackground()); } - std::vector localNaviBlocksAddr = PlayerCustScene::GetInstalledBlocks(playerMeta.packageId, gameSession); + std::vector localNaviBlocksAddr = + PlayerCustScene::GetInstalledBlocks(playerMeta.packageId, gameSession); + std::vector localNaviBlocks; for (const PackageAddress& addr : localNaviBlocksAddr) { @@ -2600,7 +2617,13 @@ void Overworld::OnlineArea::receiveMobSignal(BufferReader& reader, const Poco::B // just like the game if (mob->IsFreedomMission()) { FreedomMissionProps props{ - { player, GetProgramAdvance(), std::move(folder), mob->GetField(), mob->GetBackground() }, + { + player, + GetProgramAdvance(), + std::move(folder), + mob->GetField(), + mob->GetBackground() + }, { mob }, mob->GetTurnLimit(), sf::Sprite(*mugshot), @@ -3123,34 +3146,38 @@ void Overworld::OnlineArea::receiveSpriteDrawSignal(BufferReader& reader, const // Unique instance object const std::string& obj_id = reader.ReadString(buffer); + bool initialized = false; auto iter2 = remoteSpriteObjects.find(obj_id); if (iter2 == remoteSpriteObjects.end()) { // Add to table - remoteSpriteObjects[obj_id] = RemoteScreenSprite{ - iter->second.anim, - std::make_shared(node->getSprite()), + remoteSpriteObjects[obj_id] = RemoteScreenSpriteObject{ + sprite_id, + iter->second.anim.GetAnimationString(), }; iter2 = remoteSpriteObjects.find(obj_id); // Add index entry to the order list remoteSpriteObjectOrder.push_back(obj_id); + + // Flag that this was created this frame + initialized = true; } // Fetch - RemoteScreenSprite& obj = iter2->second; + RemoteScreenSpriteObject& obj = iter2->second; // Read mask const uint16_t mask = reader.Read(buffer); - sf::Vector2f prevPos = obj.node->getPosition(); - sf::Vector2f prevScale = obj.node->getScale(); - sf::Vector2f prevOrigin = obj.node->getOrigin(); + sf::Vector2f prevPos = obj.GetPosition(); + sf::Vector2f prevScale = obj.GetScale(); + sf::Vector2f prevOrigin = obj.GetOrigin(); // Translation X if ((mask & 0x01) == 0x01) { const float tx = static_cast(reader.Read(buffer)); - obj.node->setPosition(tx, prevPos.y); + obj.SetPosition({ tx, prevPos.y }); prevPos.x = tx; } @@ -3158,62 +3185,64 @@ void Overworld::OnlineArea::receiveSpriteDrawSignal(BufferReader& reader, const if ((mask & 0x02) == 0x02) { const float ty = static_cast(reader.Read(buffer)); - obj.node->setPosition(prevPos.x, ty); + obj.SetPosition({prevPos.x, ty}); } // Scale X if ((mask & 0x04) == 0x04) { const float sx = reader.Read(buffer); - obj.node->setScale(sx, prevScale.y); + obj.SetScale({ sx, prevScale.y }); prevScale.x = sx; } // Scale Y if ((mask & 0x08) == 0x08) { const float sy = reader.Read(buffer); - obj.node->setScale(prevScale.x, sy); + obj.SetScale({ prevScale.x, sy }); } // Rotate if ((mask & 0x10) == 0x10) { const float rot = reader.Read(buffer); - obj.node->setRotation(rot); + obj.SetRotation(rot); } // Opacity if ((mask & 0x20) == 0x20) { const uint8_t opacity = reader.Read(buffer); - sf::Color color = obj.node->getColor(); + sf::Color color = obj.GetColor(); color.a = opacity; - obj.node->setColor(color); + obj.SetColor(color); } // Anim State if ((mask & 0x40) == 0x40) { const std::string anim_state = reader.ReadString(buffer); - obj.anim.SetAnimation(anim_state); - obj.anim.Refresh(obj.node->getSprite()); + obj.SetAnimation(anim_state); } // Layer (Sorting) if ((mask & 0x80) == 0x80) { const int16_t layer = reader.Read(buffer); - obj.node->SetLayer(layer); + obj.SetLayer(layer); } // Origin X (in pixels) if ((mask & 0x100) == 0x100) { const float ox = static_cast(reader.Read(buffer)); - obj.node->setOrigin(ox, prevOrigin.y); + obj.SetOrigin({ ox, prevOrigin.y }); prevOrigin.x = ox; } // Origin Y (in pixels) if ((mask & 0x200) == 0x200) { const float oy = static_cast(reader.Read(buffer)); - obj.node->setOrigin(prevOrigin.x, oy); + obj.SetOrigin({prevOrigin.x, oy}); } + + // If this frame was initialized, snap to the pending values. + obj.Sync(); } void Overworld::OnlineArea::receiveSpriteEraseSignal(BufferReader& reader, const Poco::Buffer& buffer) @@ -3238,11 +3267,12 @@ void Overworld::OnlineArea::receiveSpriteEraseSignal(BufferReader& reader, const void Overworld::OnlineArea::receiveSpriteDeallocSignal(BufferReader& reader, const Poco::Buffer& buffer) { - auto iter = remoteSprites.find(reader.ReadString(buffer)); + const std::string& sprite_id = reader.ReadString(buffer); + auto iter = remoteSprites.find(sprite_id); if (iter == remoteSprites.end()) return; for (auto& iter2 = remoteSpriteObjects.begin(); iter2 != remoteSpriteObjects.end(); /*manual*/) { - if (iter2->second.node == iter->second.node) { + if (iter2->second.sprite_id == iter->first) { iter2 = remoteSpriteObjects.erase(iter2); } else { diff --git a/BattleNetwork/overworld/bnOverworldOnlineArea.h b/BattleNetwork/overworld/bnOverworldOnlineArea.h index d4128f494..c3b476801 100644 --- a/BattleNetwork/overworld/bnOverworldOnlineArea.h +++ b/BattleNetwork/overworld/bnOverworldOnlineArea.h @@ -75,6 +75,103 @@ namespace Overworld { std::shared_ptr node{ nullptr }; }; + struct RemoteScreenSpriteObject { + std::string sprite_id; + std::string state; + double elapsed{}; + int16_t layer{}; + struct { + sf::Vector2f pos; + sf::Vector2f origin; + sf::Vector2f scale{ 1.f, 1.f }; + sf::Color color{ sf::Color::White }; + float rotation{}; + uint8_t opacity{ 0xFF }; + } next, curr; + + void SetPosition(const sf::Vector2f pos) { next.pos = pos; } + void SetScale(const sf::Vector2f scale) { next.scale = scale; } + void SetOrigin(const sf::Vector2f origin) { next.origin = origin; } + void SetRotation(float rot) { next.rotation = rot; } + void SetOpacity(uint8_t opacity) { next.opacity = opacity; } + void SetColor(const sf::Color color) { next.color = color; } + void SetAnimation(const std::string& state) { this->state = state; elapsed = 0.f; } + void SetLayer(int16_t layer) { this->layer = layer; } + + sf::Vector2f GetPosition() const { return curr.pos; } + sf::Vector2f GetOrigin() const { return curr.origin; } + sf::Vector2f GetScale() const { return curr.scale; } + float GetRotation() const { return curr.rotation; } + uint8_t GetOpacity() const { return curr.opacity; } + sf::Color GetColor() const { return curr.color; } + std::string GetAnimation() const { return state; } + int16_t GetLayer() const { return layer; } + + void Update(double elapsed) { + this->elapsed += elapsed; + + // LTE one tenth of the screen width delta can be smoothened. + // Large delta must snap. + if (abs(next.pos.x - curr.pos.x) <= 48) { + curr.pos.x = swoosh::ease::interpolate(0.5f, curr.pos.x, next.pos.x); + } + else { + curr.pos.x = next.pos.x; + } + + // 32 is one tength of screen height delta. + if (abs(next.pos.y - curr.pos.y) <= 32) { + curr.pos.y = swoosh::ease::interpolate(0.5f, curr.pos.y, next.pos.y); + } + else { + curr.pos.y = next.pos.y; + } + + curr.scale.x = swoosh::ease::interpolate(0.5f, curr.scale.x, next.scale.x); + curr.scale.y = swoosh::ease::interpolate(0.5f, curr.scale.y, next.scale.y); + curr.origin.x = swoosh::ease::interpolate(0.5f, curr.origin.x, next.origin.x); + curr.origin.y = swoosh::ease::interpolate(0.5f, curr.origin.y, next.origin.y); + curr.rotation = swoosh::ease::interpolate(0.5f, curr.rotation, next.rotation); + + if (abs(next.opacity - curr.opacity) < 25) { + curr.opacity = static_cast( + swoosh::ease::interpolate( + 0.5f, + static_cast(curr.opacity), + static_cast(next.opacity) + ) + ); + } + else { + curr.opacity = next.opacity; + } + + const float curr_red = static_cast(curr.color.r); + const float next_red = static_cast(next.color.r); + curr.color.r = static_cast( + swoosh::ease::interpolate(0.5f, curr_red, next_red) + ); + + const float curr_green = static_cast(curr.color.g); + const float next_green = static_cast(next.color.g); + curr.color.g = static_cast( + swoosh::ease::interpolate(0.5f, curr_green, next_green) + ); + + const float curr_blue = static_cast(curr.color.b); + const float next_blue = static_cast(next.color.b); + curr.color.b = static_cast( + swoosh::ease::interpolate(0.5f, curr_blue, next_blue) + ); + + curr.color.a = curr.opacity; + } + + void Sync() { + curr = next; + } + }; + std::string host; uint16_t port{}; std::shared_ptr emoteNode; @@ -103,7 +200,7 @@ namespace Overworld { AssetMeta incomingAsset; std::vector remoteSpriteObjectOrder; std::map remoteSprites; - std::map remoteSpriteObjects; + std::map remoteSpriteObjects; std::map onlinePlayers; std::map excludedObjects; std::unordered_set excludedActors; From 3fd5763e61a456592c6fb64c508a3340146b6178 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Thu, 23 Oct 2025 19:00:00 -0700 Subject: [PATCH 144/146] Likely fix crash when removing server mod packages --- BattleNetwork/bnMob.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/BattleNetwork/bnMob.cpp b/BattleNetwork/bnMob.cpp index 2efa735fa..33f9c594a 100644 --- a/BattleNetwork/bnMob.cpp +++ b/BattleNetwork/bnMob.cpp @@ -64,6 +64,17 @@ BattleItem* Mob::GetRankedReward(int score) { } void Mob::Cleanup() { + /* + Clear out all Mutators. This appears to avoid some access violation + while mob packages are being deleted if the battle had previously been + quit before spawning finished. GetNextSpawn will pull out the unique + pointers and let them be destroyed. + + spawn.clear() does not avoid the access violation. + */ + while (GetNextSpawn()) { + + } /*iter = spawn.end(); field = nullptr; spawn.clear(); From d09f515a3603c6245b66fd43a50d76b48897eee9 Mon Sep 17 00:00:00 2001 From: Alrysc Date: Sun, 26 Oct 2025 15:29:43 -0700 Subject: [PATCH 145/146] PlayerHealthUI doesn't draw if hidden, PlayerHealthUIComponent sets properly sets scene and plays low HP sound TODO: Sound plays at wrong battle states --- BattleNetwork/bnPlayerHealthUI.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/BattleNetwork/bnPlayerHealthUI.cpp b/BattleNetwork/bnPlayerHealthUI.cpp index 52afb41db..1377fa327 100644 --- a/BattleNetwork/bnPlayerHealthUI.cpp +++ b/BattleNetwork/bnPlayerHealthUI.cpp @@ -100,6 +100,7 @@ void PlayerHealthUI::Update(double elapsed) void PlayerHealthUI::draw(sf::RenderTarget& target, sf::RenderStates states) const { + if (IsHidden()) return; //auto this_states = states; //this_states.transform *= getTransform(); @@ -141,7 +142,7 @@ PlayerHealthUIComponent::~PlayerHealthUIComponent() { void PlayerHealthUIComponent::Inject(BattleSceneBase& scene) { scene.Inject(shared_from_base()); - this->scene - &scene; + this->scene = &scene; } void PlayerHealthUIComponent::draw(sf::RenderTarget& target, sf::RenderStates states) const { @@ -184,11 +185,13 @@ void PlayerHealthUIComponent::OnUpdate(double elapsed) { isPoisoned = player->GetTile()->GetState() == TileState::poison; } - if (isBurning || isPoisoned || player->GetHealth() <= startHP * 0.25) { + const bool lowHealth = player->GetHealth() <= startHP * 0.25; + if (lowHealth || isBurning || isPoisoned) { ui.SetFontStyle(Font::Style::gradient_gold); // If HP is low, play beep with high priority - if (player->GetHealth() <= startHP * 0.25 && !isBattleOver && scene && scene->GetSelectedCardsUI().IsHidden()) { + // TODO: This plays the sound during card select, but not timestop. That should be reversed. + if (lowHealth && !isBattleOver && scene && !scene->GetSelectedCardsUI().IsHidden()) { ResourceHandle().Audio().Play(AudioType::LOW_HP, AudioPriority::high); } } From a8455864e261e753a2d9d3009ea02bb15cc2ab67 Mon Sep 17 00:00:00 2001 From: TheMaverickProgrammer <91709+TheMaverickProgrammer@users.noreply.github.com> Date: Tue, 4 Nov 2025 22:34:01 -0600 Subject: [PATCH 146/146] small server fix with opacity --- BattleNetwork/overworld/bnOverworldOnlineArea.cpp | 8 ++++---- BattleNetwork/overworld/bnOverworldOnlineArea.h | 2 +- BattleNetwork/overworld/bnOverworldPacketHeaders.h | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/BattleNetwork/overworld/bnOverworldOnlineArea.cpp b/BattleNetwork/overworld/bnOverworldOnlineArea.cpp index 346b1f6fd..d0739668d 100644 --- a/BattleNetwork/overworld/bnOverworldOnlineArea.cpp +++ b/BattleNetwork/overworld/bnOverworldOnlineArea.cpp @@ -3417,9 +3417,7 @@ void Overworld::OnlineArea::receiveSpriteDrawSignal(BufferReader& reader, const // Opacity if ((mask & 0x20) == 0x20) { const uint8_t opacity = reader.Read(buffer); - sf::Color color = obj.GetColor(); - color.a = opacity; - obj.SetColor(color); + obj.SetOpacity(opacity); } // Anim State @@ -3448,7 +3446,9 @@ void Overworld::OnlineArea::receiveSpriteDrawSignal(BufferReader& reader, const obj.SetOrigin({prevOrigin.x, oy}); } - // If this frame was initialized, snap to the pending values. + // At this time, always sync. This is b/c slow moving sprites jitter when + // the client out-predicts the server. Better heuristic for easing due to + // network ping would allow us to sync only when needed. obj.Sync(); } diff --git a/BattleNetwork/overworld/bnOverworldOnlineArea.h b/BattleNetwork/overworld/bnOverworldOnlineArea.h index 0c55cb792..8cc493d9f 100644 --- a/BattleNetwork/overworld/bnOverworldOnlineArea.h +++ b/BattleNetwork/overworld/bnOverworldOnlineArea.h @@ -140,7 +140,7 @@ namespace Overworld { static_cast(curr.opacity), static_cast(next.opacity) ) - ); + ); } else { curr.opacity = next.opacity; diff --git a/BattleNetwork/overworld/bnOverworldPacketHeaders.h b/BattleNetwork/overworld/bnOverworldPacketHeaders.h index f2b6e6202..a83b4ac52 100644 --- a/BattleNetwork/overworld/bnOverworldPacketHeaders.h +++ b/BattleNetwork/overworld/bnOverworldPacketHeaders.h @@ -6,7 +6,7 @@ namespace Overworld { constexpr std::string_view VERSION_ID = "https://github.com/OpenNetBattle/Server@Backport/2.1"; - const uint64_t VERSION_ITERATION = 43; + const uint64_t VERSION_ITERATION = 44; constexpr double PACKET_RESEND_RATE = 1.0 / 20.0;