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

@ -55,6 +55,7 @@ void Monster::STRATEGY::GHOST_OF_PIRATE_CAPTAIN(Monster&m,float fElapsedTime,std
AFTERIMAGE_FADEIN, AFTERIMAGE_FADEIN,
GHOSTSABER_SLASH=999, GHOSTSABER_SLASH=999,
TOSS_COIN, TOSS_COIN,
HIDING,
}; };
enum CannonShotType{ 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 const uint8_t PHASE_COUNT{uint8_t(DATA.GetProperty("MonsterStrategy.Ghost of Pirate Captain.Cannon Cycle").GetValueCount())};
static uint8_t TOTAL_CANNON_SHOTS{0}; static uint8_t TOTAL_CANNON_SHOTS{0};
const bool IsHiding{m.V(A::HIDING_POS)!=vf2d{}};
const auto AdvanceCannonPhase{[&m,&strategy](){ const auto AdvanceCannonPhase{[&m,&strategy](){
m.GetFloat(A::CANNON_TIMER)=0.f; 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()){ switch(PHASE()){
enum CannonPhaseType{ enum CannonPhaseType{
CANNON_SHOT, CANNON_SHOT,
@ -147,6 +126,7 @@ void Monster::STRATEGY::GHOST_OF_PIRATE_CAPTAIN(Monster&m,float fElapsedTime,std
}break; }break;
case NORMAL:{ case NORMAL:{
m.F(A::CANNON_TIMER)+=fElapsedTime; 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)])}; const int phase{std::any_cast<int>(m.VEC(A::CANNON_PHASES)[m.I(A::CANNON_PHASE)])};
switch(phase){ switch(phase){
case CANNON_SHOT:{//Normal Cannon Shot. Takes on one of five varieties. 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.F(A::TOSS_COIN_WAIT_TIMER)=ConfigFloat("Coin Toss Pause Time");
m.V(A::TOSS_COIN_TARGET)=game->GetPlayer()->GetPos(); 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)); 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); 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; }break;
case AFTERIMAGE_FADEIN:{ case AFTERIMAGE_FADEIN:{
m.F(A::CASTING_TIMER)-=fElapsedTime; 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); 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()); 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; }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; std::unordered_map<std::string,Renderable*>MonsterData::imgs;
Monster::Monster(vf2d pos,MonsterData data,bool upperLevel,bool bossMob): 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()){ for(const std::string&anim:data.GetAnimations()){
animation.AddState(anim,ANIMATION_DATA[std::format("{}_{}",name,anim)]); 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; using A=Attribute;
GetInt(A::HITS_UNTIL_DEATH)=std::max(0,GetInt(A::HITS_UNTIL_DEATH)-1); GetInt(A::HITS_UNTIL_DEATH)=std::max(0,GetInt(A::HITS_UNTIL_DEATH)-1);
ApplyIframes(GetFloat(A::IFRAME_TIME_UPON_HIT)); ApplyIframes(GetFloat(A::IFRAME_TIME_UPON_HIT));
if(strategyOnHitFunc)strategyOnHitFunc(HurtDamageInfo{this,int(mod_dmg),onUpperLevel,z,hurtFlags,damageRule},*this,strategy);
return true; return true;
} }
@ -1296,6 +1297,10 @@ void Monster::SetStrategyDeathFunction(std::function<bool(GameEvent&event,Monste
strategyDeathFunc=func; strategyDeathFunc=func;
} }
void Monster::SetStrategyOnHitFunction(std::function<void(const HurtDamageInfo damageData,Monster&monster,const StrategyName&strategyName)>func){
strategyOnHitFunc=func;
}
const bool Monster::IsNPC()const{ const bool Monster::IsNPC()const{
return MONSTER_DATA[name].IsNPC(); return MONSTER_DATA[name].IsNPC();
} }
@ -1423,7 +1428,7 @@ const std::string_view Monster::GetDisplayName()const{
} }
const bool Monster::HasArrowIndicator()const{ const bool Monster::HasArrowIndicator()const{
return MONSTER_DATA.at(GetName()).HasArrowIndicator(); return arrowIndicator;
} }
const bool Monster::ReachedTargetPos(const float maxDistanceFromTarget)const{ const bool Monster::ReachedTargetPos(const float maxDistanceFromTarget)const{

@ -292,9 +292,12 @@ private:
NPCData npcData; NPCData npcData;
float lastPathfindingCooldown=0.f; float lastPathfindingCooldown=0.f;
std::function<bool(GameEvent&,Monster&,const std::string&)>strategyDeathFunc{}; 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. //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. // 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); 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. //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; const ItemAttribute&GetBonusStat(std::string_view attr)const;
//Returns false if the monster could not be moved to the requested location due to collision. //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}; uint8_t scanLine{24};
vf2d afterImagePos{}; vf2d afterImagePos{};
Oscillator<float>floatOscillator{0.f,8.f,0.5f}; Oscillator<float>floatOscillator{0.f,8.f,0.5f};
bool arrowIndicator{false};
struct STRATEGY{ struct STRATEGY{
static std::string ERR; static std::string ERR;

@ -179,5 +179,6 @@ enum class Attribute{
TOSS_COIN_WAIT_TIMER, TOSS_COIN_WAIT_TIMER,
TOSS_COIN_TARGET, TOSS_COIN_TARGET,
LAST_COLLISION_TIMER, 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. //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); prevZoneData->properties.push_back(newTag);
}else }else
if (newTag.tag=="object"&&newTag.data.find("type")!=newTag.data.end()){ if (newTag.tag=="object"){
//This is an object with a type that doesn't fit into other categories, we can add it to ZoneData. 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"]]; std::vector<ZoneData>&zones=parsedMapInfo.ZoneData[newTag.data["type"]];
float width=1.f; float width=1.f;
float height=1.f; float height=1.f;

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

@ -1349,7 +1349,7 @@ MonsterStrategy
Ghost Saber Expand Spd = 14px Ghost Saber Expand Spd = 14px
# What HP % the boss throws a coin at the player, applying a curse, and hiding from the player. # 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 # How much time before the curse starts dealing damage to the player
Curse Damage Wait Time = 10s Curse Damage Wait Time = 10s
# How much % damage the curse does to the player every second. # How much % damage the curse does to the player every second.
@ -1358,6 +1358,8 @@ MonsterStrategy
Coin Toss Pause Time = 2s Coin Toss Pause Time = 2s
# Highest Z position the coin is tossed up. # Highest Z position the coin is tossed up.
Coin Toss Rise Amount = 72px Coin Toss Rise Amount = 72px
Shrapnel Hiding Shot Delay = 5.0s
} }
Pirate's Treasure Pirate's Treasure
{ {

Loading…
Cancel
Save