diff --git a/BattleNetwork/battlescene/States/bnCardSelectBattleState.cpp b/BattleNetwork/battlescene/States/bnCardSelectBattleState.cpp index 66482d2da..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(); @@ -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); } } diff --git a/BattleNetwork/battlescene/States/bnCharacterTransformBattleState.cpp b/BattleNetwork/battlescene/States/bnCharacterTransformBattleState.cpp index 310413763..62bec40ec 100644 --- a/BattleNetwork/battlescene/States/bnCharacterTransformBattleState.cpp +++ b/BattleNetwork/battlescene/States/bnCharacterTransformBattleState.cpp @@ -22,6 +22,32 @@ 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) + * 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 +71,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->RefreshPosition(); + player->ActivateFormAt(_index); player->SetColorMode(ColorMode::additive); player->setColor(NoopCompositeColor(ColorMode::additive)); @@ -60,8 +91,6 @@ void CharacterTransformBattleState::UpdateAnimation(double elapsed) Audio().Play(AudioType::DEFORM); } else { - player->MakeActionable(); - if (player == GetScene().GetLocalPlayer()) { // only client player should remove their index information (e.g. PVP battles) auto& widget = GetScene().GetCardSelectWidget(); @@ -70,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/States/bnCombatBattleState.cpp b/BattleNetwork/battlescene/States/bnCombatBattleState.cpp index bde96ce81..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. @@ -203,7 +202,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/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/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 eb7ec305b..8ffc7904b 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(); @@ -75,14 +79,12 @@ 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().GetCardPackagePartitioner(), card.GetProps())) { OnCardActionUsed(action, CurrentTime::AsMilli()); cardsUI->DropNextCard(); } } } - - p->GetChargeComponent().SetCharging(false); } } player_idx++; @@ -101,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(); } } @@ -156,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 @@ -170,6 +175,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); } } } @@ -196,6 +207,7 @@ void TimeFreezeBattleState::onUpdate(double elapsed) } else{ first->user->Reveal(); + first->action->EndAction(); scene.UntrackMobCharacter(first->stuntDouble); scene.GetField()->DeallocEntity(first->stuntDouble->GetID()); @@ -216,6 +228,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) @@ -228,56 +245,61 @@ 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; + + 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); 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) { - scale = swoosh::ease::linear((summonTextLength - summonTick).asSeconds().value, fadeInOutLength.asSeconds().value, 1.0); + if (summonTick >= summonTextLength) { + scale = swoosh::ease::linear((summonTextLength - summonTick).asSeconds().value, fadeSeconds, 1.0); 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) { + 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); } - 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) { - // draw TF bar underneath + 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); - 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 @@ -310,12 +332,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; @@ -327,28 +349,55 @@ 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; + + BattleSceneBase& scene = GetScene(); + Team leftTeam = scene.IsPerspectiveFlipped() ? Team::blue : Team::red; + bool flip = event.team != leftTeam; + + // helper function + auto setSummonLabelOrigin = [flip](Team team, Text& text) { + if (!flip) { + 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) { @@ -358,17 +407,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 (!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); + } + 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 @@ -398,14 +460,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().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 (!(action && action->GetMetaData().timeFreeze)) return; - if (CanCounter(action->GetActor())) { HandleTimeFreezeCounter(action, timestamp); } @@ -414,30 +476,45 @@ 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; + // 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().GetProps().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) { 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/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/battlescene/bnBattleSceneBase.cpp b/BattleNetwork/battlescene/bnBattleSceneBase.cpp index ab67d1235..88bcfc86c 100644 --- a/BattleNetwork/battlescene/bnBattleSceneBase.cpp +++ b/BattleNetwork/battlescene/bnBattleSceneBase.cpp @@ -38,14 +38,15 @@ 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), 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)), @@ -53,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), @@ -166,13 +167,22 @@ 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())); + channel.Emit(&ScriptResourceManager::SetKeyValue, "turn_count", std::to_string(turn)); } 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; @@ -212,12 +222,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,32 +239,7 @@ 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)); - - 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); } } @@ -326,12 +315,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; } @@ -341,7 +330,7 @@ const bool BattleSceneBase::IsCustGaugeFull() const return isGaugeFull; } -void BattleSceneBase::SetCustomBarProgress(double value) +void BattleSceneBase::SetCustomBarProgress(frame_time_t value) { this->customProgress = value; @@ -349,14 +338,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) @@ -410,7 +421,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().GetProps().canBoost) { + HandleCounterLoss(*action->GetActor(), true); + } } sf::Vector2f BattleSceneBase::PerspectiveOffset(const sf::Vector2f& pos) @@ -438,21 +451,25 @@ 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(); localPlayer->Init(); + localPlayer->ChangeState(); localPlayer->SetTeam(team); 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); - 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); @@ -472,6 +489,10 @@ void BattleSceneBase::SpawnLocalPlayer(int x, int y) allPlayerTeamHash[localPlayer.get()] = team; HitListener::Subscribe(*localPlayer); + + if (localPlayer->GetEmotion() == Emotion::full_synchro) { + PreparePlayerFullSynchro(localPlayer); + } } void BattleSceneBase::SpawnOtherPlayer(std::shared_ptr player, int x, int y) @@ -486,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); @@ -498,6 +519,10 @@ void BattleSceneBase::SpawnOtherPlayer(std::shared_ptr player, int x, in allPlayerTeamHash[player.get()] = team; HitListener::Subscribe(*player); + + if (player->GetEmotion() == Emotion::full_synchro) { + PreparePlayerFullSynchro(player); + } } void BattleSceneBase::LoadRedTeamMob(Mob& mob) @@ -520,21 +545,28 @@ 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 p = cardUI->GetOwnerAs(); - playsound ? Audio().Play(AudioType::COUNTER_BONUS, AudioPriority::highest) : 0; - } - cardUI->SetMultiplier(1); + // 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); + // Removes the multiplier + p->SetEmotion(Emotion::normal); + + playsound ? Audio().Play(AudioType::COUNTER_BONUS, AudioPriority::highest) : 0; + + if (&subject == localPlayer.get() && field->DoesRevealCounterFrames()) { + field->RevealCounterFrames(false); } } 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(); @@ -552,32 +584,53 @@ 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; + /* + 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--; } } @@ -706,7 +759,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); } @@ -818,7 +871,7 @@ void BattleSceneBase::onUpdate(double elapsed) { // custom bar continues to animate when it is already full if (isGaugeFull) { - customFullAnimDelta += elapsed/customDuration; + customFullAnimDelta += elapsed / customDefaultDuration.asSeconds().value; customBarShader->setUniform("factor", (float)(1.0 + customFullAnimDelta)); } @@ -1035,6 +1088,25 @@ 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); + + // Adds the multiplier + 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(); @@ -1072,6 +1144,10 @@ void BattleSceneBase::PerspectiveFlip(bool flipped) perspectiveFlip = flipped; } +bool BattleSceneBase::IsPerspectiveFlipped() { + return perspectiveFlip; +} + bool BattleSceneBase::IsPlayerDeleted() const { return isPlayerDeleted; @@ -1093,10 +1169,29 @@ std::vector> BattleSceneBase::GetOtherPlayers() std::vector> BattleSceneBase::GetAllPlayers() { std::vector> result = otherPlayers; - result.insert(result.begin(), 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; } +Mob& BattleSceneBase::GetRedTeamMob() { + assert(redTeamMob != nullptr && "redTeamMob was nullptr!"); + + return *redTeamMob; +} + +Mob& BattleSceneBase::GetBlueTeamMob() { + assert(blueTeamMob != nullptr && "blueTeamMob was nullptr!"); + + return *blueTeamMob; +} + std::shared_ptr BattleSceneBase::GetField() { @@ -1122,6 +1217,10 @@ PlayerEmotionUI& BattleSceneBase::GetEmotionWindow() return *emotionUI; } +PlayerHealthUIComponent& BattleSceneBase::GetHealthWindow() { + return *healthUI; +} + Camera& BattleSceneBase::GetCamera() { return camera; @@ -1175,6 +1274,7 @@ void BattleSceneBase::BroadcastBattleStop() void BattleSceneBase::IncrementTurnCount() { turn++; + channel.Emit(&ScriptResourceManager::SetKeyValue, "turn_count", std::to_string(turn)); } void BattleSceneBase::IncrementRoundCount() @@ -1249,10 +1349,11 @@ void BattleSceneBase::Quit(const FadeOut& mode) { current = nullptr; } + quitting = true; + // NOTE: swoosh quirk if (getController().getStackSize() == 1) { getController().pop(); - quitting = true; return; } @@ -1268,8 +1369,6 @@ void BattleSceneBase::Quit(const FadeOut& mode) { // mode == FadeOut::pixelate getController().pop>(); } - - quitting = true; } diff --git a/BattleNetwork/battlescene/bnBattleSceneBase.h b/BattleNetwork/battlescene/bnBattleSceneBase.h index d5c826499..f6bfc2dbd 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" @@ -101,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 */ @@ -110,6 +112,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 */ @@ -117,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 */ @@ -308,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; @@ -380,20 +385,25 @@ 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); + void PreparePlayerFullSynchro(const std::shared_ptr& player); bool IsPlayerDeleted() const; std::shared_ptr GetLocalPlayer(); 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(); 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 ad598264b..b9d431dd3 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" @@ -129,23 +129,45 @@ 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); } else { SpawnLocalPlayer(2, 2); + LoadBlueTeamMob(mob); + } + + // 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 remote player now that they are spawned - BlockPackageManager& blockPackages = getController().BlockPackagePartitioner().GetPartition(Game::LocalPartition); + // Run block programs on the local player now that they are spawned + BlockPackageManager& blockPackages = getController().GetBlockPackagePartitioner().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); @@ -155,21 +177,23 @@ 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++; if (props.damage >= 300) { + // Sets the multiplier player->SetEmotion(Emotion::angry); - GetSelectedCardsUI().SetMultiplier(2); } - if (player->IsSuperEffective(props.element)) { + if (player->IsInForm() && superEffective) { playerDecross = true; } } - if (victim.IsSuperEffective(props.element) && props.damage > 0) { - std::shared_ptr seSymbol = std::make_shared(); + 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 GetField()->AddEntity(seSymbol, victim.GetTile()->GetX(), victim.GetTile()->GetY()); @@ -178,12 +202,16 @@ 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)) { + if (localPlayer->CanAttack() && localPlayer->InputState().Has(InputEvents::pressed_option)) { localPlayer->SetFacing(localPlayer->GetFacingAway()); } } @@ -214,6 +242,14 @@ void FreedomMissionMobScene::onLeave() BattleSceneBase::onLeave(); } +int FreedomMissionMobScene::GetPlayerHitCount() { + return playerHitCount; +} + +FreedomMissionProps& FreedomMissionMobScene::GetProps() { + return props; +} + void FreedomMissionMobScene::IncrementTurnCount() { BattleSceneBase::IncrementTurnCount(); @@ -249,7 +285,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 @@ -272,9 +308,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; @@ -291,6 +325,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.IsCleared(); + bool blueTeamCleared = blueTeam.IsCleared(); + + if (redTeamCleared || blueTeamCleared) { + return false; + } + if (GetCustomBarProgress() >= GetCustomBarDuration() && GetTurnCount() == FreedomMissionMobScene::props.maxTurns) { overStatePtr->context = FreedomMissionOverState::Conditions::player_failed; return true; @@ -305,6 +357,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.IsCleared(); + bool blueTeamCleared = blueTeam.IsCleared(); + + if (redTeamCleared || blueTeamCleared) { + return false; + } return GetCustomBarProgress() >= GetCustomBarDuration(); }; diff --git a/BattleNetwork/battlescene/bnFreedomMissionMobScene.h b/BattleNetwork/battlescene/bnFreedomMissionMobScene.h index 36912809f..918e8bc07 100644 --- a/BattleNetwork/battlescene/bnFreedomMissionMobScene.h +++ b/BattleNetwork/battlescene/bnFreedomMissionMobScene.h @@ -55,6 +55,8 @@ class FreedomMissionMobScene final : public BattleSceneBase { void onEnter() override; void onResume() override; void onLeave() override; + FreedomMissionProps& GetProps(); + int GetPlayerHitCount(); // // override diff --git a/BattleNetwork/battlescene/bnMobBattleScene.cpp b/BattleNetwork/battlescene/bnMobBattleScene.cpp index 8a64f325e..9a658a0f5 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" @@ -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,8 +159,14 @@ void MobBattleScene::Init() SpawnLocalPlayer(2, 2); } - // Run block programs on the remote player now that they are spawned - BlockPackageManager& blockPackages = getController().BlockPackagePartitioner().GetPartition(Game::LocalPartition); + // 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().GetBlockPackagePartitioner().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); } @@ -165,24 +184,25 @@ 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++; if (props.damage >= 300) { + // Sets the multiplier player->SetEmotion(Emotion::angry); - GetSelectedCardsUI().SetMultiplier(2); } - if (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(); + 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()); @@ -258,7 +278,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 +301,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/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/bnScriptedPlayer.cpp b/BattleNetwork/bindings/bnScriptedPlayer.cpp index 498db4a0a..2addc6650 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()); } @@ -184,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, weakWrap); + stx::result_t result = CallLuaCallbackExpectingValue(charge_time_func, weakWrap, 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/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); diff --git a/BattleNetwork/bindings/bnUserTypeBaseCardAction.cpp b/BattleNetwork/bindings/bnUserTypeBaseCardAction.cpp index 1e95fd945..9084af928 100644 --- a/BattleNetwork/bindings/bnUserTypeBaseCardAction.cpp +++ b/BattleNetwork/bindings/bnUserTypeBaseCardAction.cpp @@ -66,11 +66,11 @@ 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(); + return cardAction.Unwrap()->GetMetaData().GetProps(); } ); 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 aa8ceb730..a99bee0dd 100644 --- a/BattleNetwork/bindings/bnUserTypeBasicPlayer.cpp +++ b/BattleNetwork/bindings/bnUserTypeBasicPlayer.cpp @@ -17,6 +17,10 @@ void DefineBasicPlayerUserType(sol::table& battle_namespace) { 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(); }, 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/bindings/bnUserTypeEntity.h b/BattleNetwork/bindings/bnUserTypeEntity.h index d220b52c9..0686d7810 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(); @@ -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); @@ -356,6 +360,24 @@ 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(); + }; + 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); + }; } #endif 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) { diff --git a/BattleNetwork/bindings/bnUserTypeHitbox.cpp b/BattleNetwork/bindings/bnUserTypeHitbox.cpp index ddeabff33..b8c3a919e 100644 --- a/BattleNetwork/bindings/bnUserTypeHitbox.cpp +++ b/BattleNetwork/bindings/bnUserTypeHitbox.cpp @@ -107,23 +107,114 @@ 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) { + 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) { + 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; }, + "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; }, + "drg", [](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", @@ -140,7 +231,9 @@ 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, + "Confuse", Hit::confuse ); state.new_usertype("Drag", diff --git a/BattleNetwork/bindings/bnUserTypeScriptedCardAction.cpp b/BattleNetwork/bindings/bnUserTypeScriptedCardAction.cpp index eb8e27723..304a6ed9a 100644 --- a/BattleNetwork/bindings/bnUserTypeScriptedCardAction.cpp +++ b/BattleNetwork/bindings/bnUserTypeScriptedCardAction.cpp @@ -161,11 +161,11 @@ 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(); + return cardAction.Unwrap()->GetMetaData().GetProps(); }, "update_func", sol::property( [](WeakWrapper& cardAction) { return cardAction.Unwrap()->update_func; }, 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..e560f802d 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(); }, @@ -79,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); }, @@ -128,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( @@ -155,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( 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{ diff --git a/BattleNetwork/bindings/bnUserTypeTile.cpp b/BattleNetwork/bindings/bnUserTypeTile.cpp index 5da3a8cc9..685c66c71 100644 --- a/BattleNetwork/bindings/bnUserTypeTile.cpp +++ b/BattleNetwork/bindings/bnUserTypeTile.cpp @@ -134,7 +134,10 @@ void DefineTileUserType(sol::state& state) { "Lava", TileState::lava, "Normal", TileState::normal, "Poison", TileState::poison, - "Volcano", TileState::volcano + "Volcano", TileState::volcano, + "Sea", TileState::sea, + "Sand", TileState::sand, + "Metal", TileState::metal ); state.new_enum("Highlight", 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/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)); } diff --git a/BattleNetwork/bnAnimation.cpp b/BattleNetwork/bnAnimation.cpp index c8294935f..f61a7ba1a 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(std::atoi(valueView.data())); + } + + 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); } @@ -229,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; @@ -453,8 +461,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); } @@ -499,8 +509,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..b2ab85eb4 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); @@ -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/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(); } diff --git a/BattleNetwork/bnBuster.cpp b/BattleNetwork/bnBuster.cpp index 5242a0506..29d7442e9 100644 --- a/BattleNetwork/bnBuster.cpp +++ b/BattleNetwork/bnBuster.cpp @@ -10,7 +10,7 @@ #include "bnAudioResourceManager.h" #include "bnRandom.h" -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); @@ -32,7 +32,8 @@ 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.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 204aa7030..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()); @@ -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/bnCard.cpp b/BattleNetwork/bnCard.cpp index ca13f35da..49dd8adbe 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) @@ -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; } @@ -92,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 bce3cd087..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 }; @@ -37,6 +40,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; @@ -46,7 +50,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. */ @@ -64,7 +67,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. @@ -164,14 +172,18 @@ 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: - Properties unmodded; - unsigned int multiplier{ 0 }; + std::map prevModifiers; + Properties unmodded{}; + Properties props{}; + unsigned int multiplier{ 1 }; }; } \ No newline at end of file diff --git a/BattleNetwork/bnCardAction.cpp b/BattleNetwork/bnCardAction.cpp index 5c5398019..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) @@ -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()) { 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/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; } 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/bnCharacter.cpp b/BattleNetwork/bnCharacter.cpp index 65b8c7d4a..4913640d7 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" @@ -92,7 +92,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); @@ -102,7 +102,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); } @@ -124,6 +124,8 @@ void Character::Update(double _elapsed) { actionQueue.Pop(); } } + + actionBlocked = !CanAttackImpl(); } bool Character::CanMoveTo(Battle::Tile * next) @@ -150,15 +152,19 @@ bool Character::CanMoveTo(Battle::Tile * next) const bool Character::CanAttack() const { - return !currCardAction && IsActionable(); + return !actionBlocked && CanAttackImpl(); +} + +const bool Character::CanAttackImpl() const { + return !currCardAction && !HasAnyStatusFrom(Character::blockingStatuses); } -void Character::MakeActionable() +void Character::MakeIdle() { // impl. defined } -bool Character::IsActionable() const +bool Character::IsIdle() const { return true; // impl. defined } @@ -185,12 +191,25 @@ 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(); } - 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; } @@ -212,8 +231,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(); } } diff --git a/BattleNetwork/bnCharacter.h b/BattleNetwork/bnCharacter.h index 84e540836..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 @@ -72,8 +74,8 @@ class Character: virtual void OnBattleStop() override; - virtual void MakeActionable(); - virtual bool IsActionable() const; + virtual void MakeIdle(); + virtual bool IsIdle() const; const bool IsLockoutAnimationComplete(); @@ -89,6 +91,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 +116,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/bnChargeEffectSceneNode.cpp b/BattleNetwork/bnChargeEffectSceneNode.cpp index dbdddec82..f457f1df7 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()); } @@ -82,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 b06cbf128..4c056a870 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 @@ -49,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/bnConfigScene.cpp b/BattleNetwork/bnConfigScene.cpp index 89c4fbdd7..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; @@ -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 { 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/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; } diff --git a/BattleNetwork/bnEntity.cpp b/BattleNetwork/bnEntity.cpp index 0293691d5..531beff7e 100644 --- a/BattleNetwork/bnEntity.cpp +++ b/BattleNetwork/bnEntity.cpp @@ -3,12 +3,14 @@ #include "bnTile.h" #include "bnField.h" #include "bnPlayer.h" +#include "bnWaterSplash.h" #include "bnShakingEffect.h" #include "bnShaderResourceManager.h" #include "bnTextureResourceManager.h" #include "bnAudioResourceManager.h" #include #include +#include "bnMoveEvent.h" long Entity::numOfIDs = 0; @@ -24,14 +26,14 @@ bool EntityComparitor::operator()(Entity* f, Entity* s) const // First entity ID begins at 1 Entity::Entity() : - elapsedMoveTime(0), lastComponentID(0), height(0), moveCount(0), channel(nullptr), mode(Battle::TileHighlight::none), hitboxProperties(Hit::DefaultProperties), - CounterHitPublisher() + CounterHitPublisher(), + statuses(*this) { ID = ++Entity::numOfIDs; @@ -73,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() { @@ -129,122 +138,22 @@ 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; +void Entity::UpdateMovement(double elapsed) { + if (!currMoveEvent) { + if (tile) { + RefreshPosition(); } + return; + } - 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; - } - } + currMoveEvent->OnUpdate(from_seconds(elapsed)); - 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(); - - // If we slide onto an ice block and we don't have float shoe enabled, slide - if (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 { - // 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; + if (currMoveEvent->IsFinished()) { + FinishMove(); } if (tile) { - setPosition(tile->getPosition() + Entity::tileOffset + drawOffset); + RefreshPosition(); } } @@ -336,6 +245,61 @@ void Entity::Init() { hasInit = true; } +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 + // 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 (appliedStatuses & (Hit::freeze | Hit::stun)) { + FinishMove(); + actionQueue.ClearQueue(ActionQueue::CleanupType::allow_interrupts); + } + + if ((appliedStatuses & Hit::freeze) == Hit::freeze) { + IceFreeze(); + // IceFreeze removes flash + appliedStatuses &= ~Hit::flash; + } + + + // Avoid resetting the animation + if ((appliedStatuses & Hit::blind) == Hit::blind && !(prevStatuses & Hit::blind)) { + Blind(); + } + + // Avoid resetting the animation + if ((appliedStatuses & Hit::confuse) == Hit::confuse && !(prevStatuses & Hit::confuse)) { + Confuse(); + } + + 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]) { + func(); + } + }; + + Hit::Flags statusCheck = appliedStatuses; + /// Run all status callbacks, starting from lowest set bit + Hit::Flags checkIdx = statusCheck & -statusCheck; + while (statusCheck > 0) { + if (statusCheck & checkIdx) { + flagCheckThunk(checkIdx); + } + + statusCheck = statusCheck & ~checkIdx; + checkIdx = checkIdx << 1; + } +} + void Entity::Update(double _elapsed) { ResolveFrameBattleDamage(); @@ -349,80 +313,89 @@ void Entity::Update(double _elapsed) { health = 0; // Ensure status effects do not play out - stunCooldown = frames(0); - rootCooldown = frames(0); - invincibilityCooldown = frames(0); + statuses.ClearAllStatuses(); } // 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(); + + // Tick all statuses at once + statuses.OnUpdate(_elapsed); + + Hit::Flags applied = (queuedStatuses & ~statuses.GetQueuedStatuses() & statuses.GetCurrentStatuses()); + HandleNewStatuses(prevStatuses, applied); + RefreshShader(); + 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. + bool canUpdateThisFrame = !(frozen || stunned); + if (!hit) { - if (invincibilityCooldown > frames(0)) { - unsigned frame = invincibilityCooldown.count() % 4; - if (frame < 2) { + AppliedStatus& flash = statuses.GetStatus(Hit::flash); + + if (statuses.IsApplied(Hit::flash)) { + unsigned frame = flash.remainingTime.count() % 4; + if (frame < 2 || statuses.IsApplied(Hit::drag)) { Reveal(); } else { Hide(); } - - invincibilityCooldown -= from_seconds(_elapsed); - - if (invincibilityCooldown <= frames(0)) { - Reveal(); - } - } - } - - 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); } - } - - bool canUpdateThisFrame = true; - - if(stunCooldown > frames(0)) { - canUpdateThisFrame = false; - stunCooldown -= from_seconds(_elapsed); - - if (stunCooldown <= frames(0)) { - stunCooldown = frames(0); + // 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); + // 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); @@ -469,9 +442,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 status is over, reset the flag - if (!IsSliding() && slideFromDrag) slideFromDrag = false; } @@ -526,19 +496,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); @@ -659,7 +640,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(*this, MoveData{dest, frames(0), moveStartupDelay, endlagDelay, 0.f, onBegin}) + }; actionQueue.Add(event, order, ActionDiscardOp::until_eof); return true; @@ -673,7 +657,9 @@ 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(*this, MoveData{dest, slideTime, frames(0), endlagDelay, 0.f, onBegin}) + }; actionQueue.Add(event, order, ActionDiscardOp::until_eof); return true; @@ -689,7 +675,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(*this, MoveData{dest, jumpTime, frames(0), endlagDelay, destHeight, onBegin}) + }; actionQueue.Add(event, order, ActionDiscardOp::until_eof); return true; @@ -700,19 +690,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) { AdoptNextTile(); tileOffset = {}; - currMoveEvent = {}; - actionQueue.ClearFilters(); - actionQueue.Pop(); } + + currMoveEvent = nullptr; + actionQueue.ClearFilters(); + actionQueue.Pop(); +} + +void Entity::EndDrag() { + statuses.ClearStatuses(Hit::drag); + 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; @@ -721,6 +723,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(*this, data) + }; + return RawMoveEvent(e, order); + } + + return false; +} + void Entity::HandleMoveEvent(MoveEvent& event, const ActionQueue::ExecutionType& exec) { if (exec == ActionQueue::ExecutionType::interrupt) { @@ -728,13 +741,14 @@ void Entity::HandleMoveEvent(MoveEvent& event, const ActionQueue::ExecutionType& return; } - if (currMoveEvent.dest == nullptr && !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; + 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); } @@ -795,6 +809,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; @@ -812,21 +834,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; } @@ -860,6 +882,7 @@ void Entity::SetTeam(Team _team) { void Entity::SetPassthrough(bool state) { passthrough = state; + Reveal(); } bool Entity::IsPassthrough() @@ -940,6 +963,8 @@ void Entity::Delete() deleted = true; + statuses.ClearAllStatuses(); + OnDelete(); } @@ -969,7 +994,7 @@ const Element Entity::GetElement() const void Entity::AdoptNextTile() { - Battle::Tile* next = currMoveEvent.dest; + Battle::Tile* next = currMoveEvent->data.dest; if (next == nullptr) { return; } @@ -991,6 +1016,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 @@ -1086,7 +1114,7 @@ void Entity::ClearActionQueue() const float Entity::GetJumpHeight() const { - return currMoveEvent.height; + return currMoveEvent ? currMoveEvent->GetHeight() : 0.f; } void Entity::ShowShadow(bool enabled) @@ -1184,7 +1212,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()); } @@ -1195,17 +1223,78 @@ 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) { 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 (isFire + && GetTile()->GetState() == TileState::grass) { + tileDamage = props.damage; + GetTile()->SetState(TileState::normal); + } + + + if (isElec + && GetTile()->GetState() == TileState::sea) { + tileDamage = props.damage; + } + + + if (isAqua + && GetTile()->GetState() == TileState::ice) { + props.flags |= Hit::freeze; + GetTile()->SetState(TileState::normal); + } + + if ((props.flags & Hit::breaking) == Hit::breaking && statuses.IsApplied(Hit::freeze)) { + extraDamage = props.damage; + // 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. + ClearStatuses(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() - totalDamage); + HitPublisher::Broadcast(*this, props); + } 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 @@ -1247,7 +1336,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; @@ -1269,306 +1358,142 @@ 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()) { + // 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(); 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(); - } - } - }; - - 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); - } - - if (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 { - // 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.filtered.flags & Hit::no_counter) == 0 + && (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)) { + // 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(); + } } - // 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); - - // exclude this from the next processing step - props.filtered.flags &= ~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) { + addDrag = true; + currentDrag = props.filtered.drag; } - bool flashAndFlinch = ((props.filtered.flags & Hit::flash) == Hit::flash) && ((props.filtered.flags & Hit::flinch) == Hit::flinch); - frameFreezeCancel = frameFreezeCancel || flashAndFlinch; + props.filtered.flags = props.filtered.flags & ~Hit::drag; - /** - 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 { - // 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; - - if ((props.filtered.flags & Hit::flash) == Hit::flash && frameStunCancel) { - // cancel stun - stunCooldown = frames(0); - } - else { - // refresh stun - stunCooldown = frames(120); - flagCheckThunk(Hit::stun); - } - - actionQueue.ClearQueue(ActionQueue::CleanupType::allow_interrupts); - } + const bool hasFlash = ((props.filtered.flags & Hit::flash) == Hit::flash); + if (hasFlash) { + statuses.AddStatus(Hit::flash, props.filtered.flash_duration); } - // 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::flinch); - } + if ((props.filtered.flags & Hit::freeze)) { + statuses.AddStatus(Hit::freeze, props.filtered.freeze_duration); } - // 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; - } + props.filtered.flags = props.filtered.flags & ~Hit::freeze; - // 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); - } + if ((props.filtered.flags & Hit::stun)) { + statuses.AddStatus(Hit::stun, props.filtered.stun_duration); } - // exclude this from the next processing step - props.filtered.flags &= ~Hit::flash; + props.filtered.flags = props.filtered.flags & ~Hit::stun; - // Flinch is canceled if retangibility is applied - if ((props.filtered.flags & Hit::retangible) == Hit::retangible) { - invincibilityCooldown = frames(0); - - flagCheckThunk(Hit::retangible); + if ((props.filtered.flags & Hit::bubble)) { + statuses.AddStatus(Hit::bubble, frames(150)); } - // exclude this from the next processing step - props.filtered.flags &= ~Hit::retangible; + props.filtered.flags = props.filtered.flags & ~Hit::bubble; - 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); - } + if ((props.filtered.flags & Hit::root)) { + statuses.AddStatus(Hit::root, props.filtered.root_duration); } - // exclude this from the next processing step - props.filtered.flags &= ~Hit::bubble; + props.filtered.flags = props.filtered.flags & ~Hit::root; - if ((props.filtered.flags & Hit::root) == Hit::root) { - rootCooldown = frames(120); - flagCheckThunk(Hit::root); + if ((props.filtered.flags & Hit::blind)) { + statuses.AddStatus(Hit::blind, props.filtered.blind_duration); } - // exclude this from the next processing step - props.filtered.flags &= ~Hit::root; + props.filtered.flags = props.filtered.flags & ~Hit::blind; - // 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::confuse)) { + statuses.AddStatus(Hit::confuse, props.filtered.confuse_duration); } - // exclude this from the next processing step - props.filtered.flags &= ~Hit::shake; + props.filtered.flags = props.filtered.flags & ~Hit::confuse; - // 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 } }); + // 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) { + statuses.AddStatus(curFlag); + props.filtered.flags &= ~curFlag; } - else { - Blind(frames(300)); - flagCheckThunk(Hit::blind); - } - } - // 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 - - 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 } } } // end while-loop - if (!append.empty()) { - statusQueue = append; - } - - if (postDragEffect.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 (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); + // 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 (addDrag) { + bool activeDrag = slideFromDrag; + FinishMove(); + // 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); + /* + Do not set slideFromDrag true here. This could interfere with status + processing after ResolveFrameBattleDamage. This will be set true + by the StatusBehaviorDirector instead. + */ - 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 } }); + actionQueue.Add( + MoveEvent{ + std::make_shared(*this, currentDrag) + }, + ActionOrder::immediate, ActionDiscardOp::until_resolve + ); - // 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.ClearAllStatuses(); while(statusQueue.size() > 0) { statusQueue.pop(); @@ -1584,30 +1509,9 @@ 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) { - std::shared_ptr fieldPtr = field.lock(); - - if (fieldPtr) { - if (!fieldPtr->isBattleActive) return; - } - health = _health; if (maxHealth == 0) { @@ -1691,46 +1595,78 @@ 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); + return statuses.HasStatus(Hit::blind); } -void Entity::Stun(frame_time_t maxCooldown) -{ - invincibilityCooldown = frames(0); // cancel flash - freezeCooldown = frames(0); // cancel freeze - stunCooldown = maxCooldown; +void Entity::AddStatus(Hit::Flags status) { + statuses.AddStatus(status); } -void Entity::Root(frame_time_t maxCooldown) -{ - rootCooldown = maxCooldown; +void Entity::AddStatus(Hit::Flags status, frame_time_t duration) { + statuses.AddStatus(status, duration); } -void Entity::IceFreeze(frame_time_t maxCooldown) -{ - invincibilityCooldown = frames(0); // cancel flash - stunCooldown = frames(0); // cancel stun - freezeCooldown = maxCooldown; +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) { + statuses.ClearStatuses(flags); +} + +void Entity::IceFreeze() +{ 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 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); Audio().Play(freezesfx, AudioPriority::highest); if (height <= 48) { @@ -1749,7 +1685,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(); @@ -1758,15 +1694,29 @@ 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()); } +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 && 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 1f99f7fd5..05ef77535 100644 --- a/BattleNetwork/bnEntity.h +++ b/BattleNetwork/bnEntity.h @@ -37,6 +37,8 @@ using std::string; #include "bnDefenseRule.h" #include "bnHitProperties.h" #include "stx/memory.h" +#include "bnStatusDirector.h" +#include "bnMoveEvent.h" namespace Battle { class Tile; @@ -44,32 +46,12 @@ namespace Battle { } class Field; -class BattleSceneBase; // forward decl - -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 - - //!< 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; - } +class BattleSceneBase; - //!< helper function true if normal moving - inline bool IsTeleporting() const { - return dest && deltaFrames == frames(0) && (+height) == 0.0f; - } -}; +// Defined in bnMoveEvent.h +class MoveAction; +struct MoveEvent; +struct MoveData; struct CombatHitProps { Hit::Properties hitbox; // original hitbox data @@ -109,6 +91,8 @@ class Entity : friend class Field; friend class Component; friend class BattleSceneBase; + friend class StatusBehaviorDirector; + friend class MoveAction; enum class Shadow : char { none = 0, @@ -130,12 +114,13 @@ 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 }; 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 @@ -196,7 +181,20 @@ 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 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); + bool RawMoveEvent(const MoveData& data, ActionOrder order = ActionOrder::voluntary); + void HandleMoveEvent(MoveEvent& event, const ActionQueue::ExecutionType& exec); void ClearActionQueue(); const float GetJumpHeight() const; @@ -274,6 +272,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; @@ -550,6 +550,26 @@ 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. + + Some reactions may remove statuses in [appliedStatuses]. + + 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, Hit::Flags& appliedStatuses); + /** * @brief Get the character's current health * @return @@ -613,6 +633,47 @@ 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 + * status processing. + * @param status to query + * @return true if entity has status applied OR queued, false otherwise + */ + 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 + */ + const bool IsStatusApplied(Hit::Flags status) const; + + /** + * @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 @@ -704,9 +765,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); @@ -768,11 +830,8 @@ class Entity : ActionQueue actionQueue; frame_time_t moveStartupDelay{}; std::optional moveEndlagDelay; - 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 */ @@ -795,36 +854,24 @@ class Entity : const int GetMoveCount() const; /*!< Total intended movements made. Used to calculate rank*/ /** - * @brief Stun a character for maxCooldown seconds - * @param maxCooldown + * @brief Handle setup for freeze graphics and SFX * Used internally by class * */ - void Stun(frame_time_t maxCooldown); + void IceFreeze(); /** - * @brief Stop a character from moving for maxCooldown seconds - * @param maxCooldown + * @brief Handle setup for blind graphics * Used internally by class * */ - void Root(frame_time_t maxCooldown); + void Blind(); - /** - * @brief Stop a character from moving for maxCooldown seconds - * @param maxCooldown + /* + * @brief Handle setup for confuse graphics * Used internally by class - * - */ - void IceFreeze(frame_time_t maxCooldown); - - /** - * @brief This entity should not see opponents for maxCooldown seconds - * @param maxCooldown - * Used internally by class - * */ - void Blind(frame_time_t maxCooldown); + void Confuse(); /** * @brief Query if an attack successfully countered a Character @@ -885,7 +932,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{}; @@ -894,11 +940,10 @@ class Entity : sf::Vector2f counterSlideOffset{ 0.f, 0.f }; /*!< Used when enemies delete on counter - they slide back */ std::vector> defenses; /* statusQueue; sf::Shader* whiteout{ nullptr }; /*!< Flash white when hit */ 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/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 ecead95a1..4206f7cfe 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,17 +177,93 @@ void FolderEditScene::onUpdate(double elapsed) { camera.Update((float)elapsed); setView(camera.GetView()); + // update the folder sort cursor + sf::Vector2f sortCursorOffset = sf::Vector2f(-10.f, 2.0 * (14.0 + (cursorSortIndex * 16.0))); + sortCursor.setPosition(folderSort.getPosition() + sortCursorOffset); + // Scene keyboard controls if (canInteract) { - CardView* view = nullptr; + 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 (currViewMode == ViewMode::folder) { - view = &folderView; + 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; } - else if (currViewMode == ViewMode::pool) { + + CardView* view = &folderView; + + if (currViewMode == ViewMode::pool) { 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().GetCardPackagePartitioner().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 +272,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 +303,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(); } @@ -523,28 +623,12 @@ 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; - 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 +669,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 +679,7 @@ void FolderEditScene::onUpdate(double elapsed) { else { prevViewMode = currViewMode; canInteract = true; + folderSort.setPosition(packCardHolder.getPosition() + sf::Vector2f(11 * 2, 5 * 2)); } } else { @@ -608,14 +694,12 @@ void FolderEditScene::onLeave() { } -void FolderEditScene::onExit() -{ +void FolderEditScene::onExit() { } -void FolderEditScene::onEnter() -{ +void FolderEditScene::onEnter() { folderView.currCardIndex = 0; - RefreshCurrentCardDock(folderView, folderCardSlots); + RefreshCardDock(folderView, folderCardSlots); } void FolderEditScene::onResume() { @@ -631,8 +715,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 +775,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,17 +801,17 @@ 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()); + bool hasID = getController().GetCardPackagePartitioner().GetPartition(Game::LocalPartition).HasPackage(copy.GetUUID()); cardLabel.SetColor(sf::Color::White); @@ -769,10 +857,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 +870,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 +887,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 +904,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 +925,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 +999,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 +1010,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 +1050,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 +1063,134 @@ void FolderEditScene::DrawPool(sf::RenderTarget& surface) { } } +void FolderEditScene::ComposeSortOptions() { + auto sortByID = [](const ICardView& first, const ICardView& second) -> bool { + 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 { + 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 { + 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 { + 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 { + 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{}; + + for (size_t i = 0; i < folderCardSlots.size(); i++) { + const auto& el = folderCardSlots[i]; + if (el.ViewCard().GetUUID() == first.ViewCard().GetUUID()) { + firstCount++; + + } + + if (el.ViewCard().GetUUID() == second.ViewCard().GetUUID()) { + 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{}; + + for (size_t i = 0; i < poolCardBuckets.size(); i++) { + const auto& el = poolCardBuckets[i]; + + if (el.ViewCard() == first.ViewCard()) { + firstCount = el.GetCount(); + firstCountFound = true; + } + + if (el.ViewCard() == second.ViewCard()) { + secondCount = el.GetCount(); + secondCountFound = true; + } + + if (firstCountFound && secondCountFound) break; + } + + return std::tie(firstCount, a.GetShortName(), codeA) < std::tie(secondCount, b.GetShortName(), codeB); + }; + + 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(sortByClass); + + poolSortOptions.AddOption(sortByID); + poolSortOptions.AddOption(sortByAlpha); + poolSortOptions.AddOption(sortByCode); + poolSortOptions.AddOption(sortByAttack); + poolSortOptions.AddOption(sortByElement); + poolSortOptions.AddOption(sortByPoolCopies); + poolSortOptions.AddOption(sortByClass); +} + 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 +1200,7 @@ void FolderEditScene::ExcludeFolderDataFromPool() } } -void FolderEditScene::PlaceFolderDataIntoCardSlots() -{ +void FolderEditScene::PlaceFolderDataIntoCardSlots() { CardFolder::Iter iter = folder.Begin(); while (iter != folder.End() && folderCardSlots.size() < 30) { @@ -998,9 +1215,8 @@ void FolderEditScene::PlaceFolderDataIntoCardSlots() } } -void FolderEditScene::PlaceLibraryDataIntoBuckets() -{ - auto& packageManager = getController().CardPackagePartitioner().GetPartition(Game::LocalPartition); +void FolderEditScene::PlaceLibraryDataIntoBuckets() { + auto& packageManager = getController().GetCardPackagePartitioner().GetPartition(Game::LocalPartition); std::string packageId = packageManager.FirstValidPackage(); if (packageId.empty()) return; @@ -1020,8 +1236,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,9 +1246,8 @@ void FolderEditScene::WriteNewFolderData() } } -std::shared_ptr FolderEditScene::GetIconForCard(const std::string& uuid) -{ - auto& packageManager = getController().CardPackagePartitioner().GetPartition(Game::LocalPartition); +std::shared_ptr FolderEditScene::GetIconForCard(const std::string& uuid) { + auto& packageManager = getController().GetCardPackagePartitioner().GetPartition(Game::LocalPartition); if (!packageManager.HasPackage(uuid)) return noIcon; @@ -1041,9 +1255,8 @@ 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) -{ - auto& packageManager = getController().CardPackagePartitioner().GetPartition(Game::LocalPartition); +std::shared_ptr FolderEditScene::GetPreviewForCard(const std::string& uuid) { + auto& packageManager = getController().GetCardPackagePartitioner().GetPartition(Game::LocalPartition); if (!packageManager.HasPackage(uuid)) return noPreview; @@ -1052,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*/ @@ -1061,13 +1294,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 +1308,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..da8787ac0 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,83 @@ 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{}; + 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; + }; + + 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; + + this->invert = !this->invert; + if (index != this->lastIndex) { + this->invert = false; + this->lastIndex = index; + } + + 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); + + 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; + } + + std::vector copy = this->container; + std::sort(copy.begin(), copy.end(), this->filters.at(index)); + + if (this->invert) { + std::reverse(copy.begin(), copy.end()); + } + + this->container = copy; + } + }; + + size_t cursorSortIndex{}; + SortOptions folderSortOptions{ folderCardSlots }; + SortOptions poolSortOptions{ poolCardBuckets }; + #ifdef __ANDROID__ bool canSwipe; bool touchStart; @@ -177,13 +262,15 @@ class FolderEditScene : public Scene { void DrawFolder(sf::RenderTarget& surface); void DrawPool(sf::RenderTarget& surface); + void ComposeSortOptions(); void ExcludeFolderDataFromPool(); void PlaceFolderDataIntoCardSlots(); void PlaceLibraryDataIntoBuckets(); 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; @@ -199,31 +286,28 @@ class FolderEditScene : public Scene { ~FolderEditScene(); }; -template -void FolderEditScene::RefreshCurrentCardDock(FolderEditScene::CardView& view, const std::vector& list) +template +void FolderEditScene::RefreshCardDock(FolderEditScene::CardView& view, const std::vector& list) { if (view.currCardIndex < list.size()) { - ElementType 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)); - } + T slot = list[view.currCardIndex]; // copy data, do not mutate it + + 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/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 c3be0d1ec..f83538057 100644 --- a/BattleNetwork/bnGame.cpp +++ b/BattleNetwork/bnGame.cpp @@ -531,72 +531,72 @@ 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() +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; } -ConfigSettings& Game::ConfigSettings() +ConfigSettings& Game::GetConfigSettings() { return configSettings; } diff --git a/BattleNetwork/bnGame.h b/BattleNetwork/bnGame.h index 27c76fdb0..7cfaab244 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,27 +150,27 @@ 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(); - - CardPackagePartitioner& CardPackagePartitioner(); - PlayerPackagePartitioner& PlayerPackagePartitioner(); - MobPackagePartitioner& MobPackagePartitioner(); - BlockPackagePartitioner& BlockPackagePartitioner(); - LuaLibraryPackagePartitioner& LuaLibraryPackagePartitioner(); + 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& GetCardPackagePartitioner(); + PlayerPackagePartitioner& GetPlayerPackagePartitioner(); + MobPackagePartitioner& GetMobPackagePartitioner(); + BlockPackagePartitioner& GetBlockPackagePartitioner(); + LuaLibraryPackagePartitioner& GetLuaLibraryPackagePartitioner(); static char* LocalPartition; static char* RemotePartition; static char* ServerPartition; static char* Version; - ConfigSettings& ConfigSettings(); + ConfigSettings& GetConfigSettings(); GameSession& Session(); /** diff --git a/BattleNetwork/bnHitProperties.h b/BattleNetwork/bnHitProperties.h index 3bae2a660..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; @@ -10,11 +11,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; @@ -23,6 +24,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 }; @@ -47,17 +49,25 @@ 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, - 0, + const constexpr Hit::Properties DefaultProperties = { + 0, + Flags(Hit::flinch | Hit::impact), + Element::none, + Element::none, + 0, Direction::none, true }; -} \ No newline at end of file +} 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/bnMoveEvent.cpp b/BattleNetwork/bnMoveEvent.cpp new file mode 100644 index 000000000..af3037503 --- /dev/null +++ b/BattleNetwork/bnMoveEvent.cpp @@ -0,0 +1,337 @@ +#include "bnMoveEvent.h" +#include "bnTile.h" +#include "bnWaterSplash.h" +#include "bnField.h" +#include + +/// class MoveAction /// + +MoveAction::MoveAction(Entity& 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.moveStartPosition; +} + +void MoveAction::SetOwnerStartPosition(sf::Vector2f offset) +{ + owner.moveStartPosition = offset; +} + +void MoveAction::SetOwnerJumpHeight(float height) +{ + owner.currJumpHeight = height; +} + +void MoveAction::SetOwnerPreviousDirection(Direction dir) +{ + owner.previousDirection = dir; +} + +Battle::Tile* MoveAction::GetOwnerPreviousTile() +{ + return owner.previous; +} + +void MoveAction::UpdateMoveStartPosition() { + owner.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(); + + // 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(); + } + } + } +} + +bool MoveAction::IsPendingFinish() const { + return elapsedFrames >= (data.delayFrames + data.deltaFrames + data.endlagFrames); +} + +void MoveAction::OnPostMove() { + 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() { + 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(Entity& owner, Hit::Drag drag) : drag(drag), MoveAction(owner, {}) +{ +} + +void DragAction::Begin() { + if (!firstMove) { + return; + } + PrepareMovement(); + MoveAction::Begin(); + firstMove = false; +} + +void DragAction::PrepareFinalMove() { + startedFinalMove = true; + /* + 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. + + 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(0), frames(0), frames(firstMove ? 26 : 23), 0.f, nullptr}); +} + +void DragAction::PrepareMovement() { + if (completed) { + return; + } + + 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..eaa46ba67 --- /dev/null +++ b/BattleNetwork/bnMoveEvent.h @@ -0,0 +1,137 @@ +#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 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 { + 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(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). + // 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; + virtual void OnUpdate(frame_time_t elapsed); +protected: + Entity& 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(); + + /* + 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(Entity& owner, Hit::Drag drag); + +}; \ No newline at end of file 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, 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/bnPlayer.cpp b/BattleNetwork/bnPlayer.cpp index 71cf429da..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" @@ -39,17 +40,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 actionable if possible - SetAnimation(recoilAnimHash, [this] { MakeActionable();}); - Audio().Play(AudioType::HURT, AudioPriority::lowest); - }; - - RegisterStatusCallback(Hit::flinch, Callback{ flinch }); - using namespace std::placeholders; auto handler = std::bind(&Player::HandleBusterEvent, this, _1, _2); @@ -57,9 +47,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); @@ -99,6 +89,41 @@ 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 | Character::blockingStatuses)) { + Charge(false); + 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) { + 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(); }); + Audio().Play(AudioType::HURT, AudioPriority::lowest); + } + + Character::HandleNewStatuses(prevStatuses, appliedStatuses); +} + void Player::OnUpdate(double _elapsed) { SetColorMode(ColorMode::additive); @@ -130,21 +155,26 @@ 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); } } -bool Player::IsActionable() const +bool Player::IsIdle() const { return animationComponent->GetAnimationString() == "PLAYER_IDLE"; } +const bool Player::CanAttackImpl() const +{ + return Character::CanAttackImpl() && animationComponent->GetAnimationString() != recoilAnimHash; +} + void Player::Attack() { std::shared_ptr action = nullptr; @@ -155,7 +185,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; @@ -221,8 +251,10 @@ int Player::GetMoveCount() const return Entity::GetMoveCount(); } + void Player::Charge(bool state) { + frame_time_t maxCharge = CalculateChargeTime(GetChargeLevel()); if (activeForm) { maxCharge = activeForm->CalculateChargeTime(GetChargeLevel()); @@ -232,6 +264,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); @@ -245,6 +281,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() @@ -255,8 +298,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() @@ -266,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); } @@ -374,6 +454,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 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. + */ + ResolveFrameBattleDamage(); + // Additionally clear Flinch, so Player never flinches afterwards + ClearStatuses(Character::blockingStatuses | Hit::flinch); + SaveStats(); activeForm->OnActivate(shared_from_base()); CreateMoveAnimHash(); @@ -382,6 +476,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... diff --git a/BattleNetwork/bnPlayer.h b/BattleNetwork/bnPlayer.h index 66096dde1..f2c100f4e 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 @@ -116,6 +116,8 @@ class Player : public Character, public AI { */ void Charge(bool state); + bool IsCharging(); + void SetAttackLevel(unsigned lvl); const unsigned GetAttackLevel(); @@ -161,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(); @@ -190,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 69ce9ae26..ecace78c3 100644 --- a/BattleNetwork/bnPlayerControlledState.cpp +++ b/BattleNetwork/bnPlayerControlledState.cpp @@ -21,13 +21,16 @@ 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 - bool lockout = player.IsLockoutAnimationComplete(); - bool actionable = player.IsActionable(); + const bool lockout = player.IsLockoutAnimationComplete(); + const bool isIdle = player.IsIdle(); + 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) { @@ -36,12 +39,23 @@ void PlayerControlledState::OnUpdate(double _elapsed, Player& player) { return; } - bool missChargeKey = isChargeHeld && !player.InputState().Has(InputEvents::held_shoot); + /* + 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); + + 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)) { std::shared_ptr cardsUI = player.GetFirstComponent(); - if (cardsUI && cardsUI->UseNextCard()) { + + if (cardsUI && canAttack && cardsUI->UseNextCard()) { player.chargeEffect->SetCharging(false); isChargeHeld = false; } @@ -49,7 +63,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) { @@ -61,20 +75,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 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. + 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 isIdle || IsMoving is false, yet they can charge. + To cover for this case, check for Drag. + */ + if (isIdle || 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)) { @@ -90,7 +122,11 @@ void PlayerControlledState::OnUpdate(double _elapsed, Player& player) { direction = player.GetTeam() == Team::red ? Direction::right : Direction::left; } - if(direction != Direction::none && actionable && !player.IsRooted()) { + 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(); @@ -100,12 +136,12 @@ void PlayerControlledState::OnUpdate(double _elapsed, Player& player) { anim->CancelCallbacks(); auto idle_callback = [player]() { - player->MakeActionable(); + player->MakeIdle(); }; anim->SetAnimation(move_anim, idle_callback); - anim->SetInterruptCallback(idle_callback); + //anim->SetInterruptCallback(idle_callback); }; if (player.playerControllerSlide) { diff --git a/BattleNetwork/bnPlayerHealthUI.cpp b/BattleNetwork/bnPlayerHealthUI.cpp index 145f91bc6..52afb41db 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) @@ -106,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 // //////////////////////////////////// @@ -114,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(); } @@ -127,6 +141,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 { @@ -150,6 +165,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()); @@ -172,11 +188,9 @@ 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); } } } - - ui.Update(elapsed); } diff --git a/BattleNetwork/bnPlayerHealthUI.h b/BattleNetwork/bnPlayerHealthUI.h index a46e6cb3f..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; @@ -51,6 +52,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 */ @@ -59,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/bnPlayerSelectedCardsUI.cpp b/BattleNetwork/bnPlayerSelectedCardsUI.cpp index 7c4d4f152..0d6f9cd7e 100644 --- a/BattleNetwork/bnPlayerSelectedCardsUI.cpp +++ b/BattleNetwork/bnPlayerSelectedCardsUI.cpp @@ -146,12 +146,14 @@ 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); 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,13 +212,15 @@ void PlayerSelectedCardsUI::OnUpdate(double _elapsed) { } elapsed = _elapsed; + SelectedCardsUI::OnUpdate(_elapsed); } 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().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/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/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() { diff --git a/BattleNetwork/bnScriptResourceManager.cpp b/BattleNetwork/bnScriptResourceManager.cpp index 63c584c48..f34b7f7f9 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" @@ -94,6 +95,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"); }; @@ -181,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; @@ -368,7 +386,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 @@ -444,7 +464,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)); }) ); @@ -491,6 +511,47 @@ 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()); + }); + + 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, @@ -537,7 +598,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", @@ -547,7 +610,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::held_shoot, + "Left_Shoulder", InputEvents::held_shoulder_left, + "Right_Shoulder", InputEvents::held_shoulder_right ); input_event_record.new_enum("Released", @@ -557,7 +622,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::released_shoot, + "Left_Shoulder", InputEvents::released_shoulder_left, + "Right_Shoulder", InputEvents::released_shoulder_right ); const auto& character_rank_record = state.new_enum("Rank", @@ -641,17 +708,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] { @@ -676,6 +746,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/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 }; 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/bnSelectedCardsUI.cpp b/BattleNetwork/bnSelectedCardsUI.cpp index 57ea27743..e2b6ac57e 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; + + const 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,12 +157,26 @@ bool SelectedCardsUI::UseNextCard() { Battle::Card& card = (*selectedCards)[curr]; + 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 + // 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 } - multiplierValue = 1; // reset - // add a peek event to the action queue owner->AddAction(PeekCardEvent{ this }, ActionOrder::voluntary); return true; @@ -157,11 +188,11 @@ void SelectedCardsUI::Broadcast(std::shared_ptr action) CardActionUsePublisher::Broadcast(action, CurrentTime::AsMilli()); } -std::optional> SelectedCardsUI::Peek() +const 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 {}; @@ -169,7 +200,7 @@ std::optional> SelectedCardsUI::Peek( 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 @@ -178,7 +209,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/bnSelectedCardsUI.h b/BattleNetwork/bnSelectedCardsUI.h index 8bc0e6415..9eede484d 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(); + const MaybeCard Peek(); //!< Returns true if there was a card to play, false if empty bool HandlePlayEvent(std::shared_ptr from); 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: 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/bnStatusDirector.cpp b/BattleNetwork/bnStatusDirector.cpp new file mode 100644 index 000000000..766ae0aca --- /dev/null +++ b/BattleNetwork/bnStatusDirector.cpp @@ -0,0 +1,287 @@ +#include "bnStatusDirector.h" +#include "bnEntity.h" +#include "bnHitProperties.h" + +StatusBehaviorDirector::StatusBehaviorDirector(Entity& owner) : owner(owner), queuedStatuses{ 0 }, currentStatuses{ 0 } { + currentStatuses = {}; +} + +void StatusBehaviorDirector::AddStatus(Hit::Flags statusFlag, frame_time_t maxCooldown) { + 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 | Hit::confuse); + static const Hit::Flags freezeMask = ~(Hit::flinch | Hit::flash | Hit::confuse); + + // 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 and Flinch if Drag is queued. + if ((queuedStatuses & Hit::drag) == 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 | Hit::flinch); + return; + } + + // 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; + } + + if (queuedStatuses == 0) { + return; + } + + ProcessFlags(queuedStatuses); + queuedStatuses = Hit::none; +} + +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; + } + + // 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 + // 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; + + // 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); + } + } + + // 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. + // + // 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; + } + } + + Hit::Flags toApply = GetAppliedFlags(attack); + + // 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 | Hit::confuse); + } else if (toApply & Hit::freeze) { + currentStatuses &= ~(Hit::stun | Hit::confuse); + } + + // 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; +} + +/* + 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); + + // Update only Drag if Entity is under Drag. + // Base this on Entity::slideFromDrag, as it more accurately + // represents the special case of Drag. + // 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 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. + + 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; + + if (drag.remainingTime > frames(0)) { + return; + } + + currentStatuses &= ~Hit::drag; + + 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))) { + 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::ClearAllStatuses() { + for (auto& [_, status] : statusMap) { + status.remainingTime = frames(0); + } + + queuedStatuses = Hit::none; + currentStatuses = Hit::none; +}; + +void StatusBehaviorDirector::ClearStatuses(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 { + 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 bool StatusBehaviorDirector::HasAnyStatusFrom(Hit::Flags flags) const { + return ((currentStatuses | queuedStatuses) & flags); +} + +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..919dade17 --- /dev/null +++ b/BattleNetwork/bnStatusDirector.h @@ -0,0 +1,78 @@ +#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 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 ClearStatuses(Hit::Flags flags); + /* + 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 if any flag in [flags] is uncommitted or applied. + */ + const bool HasAnyStatusFrom(Hit::Flags flags) 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); +}; 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 diff --git a/BattleNetwork/bnTile.cpp b/BattleNetwork/bnTile.cpp index a9ef6d127..ac008afd8 100644 --- a/BattleNetwork/bnTile.cpp +++ b/BattleNetwork/bnTile.cpp @@ -23,18 +23,24 @@ #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 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 { - double Tile::brokenCooldownLength = COOLDOWN; - double Tile::teamCooldownLength = COOLDOWN; - double Tile::flickerTeamCooldownLength = FLICKER; + 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 = TEAM_FLICKER; Tile::Tile(int _x, int _y) : SpriteProxyNode(), animation() { - totalElapsed = 0; + totalElapsed = frames(0); x = _x; y = _y; @@ -54,11 +60,11 @@ 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 + burncycle = frames(7); // milliseconds elapsedBurnTime = burncycle; highlightMode = TileHighlight::none; @@ -66,10 +72,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 +89,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")); @@ -241,7 +247,7 @@ namespace Battle { flickerTeamCooldown = flickerTeamCooldownLength; } else { - flickerTeamCooldown = 0; // cancel + flickerTeamCooldown = frames(0); // cancel teamCooldown = teamCooldownLength; } } @@ -292,14 +298,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; } @@ -310,6 +321,11 @@ namespace Battle { RemoveNode(volcanoSprite); } + if (_state == TileState::sea) { + seaCooldown = seaCooldownLength; + seaDamageCooldown = seaDamageCooldownLength; + } + state = _state; } @@ -327,11 +343,15 @@ 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 <= 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 <= TILE_FLICKER) ? std::move(GetAnimState(TileState::normal)) : std::move(GetAnimState(state)); } else { animState = std::move(GetAnimState(state)); @@ -504,16 +524,24 @@ namespace Battle { void Tile::Update(Field& field, double _elapsed) { willHighlight = false; - totalElapsed += _elapsed; + + totalElapsed += from_seconds(_elapsed); if (!isTimeFrozen && isBattleStarted) { + // Grass Tiles + grassHealCooldown1 -= from_seconds(_elapsed); + grassHealCooldown2 -= from_seconds(_elapsed); + // LAVA TILES - elapsedBurnTime -= _elapsed; + elapsedBurnTime -= from_seconds(_elapsed); // VOLCANO - volcanoEruptTimer -= _elapsed; + volcanoEruptTimer -= from_seconds(_elapsed); + + // Sea + seaDamageCooldown -= from_seconds(_elapsed); - if (volcanoEruptTimer <= 0) { + if (volcanoEruptTimer <= frames(0)) { volcanoErupt.Update(_elapsed, volcanoSprite->getSprite()); } @@ -531,25 +559,30 @@ 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::sea) { + seaCooldown -= frames(1); + if (seaCooldown < frames(0)) { seaCooldown = frames(0); state = TileState::normal; }; } 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; } } } RefreshTexture(); - animation.SyncTime(from_seconds(totalElapsed)); + animation.SyncTime(totalElapsed); animation.Refresh(this->getSprite()); switch (highlightMode) { @@ -557,7 +590,7 @@ namespace Battle { willHighlight = true; break; case TileHighlight::flash: - willHighlight = (int)(totalElapsed * 15) % 2 == 0; + willHighlight = (totalElapsed.count() % 8 < 4); break; default: willHighlight = false; @@ -578,6 +611,9 @@ namespace Battle { HandleTileBehaviors(field, *character); } } + + if (grassHealCooldown1 <= frames(0)) grassHealCooldown1 = frames(20); + if (grassHealCooldown2 <= frames(0)) grassHealCooldown2 = frames(180); } void Tile::ToggleTimeFreeze(bool state) @@ -650,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); } } } @@ -675,24 +710,32 @@ namespace Battle { // LAVA & POISON TILES if (!character.HasFloatShoe()) { if (GetState() == TileState::poison) { - if (elapsedBurnTime <= 0) { - if (character.Hit(Hit::Properties({ 1, Hit::pierce, Element::none, 0, Direction::none }))) { + if (elapsedBurnTime <= frames(0)) { + if (character.Hit(Hit::Properties({ 1, Hit::pierce, Element::none, Element::none, 0, Direction::none }))) { elapsedBurnTime = burncycle; } } } else { - elapsedBurnTime = 0; + elapsedBurnTime = frames(0); } - if (GetState() == TileState::lava) { - Hit::Properties props = { 50, Hit::flash | Hit::flinch, Element::none, 0, Direction::none }; + if (GetState() == TileState::lava && character.GetElement() != Element::fire) { + 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()); 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, Element::none, 0, Direction::none }))) { + seaDamageCooldown = seaDamageCooldownLength; + } + } + } } // DIRECTIONAL TILES @@ -718,13 +761,30 @@ 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); } } } } } + 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) @@ -904,6 +964,15 @@ namespace Battle { case TileState::holy: str = str + "holy"; break; + 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"; } @@ -959,24 +1028,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 || 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 + + 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 +1082,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 @@ -1024,9 +1099,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); @@ -1035,6 +1107,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); } @@ -1048,6 +1121,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); } @@ -1057,12 +1135,21 @@ namespace Battle { 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(); + + 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) @@ -1076,11 +1163,6 @@ namespace Battle { highlightMode = (TileHighlight)request; } - Element hitboxElement = spell->GetElement(); - if (hitboxElement == Element::aqua && state == TileState::volcano) { - SetState(TileState::normal); - } - field.UpdateEntityOnce(*spell, elapsed); } } diff --git a/BattleNetwork/bnTile.h b/BattleNetwork/bnTile.h index f10959d34..68c933108 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 }; @@ -335,15 +338,21 @@ namespace Battle { bool isPerspectiveFlipped{ false }; float width{}; float height{}; - static double teamCooldownLength; - static double brokenCooldownLength; - static double flickerTeamCooldownLength; - double teamCooldown{}; - double brokenCooldown{}; - double flickerTeamCooldown{}; - double totalElapsed{}; - double elapsedBurnTime{}; - double burncycle{}; + static frame_time_t teamCooldownLength; + 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{}; + 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; @@ -368,7 +377,7 @@ namespace Battle { Animation animation; Animation volcanoErupt; - double volcanoEruptTimer{ 4 }; // seconds + frame_time_t volcanoEruptTimer{ 240 }; std::shared_ptr volcanoSprite; }; diff --git a/BattleNetwork/bnTileState.h b/BattleNetwork/bnTileState.h index ddd6bd1f7..d6bda76fc 100644 --- a/BattleNetwork/bnTileState.h +++ b/BattleNetwork/bnTileState.h @@ -18,6 +18,9 @@ enum class TileState : int { directionUp, 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/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/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/cxxopts/cxxopts.hpp b/BattleNetwork/cxxopts/cxxopts.hpp index 6d230f062..085e37e8f 100644 --- a/BattleNetwork/cxxopts/cxxopts.hpp +++ b/BattleNetwork/cxxopts/cxxopts.hpp @@ -38,6 +38,7 @@ THE SOFTWARE. #include #include #include +#include #ifdef __cpp_lib_optional #include diff --git a/BattleNetwork/main.cpp b/BattleNetwork/main.cpp index 8ac4e9600..c7c2788d0 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" @@ -26,6 +27,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 +54,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")) @@ -235,7 +264,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; @@ -264,7 +293,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(); @@ -272,11 +301,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 @@ -286,6 +315,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, @@ -420,11 +463,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 89e1941d9..ccd611ff3 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" @@ -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! @@ -212,11 +218,11 @@ 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(); + 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()); @@ -228,16 +234,11 @@ 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->IsSuperEffective(props.element)) { + if (player->IsInForm() && superEffective) { // animate the transformation back to default form TrackedFormData& formData = GetPlayerFormData(player); @@ -246,6 +247,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(); @@ -401,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) { @@ -415,7 +418,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 +430,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) { @@ -468,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) { @@ -586,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++) { @@ -598,11 +604,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); @@ -771,14 +777,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 +808,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) { 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/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 { /** 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 b1e464132..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(); @@ -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); @@ -2401,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) @@ -2470,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); @@ -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; 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/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."); 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 000000000..ae1c0a618 Binary files /dev/null and b/BattleNetwork/resources/scenes/battle/spells/confused.png differ diff --git a/BattleNetwork/resources/sfx/confused.ogg b/BattleNetwork/resources/sfx/confused.ogg new file mode 100644 index 000000000..cc2fb2828 Binary files /dev/null and b/BattleNetwork/resources/sfx/confused.ogg differ 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 000000000..1328a6ccd Binary files /dev/null and b/BattleNetwork/resources/tiles/splash.png differ diff --git a/BattleNetwork/resources/tiles/tile_atlas_blue.png b/BattleNetwork/resources/tiles/tile_atlas_blue.png index 807a9c57d..702fe5866 100644 Binary files a/BattleNetwork/resources/tiles/tile_atlas_blue.png and b/BattleNetwork/resources/tiles/tile_atlas_blue.png differ diff --git a/BattleNetwork/resources/tiles/tile_atlas_red.png b/BattleNetwork/resources/tiles/tile_atlas_red.png index 2b6404c14..350655500 100644 Binary files a/BattleNetwork/resources/tiles/tile_atlas_red.png and b/BattleNetwork/resources/tiles/tile_atlas_red.png differ diff --git a/BattleNetwork/resources/tiles/tile_atlas_unknown.png b/BattleNetwork/resources/tiles/tile_atlas_unknown.png index cfe2d68bc..14a9cc97e 100644 Binary files a/BattleNetwork/resources/tiles/tile_atlas_unknown.png and b/BattleNetwork/resources/tiles/tile_atlas_unknown.png differ diff --git a/BattleNetwork/resources/tiles/tiles.animation b/BattleNetwork/resources/tiles/tiles.animation index 7d95d81e7..e2debbcd4 100644 --- a/BattleNetwork/resources/tiles/tiles.animation +++ b/BattleNetwork/resources/tiles/tiles.animation @@ -1,202 +1,250 @@ -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="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" +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="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" +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="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 diff --git a/BattleNetwork/resources/ui/folder_sort.png b/BattleNetwork/resources/ui/folder_sort.png index dca2067eb..8ec25c1b6 100644 Binary files a/BattleNetwork/resources/ui/folder_sort.png and b/BattleNetwork/resources/ui/folder_sort.png differ 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..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 { @@ -54,4 +55,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 diff --git a/CMakeLists.txt b/CMakeLists.txt index a9a662f52..7606719bf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,15 @@ 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(UNIX) + set(CMAKE_CXX_FLAGS "-fpermissive -Wchanges-meaning") +ENDIF() + +IF(APPIMAGE) + add_compile_definitions(APPIMAGE) +endif() + set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake) include(FindLua) @@ -86,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