diff --git a/Adventures in Lestoria/CharacterMenuWindow.cpp b/Adventures in Lestoria/CharacterMenuWindow.cpp index 87fa124e..960f3e3c 100644 --- a/Adventures in Lestoria/CharacterMenuWindow.cpp +++ b/Adventures in Lestoria/CharacterMenuWindow.cpp @@ -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{{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>&equips=Inventory::get("Equipment"); - const std::vector>&accessories=Inventory::get("Accessories"); - std::vector>availableEquipment; - std::copy_if(equips.begin(),equips.end(),std::back_inserter(availableEquipment),[&](const std::shared_ptrit){ - return it->GetEquipSlot()&slot; - }); - std::copy_if(accessories.begin(),accessories.end(),std::back_inserter(availableEquipment),[&](const std::shared_ptrit){ - return it->GetEquipSlot()&slot; - }); - - std::shared_ptrequipList=Component(data.component.lock()->parentMenu,"Equip List"); - equipList->RemoveAllComponents(); - for(int counter=0;const std::weak_ptrit:availableEquipment){ - std::shared_ptrequip; - const bool isAccessorySlot=slot&(EquipSlot::RING1|EquipSlot::RING2); - if(isAccessorySlot){ - equip=CharacterMenuWindow::GenerateItemDisplay(equipList,counter,it); - }else{ - equip=CharacterMenuWindow::GenerateItemDisplay(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>&equips=Inventory::get("Equipment"); + const std::vector>&accessories=Inventory::get("Accessories"); + std::vector>availableEquipment; + std::copy_if(equips.begin(),equips.end(),std::back_inserter(availableEquipment),[&](const std::shared_ptrit){ + return it->GetEquipSlot()&slot; + }); + std::copy_if(accessories.begin(),accessories.end(),std::back_inserter(availableEquipment),[&](const std::shared_ptrit){ + return it->GetEquipSlot()&slot; + }); + + std::shared_ptrequipList=Component(data.component.lock()->parentMenu,"Equip List"); + equipList->RemoveAllComponents(); + for(int counter=0;const std::weak_ptrit:availableEquipment){ + std::shared_ptrequip; + const bool isAccessorySlot=slot&(EquipSlot::RING1|EquipSlot::RING2); + if(isAccessorySlot){ + equip=CharacterMenuWindow::GenerateItemDisplay(equipList,counter,it); + }else{ + equip=CharacterMenuWindow::GenerateItemDisplay(equipList,counter,it); + } - equip->SetHoverFunc( - [&](MenuFuncData data){ - if(!data.component.lock()->GetSubcomponentParent().expired())return true; - std::weak_ptrbutton=DYNAMIC_POINTER_CAST(data.component.lock()); - if(!button.expired()){ - const std::weak_ptrbuttonItem=button.lock()->GetItem(); - std::vectorstatsBeforeEquip; - 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_ptrequippedItem=Inventory::GetEquip(slot); - std::weak_ptrotherItem; - 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_ptrstatDisplayLabel=Component(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_ptrbutton=DYNAMIC_POINTER_CAST(data.component.lock()); + if(!button.expired()){ + const std::weak_ptrbuttonItem=button.lock()->GetItem(); + std::vectorstatsBeforeEquip; + 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_ptrequippedItem=Inventory::GetEquip(slot); + std::weak_ptrotherItem; + 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_ptrstatDisplayLabel=Component(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(data.menu.GetType(),"Item Name")->SetItem(buttonItem); + Component(data.menu.GetType(),"Item Description")->SetItem(buttonItem); + Component(data.menu.GetType(),"Item Name")->Enable(); + Component(data.menu.GetType(),"Item Description")->Enable(); + }else{ + ERR("WARNING! Attempting to cast a button that isn't a RowItemDisplay!"); } - Component(data.menu.GetType(),"Item Name")->SetItem(buttonItem); - Component(data.menu.GetType(),"Item Description")->SetItem(buttonItem); - Component(data.menu.GetType(),"Item Name")->Enable(); - Component(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_ptrstatDisplayLabel=Component(CHARACTER_MENU,"Attribute "+std::string(ItemAttribute::Get(attribute.attrName).Name())+" Label"); - statDisplayLabel.lock()->SetStatChangeAmt(0); - counter++; - } - Component(data.menu.GetType(),"Item Name")->Disable(); - Component(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_ptrstatDisplayLabel=Component(CHARACTER_MENU,"Attribute "+std::string(ItemAttribute::Get(attribute.attrName).Name())+" Label"); + statDisplayLabel.lock()->SetStatChangeAmt(0); + counter++; + } + Component(data.menu.GetType(),"Item Name")->Disable(); + Component(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(data.component.lock()->parentMenu,"Equip Selection Outline")->Enable(); - equipList->Enable(); - Component(data.component.lock()->parentMenu,"Equip Selection Bottom Outline")->Enable(); - Component(data.component.lock()->parentMenu,"Equip Selection Select Button")->Enable(); - Component(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&component){ - return !ISBLANK(Inventory::GetEquip(slot))&&component.lock()->GetSubcomponentParent().expired()&&&*DYNAMIC_POINTER_CAST(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(data.component.lock()->parentMenu,"Equip Selection Outline")->Enable(); + equipList->Enable(); + Component(data.component.lock()->parentMenu,"Equip Selection Bottom Outline")->Enable(); + Component(data.component.lock()->parentMenu,"Equip Selection Select Button")->Enable(); + Component(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&component){ + return !ISBLANK(Inventory::GetEquip(slot))&&component.lock()->GetSubcomponentParent().expired()&&&*DYNAMIC_POINTER_CAST(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(data.component.lock())->GetSlot(); const std::weak_ptrequip=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){ diff --git a/Adventures in Lestoria/GhostOfPirateCaptain.cpp b/Adventures in Lestoria/GhostOfPirateCaptain.cpp index c3535a61..6e4d3a84 100644 --- a/Adventures in Lestoria/GhostOfPirateCaptain.cpp +++ b/Adventures in Lestoria/GhostOfPirateCaptain.cpp @@ -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(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(Oscillator{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; } diff --git a/Adventures in Lestoria/Monster.cpp b/Adventures in Lestoria/Monster.cpp index 6f0567b3..15a38ea2 100644 --- a/Adventures in Lestoria/Monster.cpp +++ b/Adventures in Lestoria/Monster.cpp @@ -70,7 +70,7 @@ safemap>STRATEGY_DAT std::unordered_mapMonsterData::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::functionfunc){ + 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{ diff --git a/Adventures in Lestoria/Monster.h b/Adventures in Lestoria/Monster.h index e2a1dec9..c523e2d9 100644 --- a/Adventures in Lestoria/Monster.h +++ b/Adventures in Lestoria/Monster.h @@ -292,9 +292,12 @@ private: NPCData npcData; float lastPathfindingCooldown=0.f; std::functionstrategyDeathFunc{}; + std::functionstrategyOnHitFunc{}; //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::functionfunc); + // 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::functionfunc); //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{}; OscillatorfloatOscillator{0.f,8.f,0.5f}; + bool arrowIndicator{false}; struct STRATEGY{ static std::string ERR; diff --git a/Adventures in Lestoria/MonsterAttribute.h b/Adventures in Lestoria/MonsterAttribute.h index 46d2e4ad..bea35a08 100644 --- a/Adventures in Lestoria/MonsterAttribute.h +++ b/Adventures in Lestoria/MonsterAttribute.h @@ -179,5 +179,6 @@ enum class Attribute{ TOSS_COIN_WAIT_TIMER, TOSS_COIN_TARGET, LAST_COLLISION_TIMER, - + HIDING_POS, + SHRAPNEL_CANNON_TIMER, }; \ No newline at end of file diff --git a/Adventures in Lestoria/TMXParser.h b/Adventures in Lestoria/TMXParser.h index 8e648a1d..8511d7fe 100644 --- a/Adventures in Lestoria/TMXParser.h +++ b/Adventures in Lestoria/TMXParser.h @@ -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&zones=parsedMapInfo.ZoneData[newTag.data["type"]]; float width=1.f; float height=1.f; diff --git a/Adventures in Lestoria/Version.h b/Adventures in Lestoria/Version.h index 87e53357..ed081ef7 100644 --- a/Adventures in Lestoria/Version.h +++ b/Adventures in Lestoria/Version.h @@ -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 diff --git a/Adventures in Lestoria/assets/config/MonsterStrategies.txt b/Adventures in Lestoria/assets/config/MonsterStrategies.txt index 0c974b58..6aae6127 100644 --- a/Adventures in Lestoria/assets/config/MonsterStrategies.txt +++ b/Adventures in Lestoria/assets/config/MonsterStrategies.txt @@ -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 { diff --git a/x64/Release/Adventures in Lestoria.exe b/x64/Release/Adventures in Lestoria.exe index 72952115..ea0e13af 100644 Binary files a/x64/Release/Adventures in Lestoria.exe and b/x64/Release/Adventures in Lestoria.exe differ