Add cat transform sprite. Added Witch Transform ability parameters. Implemented Transform ability. Fix Trapper Mark unit test since lock on target delays were added. Release Build 10354.

mac-build
sigonasr2 4 months ago
parent 8df671dcab
commit 21579b7bdf
  1. 59
      Adventures in Lestoria Tests/MonsterTests.cpp
  2. 1
      Adventures in Lestoria/AdventuresInLestoria.cpp
  3. 12
      Adventures in Lestoria/Animation.cpp
  4. 26
      Adventures in Lestoria/Player.cpp
  5. 7
      Adventures in Lestoria/Player.h
  6. 1
      Adventures in Lestoria/State.h
  7. 5
      Adventures in Lestoria/TMXParser.h
  8. 2
      Adventures in Lestoria/Version.h
  9. 44
      Adventures in Lestoria/Witch.cpp
  10. 1
      Adventures in Lestoria/assets/config/Player.txt
  11. 8
      Adventures in Lestoria/assets/config/classes/Witch.txt
  12. BIN
      Adventures in Lestoria/assets/gamepack.pak
  13. BIN
      Adventures in Lestoria/assets/nico-witch.png
  14. BIN
      Adventures in Lestoria/assets/nico-witch.xcf
  15. BIN
      x64/Release/Adventures in Lestoria.exe

@ -49,6 +49,7 @@ INCLUDE_MONSTER_DATA
INCLUDE_game INCLUDE_game
INCLUDE_GFX INCLUDE_GFX
INCLUDE_DAMAGENUMBER_LIST INCLUDE_DAMAGENUMBER_LIST
INCLUDE_MONSTER_LIST
TEST_MODULE_INITIALIZE(AiLTestSuite) TEST_MODULE_INITIALIZE(AiLTestSuite)
{ {
@ -83,6 +84,9 @@ namespace MonsterTests
#pragma region Setup a fake test map #pragma region Setup a fake test map
game->MAP_DATA["CAMPAIGN_1_1"]; 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(); ItemDrop::drops.clear();
#pragma endregion #pragma endregion
@ -399,52 +403,59 @@ namespace MonsterTests
,L"There should be 2 damage numbers of type DOT."); ,L"There should be 2 damage numbers of type DOT.");
} }
TEST_METHOD(TrapperMarkTest){ 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_ptr<Monster>testMonster{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(uint8_t(4),testMonster.lock()->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(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(uint8_t(1),testMonster.lock()->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(24,testMonster.lock()->GetHealth(),L"Monster should not take damage from the TriggerMark function (Mark deals 6 damage though).");
game->SetElapsedTime(10.f); 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.");
} }
}; };
} }

@ -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(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)))}; 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); 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); SetDecalMode(DecalMode::NORMAL);
if(player->GetState()==State::BLOCK){ 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()); view.DrawDecal(player->GetPos()+vf2d{0,-player->GetZ()*(std::signbit(scale.y)?-1:1)}-vf2d{12,12},GFX["block.png"].Decal());

@ -334,6 +334,18 @@ void sig::Animation::InitializeAnimations(){
pl_witch_cast_w.AddFrame({&GFX["nico-witch.png"],{vi2d{7+i,2}*24,{24,24}}}); 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; 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-back.png",5,{64,64},{0.02f,Animate2D::Style::OneShot});
CreateHorizontalAnimationSequence("ground-slam-attack-front.png",5,{64,64},{0.02f,Animate2D::Style::OneShot}); CreateHorizontalAnimationSequence("ground-slam-attack-front.png",5,{64,64},{0.02f,Animate2D::Style::OneShot});

@ -115,8 +115,7 @@ void Player::Initialize(){
SetBaseStat("Damage Reduction",0); SetBaseStat("Damage Reduction",0);
SetBaseStat("Attack Spd",0); SetBaseStat("Attack Spd",0);
cooldownSoundInstance=Audio::Engine().LoadSound("spell_cast.ogg"_SFX); cooldownSoundInstance=Audio::Engine().LoadSound("spell_cast.ogg"_SFX);
afterImage->sprite->Resize(24,24); afterImage.Create(24,24);
afterImage->Update();
} }
void Player::InitializeMinimapImage(){ void Player::InitializeMinimapImage(){
@ -521,6 +520,19 @@ void Player::Update(float fElapsedTime){
game->SetupWorldShake(0.3f); game->SetupWorldShake(0.3f);
} }
}break; }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:{ default:{
//Update animations normally. //Update animations normally.
animation.UpdateState(internal_animState,fElapsedTime); animation.UpdateState(internal_animState,fElapsedTime);
@ -757,10 +769,10 @@ void Player::Update(float fElapsedTime){
#pragma endregion #pragma endregion
#pragma region Witch #pragma region Witch
const auto RemoveScanLine=[&](uint8_t scanLine){ const auto RemoveScanLine=[&](uint8_t scanLine){
for(int x:std::ranges::iota_view(0,afterImage->sprite->width)){ for(int x:std::ranges::iota_view(0,afterImage.Sprite()->width)){
afterImage->sprite->SetPixel({x,scanLine},BLANK); afterImage.Sprite()->SetPixel({x,scanLine},BLANK);
} }
afterImage->Update(); afterImage.Decal()->Update();
}; };
//Scan Line goes through 1-23 (odd numbers) first, then 0-22. //Scan Line goes through 1-23 (odd numbers) first, then 0-22.
@ -1803,11 +1815,11 @@ const std::unordered_set<std::string>&Player::GetMyClass()const{
} }
void Player::SetupAfterImage(){ void Player::SetupAfterImage(){
game->SetDrawTarget(afterImage->sprite); game->SetDrawTarget(afterImage.Sprite());
game->Clear(BLANK); 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->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); game->SetDrawTarget(nullptr);
afterImage->Update(); afterImage.Decal()->Update();
removeLineTimer=TIME_BETWEEN_LINE_REMOVALS; removeLineTimer=TIME_BETWEEN_LINE_REMOVALS;
scanLine=1U; scanLine=1U;
} }

@ -408,12 +408,17 @@ protected:
float footstepTimer=0.f; float footstepTimer=0.f;
float ySquishFactor{1.f}; float ySquishFactor{1.f};
size_t cooldownSoundInstance=std::numeric_limits<size_t>::max(); size_t cooldownSoundInstance=std::numeric_limits<size_t>::max();
std::unique_ptr<Decal>afterImage; Renderable afterImage;
Animate2D::AnimationState internal_animState; Animate2D::AnimationState internal_animState;
float removeLineTimer{}; float removeLineTimer{};
const float TIME_BETWEEN_LINE_REMOVALS{0.025f}; const float TIME_BETWEEN_LINE_REMOVALS{0.025f};
uint8_t scanLine{24}; //0-23. uint8_t scanLine{24}; //0-23.
void SetupAfterImage(); void SetupAfterImage();
vf2d afterImagePos{};
float transformTargetDir{};
float leapTimer{};
float totalLeapTime{};
vf2d leapStartingPos{};
}; };
#pragma region Warrior #pragma region Warrior

