Merge pull request 'GhostOfPirateCaptain' (#95) from GhostOfPirateCaptain into master
All checks were successful
Emscripten Build / Build_and_Deploy_Web_Build (push) Successful in 7m16s

Reviewed-on: #95
This commit is contained in:
sigonasr2 2026-01-28 15:53:21 -06:00
commit 0230bb6b22
10 changed files with 161 additions and 21 deletions

View File

@ -47,6 +47,7 @@ using A=Attribute;
INCLUDE_game
INCLUDE_MONSTER_LIST
INCLUDE_BULLET_LIST
void Monster::STRATEGY::GHOST_OF_PIRATE_CAPTAIN(Monster&m,float fElapsedTime,std::string strategy){
enum PhaseName{
@ -56,6 +57,7 @@ void Monster::STRATEGY::GHOST_OF_PIRATE_CAPTAIN(Monster&m,float fElapsedTime,std
GHOSTSABER_SLASH=999,
TOSS_COIN,
HIDING,
FINAL, //The final phase of the fight.
};
enum CannonShotType{
@ -127,6 +129,7 @@ void Monster::STRATEGY::GHOST_OF_PIRATE_CAPTAIN(Monster&m,float fElapsedTime,std
case NORMAL:{
m.F(A::CANNON_TIMER)+=fElapsedTime;
m.F(A::SHRAPNEL_CANNON_TIMER)+=fElapsedTime;
m.SetStrategyOnHitFunction({});
const int phase{std::any_cast<int>(m.VEC(A::CANNON_PHASES)[m.I(A::CANNON_PHASE)])};
switch(phase){
case CANNON_SHOT:{//Normal Cannon Shot. Takes on one of five varieties.
@ -154,10 +157,8 @@ void Monster::STRATEGY::GHOST_OF_PIRATE_CAPTAIN(Monster&m,float fElapsedTime,std
if(m.I(A::CANNON_SHOT_COUNT)%2==0)CreateBullet(FallingBullet)("cannonball.png",game->GetPlayer()->GetPos(),ConfigVec("Cannon Vel"),ConfigFloatArr("Cannon Vel",2),ConfigFloat("Indicator Time"),ConfigPixels("Cannon Radius"),ConfigInt("Cannon Damage"),m.OnUpperLevel(),false,ConfigFloat("Cannon Knockback Amt"),ConfigFloat("Cannon Shot Impact Time"),false,ConfigPixel("Cannon Spell Circle Color"),vf2d{ConfigFloat("Cannon Radius")/100.f*1.75f,ConfigFloat("Cannon Radius")/100.f*1.75f},util::random(2*PI),util::random(2*PI),util::degToRad(ConfigFloat("Cannon Spell Circle Rotation Spd")),ConfigPixel("Cannon Spell Insignia Color"),util::random(2*PI),util::degToRad(ConfigFloat("Cannon Spell Insignia Rotation Spd")))EndBullet;
}break;
case PREDICTION:{
LOG(std::format("Previous Pos: {} Current: {}",game->GetPlayer()->GetPreviousPos().str(),game->GetPlayer()->GetPos().str()));
const float angle{util::angleTo(game->GetPlayer()->GetPreviousPos(),game->GetPlayer()->GetPos())};
const float range{util::random_range(0,100.f*game->GetPlayer()->GetMoveSpdMult())*ConfigFloat("Cannon Shot Impact Time")};
LOG(std::format("Range/Angle: {}",vf2d{range,angle}.str()));
const vf2d targetPos{game->GetPlayer()->GetPos()+vf2d{range,angle}.cart()};
CreateBullet(FallingBullet)("cannonball.png",targetPos,ConfigVec("Cannon Vel"),ConfigFloatArr("Cannon Vel",2),ConfigFloat("Indicator Time"),ConfigPixels("Cannon Radius"),ConfigInt("Cannon Damage"),m.OnUpperLevel(),false,ConfigFloat("Cannon Knockback Amt"),ConfigFloat("Cannon Shot Impact Time"),false,ConfigPixel("Cannon Spell Circle Color"),vf2d{ConfigFloat("Cannon Radius")/100.f*1.75f,ConfigFloat("Cannon Radius")/100.f*1.75f},util::random(2*PI),util::random(2*PI),util::degToRad(ConfigFloat("Cannon Spell Circle Rotation Spd")),ConfigPixel("Cannon Spell Insignia Color"),util::random(2*PI),util::degToRad(ConfigFloat("Cannon Spell Insignia Rotation Spd")))EndBullet;
}break;
@ -183,7 +184,8 @@ void Monster::STRATEGY::GHOST_OF_PIRATE_CAPTAIN(Monster&m,float fElapsedTime,std
m.I(A::CURSE_THRESHOLD_ARRAY_IND)++;
m.F(A::TOSS_COIN_WAIT_TIMER)=ConfigFloat("Coin Toss Pause Time");
m.V(A::TOSS_COIN_TARGET)=game->GetPlayer()->GetPos();
game->AddEffect(std::make_unique<FlipCoinEffect>(Oscillator<vf2d>{m.GetPos(),m.V(A::TOSS_COIN_TARGET),1.f/m.F(A::TOSS_COIN_WAIT_TIMER)/2.f},ConfigFloat("Coin Toss Rise Amount"),m.F(A::TOSS_COIN_WAIT_TIMER),"coin.png",m.OnUpperLevel(),3.f));
const bool OnLastCursePhase{Config("Curse Thresholds").GetValueCount()==m.I(A::CURSE_THRESHOLD_ARRAY_IND)};
if(!OnLastCursePhase)game->AddEffect(std::make_unique<FlipCoinEffect>(Oscillator<vf2d>{m.GetPos(),m.V(A::TOSS_COIN_TARGET),1.f/m.F(A::TOSS_COIN_WAIT_TIMER)/2.f},ConfigFloat("Coin Toss Rise Amount"),m.F(A::TOSS_COIN_WAIT_TIMER),"coin.png",m.OnUpperLevel(),3.f));
#pragma region Determine a hiding spot
const auto&hidingSpots{game->GetZones().at("Hiding Spot")};
@ -234,19 +236,36 @@ void Monster::STRATEGY::GHOST_OF_PIRATE_CAPTAIN(Monster&m,float fElapsedTime,std
m.F(A::TOSS_COIN_WAIT_TIMER)-=fElapsedTime;
if(m.F(A::TOSS_COIN_WAIT_TIMER)<=0.f){
const float curseDmgPctOverTime{ConfigFloat("Curse Damage")};
game->GetPlayer()->AddBuff(BuffType::PIRATE_GHOST_CAPTAIN_PRECURSE,ConfigFloat("Curse Damage Wait Time"),ceil(game->GetPlayer()->GetMaxHealth()*ConfigFloat("Curse Damage")/100.f)+1,[curseDmgPctOverTime](Player*attachedTarget,Buff&b){
attachedTarget->AddBuff(BuffType::PIRATE_GHOST_CAPTAIN_CURSE_DOT,BuffRestorationType::OVER_TIME,BuffOverTimeType::HP_PCT_DAMAGE_OVER_TIME,INFINITY,curseDmgPctOverTime,1.f);
});
game->SpawnMonster(m.V(A::TOSS_COIN_TARGET),MONSTER_DATA["Pirate's Coin"],m.OnUpperLevel());
const bool OnLastCursePhase{Config("Curse Thresholds").GetValueCount()==m.I(A::CURSE_THRESHOLD_ARRAY_IND)};
if(OnLastCursePhase){ //Permanent Curse for the rest of the fight.
game->GetPlayer()->AddBuff(BuffType::PIRATE_GHOST_CAPTAIN_CURSE_DOT,BuffRestorationType::OVER_TIME,BuffOverTimeType::HP_PCT_DAMAGE_OVER_TIME,INFINITY,curseDmgPctOverTime,1.f);
}else{
game->GetPlayer()->AddBuff(BuffType::PIRATE_GHOST_CAPTAIN_PRECURSE,ConfigFloat("Curse Damage Wait Time"),ceil(game->GetPlayer()->GetMaxHealth()*ConfigFloat("Curse Damage")/100.f)+1,[curseDmgPctOverTime](Player*attachedTarget,Buff&b){
attachedTarget->AddBuff(BuffType::PIRATE_GHOST_CAPTAIN_CURSE_DOT,BuffRestorationType::OVER_TIME,BuffOverTimeType::HP_PCT_DAMAGE_OVER_TIME,INFINITY,curseDmgPctOverTime,1.f);
});
game->SpawnMonster(m.V(A::TOSS_COIN_TARGET),MONSTER_DATA["Pirate's Coin"],m.OnUpperLevel());
}
m.SetupAfterImage();
m.afterImagePos=m.GetPos();
m.SetPos(m.V(A::HIDING_POS));
m.SetVelocity({});
m.arrowIndicator=false; //While the boss is hiding, the indicator will not show up.
m.SetStrategyOnHitFunction([&m](const HurtDamageInfo damageData,Monster&monster,const StrategyName&strategyName)->void{
m.SetPhase(strategyName,NORMAL);
m.arrowIndicator=true;
m.SetStrategyOnHitFunction({});
m.SetStrategyOnHitFunction([OnLastCursePhase,&m,strategy](const HurtDamageInfo damageData,Monster&monster,const StrategyName&strategyName)->void{
monster.SetPhase(strategyName,OnLastCursePhase?FINAL:NORMAL);
monster.arrowIndicator=true;
if(OnLastCursePhase){
const std::string_view PIRATES_TREASURE{"Pirate's Treasure"};
for(std::shared_ptr<Monster>&m:MONSTER_LIST
|std::views::filter([&PIRATES_TREASURE](std::shared_ptr<Monster>&m){return m->GetStrategyName()==PIRATES_TREASURE;})){
m->SetPhase("Pirate's Treasure",1); //HARDCODED to the LOCKED phase for the Pirate's Treasure
}
for(int i:std::ranges::iota_view(0,ConfigInt("Final Ghost Saber Count"))){
const float playerToMonsterAngle{util::pointTo(game->GetPlayer()->GetPos(),m.GetPos()).polar().y};
const float rotationMult=((i%2==0)?1.f:-1.f);
if(i>=ConfigInt("Final Ghost Saber Count")/2)CreateBullet(GhostSaber)(m.GetPos(),m.GetWeakPointer(),INFINITE,ConfigFloat("Ghost Saber Distance")+i*ConfigFloat("Final Ghost Saber Separation Amount"),ConfigFloat("Ghost Saber Knockback Amt"),playerToMonsterAngle,ConfigFloat("Ghost Saber Radius"),0.f,ConfigInt("Ghost Saber Damage"),m.OnUpperLevel(),util::degToRad(ConfigFloat("Ghost Saber Rotation Spd")*-rotationMult/sqrtf(i+util::random_range(1.f,i))))EndBullet;
CreateBullet(GhostSaber)(m.GetPos(),m.GetWeakPointer(),INFINITE,ConfigFloat("Ghost Saber Distance")+i*ConfigFloat("Final Ghost Saber Separation Amount"),ConfigFloat("Ghost Saber Knockback Amt"),playerToMonsterAngle,ConfigFloat("Ghost Saber Radius"),0.f,ConfigInt("Ghost Saber Damage"),m.OnUpperLevel(),util::degToRad(ConfigFloat("Final Ghost Saber Rotation Spd")*rotationMult/sqrtf(i)))EndBullet;
}
}
});
SETPHASE(HIDING);
}
@ -277,10 +296,8 @@ void Monster::STRATEGY::GHOST_OF_PIRATE_CAPTAIN(Monster&m,float fElapsedTime,std
if(m.I(A::CANNON_SHOT_COUNT)%2==0)CreateBullet(FallingBullet)("cannonball.png",game->GetPlayer()->GetPos(),ConfigVec("Cannon Vel"),ConfigFloatArr("Cannon Vel",2),ConfigFloat("Indicator Time"),ConfigPixels("Cannon Radius"),ConfigInt("Cannon Damage"),m.OnUpperLevel(),false,ConfigFloat("Cannon Knockback Amt"),ConfigFloat("Cannon Shot Impact Time"),false,ConfigPixel("Cannon Spell Circle Color"),vf2d{ConfigFloat("Cannon Radius")/100.f*1.75f,ConfigFloat("Cannon Radius")/100.f*1.75f},util::random(2*PI),util::random(2*PI),util::degToRad(ConfigFloat("Cannon Spell Circle Rotation Spd")),ConfigPixel("Cannon Spell Insignia Color"),util::random(2*PI),util::degToRad(ConfigFloat("Cannon Spell Insignia Rotation Spd")))EndBullet;
}break;
case PREDICTION:{
LOG(std::format("Previous Pos: {} Current: {}",game->GetPlayer()->GetPreviousPos().str(),game->GetPlayer()->GetPos().str()));
const float angle{util::angleTo(game->GetPlayer()->GetPreviousPos(),game->GetPlayer()->GetPos())};
const float range{util::random_range(0,100.f*game->GetPlayer()->GetMoveSpdMult())*ConfigFloat("Cannon Shot Impact Time")};
LOG(std::format("Range/Angle: {}",vf2d{range,angle}.str()));
const vf2d targetPos{game->GetPlayer()->GetPos()+vf2d{range,angle}.cart()};
CreateBullet(FallingBullet)("cannonball.png",targetPos,ConfigVec("Cannon Vel"),ConfigFloatArr("Cannon Vel",2),ConfigFloat("Indicator Time"),ConfigPixels("Cannon Radius"),ConfigInt("Cannon Damage"),m.OnUpperLevel(),false,ConfigFloat("Cannon Knockback Amt"),ConfigFloat("Cannon Shot Impact Time"),false,ConfigPixel("Cannon Spell Circle Color"),vf2d{ConfigFloat("Cannon Radius")/100.f*1.75f,ConfigFloat("Cannon Radius")/100.f*1.75f},util::random(2*PI),util::random(2*PI),util::degToRad(ConfigFloat("Cannon Spell Circle Rotation Spd")),ConfigPixel("Cannon Spell Insignia Color"),util::random(2*PI),util::degToRad(ConfigFloat("Cannon Spell Insignia Rotation Spd")))EndBullet;
}break;
@ -294,5 +311,62 @@ void Monster::STRATEGY::GHOST_OF_PIRATE_CAPTAIN(Monster&m,float fElapsedTime,std
m.F(A::SHRAPNEL_CANNON_TIMER)=0.f;
}
}break;
case FINAL:{
RUN_TOWARDS(m,fElapsedTime,"Run Towards");
m.target=ConfigVec("Final Standing Spot");
m.PerformAnimation("SLASH",Direction::SOUTH);
m.SetStrategyOnHitFunction({});
m.SetStrategyDeathFunction([](GameEvent&event,Monster&m,const std::string&strategyName){
for(std::unique_ptr<IBullet>&bullet:BULLET_LIST|std::views::filter([](std::unique_ptr<IBullet>&bullet){return !bullet->friendly;}))bullet->lifetime=0.f;
const std::string_view PIRATES_TREASURE{"Pirate's Treasure"};
for(std::shared_ptr<Monster>&treasure:MONSTER_LIST
|std::views::filter([&PIRATES_TREASURE](std::shared_ptr<Monster>&m){return m->GetStrategyName()==PIRATES_TREASURE;})){
m.target=treasure->GetPos();
}
m.B(A::IGNORE_DEFAULT_ANIMATIONS)=true;
RUN_TOWARDS(m,game->GetElapsedTime(),"Run Towards");
m.PerformAnimation("DEATH");
return true;
});
m.F(A::CANNON_TIMER)+=fElapsedTime;
if(m.F(A::CANNON_TIMER)>=ConfigFloat("Final Cannon Shot Delay")){
switch(m.I(A::CANNON_SHOT_TYPE)){
case BOMBARDMENT:{
const float randomAng{util::random_range(0,2*PI)};
const float range{util::random_range(0,ConfigPixels("Bombardment Max Distance"))};
const vf2d targetPos{game->GetPlayer()->GetPos()+vf2d{range,randomAng}.cart()};
CreateBullet(FallingBullet)("cannonball.png",targetPos,ConfigVec("Cannon Vel"),ConfigFloatArr("Cannon Vel",2),ConfigFloat("Indicator Time"),ConfigPixels("Cannon Radius"),ConfigInt("Cannon Damage"),m.OnUpperLevel(),false,ConfigFloat("Cannon Knockback Amt"),ConfigFloat("Cannon Shot Impact Time"),false,ConfigPixel("Cannon Spell Circle Color"),vf2d{ConfigFloat("Cannon Radius")/100.f*1.75f,ConfigFloat("Cannon Radius")/100.f*1.75f},util::random(2*PI),util::random(2*PI),util::degToRad(ConfigFloat("Cannon Spell Circle Rotation Spd")),ConfigPixel("Cannon Spell Insignia Color"),util::random(2*PI),util::degToRad(ConfigFloat("Cannon Spell Insignia Rotation Spd")))EndBullet;
}break;
case PRECISE_BOMBARDMENT:{
const float randomAng{util::random_range(0,2*PI)};
const float range{util::random_range(0,ConfigPixels("Precise Bombardment Max Distance"))};
const vf2d targetPos{game->GetPlayer()->GetPos()+vf2d{range,randomAng}.cart()};
CreateBullet(FallingBullet)("cannonball.png",targetPos,ConfigVec("Cannon Vel"),ConfigFloatArr("Cannon Vel",2),ConfigFloat("Indicator Time"),ConfigPixels("Cannon Radius"),ConfigInt("Cannon Damage"),m.OnUpperLevel(),false,ConfigFloat("Cannon Knockback Amt"),ConfigFloat("Cannon Shot Impact Time"),false,ConfigPixel("Cannon Spell Circle Color"),vf2d{ConfigFloat("Cannon Radius")/100.f*1.75f,ConfigFloat("Cannon Radius")/100.f*1.75f},util::random(2*PI),util::random(2*PI),util::degToRad(ConfigFloat("Cannon Spell Circle Rotation Spd")),ConfigPixel("Cannon Spell Insignia Color"),util::random(2*PI),util::degToRad(ConfigFloat("Cannon Spell Insignia Rotation Spd")))EndBullet;
}break;
case LINE:{
//Draw a line from one side of the screen to the other, drawing through the middle.
if(m.I(A::CANNON_SHOT_COUNT)==0)m.F(A::LINE_SHOT_ANG)=util::random_range(0,2*PI);
const vf2d targetPos{geom2d::line{game->GetPlayer()->GetPos()+vf2d{float(game->ScreenHeight()),m.F(A::LINE_SHOT_ANG)}.cart(),game->GetPlayer()->GetPos()+vf2d{float(game->ScreenHeight()),m.F(A::LINE_SHOT_ANG)+PI}.cart()}.upoint(float(m.I(A::CANNON_SHOT_COUNT))/TOTAL_CANNON_SHOTS)};
CreateBullet(FallingBullet)("cannonball.png",targetPos,ConfigVec("Cannon Vel"),ConfigFloatArr("Cannon Vel",2),ConfigFloat("Indicator Time"),ConfigPixels("Cannon Radius"),ConfigInt("Cannon Damage"),m.OnUpperLevel(),false,ConfigFloat("Cannon Knockback Amt"),ConfigFloat("Cannon Shot Impact Time"),false,ConfigPixel("Cannon Spell Circle Color"),vf2d{ConfigFloat("Cannon Radius")/100.f*1.75f,ConfigFloat("Cannon Radius")/100.f*1.75f},util::random(2*PI),util::random(2*PI),util::degToRad(ConfigFloat("Cannon Spell Circle Rotation Spd")),ConfigPixel("Cannon Spell Insignia Color"),util::random(2*PI),util::degToRad(ConfigFloat("Cannon Spell Insignia Rotation Spd")))EndBullet;
}break;
case SHARPSHOOTER:{
if(m.I(A::CANNON_SHOT_COUNT)%2==0)CreateBullet(FallingBullet)("cannonball.png",game->GetPlayer()->GetPos(),ConfigVec("Cannon Vel"),ConfigFloatArr("Cannon Vel",2),ConfigFloat("Indicator Time"),ConfigPixels("Cannon Radius"),ConfigInt("Cannon Damage"),m.OnUpperLevel(),false,ConfigFloat("Cannon Knockback Amt"),ConfigFloat("Cannon Shot Impact Time"),false,ConfigPixel("Cannon Spell Circle Color"),vf2d{ConfigFloat("Cannon Radius")/100.f*1.75f,ConfigFloat("Cannon Radius")/100.f*1.75f},util::random(2*PI),util::random(2*PI),util::degToRad(ConfigFloat("Cannon Spell Circle Rotation Spd")),ConfigPixel("Cannon Spell Insignia Color"),util::random(2*PI),util::degToRad(ConfigFloat("Cannon Spell Insignia Rotation Spd")))EndBullet;
}break;
case PREDICTION:{
const float angle{util::angleTo(game->GetPlayer()->GetPreviousPos(),game->GetPlayer()->GetPos())};
const float range{util::random_range(0,100.f*game->GetPlayer()->GetMoveSpdMult())*ConfigFloat("Cannon Shot Impact Time")};
const vf2d targetPos{game->GetPlayer()->GetPos()+vf2d{range,angle}.cart()};
CreateBullet(FallingBullet)("cannonball.png",targetPos,ConfigVec("Cannon Vel"),ConfigFloatArr("Cannon Vel",2),ConfigFloat("Indicator Time"),ConfigPixels("Cannon Radius"),ConfigInt("Cannon Damage"),m.OnUpperLevel(),false,ConfigFloat("Cannon Knockback Amt"),ConfigFloat("Cannon Shot Impact Time"),false,ConfigPixel("Cannon Spell Circle Color"),vf2d{ConfigFloat("Cannon Radius")/100.f*1.75f,ConfigFloat("Cannon Radius")/100.f*1.75f},util::random(2*PI),util::random(2*PI),util::degToRad(ConfigFloat("Cannon Spell Circle Rotation Spd")),ConfigPixel("Cannon Spell Insignia Color"),util::random(2*PI),util::degToRad(ConfigFloat("Cannon Spell Insignia Rotation Spd")))EndBullet;
}break;
}
AdvanceCannonPhase();
m.I(A::CANNON_SHOT_COUNT)++;
}
if(m.F(A::SHRAPNEL_CANNON_TIMER)>=ConfigFloat("Final Shrapnel Timer")){
m.I(A::SHRAPNEL_SHOT_COUNT)=ConfigInt("Shrapnel Shot Bullet Count");
m.F(A::SHRAPNEL_SHOT_FALL_TIMER)=ConfigFloat("Shrapnel Shot Bullet Separation");
m.F(A::SHRAPNEL_CANNON_TIMER)=0.f+util::random(0.5f); // A little randomness to offset the two timers if they happen to occur at once.
}
}break;
}
}

View File

@ -40,7 +40,7 @@ All rights reserved.
#include "Attributable.h"
GhostSaber::GhostSaber(const vf2d pos,const std::weak_ptr<Monster>target,const float lifetime,const float distFromTarget,const float knockbackAmt,const float initialRot,const float radius,const float expandSpd,const int damage,const bool upperLevel,const float rotSpd,const bool friendly,const Pixel col,const vf2d scale,const float image_angle)
:Bullet(target.lock()->GetPos()+vf2d{distFromTarget,initialRot}.cart(),{},radius,damage,"ghost_dagger.png",upperLevel,false,INFINITE,false,friendly,col,scale,image_angle),attachedMonster(target),rotSpd(rotSpd),distFromTarget(distFromTarget),rot(initialRot),aliveTime(lifetime),knockbackAmt(knockbackAmt),expandSpd(expandSpd){}
:Bullet(target.lock()->GetPos()+vf2d{distFromTarget,initialRot}.cart(),{},radius,damage,"ghost_dagger.png",upperLevel,true,INFINITE,false,friendly,col,scale,image_angle),attachedMonster(target),rotSpd(rotSpd),distFromTarget(distFromTarget),rot(initialRot),aliveTime(lifetime),knockbackAmt(knockbackAmt),expandSpd(expandSpd){}
void GhostSaber::Update(float fElapsedTime){
alphaOscillator.Update(fElapsedTime);
particleTimer-=fElapsedTime;
@ -63,7 +63,7 @@ void GhostSaber::Update(float fElapsedTime){
};
BulletDestroyState GhostSaber::PlayerHit(Player*player){
player->ApplyIframes(0.2f);
player->ApplyIframes(0.8f);
player->Knockback(vf2d{knockbackAmt,rot}.cart());
if(!attachedMonster.expired()){
const int GHOSTSABER_SLASH_PHASEID{999};
@ -72,12 +72,14 @@ BulletDestroyState GhostSaber::PlayerHit(Player*player){
attachedMonster.lock()->PerformAnimation("SLASHING");
attachedMonster.lock()->GetFloat(Attribute::GHOST_SABER_SLASH_ANIMATION_TIMER)=attachedMonster.lock()->GetCurrentAnimation().GetTotalAnimationDuration();
}
hitList.clear(); //Can keep hitting same target repeatedly
return BulletDestroyState::KEEP_ALIVE;
}
BulletDestroyState GhostSaber::MonsterHit(Monster&monster,const uint8_t markStacksBeforeHit){
monster.ApplyIframes(0.2f);
monster.ApplyIframes(0.8f);
monster.Knockback(vf2d{knockbackAmt,rot}.cart());
hitList.clear(); //Can keep hitting same target repeatedly
return BulletDestroyState::KEEP_ALIVE;
}

View File

@ -51,5 +51,6 @@ void Monster::STRATEGY::PIRATES_COIN(Monster&m,float fElapsedTime,std::string st
m._DealTrueDamage(m.GetHealth(),HurtFlag::NO_DAMAGE_NUMBER);
m.SetLifetime(0.f);
m.SetSize(0.f,false);
game->GetPlayer()->NotificationDisplay(ConfigString("Coin Owned Text"),INFINITE);
}
}

View File

@ -38,13 +38,20 @@ All rights reserved.
#include "AdventuresInLestoria.h"
#include "MonsterStrategyHelpers.h"
#include<ranges>
#include"ItemDrop.h"
INCLUDE_game
INCLUDE_MONSTER_LIST
INCLUDE_ITEM_DATA
void Monster::STRATEGY::PIRATES_TREASURE(Monster&m,float fElapsedTime,std::string strategy){
enum Phase{
NORMAL,
LOCKED,
LOCK = 1,
LOCKED = 2, //The locked phase will remain at 1 so that the Ghost Boss can utilize this specific phase number.
WAITING= 3,
REWARDED = 4,
};
switch(PHASE()){
@ -56,11 +63,42 @@ void Monster::STRATEGY::PIRATES_TREASURE(Monster&m,float fElapsedTime,std::strin
game->GetPlayer()->RemoveBuff(BuffType::PIRATE_GHOST_CAPTAIN_PRECURSE);
game->GetPlayer()->RemoveBuff(BuffType::PIRATE_GHOST_CAPTAIN_CURSE_COIN);
game->GetPlayer()->RemoveBuff(BuffType::PIRATE_GHOST_CAPTAIN_CURSE_DOT);
game->GetPlayer()->NotificationDisplay("",0);
}
}break;
case LOCK:{
m.SetCollisionRadius(m.GetCollisionRadius()+12);
SETPHASE(LOCKED);
}break;
case LOCKED:{
m.PerformAnimation("LOCKED");
m.SetCollisionRadius(m.GetOriginalCollisionRadius()+12);
bool isBossDead{true};
for(std::shared_ptr<Monster>monster:MONSTER_LIST|std::views::filter([](std::shared_ptr<Monster>&monster){return monster->isBoss&&monster->GetDisplayName()=="Ghost of Pirate Captain";})){
if(monster->IsAlive()){
isBossDead=false;
break;
}
}
if(isBossDead){
SETPHASE(WAITING);
m.SetCollisionRadius(m.GetCollisionRadius()-12);
game->GetPlayer()->NotificationDisplay("The Ghost Pirate Captain rewards you handsomely",10);
}
}break;
case WAITING:{
const float distToPlayer{util::distance(game->GetPlayer()->GetPos(),m.GetPos())};
if(distToPlayer<=ConfigFloat("Open Distance")){
m.PerformAnimation("REWARD");
for(const int ind:std::ranges::iota_view(size_t(0),Config("Reward Items").GetValueCount())){
ItemDrop::SpawnItem(const_cast<ItemInfo*>(&ITEM_DATA.at(ConfigStringArr("Reward Items",ind))),m.GetPos(),m.OnUpperLevel());
}
SETPHASE(REWARDED);
}
else m.PerformIdleAnimation();
}break;
case REWARDED:{
m.PerformAnimation("REWARD");
m._DealTrueDamage(m.GetHealth(),HurtFlag::NO_DAMAGE_NUMBER);
}break;
}
}

View File

@ -16,6 +16,11 @@ For Demo Branch:
Cherry pick b715dd994d4921e9be1811ceb7eb84120a5e6455 from MiscFixes
Cherry pick f398adcde6bb5f8c3c0581f13f18e73d4417a46c from ConnectionPointFixes
Cherry pick fb5a72267c5db89b7333287e12f05b614b71c23b from MiscFixes
Cherry pick 6355054d6c8e76c6aa4b18760a293e3d1a020752 from master
Remove coin toss for final phase
Issues with cannon fire in the final phase
Rebalance final phase ghost sabers
DEMO
====

View File

@ -39,7 +39,7 @@ All rights reserved.
#define VERSION_MAJOR 1
#define VERSION_MINOR 3
#define VERSION_PATCH 0
#define VERSION_BUILD 12329
#define VERSION_BUILD 12364
#define stringify(a) stringify_(a)
#define stringify_(a) #a

View File

@ -1349,7 +1349,7 @@ MonsterStrategy
Ghost Saber Expand Spd = 14px
# What HP % the boss throws a coin at the player, applying a curse, and hiding from the player.
Curse Thresholds = 95%, 40%, 10%
Curse Thresholds = 70%, 40%, 10%
# How much time before the curse starts dealing damage to the player
Curse Damage Wait Time = 10s
# How much % damage the curse does to the player every second.
@ -1360,10 +1360,23 @@ MonsterStrategy
Coin Toss Rise Amount = 72px
Shrapnel Hiding Shot Delay = 5.0s
# FINAL PHASE SETTINGS
# The coordinates on the map indicating where the boss stands for the final portion of the fight.
Final Standing Spot = 888,1104
Final Ghost Saber Count = 10
Final Ghost Saber Rotation Spd = 75deg/s
# Amount of separation between each ghost saber
Final Ghost Saber Separation Amount = 32px
# How often the cannons fire during the final phase.
Final Shrapnel Timer = 5s
Final Cannon Shot Delay = 0.5s
}
Pirate's Treasure
{
Open Distance = 64px
# Can be a comma-separated list for multiple drops.
Reward Items = "Captain's Diamond Ring"
}
Pirate's Coin
{
@ -1371,6 +1384,9 @@ MonsterStrategy
Coin Rise Timer = 2s
# Highest Z position the collected coin icon appears at.
Coin Collect Rise Amount = 36px
# The hint text displayed when the player is holding the coin.
Coin Owned Text = "This treasure does not belong to you..."
}
Skeleton Barbarian
{

View File

@ -1850,7 +1850,7 @@ Monsters
SLASHING = 4, 0.1, OneShot
DEATH = 4, 0.25, OneShot
STABBING = 3, 0.1, OneShot
SLASH = 1, 0.1, OneShot
SLASH = 2, 0.1, OneShot
STAB = 1, 0.1, OneShot
SHOOTING = 3, 0.2, OneShot
SHOOT = 1, 0.1, OneShot
@ -1858,8 +1858,11 @@ Monsters
DRINK = 2, 0.65, PingPong
}
### NOTE: The Pirate will float over to the Pirate;s Treasuure after the fight and reward the ring through it.
### Modify the drop in the Pirate's Treasure monster strategy.
# Drop Item Name, Drop Percentage(0-100%), Drop Min Quantity, Drop Max Quantity
# DROP[0] = Octopus Ring,100%,1,1
Hurt Sound = Monster Hurt
Death Sound = Slime Dead
@ -1901,6 +1904,7 @@ Monsters
WALK = 1, 1.0, OneShot
LOCKED = 1, 1.0, OneShot
OPEN = 1, 1.0, OneShot
REWARD = 1, 1.0, OneShot
}
# Drop Item Name, Drop Percentage(0-100%), Drop Min Quantity, Drop Max Quantity