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.

This commit is contained in:
sigonasr2 2024-07-27 09:10:40 -05:00
parent 41aab6d8dd
commit 223bf1b2fe
15 changed files with 131 additions and 35 deletions

View File

@ -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_ptr<Monster>testMonster{MONSTER_LIST.front()};
Assert::AreEqual(uint8_t(0),testMonster.GetMarkStacks(),L"Monster has 0 marks initially.");
Assert::AreEqual(uint8_t(0),testMonster.lock()->GetMarkStacks(),L"Monster has 0 marks initially.");
testMonster.ApplyMark(7.f,5U);
testMonster.lock()->ApplyMark(7.f,5U);
Assert::AreEqual(uint8_t(5),testMonster.GetMarkStacks(),L"Monster has 5 marks after receiving a buff.");
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.
testMonster.Hurt(1,testMonster.OnUpperLevel(),testMonster.GetZ());
Assert::AreEqual(uint8_t(5),testMonster.lock()->GetMarkStacks(),L"Monster has 5 marks after receiving a buff.");
Assert::AreEqual(uint8_t(5),testMonster.GetMarkStacks(),L"Monster should still have 5 marks after taking damage from a normal attack.");
testMonster.lock()->Hurt(1,testMonster.lock()->OnUpperLevel(),testMonster.lock()->GetZ());
testMonster.Hurt(1,testMonster.OnUpperLevel(),testMonster.GetZ(),HurtFlag::PLAYER_ABILITY);
Assert::AreEqual(uint8_t(5),testMonster.lock()->GetMarkStacks(),L"Monster should still have 5 marks after taking damage from a normal attack.");
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.");
testMonster.lock()->Hurt(1,testMonster.lock()->OnUpperLevel(),testMonster.lock()->GetZ(),HurtFlag::PLAYER_ABILITY);
testMonster.Hurt(1,testMonster.OnUpperLevel(),testMonster.GetZ(),HurtFlag::DOT);
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.");
Assert::AreEqual(uint8_t(4),testMonster.GetMarkStacks(),L"Monster should still have 4 marks remaining (DOTs don't remove a mark).");
testMonster.lock()->Hurt(1,testMonster.lock()->OnUpperLevel(),testMonster.lock()->GetZ(),HurtFlag::DOT);
testMonster._DealTrueDamage(1);
Assert::AreEqual(uint8_t(4),testMonster.lock()->GetMarkStacks(),L"Monster should still have 4 marks remaining (DOTs don't remove a mark).");
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.");
testMonster.lock()->_DealTrueDamage(1);
testMonster.Heal(testMonster.GetMaxHealth()); //Heal the monster so it doesn't die.
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._DealTrueDamage(1,HurtFlag::PLAYER_ABILITY);
testMonster.lock()->Heal(testMonster.lock()->GetMaxHealth()); //Heal the monster so it doesn't die.
Assert::AreEqual(uint8_t(3),testMonster.GetMarkStacks(),L"Monster should have 3 marks remaining after taking true damage.");
testMonster.lock()->_DealTrueDamage(1,HurtFlag::PLAYER_ABILITY);
testMonster._DealTrueDamage(10,HurtFlag::DOT|HurtFlag::PLAYER_ABILITY);
Assert::AreEqual(uint8_t(3),testMonster.lock()->GetMarkStacks(),L"Monster should have 3 marks remaining after taking true damage.");
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...");
testMonster.lock()->_DealTrueDamage(10,HurtFlag::DOT|HurtFlag::PLAYER_ABILITY);
testMonster.Heal(testMonster.GetMaxHealth());
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.TriggerMark();
testMonster.lock()->Heal(testMonster.lock()->GetMaxHealth());
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).");
testMonster.lock()->TriggerMark();
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.");
}
};
}

View File

@ -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());

View File

@ -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});

View File

@ -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<std::string>&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;
}

View File

@ -408,12 +408,17 @@ protected:
float footstepTimer=0.f;
float ySquishFactor{1.f};
size_t cooldownSoundInstance=std::numeric_limits<size_t>::max();
std::unique_ptr<Decal>afterImage;
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

View File

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

View File

@ -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;

View File

@ -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

View File

@ -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::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;
};
#pragma endregion

View File

@ -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

View File

@ -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
{

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 20 KiB