Fight tweaks, balancing, fixes, and reward adjustments. Fight is good to go mechanically. Release Build 12364.
All checks were successful
Emscripten Build / Build_and_Deploy_Web_Build (push) Successful in 7m14s

This commit is contained in:
sigonasr2 2026-01-28 15:45:55 -06:00
parent 0121fe969e
commit 0756220b49
9 changed files with 74 additions and 18 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{
@ -156,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;
@ -185,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")};
@ -261,8 +261,9 @@ void Monster::STRATEGY::GHOST_OF_PIRATE_CAPTAIN(Monster&m,float fElapsedTime,std
}
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 int rotationMult=((i%2==0)?1:-1);
CreateBullet(GhostSaber)(m.GetPos(),m.GetWeakPointer(),ConfigFloat("Ghost Saber Lifetime"),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))EndBullet;
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;
}
}
});
@ -295,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;
@ -316,8 +315,21 @@ void Monster::STRATEGY::GHOST_OF_PIRATE_CAPTAIN(Monster&m,float fElapsedTime,std
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 Timer")){
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)};
@ -341,10 +353,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;
@ -352,7 +362,7 @@ void Monster::STRATEGY::GHOST_OF_PIRATE_CAPTAIN(Monster&m,float fElapsedTime,std
AdvanceCannonPhase();
m.I(A::CANNON_SHOT_COUNT)++;
}
if(m.F(A::SHRAPNEL_CANNON_TIMER)>=ConfigFloat("Final Cannon Timer")){
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.

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

@ -38,14 +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,
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()){
@ -65,6 +71,34 @@ void Monster::STRATEGY::PIRATES_TREASURE(Monster&m,float fElapsedTime,std::strin
SETPHASE(LOCKED);
}break;
case LOCKED:{
m.PerformAnimation("LOCKED");
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

@ -19,6 +19,8 @@ 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 12342
#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 = 98%
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.
@ -1365,14 +1365,18 @@ MonsterStrategy
# 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 = 24px
Final Ghost Saber Separation Amount = 32px
# How often the cannons fire during the final phase.
Final Cannon Timer = 5s
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
{

View File

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