Arrow indicator for monsters now adjustable. Additional zones added without a class now properly show up in the filterable zones list. Added coin toss, hide, and hit boss mechanic. Release Build 12109.

DynamicItemDescriptions
sigonasr2 1 month ago
parent dbf140e0bd
commit 842a32e947
  1. 285
      Adventures in Lestoria/CharacterMenuWindow.cpp
  2. 109
      Adventures in Lestoria/GhostOfPirateCaptain.cpp
  3. 9
      Adventures in Lestoria/Monster.cpp
  4. 4
      Adventures in Lestoria/Monster.h
  5. 3
      Adventures in Lestoria/MonsterAttribute.h
  6. 4
      Adventures in Lestoria/TMXParser.h
  7. 2
      Adventures in Lestoria/Version.h
  8. 4
      Adventures in Lestoria/assets/config/MonsterStrategies.txt
  9. BIN
      x64/Release/Adventures in Lestoria.exe

@ -164,6 +164,8 @@ void Menu::InitializeCharacterMenuWindow(){
return attrStr;
};
const bool CanModifyEquipSlots{game->GetCurrentMap().GetMapType()==Map::MapType::HUB||game->GetCurrentMap().GetMapType()==Map::MapType::WORLD_MAP};
int equipSlot=1;
for(int i=0;i<8;i++){
float x=31+(i%2)*33;
@ -177,137 +179,138 @@ void Menu::InitializeCharacterMenuWindow(){
EquipSlot slot=EquipSlot(equipSlot);
auto equipmentSlot=characterMenuWindow->ADD("Equip Slot "+CharacterMenuWindow::slotNames[i],EquipSlotButton)(geom2d::rect<float>{{x,y+28},{24,24}},slot,
[&](MenuFuncData data){
if(game->GetCurrentMap().GetMapType()!=Map::MapType::HUB&&game->GetCurrentMap().GetMapType()!=Map::MapType::WORLD_MAP){
game->AddNotification(AiL::Notification{"Cannot change equipment in a stage!",5.f,YELLOW});
return false;
}
EquipSlot slot=EquipSlot(data.component.lock()->I(Attribute::EQUIP_TYPE));
data.menu.I(A::EQUIP_TYPE)=int(slot);
const std::vector<std::shared_ptr<Item>>&equips=Inventory::get("Equipment");
const std::vector<std::shared_ptr<Item>>&accessories=Inventory::get("Accessories");
std::vector<std::weak_ptr<Item>>availableEquipment;
std::copy_if(equips.begin(),equips.end(),std::back_inserter(availableEquipment),[&](const std::shared_ptr<Item>it){
return it->GetEquipSlot()&slot;
});
std::copy_if(accessories.begin(),accessories.end(),std::back_inserter(availableEquipment),[&](const std::shared_ptr<Item>it){
return it->GetEquipSlot()&slot;
});
std::shared_ptr<ScrollableWindowComponent>equipList=Component<ScrollableWindowComponent>(data.component.lock()->parentMenu,"Equip List");
equipList->RemoveAllComponents();
for(int counter=0;const std::weak_ptr<Item>it:availableEquipment){
std::shared_ptr<RowItemDisplay>equip;
const bool isAccessorySlot=slot&(EquipSlot::RING1|EquipSlot::RING2);
if(isAccessorySlot){
equip=CharacterMenuWindow::GenerateItemDisplay<AccessoryRowItemDisplay>(equipList,counter,it);
}else{
equip=CharacterMenuWindow::GenerateItemDisplay<RowItemDisplay>(equipList,counter,it);
}
if(CanModifyEquipSlots){
EquipSlot slot=EquipSlot(data.component.lock()->I(Attribute::EQUIP_TYPE));
data.menu.I(A::EQUIP_TYPE)=int(slot);
const std::vector<std::shared_ptr<Item>>&equips=Inventory::get("Equipment");
const std::vector<std::shared_ptr<Item>>&accessories=Inventory::get("Accessories");
std::vector<std::weak_ptr<Item>>availableEquipment;
std::copy_if(equips.begin(),equips.end(),std::back_inserter(availableEquipment),[&](const std::shared_ptr<Item>it){
return it->GetEquipSlot()&slot;
});
std::copy_if(accessories.begin(),accessories.end(),std::back_inserter(availableEquipment),[&](const std::shared_ptr<Item>it){
return it->GetEquipSlot()&slot;
});
std::shared_ptr<ScrollableWindowComponent>equipList=Component<ScrollableWindowComponent>(data.component.lock()->parentMenu,"Equip List");
equipList->RemoveAllComponents();
for(int counter=0;const std::weak_ptr<Item>it:availableEquipment){
std::shared_ptr<RowItemDisplay>equip;
const bool isAccessorySlot=slot&(EquipSlot::RING1|EquipSlot::RING2);
if(isAccessorySlot){
equip=CharacterMenuWindow::GenerateItemDisplay<AccessoryRowItemDisplay>(equipList,counter,it);
}else{
equip=CharacterMenuWindow::GenerateItemDisplay<RowItemDisplay>(equipList,counter,it);
}
equip->SetHoverFunc(
[&](MenuFuncData data){
if(!data.component.lock()->GetSubcomponentParent().expired())return true;
std::weak_ptr<RowItemDisplay>button=DYNAMIC_POINTER_CAST<RowItemDisplay>(data.component.lock());
if(!button.expired()){
const std::weak_ptr<Item>buttonItem=button.lock()->GetItem();
std::vector<float>statsBeforeEquip;
EquipSlot slot=EquipSlot(button.lock()->I(Attribute::EQUIP_TYPE));
for(const CharacterMenuWindow::AttributeData&attribute:CharacterMenuWindow::displayAttrs){
statsBeforeEquip.push_back(attribute.calcFunc());
}
int healthBeforeEquip=game->GetPlayer()->GetHealth();
int manaBeforeEquip=game->GetPlayer()->GetMana();
std::weak_ptr<Item>equippedItem=Inventory::GetEquip(slot);
std::weak_ptr<Item>otherItem;
if(slot&EquipSlot::RING1)otherItem=Inventory::GetEquip(EquipSlot::RING2);
else
if(slot&EquipSlot::RING2)otherItem=Inventory::GetEquip(EquipSlot::RING1);
if(Item::SelectedEquipIsDifferent(button.lock()->GetItem(),slot)){ //If we find that the opposite ring slot is equipped to us, this would be an item swap or the exact same ring, therefore no stat calculations apply.
Inventory::EquipItem(buttonItem,slot);
for(int counter=0;const CharacterMenuWindow::AttributeData&attribute:CharacterMenuWindow::displayAttrs){
std::weak_ptr<StatLabel>statDisplayLabel=Component<StatLabel>(CHARACTER_MENU,"Attribute "+std::string(ItemAttribute::Get(attribute.attrName).Name())+" Label");
int statChangeAmt=attribute.calcFunc()-statsBeforeEquip[counter];
statDisplayLabel.lock()->SetStatChangeAmt(statChangeAmt);
counter++;
equip->SetHoverFunc(
[&](MenuFuncData data){
if(!data.component.lock()->GetSubcomponentParent().expired())return true;
std::weak_ptr<RowItemDisplay>button=DYNAMIC_POINTER_CAST<RowItemDisplay>(data.component.lock());
if(!button.expired()){
const std::weak_ptr<Item>buttonItem=button.lock()->GetItem();
std::vector<float>statsBeforeEquip;
EquipSlot slot=EquipSlot(button.lock()->I(Attribute::EQUIP_TYPE));
for(const CharacterMenuWindow::AttributeData&attribute:CharacterMenuWindow::displayAttrs){
statsBeforeEquip.push_back(attribute.calcFunc());
}
Inventory::UnequipItem(slot);
if(!ISBLANK(equippedItem)){
Inventory::EquipItem(equippedItem,slot);
game->GetPlayer()->Heal(healthBeforeEquip-game->GetPlayer()->GetHealth(),true);
game->GetPlayer()->RestoreMana(manaBeforeEquip-game->GetPlayer()->GetMana(),true);
}
if(!ISBLANK(otherItem)){
if(slot&EquipSlot::RING1)Inventory::EquipItem(otherItem,EquipSlot::RING2);
else
if(slot&EquipSlot::RING2)Inventory::EquipItem(otherItem,EquipSlot::RING1);
int healthBeforeEquip=game->GetPlayer()->GetHealth();
int manaBeforeEquip=game->GetPlayer()->GetMana();
std::weak_ptr<Item>equippedItem=Inventory::GetEquip(slot);
std::weak_ptr<Item>otherItem;
if(slot&EquipSlot::RING1)otherItem=Inventory::GetEquip(EquipSlot::RING2);
else
if(slot&EquipSlot::RING2)otherItem=Inventory::GetEquip(EquipSlot::RING1);
if(Item::SelectedEquipIsDifferent(button.lock()->GetItem(),slot)){ //If we find that the opposite ring slot is equipped to us, this would be an item swap or the exact same ring, therefore no stat calculations apply.
Inventory::EquipItem(buttonItem,slot);
for(int counter=0;const CharacterMenuWindow::AttributeData&attribute:CharacterMenuWindow::displayAttrs){
std::weak_ptr<StatLabel>statDisplayLabel=Component<StatLabel>(CHARACTER_MENU,"Attribute "+std::string(ItemAttribute::Get(attribute.attrName).Name())+" Label");
int statChangeAmt=attribute.calcFunc()-statsBeforeEquip[counter];
statDisplayLabel.lock()->SetStatChangeAmt(statChangeAmt);
counter++;
}
Inventory::UnequipItem(slot);
if(!ISBLANK(equippedItem)){
Inventory::EquipItem(equippedItem,slot);
game->GetPlayer()->Heal(healthBeforeEquip-game->GetPlayer()->GetHealth(),true);
game->GetPlayer()->RestoreMana(manaBeforeEquip-game->GetPlayer()->GetMana(),true);
}
if(!ISBLANK(otherItem)){
if(slot&EquipSlot::RING1)Inventory::EquipItem(otherItem,EquipSlot::RING2);
else
if(slot&EquipSlot::RING2)Inventory::EquipItem(otherItem,EquipSlot::RING1);
}
}
Component<MenuItemLabel>(data.menu.GetType(),"Item Name")->SetItem(buttonItem);
Component<MenuItemLabel>(data.menu.GetType(),"Item Description")->SetItem(buttonItem);
Component<MenuItemLabel>(data.menu.GetType(),"Item Name")->Enable();
Component<MenuItemLabel>(data.menu.GetType(),"Item Description")->Enable();
}else{
ERR("WARNING! Attempting to cast a button that isn't a RowItemDisplay!");
}
Component<MenuItemLabel>(data.menu.GetType(),"Item Name")->SetItem(buttonItem);
Component<MenuItemLabel>(data.menu.GetType(),"Item Description")->SetItem(buttonItem);
Component<MenuItemLabel>(data.menu.GetType(),"Item Name")->Enable();
Component<MenuItemLabel>(data.menu.GetType(),"Item Description")->Enable();
}else{
ERR("WARNING! Attempting to cast a button that isn't a RowItemDisplay!");
}
return true;
});
equip->SetMouseOutFunc(
[](MenuFuncData data){
for(int counter=0;const CharacterMenuWindow::AttributeData&attribute:CharacterMenuWindow::displayAttrs){
std::weak_ptr<StatLabel>statDisplayLabel=Component<StatLabel>(CHARACTER_MENU,"Attribute "+std::string(ItemAttribute::Get(attribute.attrName).Name())+" Label");
statDisplayLabel.lock()->SetStatChangeAmt(0);
counter++;
}
Component<MenuItemLabel>(data.menu.GetType(),"Item Name")->Disable();
Component<MenuItemLabel>(data.menu.GetType(),"Item Description")->Disable();
return true;
});
equip->SetShowQuantity(false);
equip->SetSelectionType(SelectionType::NONE);
equip->I(Attribute::EQUIP_TYPE)=int(slot);
if(Inventory::GetEquip(slot)==it){
equip->SetSelected(true);
}
equip->SetCompactDescriptions(NON_COMPACT);
return true;
});
equip->SetMouseOutFunc(
[](MenuFuncData data){
for(int counter=0;const CharacterMenuWindow::AttributeData&attribute:CharacterMenuWindow::displayAttrs){
std::weak_ptr<StatLabel>statDisplayLabel=Component<StatLabel>(CHARACTER_MENU,"Attribute "+std::string(ItemAttribute::Get(attribute.attrName).Name())+" Label");
statDisplayLabel.lock()->SetStatChangeAmt(0);
counter++;
}
Component<MenuItemLabel>(data.menu.GetType(),"Item Name")->Disable();
Component<MenuItemLabel>(data.menu.GetType(),"Item Description")->Disable();
return true;
});
counter++;
}
equip->SetShowQuantity(false);
equip->SetSelectionType(SelectionType::NONE);
equipList->I(Attribute::INDEXED_THEME)=data.component.lock()->I(Attribute::INDEXED_THEME);
Component<MenuComponent>(data.component.lock()->parentMenu,"Equip Selection Outline")->Enable();
equipList->Enable();
Component<MenuComponent>(data.component.lock()->parentMenu,"Equip Selection Bottom Outline")->Enable();
Component<MenuComponent>(data.component.lock()->parentMenu,"Equip Selection Select Button")->Enable();
Component<CharacterRotatingDisplay>(data.component.lock()->parentMenu,"Character Rotating Display")->Disable();
equipmentWindowOpened=true;
auto equipmentList=equipList->GetComponents();
auto itemEquipped=std::find_if(equipmentList.begin(),equipmentList.end(),[&](std::weak_ptr<MenuComponent>&component){
return !ISBLANK(Inventory::GetEquip(slot))&&component.lock()->GetSubcomponentParent().expired()&&&*DYNAMIC_POINTER_CAST<RowItemDisplay>(component)->GetItem().lock()==&*Inventory::GetEquip(slot).lock();
});
if(itemEquipped!=equipmentList.end()){
data.menu.SetSelection(*itemEquipped,true,true);
if(Menu::UsingMouseNavigation()){
equipList->HandleOutsideDisabledButtonSelection(*itemEquipped);
equip->I(Attribute::EQUIP_TYPE)=int(slot);
if(Inventory::GetEquip(slot)==it){
equip->SetSelected(true);
}
equip->SetCompactDescriptions(NON_COMPACT);
counter++;
}
data.menu.I(A::ITEM_SLOT)=equipList->GetComponentIndex(*itemEquipped);
}else
if(equipmentList.size()>0){
data.menu.SetSelection(equipmentList[0],true,true);
if(Menu::UsingMouseNavigation()){
equipList->HandleOutsideDisabledButtonSelection(equipmentList[0]);
equipList->I(Attribute::INDEXED_THEME)=data.component.lock()->I(Attribute::INDEXED_THEME);
Component<MenuComponent>(data.component.lock()->parentMenu,"Equip Selection Outline")->Enable();
equipList->Enable();
Component<MenuComponent>(data.component.lock()->parentMenu,"Equip Selection Bottom Outline")->Enable();
Component<MenuComponent>(data.component.lock()->parentMenu,"Equip Selection Select Button")->Enable();
Component<CharacterRotatingDisplay>(data.component.lock()->parentMenu,"Character Rotating Display")->Disable();
equipmentWindowOpened=true;
auto equipmentList=equipList->GetComponents();
auto itemEquipped=std::find_if(equipmentList.begin(),equipmentList.end(),[&](std::weak_ptr<MenuComponent>&component){
return !ISBLANK(Inventory::GetEquip(slot))&&component.lock()->GetSubcomponentParent().expired()&&&*DYNAMIC_POINTER_CAST<RowItemDisplay>(component)->GetItem().lock()==&*Inventory::GetEquip(slot).lock();
});
if(itemEquipped!=equipmentList.end()){
data.menu.SetSelection(*itemEquipped,true,true);
if(Menu::UsingMouseNavigation()){
equipList->HandleOutsideDisabledButtonSelection(*itemEquipped);
}
data.menu.I(A::ITEM_SLOT)=equipList->GetComponentIndex(*itemEquipped);
}else
if(equipmentList.size()>0){
data.menu.SetSelection(equipmentList[0],true,true);
if(Menu::UsingMouseNavigation()){
equipList->HandleOutsideDisabledButtonSelection(equipmentList[0]);
}
data.menu.I(A::ITEM_SLOT)=0;
}else{
data.menu.SetSelection("Equip Selection Select Button"sv);
}
data.menu.I(A::ITEM_SLOT)=0;
return true;
}else{
data.menu.SetSelection("Equip Selection Select Button"sv);
game->AddNotification(AiL::Notification{"Cannot change equipment in a stage!",5.f,YELLOW});
return false;
}
return true;
},[](MenuFuncData data){//On Mouse Hover
EquipSlot slot=DYNAMIC_POINTER_CAST<EquipSlotButton>(data.component.lock())->GetSlot();
const std::weak_ptr<Item>equip=Inventory::GetEquip(slot);
@ -372,18 +375,20 @@ void Menu::InitializeCharacterMenuWindow(){
}
}
return "";
},[](MenuType type){
},[&CanModifyEquipSlots](MenuType type){
if(!Menu::menus[type]->GetSelection().expired()&&
Menu::menus[type]->GetSelection().lock()->GetName().starts_with("Equip Slot ")){
EquipSlot slot=EquipSlot(Menu::menus[type]->GetSelection().lock()->I(Attribute::EQUIP_TYPE));
if(!ISBLANK(Inventory::GetEquip(slot))){
Inventory::UnequipItem(slot);
if(slot&EquipSlot::RING1||slot&EquipSlot::RING2){
SoundEffect::PlaySFX("Unequip Accessory",SoundEffect::CENTERED);
}else{
SoundEffect::PlaySFX("Unequip Armor",SoundEffect::CENTERED);
if(CanModifyEquipSlots){
EquipSlot slot=EquipSlot(Menu::menus[type]->GetSelection().lock()->I(Attribute::EQUIP_TYPE));
if(!ISBLANK(Inventory::GetEquip(slot))){
Inventory::UnequipItem(slot);
if(slot&EquipSlot::RING1||slot&EquipSlot::RING2){
SoundEffect::PlaySFX("Unequip Accessory",SoundEffect::CENTERED);
}else{
SoundEffect::PlaySFX("Unequip Armor",SoundEffect::CENTERED);
}
}
}
}else game->AddNotification(AiL::Notification{"Cannot change equipment in a stage!",5.f,YELLOW});
}
}}},
{{game->KEY_FACELEFT,Pressed},{[](MenuFuncData data){
@ -395,18 +400,20 @@ void Menu::InitializeCharacterMenuWindow(){
}
}
return "";
},[](MenuType type){
},[&CanModifyEquipSlots](MenuType type){
if(!Menu::menus[type]->GetSelection().expired()&&
Menu::menus[type]->GetSelection().lock()->GetName().starts_with("Equip Slot ")){
EquipSlot slot=EquipSlot(Menu::menus[type]->GetSelection().lock()->I(Attribute::EQUIP_TYPE));
if(!ISBLANK(Inventory::GetEquip(slot))){
Inventory::UnequipItem(slot);
if(slot&EquipSlot::RING1||slot&EquipSlot::RING2){
SoundEffect::PlaySFX("Unequip Accessory",SoundEffect::CENTERED);
}else{
SoundEffect::PlaySFX("Unequip Armor",SoundEffect::CENTERED);
if(CanModifyEquipSlots){
EquipSlot slot=EquipSlot(Menu::menus[type]->GetSelection().lock()->I(Attribute::EQUIP_TYPE));
if(!ISBLANK(Inventory::GetEquip(slot))){
Inventory::UnequipItem(slot);
if(slot&EquipSlot::RING1||slot&EquipSlot::RING2){
SoundEffect::PlaySFX("Unequip Accessory",SoundEffect::CENTERED);
}else{
SoundEffect::PlaySFX("Unequip Armor",SoundEffect::CENTERED);
}
}
}
}else game->AddNotification(AiL::Notification{"Cannot change equipment in a stage!",5.f,YELLOW});
}
}}},
{game->KEY_BACK,{"Back",[](MenuType type){

@ -55,6 +55,7 @@ void Monster::STRATEGY::GHOST_OF_PIRATE_CAPTAIN(Monster&m,float fElapsedTime,std
AFTERIMAGE_FADEIN,
GHOSTSABER_SLASH=999,
TOSS_COIN,
HIDING,
};
enum CannonShotType{
@ -67,6 +68,7 @@ void Monster::STRATEGY::GHOST_OF_PIRATE_CAPTAIN(Monster&m,float fElapsedTime,std
static const uint8_t PHASE_COUNT{uint8_t(DATA.GetProperty("MonsterStrategy.Ghost of Pirate Captain.Cannon Cycle").GetValueCount())};
static uint8_t TOTAL_CANNON_SHOTS{0};
const bool IsHiding{m.V(A::HIDING_POS)!=vf2d{}};
const auto AdvanceCannonPhase{[&m,&strategy](){
m.GetFloat(A::CANNON_TIMER)=0.f;
@ -99,29 +101,6 @@ void Monster::STRATEGY::GHOST_OF_PIRATE_CAPTAIN(Monster&m,float fElapsedTime,std
}
}
m.F(A::GHOST_SABER_TIMER)-=fElapsedTime;
m.F(A::LAST_COLLISION_TIMER)-=fElapsedTime;
if(m.B(A::FIRST_WAVE_COMPLETE)){
if(m.F(A::LAST_COLLISION_TIMER)<=0.f){
m.UpdateFacingDirection(m.GetFacingDirectionToTarget(game->GetPlayer()->GetPos()));
m.SetVelocity(util::pointTo(m.GetPos(),game->GetPlayer()->GetPos())*100.f*m.GetMoveSpdMult());
const float distToPlayer{util::distance(m.GetPos(),game->GetPlayer()->GetPos())};
if(m.F(A::GHOST_SABER_TIMER)<=0.f&&distToPlayer<=ConfigPixels("Ghost Saber Activation Range")){
m.F(A::GHOST_SABER_TIMER)=ConfigFloat("Ghost Saber Cooldown");
const float playerToMonsterAngle{util::pointTo(game->GetPlayer()->GetPos(),m.GetPos()).polar().y};
CreateBullet(GhostSaber)(m.GetPos(),m.GetWeakPointer(),ConfigFloat("Ghost Saber Lifetime"),ConfigFloat("Ghost Saber Distance"),ConfigFloat("Ghost Saber Knockback Amt"),playerToMonsterAngle,ConfigFloat("Ghost Saber Radius"),ConfigFloat("Ghost Saber Expand Spd"),ConfigInt("Ghost Saber Damage"),m.OnUpperLevel(),util::degToRad(ConfigFloat("Ghost Saber Rotation Spd")))EndBullet;
}
}
if(m.B(A::COLLIDED_WITH_PLAYER)){
m.PerformAnimation("SLASHING");
m.F(A::GHOST_SABER_SLASH_ANIMATION_TIMER)=m.GetCurrentAnimation().GetTotalAnimationDuration();
m.F(A::LAST_COLLISION_TIMER)=ConfigFloat("Collision Recovery Time");
m.I(A::PREVIOUS_PHASE)=PHASE();
SETPHASE(GHOSTSABER_SLASH);
}
}
switch(PHASE()){
enum CannonPhaseType{
CANNON_SHOT,
@ -147,6 +126,7 @@ void Monster::STRATEGY::GHOST_OF_PIRATE_CAPTAIN(Monster&m,float fElapsedTime,std
}break;
case NORMAL:{
m.F(A::CANNON_TIMER)+=fElapsedTime;
m.F(A::SHRAPNEL_CANNON_TIMER)+=fElapsedTime;
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.
@ -204,8 +184,37 @@ void Monster::STRATEGY::GHOST_OF_PIRATE_CAPTAIN(Monster&m,float fElapsedTime,std
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));
#pragma region Determine a hiding spot
const auto&hidingSpots{game->GetZones().at("Hiding Spot")};
if(hidingSpots.size()==0)ERR("WARNING! Could not find any zones with the name \"Hiding Spot\" on the map!! THIS SHOULD NOT BE HAPPENING!")
m.V(A::HIDING_POS)=hidingSpots[util::random()%hidingSpots.size()].zone.middle();
#pragma endregion
SETPHASE(TOSS_COIN);
}
m.F(A::GHOST_SABER_TIMER)-=fElapsedTime;
m.F(A::LAST_COLLISION_TIMER)-=fElapsedTime;
if(m.B(A::FIRST_WAVE_COMPLETE)){
if(m.F(A::LAST_COLLISION_TIMER)<=0.f){
m.UpdateFacingDirection(m.GetFacingDirectionToTarget(game->GetPlayer()->GetPos()));
m.SetVelocity(util::pointTo(m.GetPos(),game->GetPlayer()->GetPos())*100.f*m.GetMoveSpdMult());
const float distToPlayer{util::distance(m.GetPos(),game->GetPlayer()->GetPos())};
if(m.F(A::GHOST_SABER_TIMER)<=0.f&&distToPlayer<=ConfigPixels("Ghost Saber Activation Range")){
m.F(A::GHOST_SABER_TIMER)=ConfigFloat("Ghost Saber Cooldown");
const float playerToMonsterAngle{util::pointTo(game->GetPlayer()->GetPos(),m.GetPos()).polar().y};
CreateBullet(GhostSaber)(m.GetPos(),m.GetWeakPointer(),ConfigFloat("Ghost Saber Lifetime"),ConfigFloat("Ghost Saber Distance"),ConfigFloat("Ghost Saber Knockback Amt"),playerToMonsterAngle,ConfigFloat("Ghost Saber Radius"),ConfigFloat("Ghost Saber Expand Spd"),ConfigInt("Ghost Saber Damage"),m.OnUpperLevel(),util::degToRad(ConfigFloat("Ghost Saber Rotation Spd")))EndBullet;
}
}
if(m.B(A::COLLIDED_WITH_PLAYER)){
m.PerformAnimation("SLASHING");
m.F(A::GHOST_SABER_SLASH_ANIMATION_TIMER)=m.GetCurrentAnimation().GetTotalAnimationDuration();
m.F(A::LAST_COLLISION_TIMER)=ConfigFloat("Collision Recovery Time");
m.I(A::PREVIOUS_PHASE)=PHASE();
SETPHASE(GHOSTSABER_SLASH);
}
}
}break;
case AFTERIMAGE_FADEIN:{
m.F(A::CASTING_TIMER)-=fElapsedTime;
@ -229,7 +238,59 @@ void Monster::STRATEGY::GHOST_OF_PIRATE_CAPTAIN(Monster&m,float fElapsedTime,std
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());
SETPHASE(NORMAL);
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;
});
SETPHASE(HIDING);
}
}break;
case HIDING:{
m.F(A::CANNON_TIMER)+=fElapsedTime;
if(m.F(A::CANNON_TIMER)>=ConfigFloat("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:{
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;
}
AdvanceCannonPhase();
m.I(A::CANNON_SHOT_COUNT)++;
}
if(m.F(A::SHRAPNEL_CANNON_TIMER)>=ConfigFloat("Shrapnel Hiding Shot Delay")){
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;
}
}break;
}

@ -70,7 +70,7 @@ safemap<std::string,std::function<void(Monster&,float,std::string)>>STRATEGY_DAT
std::unordered_map<std::string,Renderable*>MonsterData::imgs;
Monster::Monster(vf2d pos,MonsterData data,bool upperLevel,bool bossMob):
pos(pos),spawnPos(pos),hp(data.GetHealth()),size(data.GetSizeMult()),targetSize(data.GetSizeMult()),strategy(data.GetAIStrategy()),name(data.GetInternalName()),upperLevel(upperLevel),isBoss(bossMob),facingDirection(Direction::WEST),lifetime(GetTotalLifetime()),collisionRadius(data.GetCollisionRadius()){
pos(pos),spawnPos(pos),hp(data.GetHealth()),size(data.GetSizeMult()),targetSize(data.GetSizeMult()),strategy(data.GetAIStrategy()),name(data.GetInternalName()),upperLevel(upperLevel),isBoss(bossMob),facingDirection(Direction::WEST),lifetime(GetTotalLifetime()),collisionRadius(data.GetCollisionRadius()),arrowIndicator(data.HasArrowIndicator()){
for(const std::string&anim:data.GetAnimations()){
animation.AddState(anim,ANIMATION_DATA[std::format("{}_{}",name,anim)]);
}
@ -859,6 +859,7 @@ bool Monster::_Hurt(int damage,bool onUpperLevel,float z,const TrueDamageFlag da
using A=Attribute;
GetInt(A::HITS_UNTIL_DEATH)=std::max(0,GetInt(A::HITS_UNTIL_DEATH)-1);
ApplyIframes(GetFloat(A::IFRAME_TIME_UPON_HIT));
if(strategyOnHitFunc)strategyOnHitFunc(HurtDamageInfo{this,int(mod_dmg),onUpperLevel,z,hurtFlags,damageRule},*this,strategy);
return true;
}
@ -1296,6 +1297,10 @@ void Monster::SetStrategyDeathFunction(std::function<bool(GameEvent&event,Monste
strategyDeathFunc=func;
}
void Monster::SetStrategyOnHitFunction(std::function<void(const HurtDamageInfo damageData,Monster&monster,const StrategyName&strategyName)>func){
strategyOnHitFunc=func;
}
const bool Monster::IsNPC()const{
return MONSTER_DATA[name].IsNPC();
}
@ -1423,7 +1428,7 @@ const std::string_view Monster::GetDisplayName()const{
}
const bool Monster::HasArrowIndicator()const{
return MONSTER_DATA.at(GetName()).HasArrowIndicator();
return arrowIndicator;
}
const bool Monster::ReachedTargetPos(const float maxDistanceFromTarget)const{

@ -292,9 +292,12 @@ private:
NPCData npcData;
float lastPathfindingCooldown=0.f;
std::function<bool(GameEvent&,Monster&,const std::string&)>strategyDeathFunc{};
std::function<void(const HurtDamageInfo damageInfo,Monster&,const std::string&)>strategyOnHitFunc{};
//Sets the strategy death function that runs when a monster dies.
// The function should return false to indicate the event is over. If the event should keep running, return true.
void SetStrategyDeathFunction(std::function<bool(GameEvent&event,Monster&monster,const StrategyName&strategyName)>func);
// The function is called immediately after taking damage. Note the damage has already gone through, and the damage data contains the final damage that was received after all damage reductions were applied. It cannot be modified.
void SetStrategyOnHitFunction(std::function<void(const HurtDamageInfo damageData,Monster&monster,const StrategyName&strategyName)>func);
//If you are trying to change a Get() stat, use the STAT_UP buff (and the optional argument) to supply an attribute you want to apply.
const ItemAttribute&GetBonusStat(std::string_view attr)const;
//Returns false if the monster could not be moved to the requested location due to collision.
@ -347,6 +350,7 @@ private:
uint8_t scanLine{24};
vf2d afterImagePos{};
Oscillator<float>floatOscillator{0.f,8.f,0.5f};
bool arrowIndicator{false};
struct STRATEGY{
static std::string ERR;

@ -179,5 +179,6 @@ enum class Attribute{
TOSS_COIN_WAIT_TIMER,
TOSS_COIN_TARGET,
LAST_COLLISION_TIMER,
HIDING_POS,
SHRAPNEL_CANNON_TIMER,
};

@ -603,8 +603,8 @@ class TMXParser{
//This is a property for a zone that doesn't fit into the other categories, we add it to the previous zone data encountered.
prevZoneData->properties.push_back(newTag);
}else
if (newTag.tag=="object"&&newTag.data.find("type")!=newTag.data.end()){
//This is an object with a type that doesn't fit into other categories, we can add it to ZoneData.
if (newTag.tag=="object"){
if(newTag.data["type"]=="")newTag.data["type"]=newTag.data["name"]; //Found a blank type name! Try to set the type as its name.
std::vector<ZoneData>&zones=parsedMapInfo.ZoneData[newTag.data["type"]];
float width=1.f;
float height=1.f;

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

@ -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 = 70%, 40%, 10%
Curse Thresholds = 95%, 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.
@ -1358,6 +1358,8 @@ MonsterStrategy
Coin Toss Pause Time = 2s
# Highest Z position the coin is tossed up.
Coin Toss Rise Amount = 72px
Shrapnel Hiding Shot Delay = 5.0s
}
Pirate's Treasure
{

Loading…
Cancel
Save