@ -59,5 +59,6 @@ namespace State{
DEATH, DEATH,
ROLL, ROLL,
DEADLYDASH, DEADLYDASH,
LEAP,
}; };
} }

@ -118,9 +118,14 @@ struct NPCData{
NPCData(XMLTag npcTag); NPCData(XMLTag npcTag);
}; };
namespace MonsterTests{
class MonsterTest;
}
struct Map{ struct Map{
friend class AiL; friend class AiL;
friend class TMXParser; friend class TMXParser;
friend class MonsterTests::MonsterTest;
private: private:
MapTag MapData; MapTag MapData;
std::string name; std::string name;

@ -39,7 +39,7 @@ All rights reserved.
#define VERSION_MAJOR 1 #define VERSION_MAJOR 1
#define VERSION_MINOR 2 #define VERSION_MINOR 2
#define VERSION_PATCH 3 #define VERSION_PATCH 3
#define VERSION_BUILD 10347 #define VERSION_BUILD 10356
#define stringify(a) stringify_(a) #define stringify(a) stringify_(a)
#define stringify_(a) #a #define stringify_(a) #a

@ -43,6 +43,7 @@ All rights reserved.
#include "config.h" #include "config.h"
#include "SoundEffect.h" #include "SoundEffect.h"
#include "BulletTypes.h" #include "BulletTypes.h"
#include "util.h"
INCLUDE_MONSTER_LIST INCLUDE_MONSTER_LIST
INCLUDE_BULLET_LIST INCLUDE_BULLET_LIST
@ -63,6 +64,41 @@ void Witch::Initialize(){
SETUP_CLASS(Witch) SETUP_CLASS(Witch)
void Witch::OnUpdate(float fElapsedTime){ 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(){ bool Witch::AutoAttack(){
@ -78,6 +114,14 @@ void Witch::InitializeClassAbilities(){
Witch::rightClickAbility.action= Witch::rightClickAbility.action=
[](Player*p,vf2d pos={}){ [](Player*p,vf2d pos={}){
p->SetupAfterImage(); p->SetupAfterImage();
p->afterImagePos=p->leapStartingPos=p->GetPos();
geom2d::line<float>targetLine{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; return true;
}; };
#pragma endregion #pragma endregion

@ -96,6 +96,7 @@ Player
PLAYER_ANIMATION[22] = WITCH_ATTACK PLAYER_ANIMATION[22] = WITCH_ATTACK
PLAYER_ANIMATION[23] = WITCH_CAST PLAYER_ANIMATION[23] = WITCH_CAST
PLAYER_ANIMATION[24] = WITCH_IDLE PLAYER_ANIMATION[24] = WITCH_IDLE
PLAYER_ANIMATION[25] = WITCH_TRANSFORM
} }
PlayerXP PlayerXP

@ -45,13 +45,17 @@ Witch
# Whether or not this ability cancels casts. # Whether or not this ability cancels casts.
CancelCast = 1 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. #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 1 = 0, 0, 64, 192
Cooldown Bar Color 2 = 0, 0, 128, 192 Cooldown Bar Color 2 = 0, 0, 128, 192
Precast Time = 0 Precast Time = 0
Casting Range = 400 Casting Range = 0
Casting Size = 100 Casting Size = 0
} }
Ability 1 Ability 1
{ {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Loading…
Cancel
Save