Switch controller auto-targeting to use logic from Hurt function. This means the auto targeting tries to ensure it hits some target, but prefers a currently vulnerable target over an invulnerable / unhittable one. Cleanup the code to use std::optional. Add in a helper function to get nearest monster. Apply Mark to nearest selected target when Trapper Mark Target ability is used. Release Build 10275.

This commit is contained in:
sigonasr2 2024-07-23 00:09:28 -05:00
parent 60b45cf6b1
commit b3c5894be7
15 changed files with 127 additions and 46 deletions

View File

@ -44,22 +44,32 @@ INCLUDE_ANIMATION_DATA
INCLUDE_game
Effect::Effect(vf2d pos,float lifetime,std::string imgFile,bool upperLevel,float size,float fadeout,vf2d spd,Pixel col,float rotation,float rotationSpd,bool additiveBlending)
:Effect::Effect(pos,lifetime,imgFile,upperLevel,vf2d{size,size},fadeout,spd,col,rotation,rotationSpd,additiveBlending){
:Effect::Effect(pos,lifetime,imgFile,upperLevel,0.f,fadeout,vf2d{size,size},spd,col,rotation,rotationSpd,additiveBlending){
this->animation.AddState(imgFile,ANIMATION_DATA.at(imgFile));
}
Effect::Effect(vf2d pos,float lifetime,std::string imgFile,bool upperLevel,vf2d size,float fadeout,vf2d spd,Pixel col,float rotation,float rotationSpd,bool additiveBlending)
:pos(pos),lifetime(lifetime),upperLevel(upperLevel),size(size),fadeout(fadeout),original_fadeoutTime(fadeout),spd(spd),col(col),rotation(rotation),rotationSpd(rotationSpd),additiveBlending(additiveBlending){
:pos(pos),lifetime(lifetime),upperLevel(upperLevel),size(size),original_fadeInTime(fadein),fadeout(fadeout),original_fadeOutTime(fadeout),spd(spd),col(col),rotation(rotation),rotationSpd(rotationSpd),additiveBlending(additiveBlending){
this->animation.AddState(imgFile,ANIMATION_DATA.at(imgFile));
}
Effect::Effect(vf2d pos,float lifetime,std::string imgFile,bool upperLevel,float fadein,float fadeout,vf2d size,vf2d spd,Pixel col,float rotation,float rotationSpd,bool additiveBlending)
:pos(pos),lifetime(lifetime),upperLevel(upperLevel),size(size),fadein(fadein),original_fadeInTime(fadein),fadeout(fadeout),original_fadeOutTime(fadeout),spd(spd),col(col),rotation(rotation),rotationSpd(rotationSpd),additiveBlending(additiveBlending){
this->animation.AddState(imgFile,ANIMATION_DATA.at(imgFile));
}
bool Effect::Update(float fElapsedTime){
lifetime-=fElapsedTime;
if(lifetime<=0){
fadeout-=fElapsedTime;
if(fadeout<=0){
dead=true;
return false;
aliveTime+=fElapsedTime;
if(fadein<original_fadeInTime){
fadein=std::min(original_fadeInTime,fadein+fElapsedTime);
}else{
lifetime-=fElapsedTime;
if(lifetime<=0){
fadeout-=fElapsedTime;
if(fadeout<=0){
dead=true;
return false;
}
}
}
rotation+=rotationSpd*fElapsedTime;
@ -70,10 +80,14 @@ bool Effect::Update(float fElapsedTime){
void Effect::Draw()const{
if(additiveBlending)game->SetDecalMode(DecalMode::ADDITIVE);
if(fadeout==0){
if(fadeout==0&&fadein==original_fadeInTime){
game->view.DrawPartialRotatedDecal(pos,GetFrame().GetSourceImage()->Decal(),rotation,GetFrame().GetSourceRect().size/2,GetFrame().GetSourceRect().pos,GetFrame().GetSourceRect().size,size,col);
} else {
game->view.DrawPartialRotatedDecal(pos,GetFrame().GetSourceImage()->Decal(),rotation,GetFrame().GetSourceRect().size/2,GetFrame().GetSourceRect().pos,GetFrame().GetSourceRect().size,size,{col.r,col.g,col.b,uint8_t(fadeout/original_fadeoutTime*col.a)});
}else
if(fadein==original_fadeInTime){
game->view.DrawPartialRotatedDecal(pos,GetFrame().GetSourceImage()->Decal(),rotation,GetFrame().GetSourceRect().size/2,GetFrame().GetSourceRect().pos,GetFrame().GetSourceRect().size,size,{col.r,col.g,col.b,uint8_t(fadein/original_fadeInTime*col.a)});
}else
if(fadeout==0){
game->view.DrawPartialRotatedDecal(pos,GetFrame().GetSourceImage()->Decal(),rotation,GetFrame().GetSourceRect().size/2,GetFrame().GetSourceRect().pos,GetFrame().GetSourceRect().size,size,{col.r,col.g,col.b,uint8_t(fadeout/original_fadeOutTime*col.a)});
}
game->SetDecalMode(DecalMode::NORMAL);
}

View File

@ -54,6 +54,7 @@ struct Effect{
vf2d pos={0,0};
float lifetime=0;
float fadeout=0;
float fadein=0;
vf2d size={1,1};
Pixel col=WHITE;
vf2d spd={};
@ -65,18 +66,21 @@ private:
public:
Effect(vf2d pos,float lifetime,std::string imgFile,bool upperLevel,float size=1.0f,float fadeout=0.0f,vf2d spd={},Pixel col=WHITE,float rotation=0,float rotationSpd=0,bool additiveBlending=false);
Effect(vf2d pos,float lifetime,std::string imgFile,bool upperLevel,vf2d size={1,1},float fadeout=0.0f,vf2d spd={},Pixel col=WHITE,float rotation=0,float rotationSpd=0,bool additiveBlending=false);
Effect(vf2d pos,float lifetime,std::string imgFile,bool upperLevel,float fadein,float fadeout,vf2d size,vf2d spd,Pixel col=WHITE,float rotation=0,float rotationSpd=0,bool additiveBlending=false);
virtual bool Update(float fElapsedTime);
Animate2D::Frame GetFrame()const;
virtual void Draw()const;
bool OnUpperLevel();
const EffectType GetType()const;
protected:
float original_fadeoutTime;
float original_fadeOutTime;
float original_fadeInTime;
EffectType type{EffectType::NONE};
private:
Animate2D::Animation<std::string>animation;
Animate2D::AnimationState internal_animState;
bool upperLevel=false;
double aliveTime{};
};
struct Meteor:Effect{

View File

@ -52,7 +52,7 @@ void ForegroundEffect::Draw()const{
if(fadeout==0){
game->DrawPartialRotatedDecal(pos,GetFrame().GetSourceImage()->Decal(),rotation,GetFrame().GetSourceRect().size/2,GetFrame().GetSourceRect().pos,GetFrame().GetSourceRect().size,size,col);
} else {
game->DrawPartialRotatedDecal(pos,GetFrame().GetSourceImage()->Decal(),rotation,GetFrame().GetSourceRect().size/2,GetFrame().GetSourceRect().pos,GetFrame().GetSourceRect().size,size,{col.r,col.g,col.b,uint8_t(fadeout/original_fadeoutTime*255)});
game->DrawPartialRotatedDecal(pos,GetFrame().GetSourceImage()->Decal(),rotation,GetFrame().GetSourceRect().size/2,GetFrame().GetSourceRect().pos,GetFrame().GetSourceRect().size,size,{col.r,col.g,col.b,uint8_t(fadeout/original_fadeOutTime*255)});
}
game->SetDecalMode(DecalMode::NORMAL);
}

View File

@ -81,5 +81,5 @@ void Meteor::Draw()const{
if(lifetime<=0){
meteorOffset=pos-vf2d{0,GetFrame().GetSourceRect().size.y/4.f}*size;
}
game->view.DrawPartialRotatedDecal(meteorOffset,GetFrame().GetSourceImage()->Decal(),rotation,GetFrame().GetSourceRect().size/2,GetFrame().GetSourceRect().pos,GetFrame().GetSourceRect().size,size,{col.r,col.g,col.b,uint8_t(fadeout/original_fadeoutTime*255)});
game->view.DrawPartialRotatedDecal(meteorOffset,GetFrame().GetSourceImage()->Decal(),rotation,GetFrame().GetSourceRect().size/2,GetFrame().GetSourceRect().pos,GetFrame().GetSourceRect().size,size,{col.r,col.g,col.b,uint8_t(fadeout/original_fadeOutTime*255)});
}

View File

@ -473,23 +473,23 @@ void Monster::Draw()const{
}
#pragma region Render Trapper Marked Targets
const uint8_t markStackCount{GetMarkStacks()};
if(markStackCount>0){
float markRotation{-util::lerp(0.f,10.f,markApplicationTimer/0.5f)*sin(PI*markApplicationTimer)};
vf2d markScale{vf2d{}.lerp(vf2d{GetSizeMult(),GetSizeMult()},(0.5f-markApplicationTimer)/0.5f)};
const Animate2D::Frame&markImg{ANIMATION_DATA["target.png"].GetFrame(game->GetRunTime())};
Pixel markCol{markStackCount>1?WHITE:RED};
const std::vector<Buff>&buffList{GetBuffs(BuffType::TRAPPER_MARK)};
float remainingStackDuration{};
for(const Buff&b:buffList){
if(b.type==BuffType::TRAPPER_MARK){
remainingStackDuration=b.duration;
break;
const uint8_t markStackCount{GetMarkStacks()};
if(markStackCount>0){
float markRotation{-util::lerp(0.f,10.f,markApplicationTimer/0.5f)*sin(PI*markApplicationTimer)};
vf2d markScale{vf2d{}.lerp(vf2d{GetSizeMult(),GetSizeMult()},(0.5f-markApplicationTimer)/0.5f)};
const Animate2D::Frame&markImg{ANIMATION_DATA["target.png"].GetFrame(game->GetRunTime())};
Pixel markCol{markStackCount>1?WHITE:RED};
const std::vector<Buff>&buffList{GetBuffs(BuffType::TRAPPER_MARK)};
float remainingStackDuration{};
for(const Buff&b:buffList){
if(b.type==BuffType::TRAPPER_MARK){
remainingStackDuration=b.duration;
break;
}
}
if(remainingStackDuration<1.f)markCol.a*=remainingStackDuration;
game->view.DrawPartialRotatedDecal(drawPos,markImg.GetSourceImage()->Decal(),markRotation,markImg.GetSourceRect().size/2.f,markImg.GetSourceRect().pos,markImg.GetSourceRect().size,markScale,markCol);
}
if(remainingStackDuration<1.f)markCol.a*=remainingStackDuration;
game->view.DrawPartialRotatedDecal(drawPos,markImg.GetSourceImage()->Decal(),markRotation,markImg.GetSourceRect().size/2.f,markImg.GetSourceRect().pos,markImg.GetSourceRect().size,markScale,markCol);
}
#pragma endregion
if(GameSettings::TerrainCollisionBoxesEnabled()&&IsSolid()&&solidFadeTimer>0.f){
@ -640,7 +640,7 @@ bool Monster::_Hurt(int damage,bool onUpperLevel,float z,const TrueDamageFlag da
const bool NormalDamageCalculationRequired{!IsDOT&&!TrueDamage};
const bool PlayHitSoundEffect{!IsDOT};
if(!TrueDamage&&!IsDOT&&(Invulnerable()||!IsAlive()||onUpperLevel!=OnUpperLevel()||AttackAvoided(z)))return false;
if(!TrueDamage&&!IsDOT&&InUndamageableState(onUpperLevel,z))return false;
if(game->InBossEncounter()){
game->StartBossEncounter();
}
@ -674,6 +674,7 @@ bool Monster::_Hurt(int damage,bool onUpperLevel,float z,const TrueDamageFlag da
dotNumberPtr.get()->RecalculateSize();
}else{
dotNumberPtr=std::make_shared<DamageNumber>(pos-vf2d{0,GetCollisionRadius()/2.f},int(mod_dmg),false,DamageNumberType::DOT);
dotNumberPtr->riseSpd=dotNumberPtr->originalRiseSpd=-10.f;
DAMAGENUMBER_LIST.push_back(dotNumberPtr);
}
lastDotTimer=0.05f;
@ -1260,4 +1261,38 @@ void Monster::ApplyMark(float time,uint8_t stackCount){
}
}else AddBuff(BuffType::TRAPPER_MARK,time,stackCount);
markApplicationTimer=0.5f;
}
std::optional<Monster*>Monster::GetNearestMonster(const vf2d point,const float maxDistance,const bool onUpperLevel,const float z){
std::optional<Monster*>closestMonster;
std::optional<Monster*>closestGenericMonster;
for(std::unique_ptr<Monster>&m:MONSTER_LIST){
geom2d::line<float>aimingLine=geom2d::line<float>(point,m->GetPos());
float distToMonster=aimingLine.length();
float distToClosestPoint,distToClosestGenericPoint;
if(closestMonster.has_value())distToClosestPoint=geom2d::line<float>(point,closestMonster.value()->GetPos()).length();
else distToClosestPoint=std::numeric_limits<float>::max();
if(closestGenericMonster.has_value())distToClosestGenericPoint=geom2d::line<float>(point,closestGenericMonster.value()->GetPos()).length();
else distToClosestGenericPoint=std::numeric_limits<float>::max();
if(!m->InUndamageableState(onUpperLevel,z)){
if(distToClosestPoint>distToMonster&&distToMonster<=maxDistance){
closestMonster=&*m;
}
}
if(m->IsAlive()&&distToClosestGenericPoint>distToMonster&&distToMonster<=maxDistance){
closestGenericMonster=&*m;
}
}
if(closestMonster.has_value()){
return closestMonster;
}else
if(closestGenericMonster.has_value()){
return closestGenericMonster;
}
return {};
}
const bool Monster::InUndamageableState(const bool onUpperLevel,const float z)const{
return Invulnerable()||!IsAlive()||onUpperLevel!=OnUpperLevel()||AttackAvoided(z);
}

View File

@ -202,6 +202,9 @@ public:
const uint8_t GetMarkStacks()const; //Number of Trapper marks on this target.
void TriggerMark(); //Deals no damage, but causes a mark proc to occur.
void ApplyMark(float time,uint8_t stackCount); //Adds stackCount mark stacks to the target, refreshing the buff to time time.
//Gets the nearest target that can be immediately targeted
static std::optional<Monster*>GetNearestMonster(const vf2d point,const float maxDistance,const bool onUpperLevel,const float z);
const bool InUndamageableState(const bool onUpperLevel,const float z)const;
private:
//NOTE: Marking a monster for deletion does not trigger any death events. It just simply removes the monster from the field!!
// The way this works is that monsters marked for deletion will cause the monster update loop to detect there's at least one or more monsters that must be deleted and will call erase_if on the list at the end of the iteration loop.

View File

@ -1575,33 +1575,45 @@ const vf2d Player::GetAimingLocation(bool useWalkDir,bool invert){
if(xAxis!=0.f||yAxis!=0.f){
return {(game->ScreenWidth()*xAxis)/2+game->ScreenWidth()/2,(game->ScreenHeight()*yAxis)/2+game->ScreenHeight()/2};
}else{
if(useWalkDir&&movementVelocity!=vf2d{0,0}){
xAxis=aimingAngle.cart().x;
yAxis=aimingAngle.cart().y;
}
const vf2d MAX{std::numeric_limits<float>::max(),std::numeric_limits<float>::max()};
if(xAxis!=0.f||yAxis!=0.f){
return {(game->ScreenWidth()*xAxis)/2+game->ScreenWidth()/2,(game->ScreenHeight()*yAxis)/2+game->ScreenHeight()/2};
}else{
//Find the closest monster target.
vf2d closestPoint={std::numeric_limits<float>::max(),std::numeric_limits<float>::max()};
//Find the closest monster target. Provide a "Generic" target in case a target that is invulnerable is the only target that is available (and alive).
std::optional<vf2d>closestPoint;
std::optional<vf2d>closestGenericPoint; //Even if the monster is invulnerable, it might be worth targeting if a normal target is not found.
for(std::unique_ptr<Monster>&m:MONSTER_LIST){
if(m->IsAlive()&&!m->Invulnerable()){
geom2d::line<float>aimingLine=geom2d::line<float>(GetPos(),m->GetPos());
float distToMonster=aimingLine.length();
float distToClosestPoint=geom2d::line<float>(GetPos(),closestPoint).length();
geom2d::line<float>aimingLine=geom2d::line<float>(GetPos(),m->GetPos());
float distToMonster=aimingLine.length();
float distToClosestPoint=geom2d::line<float>(GetPos(),closestPoint.value_or(MAX)).length();
float distToClosestGenericPoint=geom2d::line<float>(GetPos(),closestGenericPoint.value_or(MAX)).length();
if(!m->InUndamageableState(OnUpperLevel(),GetZ())){
if(distToClosestPoint>distToMonster&&distToMonster<=operator""_Pixels("Player.Auto Aim Detection Distance"_F)){
closestPoint=m->GetPos();
}
}
if(m->IsAlive()&&distToClosestGenericPoint>distToMonster&&distToMonster<=operator""_Pixels("Player.Auto Aim Detection Distance"_F)){
closestGenericPoint=m->GetPos();
}
}
if(closestPoint!=vf2d{std::numeric_limits<float>::max(),std::numeric_limits<float>::max()}){
geom2d::line<float>aimingLine=geom2d::line<float>(GetPos(),closestPoint);
vf2d aimingPoint=aimingLine.rpoint(invert?(-operator""_Pixels("Player.Aiming Cursor Max Distance"_F)):std::min(aimingLine.length()+24.f,float(operator""_Pixels("Player.Aiming Cursor Max Distance"_F))));
return game->GetScreenSize()/2+aimingPoint-GetPos();
std::optional<geom2d::line<float>>aimingLine;
if(closestPoint.has_value()){
aimingLine=geom2d::line<float>(GetPos(),closestPoint.value());
}else
return game->GetScreenSize()/2+vf2d{float(operator""_Pixels("Player.Aiming Cursor Max Distance"_F)),aimingAngle.y}.cart();
if(closestGenericPoint.has_value()){
aimingLine=geom2d::line<float>(GetPos(),closestGenericPoint.value());
}
if(aimingLine.has_value()){
vf2d aimingPoint=aimingLine.value().rpoint(invert?(-operator""_Pixels("Player.Aiming Cursor Max Distance"_F)):std::min(aimingLine.value().length()+24.f,float(operator""_Pixels("Player.Aiming Cursor Max Distance"_F))));
return game->GetScreenSize()/2+aimingPoint-GetPos();
}else return game->GetScreenSize()/2+vf2d{float(operator""_Pixels("Player.Aiming Cursor Max Distance"_F)),aimingAngle.y}.cart();
}
}
}else{

View File

@ -98,6 +98,6 @@ void PulsatingFire::Draw()const{
effectSpr=&ANIMATION_DATA["fire_ring0.png"];
}
const Renderable*img=effectSpr->GetFrame(0).GetSourceImage();
game->view.DrawPartialDecal(pos-effectSpr->GetFrame(0).GetSourceRect().size/2*size,img->Decal(),effectSpr->GetFrame(0).GetSourceRect().pos,effectSpr->GetFrame(0).GetSourceRect().size,size,{255,uint8_t(pulsatingFireValues[i]*256),0,uint8_t((63*(sin("Wizard.Ability 3.FireRingOscillatingFrequency"_F*lifetime+PI*pulsatingFireValues[i]))+63)*(fadeout/original_fadeoutTime))});
game->view.DrawPartialDecal(pos-effectSpr->GetFrame(0).GetSourceRect().size/2*size,img->Decal(),effectSpr->GetFrame(0).GetSourceRect().pos,effectSpr->GetFrame(0).GetSourceRect().size,size,{255,uint8_t(pulsatingFireValues[i]*256),0,uint8_t((63*(sin("Wizard.Ability 3.FireRingOscillatingFrequency"_F*lifetime+PI*pulsatingFireValues[i]))+63)*(fadeout/original_fadeOutTime))});
}
}

View File

@ -96,7 +96,19 @@ void Trapper::InitializeClassAbilities(){
#pragma region Trapper Ability 1 (Mark Target)
Trapper::ability1.action=
[](Player*p,vf2d pos={}){
std::optional<Monster*>nearestMonster{Monster::GetNearestMonster(pos,Trapper::ability1.precastInfo.range,p->OnUpperLevel(),p->GetZ())};
vf2d targetPos{pos};
if(nearestMonster.has_value()){
targetPos=nearestMonster.value()->GetPos();
nearestMonster.value()->ApplyMark("Trapper.Ability 1.Duration"_F,"Trapper.Ability 1.Stack Count"_I);
}
for(int i:std::ranges::iota_view(0,int(util::distance(p->GetPos(),targetPos)/16))){
float drawDist{i*16.f};
float fadeInTime{i*0.05f};
float fadeOutTime{0.5f+i*0.05f};
float effectSize{util::random(0.4f)};
game->AddEffect(std::make_unique<Effect>(geom2d::line<float>(p->GetPos(),targetPos).rpoint(drawDist),0.f,"mark_trail.png",p->OnUpperLevel(),fadeInTime,fadeOutTime,vf2d{effectSize,effectSize},vf2d{},Pixel{255,255,255,uint8_t(util::random_range(60,150))},0.f,0.f,true),true);
}
return true;
};
#pragma endregion

View File

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

View File

@ -59,7 +59,7 @@ Trapper
CancelCast = 0
Duration = 7s
Hit Count = 5
Stack Count = 5
Damage Increase Bonus = 60%
#RGB Values. Color 1 is the circle at full cooldown, Color 2 is the color at empty cooldown.

View File

@ -110,6 +110,7 @@ Images
GFX_Dagger = dagger.png
GFX_Shine = shine.png
GFX_TargetMark = target.png
GFX_MarkTrail = mark_trail.png
GFX_Thief_Sheet = nico-thief.png
GFX_Trapper_Sheet = nico-trapper.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 941 B