diff --git a/Adventures in Lestoria Tests/MonsterTests.cpp b/Adventures in Lestoria Tests/MonsterTests.cpp index 2df8b7ce..9a1b963e 100644 --- a/Adventures in Lestoria Tests/MonsterTests.cpp +++ b/Adventures in Lestoria Tests/MonsterTests.cpp @@ -49,6 +49,7 @@ INCLUDE_MONSTER_DATA INCLUDE_game INCLUDE_GFX INCLUDE_DAMAGENUMBER_LIST +INCLUDE_MONSTER_LIST TEST_MODULE_INITIALIZE(AiLTestSuite) { @@ -83,6 +84,9 @@ namespace MonsterTests #pragma region Setup a fake test map game->MAP_DATA["CAMPAIGN_1_1"]; + game->MAP_DATA["CAMPAIGN_1_1"].ZoneData["UpperZone"]; + game->MAP_DATA["CAMPAIGN_1_1"].ZoneData["LowerZone"]; + game->currentLevel="CAMPAIGN_1_1"; ItemDrop::drops.clear(); #pragma endregion @@ -399,52 +403,59 @@ namespace MonsterTests ,L"There should be 2 damage numbers of type DOT."); } TEST_METHOD(TrapperMarkTest){ - Monster testMonster{{},MONSTER_DATA["TestName"]}; + MonsterData testMonsterData{"TestName","Test Monster",30,10,5,{MonsterDropData{"Health Potion",100.f,1,1}},200.f}; + game->SpawnMonster({},testMonsterData); + game->SetElapsedTime(0.1f); + game->OnUserUpdate(0.1f); //A monster that is spawned needs to be added to the monster list in the next tick. + std::weak_ptrtestMonster{MONSTER_LIST.front()}; + + Assert::AreEqual(uint8_t(0),testMonster.lock()->GetMarkStacks(),L"Monster has 0 marks initially."); - Assert::AreEqual(uint8_t(0),testMonster.GetMarkStacks(),L"Monster has 0 marks initially."); + testMonster.lock()->ApplyMark(7.f,5U); - testMonster.ApplyMark(7.f,5U); + game->SetElapsedTime(0.3f); + game->OnUserUpdate(0.3f); //A monster that had a mark applied needs to be added as a lock on target in the next tick. - Assert::AreEqual(uint8_t(5),testMonster.GetMarkStacks(),L"Monster has 5 marks after receiving a buff."); + Assert::AreEqual(uint8_t(5),testMonster.lock()->GetMarkStacks(),L"Monster has 5 marks after receiving a buff."); - testMonster.Hurt(1,testMonster.OnUpperLevel(),testMonster.GetZ()); + testMonster.lock()->Hurt(1,testMonster.lock()->OnUpperLevel(),testMonster.lock()->GetZ()); - Assert::AreEqual(uint8_t(5),testMonster.GetMarkStacks(),L"Monster should still have 5 marks after taking damage from a normal attack."); + Assert::AreEqual(uint8_t(5),testMonster.lock()->GetMarkStacks(),L"Monster should still have 5 marks after taking damage from a normal attack."); - testMonster.Hurt(1,testMonster.OnUpperLevel(),testMonster.GetZ(),HurtFlag::PLAYER_ABILITY); + testMonster.lock()->Hurt(1,testMonster.lock()->OnUpperLevel(),testMonster.lock()->GetZ(),HurtFlag::PLAYER_ABILITY); - Assert::AreEqual(uint8_t(4),testMonster.GetMarkStacks(),L"Monster should have 4 marks remaining."); - Assert::AreEqual(22,testMonster.GetHealth(),L"Mark deals 60% of the player's attack. And 2 damage already taken from earlier."); + Assert::AreEqual(uint8_t(4),testMonster.lock()->GetMarkStacks(),L"Monster should have 4 marks remaining."); + Assert::AreEqual(22,testMonster.lock()->GetHealth(),L"Mark deals 60% of the player's attack. And 2 damage already taken from earlier."); - testMonster.Hurt(1,testMonster.OnUpperLevel(),testMonster.GetZ(),HurtFlag::DOT); + testMonster.lock()->Hurt(1,testMonster.lock()->OnUpperLevel(),testMonster.lock()->GetZ(),HurtFlag::DOT); - Assert::AreEqual(uint8_t(4),testMonster.GetMarkStacks(),L"Monster should still have 4 marks remaining (DOTs don't remove a mark)."); + Assert::AreEqual(uint8_t(4),testMonster.lock()->GetMarkStacks(),L"Monster should still have 4 marks remaining (DOTs don't remove a mark)."); - testMonster._DealTrueDamage(1); + testMonster.lock()->_DealTrueDamage(1); - Assert::AreEqual(uint8_t(4),testMonster.GetMarkStacks(),L"Monster should still have 4 marks remaining after taking true damage from something not marked as a player ability."); + Assert::AreEqual(uint8_t(4),testMonster.lock()->GetMarkStacks(),L"Monster should still have 4 marks remaining after taking true damage from something not marked as a player ability."); - testMonster.Heal(testMonster.GetMaxHealth()); //Heal the monster so it doesn't die. + testMonster.lock()->Heal(testMonster.lock()->GetMaxHealth()); //Heal the monster so it doesn't die. - testMonster._DealTrueDamage(1,HurtFlag::PLAYER_ABILITY); + testMonster.lock()->_DealTrueDamage(1,HurtFlag::PLAYER_ABILITY); - Assert::AreEqual(uint8_t(3),testMonster.GetMarkStacks(),L"Monster should have 3 marks remaining after taking true damage."); + Assert::AreEqual(uint8_t(3),testMonster.lock()->GetMarkStacks(),L"Monster should have 3 marks remaining after taking true damage."); - testMonster._DealTrueDamage(10,HurtFlag::DOT|HurtFlag::PLAYER_ABILITY); + testMonster.lock()->_DealTrueDamage(10,HurtFlag::DOT|HurtFlag::PLAYER_ABILITY); - Assert::AreEqual(uint8_t(2),testMonster.GetMarkStacks(),L"Monster should have 2 marks remaining after taking true damage, even though it's classified as a DOT. This is an edge case, but it's really meaningless here..."); + Assert::AreEqual(uint8_t(2),testMonster.lock()->GetMarkStacks(),L"Monster should have 2 marks remaining after taking true damage, even though it's classified as a DOT. This is an edge case, but it's really meaningless here..."); - testMonster.Heal(testMonster.GetMaxHealth()); + testMonster.lock()->Heal(testMonster.lock()->GetMaxHealth()); - testMonster.TriggerMark(); + testMonster.lock()->TriggerMark(); - Assert::AreEqual(uint8_t(1),testMonster.GetMarkStacks(),L"Monster should have 1 mark remaining after using TriggerMark function"); - Assert::AreEqual(24,testMonster.GetHealth(),L"Monster should not take damage from the TriggerMark function (Mark deals 6 damage though)."); + Assert::AreEqual(uint8_t(1),testMonster.lock()->GetMarkStacks(),L"Monster should have 1 mark remaining after using TriggerMark function"); + Assert::AreEqual(24,testMonster.lock()->GetHealth(),L"Monster should not take damage from the TriggerMark function (Mark deals 6 damage though)."); game->SetElapsedTime(10.f); - testMonster.Update(10.f); + testMonster.lock()->Update(10.f); - Assert::AreEqual(uint8_t(0),testMonster.GetMarkStacks(),L"The marks should have expired after 10 seconds."); + Assert::AreEqual(uint8_t(0),testMonster.lock()->GetMarkStacks(),L"The marks should have expired after 10 seconds."); } }; } \ No newline at end of file diff --git a/Adventures in Lestoria/AdventuresInLestoria.cpp b/Adventures in Lestoria/AdventuresInLestoria.cpp index 26012880..086ac72a 100644 --- a/Adventures in Lestoria/AdventuresInLestoria.cpp +++ b/Adventures in Lestoria/AdventuresInLestoria.cpp @@ -1036,6 +1036,7 @@ void AiL::RenderWorld(float fElapsedTime){ else if(adrenalineRushBuffs.size()>0)playerCol={uint8_t(255*abs(sin(6.f*adrenalineRushBuffs[0].duration))),255,uint8_t(255*abs(sin(6.f*adrenalineRushBuffs[0].duration)))}; else if(movespeedBuffs.size()>0)playerCol={uint8_t(255*abs(sin(2.f*movespeedBuffs[0].duration))),255,uint8_t(255*abs(sin(2.f*movespeedBuffs[0].duration)))}; view.DrawPartialSquishedRotatedDecal(pos+vf2d{0,-player->GetZ()*(std::signbit(scale.y)?-1:1)},player->GetFrame().GetSourceImage()->Decal(),player->GetSpinAngle(),{12,12},player->GetFrame().GetSourceRect().pos,player->GetFrame().GetSourceRect().size,playerScale*scale,{1.f,player->ySquishFactor},playerCol); + DrawAfterImage:view.DrawRotatedDecal(player->afterImagePos,player->afterImage.Decal(),0.f,player->afterImage.Sprite()->Size()/2,{player->GetSizeMult(),player->GetSizeMult()},{0xFFDCDA}); SetDecalMode(DecalMode::NORMAL); if(player->GetState()==State::BLOCK){ view.DrawDecal(player->GetPos()+vf2d{0,-player->GetZ()*(std::signbit(scale.y)?-1:1)}-vf2d{12,12},GFX["block.png"].Decal()); diff --git a/Adventures in Lestoria/Animation.cpp b/Adventures in Lestoria/Animation.cpp index e8e07798..f28709ba 100644 --- a/Adventures in Lestoria/Animation.cpp +++ b/Adventures in Lestoria/Animation.cpp @@ -334,6 +334,18 @@ void sig::Animation::InitializeAnimations(){ pl_witch_cast_w.AddFrame({&GFX["nico-witch.png"],{vi2d{7+i,2}*24,{24,24}}}); } ANIMATION_DATA["WITCH_CAST_W"]=pl_witch_cast_w; + Animate2D::FrameSequence pl_witch_transform_s(0.1f); + pl_witch_transform_s.AddFrame({&GFX["nico-witch.png"],{vi2d{0,4}*24,{24,24}}}); + Animate2D::FrameSequence pl_witch_transform_n(0.1f); + pl_witch_transform_n.AddFrame({&GFX["nico-witch.png"],{vi2d{0,5}*24,{24,24}}}); + Animate2D::FrameSequence pl_witch_transform_w(0.1f); + pl_witch_transform_w.AddFrame({&GFX["nico-witch.png"],{vi2d{0,6}*24,{24,24}}}); + Animate2D::FrameSequence pl_witch_transform_e(0.1f); + pl_witch_transform_e.AddFrame({&GFX["nico-witch.png"],{vi2d{0,7}*24,{24,24}}}); + ANIMATION_DATA["WITCH_TRANSFORM_S"]=pl_witch_transform_s; + ANIMATION_DATA["WITCH_TRANSFORM_N"]=pl_witch_transform_n; + ANIMATION_DATA["WITCH_TRANSFORM_W"]=pl_witch_transform_w; + ANIMATION_DATA["WITCH_TRANSFORM_E"]=pl_witch_transform_e; CreateHorizontalAnimationSequence("ground-slam-attack-back.png",5,{64,64},{0.02f,Animate2D::Style::OneShot}); CreateHorizontalAnimationSequence("ground-slam-attack-front.png",5,{64,64},{0.02f,Animate2D::Style::OneShot}); diff --git a/Adventures in Lestoria/Player.cpp b/Adventures in Lestoria/Player.cpp index 6da9d399..465c2a03 100644 --- a/Adventures in Lestoria/Player.cpp +++ b/Adventures in Lestoria/Player.cpp @@ -115,8 +115,7 @@ void Player::Initialize(){ SetBaseStat("Damage Reduction",0); SetBaseStat("Attack Spd",0); cooldownSoundInstance=Audio::Engine().LoadSound("spell_cast.ogg"_SFX); - afterImage->sprite->Resize(24,24); - afterImage->Update(); + afterImage.Create(24,24); } void Player::InitializeMinimapImage(){ @@ -521,6 +520,19 @@ void Player::Update(float fElapsedTime){ game->SetupWorldShake(0.3f); } }break; + case State::LEAP:{ + leapTimer-=fElapsedTime; + if(leapTimer<=0.f){ + SetState(State::NORMAL); + SetupAfterImage(); + afterImagePos=GetPos(); + SetZ(0.f); + break; + } + SetZ((sin((1.f/totalLeapTime)*PI*leapTimer)/2.f+0.5f)*"Witch.Right Click Ability.Leap Max Z"_F); + SetVelocity(vf2d{"Witch.Right Click Ability.Leap Velocity"_F/100.f*24,transformTargetDir}.cart()); + animation.UpdateState(internal_animState,fElapsedTime); + }break; default:{ //Update animations normally. animation.UpdateState(internal_animState,fElapsedTime); @@ -757,10 +769,10 @@ void Player::Update(float fElapsedTime){ #pragma endregion #pragma region Witch const auto RemoveScanLine=[&](uint8_t scanLine){ - for(int x:std::ranges::iota_view(0,afterImage->sprite->width)){ - afterImage->sprite->SetPixel({x,scanLine},BLANK); + for(int x:std::ranges::iota_view(0,afterImage.Sprite()->width)){ + afterImage.Sprite()->SetPixel({x,scanLine},BLANK); } - afterImage->Update(); + afterImage.Decal()->Update(); }; //Scan Line goes through 1-23 (odd numbers) first, then 0-22. @@ -1803,11 +1815,11 @@ const std::unordered_set&Player::GetMyClass()const{ } void Player::SetupAfterImage(){ - game->SetDrawTarget(afterImage->sprite); + game->SetDrawTarget(afterImage.Sprite()); game->Clear(BLANK); game->DrawPartialSprite({},animation.GetFrame(internal_animState).GetSourceImage()->Sprite(),animation.GetFrame(internal_animState).GetSourceRect().pos,animation.GetFrame(internal_animState).GetSourceRect().size,1U,0U,{255,255,254}); //Off-white so that the sprite is rendered completely in white. game->SetDrawTarget(nullptr); - afterImage->Update(); + afterImage.Decal()->Update(); removeLineTimer=TIME_BETWEEN_LINE_REMOVALS; scanLine=1U; } \ No newline at end of file diff --git a/Adventures in Lestoria/Player.h b/Adventures in Lestoria/Player.h index cfb74eed..9a8da115 100644 --- a/Adventures in Lestoria/Player.h +++ b/Adventures in Lestoria/Player.h @@ -408,12 +408,17 @@ protected: float footstepTimer=0.f; float ySquishFactor{1.f}; size_t cooldownSoundInstance=std::numeric_limits::max(); - std::unique_ptrafterImage; + Renderable afterImage; Animate2D::AnimationState internal_animState; float removeLineTimer{}; const float TIME_BETWEEN_LINE_REMOVALS{0.025f}; uint8_t scanLine{24}; //0-23. void SetupAfterImage(); + vf2d afterImagePos{}; + float transformTargetDir{}; + float leapTimer{}; + float totalLeapTime{}; + vf2d leapStartingPos{}; }; #pragma region Warrior diff --git a/Adventures in Lestoria/State.h b/Adventures in Lestoria/State.h index 593f8996..bfcb71e4 100644 --- a/Adventures in Lestoria/State.h +++ b/Adventures in Lestoria/State.h @@ -59,5 +59,6 @@ namespace State{ DEATH, ROLL, DEADLYDASH, + LEAP, }; } \ No newline at end of file diff --git a/Adventures in Lestoria/TMXParser.h b/Adventures in Lestoria/TMXParser.h index a668ae31..480b7ddc 100644 --- a/Adventures in Lestoria/TMXParser.h +++ b/Adventures in Lestoria/TMXParser.h @@ -118,9 +118,14 @@ struct NPCData{ NPCData(XMLTag npcTag); }; +namespace MonsterTests{ + class MonsterTest; +} + struct Map{ friend class AiL; friend class TMXParser; + friend class MonsterTests::MonsterTest; private: MapTag MapData; std::string name; diff --git a/Adventures in Lestoria/Version.h b/Adventures in Lestoria/Version.h index 41602d54..4fcb88bc 100644 --- a/Adventures in Lestoria/Version.h +++ b/Adventures in Lestoria/Version.h @@ -39,7 +39,7 @@ All rights reserved. #define VERSION_MAJOR 1 #define VERSION_MINOR 2 #define VERSION_PATCH 3 -#define VERSION_BUILD 10347 +#define VERSION_BUILD 10356 #define stringify(a) stringify_(a) #define stringify_(a) #a diff --git a/Adventures in Lestoria/Witch.cpp b/Adventures in Lestoria/Witch.cpp index 78860bbf..21c687fd 100644 --- a/Adventures in Lestoria/Witch.cpp +++ b/Adventures in Lestoria/Witch.cpp @@ -43,6 +43,7 @@ All rights reserved. #include "config.h" #include "SoundEffect.h" #include "BulletTypes.h" +#include "util.h" INCLUDE_MONSTER_LIST INCLUDE_BULLET_LIST @@ -63,6 +64,41 @@ void Witch::Initialize(){ SETUP_CLASS(Witch) void Witch::OnUpdate(float fElapsedTime){ + if(attack_cooldown_timer>0){ + idle_n="WITCH_IDLE_ATTACK_N"; + idle_e="WITCH_IDLE_ATTACK_E"; + idle_s="WITCH_IDLE_ATTACK_S"; + idle_w="WITCH_IDLE_ATTACK_W"; + walk_n="WITCH_ATTACK_N"; + walk_e="WITCH_ATTACK_E"; + walk_s="WITCH_ATTACK_S"; + walk_w="WITCH_ATTACK_W"; + } else { + idle_n="WITCH_IDLE_N"; + idle_e="WITCH_IDLE_E"; + idle_s="WITCH_IDLE_S"; + idle_w="WITCH_IDLE_W"; + walk_n="WITCH_WALK_N"; + walk_e="WITCH_WALK_E"; + walk_s="WITCH_WALK_S"; + walk_w="WITCH_WALK_W"; + } + if(GetState()==State::CASTING){ + switch(GetFacingDirection()){ + case UP:{ + UpdateAnimation("WITCH_CAST_N",WIZARD|WITCH); + }break; + case DOWN:{ + UpdateAnimation("WITCH_CAST_S",WIZARD|WITCH); + }break; + case LEFT:{ + UpdateAnimation("WITCH_CAST_W",WIZARD|WITCH); + }break; + case RIGHT:{ + UpdateAnimation("WITCH_CAST_E",WIZARD|WITCH); + }break; + } + } } bool Witch::AutoAttack(){ @@ -78,6 +114,14 @@ void Witch::InitializeClassAbilities(){ Witch::rightClickAbility.action= [](Player*p,vf2d pos={}){ p->SetupAfterImage(); + p->afterImagePos=p->leapStartingPos=p->GetPos(); + geom2d::linetargetLine{p->GetPos(),p->GetWorldAimingLocation(Player::USE_WALK_DIR,Player::INVERTED)}; + const float LeapMaxRange{"Witch.Right Click Ability.Leap Velocity"_F*"Witch.Right Click Ability.Leap Max Range Time"_F}; + p->leapTimer=p->totalLeapTime=std::min("Witch.Right Click Ability.Leap Max Range Time"_F,util::lerp(0.f,"Witch.Right Click Ability.Leap Max Range Time"_F,targetLine.length()/(LeapMaxRange/100.f*24))); + p->transformTargetDir=targetLine.vector().polar().y; + p->SetAnimationBasedOnTargetingDirection("TRANSFORM",p->transformTargetDir); + p->ApplyIframes("Witch.Right Click Ability.Leap Max Range Time"_F+0.1f); + p->SetState(State::LEAP); return true; }; #pragma endregion diff --git a/Adventures in Lestoria/assets/config/Player.txt b/Adventures in Lestoria/assets/config/Player.txt index d3114205..0495dd07 100644 --- a/Adventures in Lestoria/assets/config/Player.txt +++ b/Adventures in Lestoria/assets/config/Player.txt @@ -96,6 +96,7 @@ Player PLAYER_ANIMATION[22] = WITCH_ATTACK PLAYER_ANIMATION[23] = WITCH_CAST PLAYER_ANIMATION[24] = WITCH_IDLE + PLAYER_ANIMATION[25] = WITCH_TRANSFORM } PlayerXP diff --git a/Adventures in Lestoria/assets/config/classes/Witch.txt b/Adventures in Lestoria/assets/config/classes/Witch.txt index ba9c526a..cde166c3 100644 --- a/Adventures in Lestoria/assets/config/classes/Witch.txt +++ b/Adventures in Lestoria/assets/config/classes/Witch.txt @@ -45,13 +45,17 @@ Witch # Whether or not this ability cancels casts. CancelCast = 1 + Leap Velocity = 800 + Leap Max Range Time = 0.5s + Leap Max Z = 15px + #RGB Values. Color 1 is the circle at full cooldown, Color 2 is the color at empty cooldown. Cooldown Bar Color 1 = 0, 0, 64, 192 Cooldown Bar Color 2 = 0, 0, 128, 192 Precast Time = 0 - Casting Range = 400 - Casting Size = 100 + Casting Range = 0 + Casting Size = 0 } Ability 1 { diff --git a/Adventures in Lestoria/assets/gamepack.pak b/Adventures in Lestoria/assets/gamepack.pak index 18da1ae9..8c8e2176 100644 Binary files a/Adventures in Lestoria/assets/gamepack.pak and b/Adventures in Lestoria/assets/gamepack.pak differ diff --git a/Adventures in Lestoria/assets/nico-witch.png b/Adventures in Lestoria/assets/nico-witch.png index 55501c3c..ec3871e5 100644 Binary files a/Adventures in Lestoria/assets/nico-witch.png and b/Adventures in Lestoria/assets/nico-witch.png differ diff --git a/Adventures in Lestoria/assets/nico-witch.xcf b/Adventures in Lestoria/assets/nico-witch.xcf index c6ae7b31..b65610a9 100644 Binary files a/Adventures in Lestoria/assets/nico-witch.xcf and b/Adventures in Lestoria/assets/nico-witch.xcf differ diff --git a/x64/Release/Adventures in Lestoria.exe b/x64/Release/Adventures in Lestoria.exe index 96f4004d..4bb87f68 100644 Binary files a/x64/Release/Adventures in Lestoria.exe and b/x64/Release/Adventures in Lestoria.exe differ