The open source repository for the action RPG game in development by Sig Productions titled 'Adventures in Lestoria'! https://forums.lestoria.net
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
AdventuresInLestoria/Adventures in Lestoria/Monster.cpp

544 lines
18 KiB

#pragma region License
/*
License (OLC-3)
~~~~~~~~~~~~~~~
Copyright 2024 Joshua Sigona <sigonasr2@gmail.com>
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions or derivations of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions or derivative works in binary form must reproduce the above
copyright notice. This list of conditions and the following disclaimer must be
reproduced in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may
be used to endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
Portions of this software are copyright © 2023 The FreeType
Project (www.freetype.org). Please see LICENSE_FT.txt for more information.
All rights reserved.
*/
#pragma endregion
#include "Monster.h"
#include "DamageNumber.h"
#include "AdventuresInLestoria.h"
#include "Bullet.h"
#include "BulletTypes.h"
#include "DEFINES.h"
#include "safemap.h"
#include "MonsterStrategyHelpers.h"
#include "util.h"
#include "MonsterAttribute.h"
#include "ItemDrop.h"
#include "SoundEffect.h"
INCLUDE_ANIMATION_DATA
INCLUDE_MONSTER_DATA
INCLUDE_MONSTER_LIST
INCLUDE_DAMAGENUMBER_LIST
INCLUDE_game
INCLUDE_BULLET_LIST
INCLUDE_DATA
INCLUDE_GFX
safemap<std::string,std::function<void(Monster&,float,std::string)>>STRATEGY_DATA;
std::map<std::string,Renderable*>MonsterData::imgs;
Monster::Monster(vf2d pos,MonsterData data,bool upperLevel,bool bossMob):
pos(pos),hp(data.GetHealth()),size(data.GetSizeMult()),targetSize(data.GetSizeMult()),strategy(data.GetAIStrategy()),name(data.GetDisplayName()),upperLevel(upperLevel),isBoss(bossMob),facingDirection(DOWN){
bool firstAnimation=true;
for(std::string&anim:data.GetAnimations()){
animation.AddState(anim,ANIMATION_DATA[anim]);
if(firstAnimation){
animation.ChangeState(internal_animState,anim);
firstAnimation=false;
}
}
stats.A("Health")=data.GetHealth();
stats.A("Attack")=data.GetAttack();
stats.A("Move Spd %")=data.GetMoveSpdMult();
randomFrameOffset=(util::random()%1000)/1000.f;
monsterWalkSoundTimer=util::random(1.f);
}
vf2d&Monster::GetPos(){
return pos;
}
int Monster::GetHealth(){
return hp;
}
int Monster::GetAttack(){
float mod_atk=float(stats.A("Attack"));
mod_atk+=Get("Attack %");
mod_atk+=Get("Attack");
return int(mod_atk);
}
float Monster::GetMoveSpdMult(){
float mod_moveSpd=stats.A("Move Spd %");
for(Buff&b:GetBuffs(SLOWDOWN)){
mod_moveSpd-=stats.A("Move Spd %")*b.intensity;
}
return mod_moveSpd;
}
float Monster::GetSizeMult(){
return size;
}
Animate2D::Frame Monster::GetFrame(){
return animation.GetFrame(internal_animState);
}
void Monster::UpdateAnimation(std::string state){
animation.ChangeState(internal_animState,state);
}
void Monster::PerformJumpAnimation(){
animation.ChangeState(internal_animState,MONSTER_DATA[name].GetJumpAnimation());
}
void Monster::PerformShootAnimation(){
animation.ChangeState(internal_animState,MONSTER_DATA[name].GetShootAnimation());
}
void Monster::PerformIdleAnimation(){
animation.ChangeState(internal_animState,MONSTER_DATA[name].GetIdleAnimation());
}
bool Monster::SetX(float x){
vf2d newPos={x,pos.y};
vi2d tilePos=vi2d(newPos/float(game->GetCurrentMapData().tilewidth))*game->GetCurrentMapData().tilewidth;
geom2d::rect<float>collisionRect=game->GetTileCollision(game->GetCurrentLevel(),newPos,upperLevel);
if(collisionRect.pos==game->NO_COLLISION.pos&&collisionRect.size==game->NO_COLLISION.size){
pos.x=std::clamp(x,game->GetCurrentMapData().tilewidth/2.f*GetSizeMult(),float(game->GetCurrentMapData().width*game->GetCurrentMapData().tilewidth-game->GetCurrentMapData().tilewidth/2.f*GetSizeMult()));
Moved();
return true;
} else {
geom2d::rect<float>collision={collisionRect.pos,collisionRect.size};
collision.pos+=tilePos;
if(!geom2d::overlaps(geom2d::circle<float>(newPos,12*GetSizeMult()),collision)){
pos.x=std::clamp(x,game->GetCurrentMapData().tilewidth/2.f*GetSizeMult(),float(game->GetCurrentMapData().width*game->GetCurrentMapData().tilewidth-game->GetCurrentMapData().tilewidth/2.f*GetSizeMult()));
Moved();
return true;
}
}
return false;
}
bool Monster::SetY(float y){
vf2d newPos={pos.x,y};
vi2d tilePos=vi2d(newPos/float(game->GetCurrentMapData().tilewidth))*game->GetCurrentMapData().tilewidth;
geom2d::rect<float>collisionRect=game->GetTileCollision(game->GetCurrentLevel(),newPos,upperLevel);
if(collisionRect.pos==game->NO_COLLISION.pos&&collisionRect.size==game->NO_COLLISION.size){
pos.y=std::clamp(y,game->GetCurrentMapData().tilewidth/2.f*GetSizeMult(),float(game->GetCurrentMapData().height*game->GetCurrentMapData().tilewidth-game->GetCurrentMapData().tilewidth/2.f*GetSizeMult()));
Moved();
return true;
} else {
geom2d::rect<float>collision={collisionRect.pos,collisionRect.size};
collision.pos+=tilePos;
if(!geom2d::overlaps(geom2d::circle<float>(newPos,game->GetCurrentMapData().tilewidth/2*GetSizeMult()),collision)){
pos.y=std::clamp(y,game->GetCurrentMapData().tilewidth/2.f*GetSizeMult(),float(game->GetCurrentMapData().height*game->GetCurrentMapData().tilewidth-game->GetCurrentMapData().tilewidth/2.f*GetSizeMult()));
Moved();
return true;
}
}
return false;
}
bool Monster::Update(float fElapsedTime){
lastHitTimer=std::max(0.f,lastHitTimer-fElapsedTime);
iframe_timer=std::max(0.f,iframe_timer-fElapsedTime);
monsterHurtSoundCooldown=std::max(0.f,monsterHurtSoundCooldown-fElapsedTime);
if(size!=targetSize){
if(size>targetSize){
size=std::max(targetSize,size-AiL::SIZE_CHANGE_SPEED*fElapsedTime);
}else{
size=std::min(targetSize,size+AiL::SIZE_CHANGE_SPEED*fElapsedTime);
}
}
if(IsAlive()){
for(std::vector<Buff>::iterator it=buffList.begin();it!=buffList.end();++it){
Buff&b=*it;
b.duration-=fElapsedTime;
if(b.duration<=0){
it=buffList.erase(it);
if(it==buffList.end())break;
}
}
if(!HasIframes()){
for(Monster&m:MONSTER_LIST){
if(&m==this)continue;
if(!m.HasIframes()&&OnUpperLevel()==m.OnUpperLevel()&&abs(m.GetZ()-GetZ())<=1&&geom2d::overlaps(geom2d::circle(pos,12*size/2),geom2d::circle(m.GetPos(),12*m.GetSizeMult()/2))){
m.Collision(*this);
geom2d::line line(pos,m.GetPos());
float dist = line.length();
m.SetPos(line.rpoint(dist*1.1f));
if(m.IsAlive()){
vel=line.vector().norm()*-128;
}
}
}
if(!game->GetPlayer()->HasIframes()&&abs(game->GetPlayer()->GetZ()-GetZ())<=1&&game->GetPlayer()->OnUpperLevel()==OnUpperLevel()&&geom2d::overlaps(geom2d::circle(pos,12*size/2),geom2d::circle(game->GetPlayer()->GetPos(),12*game->GetPlayer()->GetSizeMult()/2))){
geom2d::line line(pos,game->GetPlayer()->GetPos());
float dist = line.length();
SetPos(line.rpoint(-0.1f));
vel=line.vector().norm()*-128;
}
}
if(GetState()==State::NORMAL){
if(game->GetPlayer()->GetX()>pos.x){
facingDirection=RIGHT;
} else {
facingDirection=LEFT;
}
}
Monster::STRATEGY::RUN_STRATEGY(*this,fElapsedTime);
if(vel.x>0){
vel.x=std::max(0.f,vel.x-friction*fElapsedTime);
} else {
vel.x=std::min(0.f,vel.x+friction*fElapsedTime);
}
if(vel.y>0){
vel.y=std::max(0.f,vel.y-friction*fElapsedTime);
} else {
vel.y=std::min(0.f,vel.y+friction*fElapsedTime);
}
if(vel!=vf2d{0,0}){
SetX(pos.x+vel.x*fElapsedTime);
SetY(pos.y+vel.y*fElapsedTime);
}
}
if(!IsAlive()){
deathTimer+=fElapsedTime;
if(deathTimer>3){
return false;
}
}
animation.UpdateState(internal_animState,randomFrameOffset+fElapsedTime);
randomFrameOffset=0;
return true;
}
Key Monster::GetFacingDirection(){
return facingDirection;
}
void Monster::Draw(){
if(GetZ()>0){
vf2d shadowScale=vf2d{8*GetSizeMult()/3.f,1}/std::max(1.f,GetZ()/24);
game->view.DrawDecal(GetPos()-vf2d{3,3}*shadowScale/2+vf2d{0,6*GetSizeMult()},GFX["circle.png"].Decal(),shadowScale,BLACK);
}
game->view.DrawPartialRotatedDecal(GetPos()-vf2d{0,GetZ()},GetFrame().GetSourceImage()->Decal(),0,GetFrame().GetSourceRect().size/2,GetFrame().GetSourceRect().pos,GetFrame().GetSourceRect().size,vf2d(GetSizeMult()*(GetFacingDirection()==RIGHT?-1:1),GetSizeMult()),GetBuffs(BuffType::SLOWDOWN).size()>0?Pixel{uint8_t(255*abs(sin(1.4*GetBuffs(BuffType::SLOWDOWN)[0].duration))),uint8_t(255*abs(sin(1.4*GetBuffs(BuffType::SLOWDOWN)[0].duration))),uint8_t(128+127*abs(sin(1.4*GetBuffs(BuffType::SLOWDOWN)[0].duration)))}:WHITE);
for(float index=0.f;index<path.points.size();index+=0.01f){
Pixel col=DARK_GREY;
if(index<pathIndex){
col=VERY_DARK_GREY;
}
if(index>pathIndex+1){
col=GREY;
}
game->view.FillRectDecal(path.GetSplinePoint(index).pos,{1,1},col);
}
for(size_t counter=0;Pathfinding::sPoint2D&point:path.points){
Pixel col=CYAN;
if(counter<pathIndex){
col=RED;
}
if(counter>pathIndex+1){
col=YELLOW;
}
game->view.FillRectDecal(point.pos,{3,3},col);
counter++;
}
}
void Monster::DrawReflection(float drawRatioX,float multiplierX){
game->SetDecalMode(DecalMode::ADDITIVE);
game->view.DrawPartialRotatedDecal(GetPos()+vf2d{drawRatioX*GetFrame().GetSourceRect().size.x,GetZ()+(GetFrame().GetSourceRect().size.y-16)*GetSizeMult()},GetFrame().GetSourceImage()->Decal(),0,GetFrame().GetSourceRect().size/2,GetFrame().GetSourceRect().pos,GetFrame().GetSourceRect().size,vf2d(GetSizeMult()*(GetFacingDirection()==RIGHT?-1:1)*multiplierX,GetSizeMult()*-1),GetBuffs(BuffType::SLOWDOWN).size()>0?Pixel{uint8_t(255*abs(sin(1.4*GetBuffs(BuffType::SLOWDOWN)[0].duration))),uint8_t(255*abs(sin(1.4*GetBuffs(BuffType::SLOWDOWN)[0].duration))),uint8_t(128+127*abs(sin(1.4*GetBuffs(BuffType::SLOWDOWN)[0].duration)))}:WHITE);
game->SetDecalMode(DecalMode::NORMAL);
}
void Monster::Collision(Player*p){
if(MONSTER_DATA[name].GetCollisionDmg()>0&&!hasHitPlayer){
if(p->Hurt(MONSTER_DATA[name].GetCollisionDmg(),OnUpperLevel(),GetZ())){
hasHitPlayer=true;
}
}
Collision();
}
void Monster::Collision(Monster&m){
Collision();
}
void Monster::Collision(){
if(strategy=="Run Towards"&&GetState()==State::MOVE_TOWARDS&&util::random(float(Monster::STRATEGY::_GetInt(*this,"BumpStopChance",strategy)))<1){//The run towards strategy causes state to return to normal upon a collision.
SetState(State::NORMAL);
targetAcquireTimer=0;
}
}
void Monster::SetVelocity(vf2d vel){
this->vel=vel;
}
bool Monster::SetPos(vf2d pos){
bool resultX=SetX(pos.x);
bool resultY=SetY(pos.y);
if(resultY&&!resultX){
resultX=SetX(pos.x);
}
return resultX||resultY;
}
void Monster::Moved(){
std::map<std::string,std::vector<ZoneData>>&zoneData=game->GetZoneData(game->GetCurrentLevel());
for(ZoneData&upperLevelZone:zoneData["UpperZone"]){
if(geom2d::overlaps(upperLevelZone.zone,pos)){
upperLevel=true;
}
}
for(ZoneData&lowerLevelZone:zoneData["LowerZone"]){
if(geom2d::overlaps(lowerLevelZone.zone,pos)){
upperLevel=false;
}
}
monsterWalkSoundTimer+=game->GetElapsedTime();
if(monsterWalkSoundTimer>1.f){
monsterWalkSoundTimer-=1.f;
SoundEffect::PlaySFX(GetWalkSound(),GetPos());
}
}
std::string Monster::GetDeathAnimationName(){
return MONSTER_DATA[name].GetDeathAnimation();
}
bool Monster::Hurt(int damage,bool onUpperLevel,float z){
if(!IsAlive()||onUpperLevel!=OnUpperLevel()||HasIframes()||abs(GetZ()-z)>1) return false;
if(game->InBossEncounter()){
game->StartBossEncounter();
}
game->GetPlayer()->ResetLastCombatTime();
float mod_dmg=float(damage);
#pragma region Handle Crits
bool crit=false;
if(util::random(1)<game->GetPlayer()->GetCritRatePct()){
mod_dmg*=1+game->GetPlayer()->GetCritDmgPct();
crit=true;
}
#pragma endregion
for(Buff&b:GetBuffs(BuffType::DAMAGE_REDUCTION)){
mod_dmg-=damage*b.intensity;
}
mod_dmg=std::ceil(mod_dmg);
hp=std::max(0,hp-int(mod_dmg));
if(lastHitTimer>0){
damageNumberPtr.get()->damage+=int(mod_dmg);
damageNumberPtr.get()->pauseTime=0.4f;
} else {
damageNumberPtr=std::make_shared<DamageNumber>(pos,int(mod_dmg));
DAMAGENUMBER_LIST.push_back(damageNumberPtr);
}
#pragma region Change Label to Crit
if(crit){
damageNumberPtr.get()->type=CRIT;
}
#pragma endregion
lastHitTimer=0.05f;
if(!IsAlive()){
OnDeath();
SoundEffect::PlaySFX(GetDeathSound(),GetPos());
}else{
hp=std::max(1,hp); //Make sure it stays alive if it's supposed to be alive...
if(monsterHurtSoundCooldown==0.f){
monsterHurtSoundCooldown=util::random(0.5f)+0.5f;
SoundEffect::PlaySFX(GetHurtSound(),GetPos());
}
}
if(game->InBossEncounter()){
game->BossDamageDealt(int(mod_dmg));
}
GetInt(Attribute::HITS_UNTIL_DEATH)=std::max(0,GetInt(Attribute::HITS_UNTIL_DEATH)-1);
iframe_timer=GetFloat(Attribute::IFRAME_TIME_UPON_HIT);
return true;
}
bool Monster::IsAlive(){
return hp>0||!diesNormally;
}
vf2d&Monster::GetTargetPos(){
return target;
}
MonsterSpawner::MonsterSpawner(){}
MonsterSpawner::MonsterSpawner(vf2d pos,vf2d range,std::vector<std::pair<std::string,vf2d>>monsters,bool upperLevel,std::string bossNameDisplay)
:pos(pos),range(range),monsters(monsters),upperLevel(upperLevel),bossNameDisplay(bossNameDisplay){
}
bool MonsterSpawner::SpawnTriggered(){
return triggered;
}
vf2d MonsterSpawner::GetRange(){
return range;
}
vf2d MonsterSpawner::GetPos(){
return pos;
}
void MonsterSpawner::SetTriggered(bool trigger,bool spawnMonsters){
triggered=trigger;
if(spawnMonsters){
for(std::pair<std::string,vf2d>&monsterInfo:monsters){
game->SpawnMonster(pos+monsterInfo.second,MONSTER_DATA[monsterInfo.first],DoesUpperLevelSpawning(),bossNameDisplay!="");
}
if(bossNameDisplay!=""){
game->SetBossNameDisplay(bossNameDisplay);
}
}
}
bool MonsterSpawner::DoesUpperLevelSpawning(){
return upperLevel;
}
bool Monster::OnUpperLevel(){
return upperLevel;
}
void Monster::AddBuff(BuffType type,float duration,float intensity){
buffList.push_back(Buff{type,duration,intensity});
}
void Monster::StartPathfinding(float pathingTime){
SetState(State::PATH_AROUND);
path=game->pathfinder.Solve_WalkPath(pos,target,12,OnUpperLevel());
if(path.points.size()>0){
pathIndex=0.f;
//We gives this mob 5 seconds to figure out a path to the target.
targetAcquireTimer=pathingTime;
}
}
void Monster::PathAroundBehavior(float fElapsedTime){
if(path.points.size()>0){
//Move towards the new path.
geom2d::line moveTowardsLine=geom2d::line(pos,path.GetSplinePoint(pathIndex).pos);
if(moveTowardsLine.length()>2){
SetPos(pos+moveTowardsLine.vector().norm()*100*fElapsedTime*GetMoveSpdMult());
if(moveTowardsLine.vector().x>0){
facingDirection=RIGHT;
} else {
facingDirection=LEFT;
}
}else{
if(pathIndex>=path.points.size()){
//We have reached the end of the path!
pathIndex=0;
targetAcquireTimer=0;
}else{
pathIndex+=0.5f;
}
}
} else {
//We actually can't do anything so just quit.
targetAcquireTimer=0;
}
}
std::vector<Buff>Monster::GetBuffs(BuffType buff){
std::vector<Buff>filteredBuffs;
std::copy_if(buffList.begin(),buffList.end(),std::back_inserter(filteredBuffs),[buff](Buff&b){return b.type==buff;});
return filteredBuffs;
}
State::State Monster::GetState(){
return state;
}
void Monster::SetState(State::State newState){
state=newState;
}
void Monster::InitializeStrategies(){
STRATEGY_DATA.insert("Run Towards",Monster::STRATEGY::RUN_TOWARDS);
STRATEGY_DATA.insert("Shoot Afar",Monster::STRATEGY::SHOOT_AFAR);
STRATEGY_DATA.insert("Turret",Monster::STRATEGY::TURRET);
STRATEGY_DATA.insert("Slime King",Monster::STRATEGY::SLIMEKING);
STRATEGY_DATA.insert("Run Away",Monster::STRATEGY::RUN_AWAY);
STRATEGY_DATA.SetInitialized();
}
bool Monster::HasIframes(){
return iframe_timer>0;
}
float Monster::GetZ(){
return z;
}
const std::function<void(Monster&,float,std::string)>&Monster::GetStrategy()const{
return STRATEGY_DATA[strategy];
}
void Monster::SetSize(float newSize,bool immediate){
if(immediate){
size=targetSize=newSize;
}else{
targetSize=newSize;
}
}
void Monster::SetZ(float z){
this->z=z;
}
void Monster::SetStrategyDrawFunction(std::function<void(AiL*)>func){
strategyDraw=func;
}
void Monster::OnDeath(){
animation.ChangeState(internal_animState,GetDeathAnimationName());
if(isBoss){
game->ReduceBossEncounterMobCount();
}
for(MonsterDropData data:MONSTER_DATA.at(name).GetDropData()){
if(util::random(100)<=data.dropChance){
//This isn't necessarily fair odds for each quantity dropped.
int dropQuantity=int(data.minQty+std::round(util::random(float(data.maxQty-data.minQty))));
for(int i=0;i<dropQuantity;i++){
ItemDrop::SpawnItem(data.item,GetPos(),OnUpperLevel());
}
}
}
game->GetPlayer()->AddAccumulatedXP(MONSTER_DATA.at(name).GetXP());
}
const ItemAttributable&Monster::GetStats()const{
return stats;
}
ItemAttribute&Monster::Get(std::string_view attr){
return ItemAttribute::Get(attr,this);
}
const uint32_t MonsterData::GetXP()const{
return xp;
}
const EventName&Monster::GetHurtSound(){
return MONSTER_DATA[name].GetHurtSound();
}
const EventName&Monster::GetDeathSound(){
return MONSTER_DATA[name].GetDeathSound();
}
const EventName&Monster::GetWalkSound(){
return MONSTER_DATA[name].GetWalkSound();
}