Compare commits

..

80 Commits
demo ... master

Author SHA1 Message Date
sigonasr2 c9a29b2705 Fix arc not showing up. 3-5 had a missing spawn point set. More Octopus Arm behavior fixes. Release Build 11808. 1 week ago
sigonasr2 9b94a90326 Octopus Boss implementation 1 week ago
Quapsel b7e486690d Monstespawns for 3_7 & 3_8 finished. 2 weeks ago
Quapsel f1bf83eb17 Monster spawns done for 3_5, 3_6 & 3_B1 2 weeks ago
sigonasr2 ef1d4e5470 Make lerp functions require both arguments to be the same for deduced return type. Fix buggy monster spawns to account for monster sizes. BOTTOM RIGHT EDGE FOR COORDINATES ??? Release Build 11794. 2 weeks ago
sigonasr2 10b8e68b22 Merge branch 'master' of http://sig.projectdivar.com/sigonasr2/AdventuresInLestoria 2 weeks ago
sigonasr2 f5038d4a24 Add in more AI. 2 weeks ago
Quapsel 6e435a5c54 Monster Spawns for 3_3 and 3_4 added. Spawn Group locations for all chapter 3 maps finished. 3 weeks ago
Quapsel 5e02dd6590 3_2 Monste spawns finished. 3_3 - 3_6 Spawn Groups placed. 3 weeks ago
Quapsel 19dcf0fad0 Monster Spawns for 3_1. 3 weeks ago
sigonasr2 debc23bfa3 Additional AI setup. 3 weeks ago
sigonasr2 81bfbfb860 Add map setup and monster templates for Octopus fight. 3 weeks ago
sigonasr2 7cf3c53e91 Fix all octopus arm AI config variable references to actually use the Config functions instead of direct config file parameters. Add in attack cone effect. 3 weeks ago
sigonasr2 139d4f4eac Merge branch 'master' of http://sig.projectdivar.com/sigonasr2/AdventuresInLestoria 3 weeks ago
sigonasr2 16708108e6 Move Attack Arc function into its own function for portability. 3 weeks ago
Quapsel 61b8efbb9f Chapter 4 presets completed. 4_1 Added. 4 weeks ago
Quapsel f181fca2a1 hub border, border extension in 1 random map, chapter 4 presets almost finished. 4 weeks ago
sigonasr2 46ba7a780d Octopus arm AI 1 month ago
sigonasr2 7b165a64d5 Arc implementation completed. Release Build 11776. 1 month ago
sigonasr2 d85cef1b86 Arcs, Cones, Polygon overlap code. Octopus Arm AI initialized. 1 month ago
sigonasr2 1bb0ceb096 Add monster entries and animations for Giant Octopus boss and Octopus Arm. Release Build 11756. 1 month ago
sigonasr2 3a3d405272 Implemented lingering effect item script capabilities. Added Molotov item and item graphic. Added burning sound effect. Release Build 11755. 1 month ago
sigonasr2 868a089666 Separate ThrowingProjectiles from PoisonBottle class to create generic throwable item bullets. Added throwable projectile item script. Made Bomb item functional. Release Build 11748. 1 month ago
sigonasr2 8c3dd0f071 [demo] Fix stage alpha transitions in story view. Release Build 11576. 1 month ago
sigonasr2 2b0c1c070a [demo] Add Warrior story image to list of files. Missing asset. Release Build 11575. 1 month ago
sigonasr2 598b3b66c7 Add new chapter 3 consumables to item database and placeholder icons. 1 month ago
sigonasr2 b11d209813 Implemented Sandworm AI. Change added velocity function to be a standalone velocity variable added to original player velocity. Release Build 11725. 2 months ago
sigonasr2 446be1e02f Add collision intangibility for monsters with a collision radius of zero. Add unit test to check for it. Release Build 11714. 2 months ago
sigonasr2 e0644fe809 Sandworm implementation + Monster collision radius changing implemented. 2 months ago
sigonasr2 e9f02ea885 Move player invulnerability frame transparency from affecting globally to all monsters to affecting only the player's sprite. Release Build 11711. 2 months ago
sigonasr2 2f441f76b7 Implement missing SetAbility4 function stub. Add HasBuff function to both the Player and Monster classes. Implemented Giant Crab AI. 219/219 tests passing. Release Build 11710. 2 months ago
sigonasr2 3684e0f211 Implemented Crab AI. Release Build 11683. 2 months ago
sigonasr2 94de6bb10e Remove hidden incorrect collision on various stages. Release Build 11674. 2 months ago
sigonasr2 9ec850fdd0 Implemented Seagull AI. Release Build 11673. 2 months ago
sigonasr2 6b0c17b5ff Add ambient wave and gull sounds to Chapter 3 stages. Add slight pitch variance to environmental audio. Release Build 11667. 2 months ago
sigonasr2 aae5726f3d Finish implementing Pirate Captain and Parrot behaviors. Refactor monster phase system to be per-strategy instead of a global phase for all strategies. Release Build 11666. 2 months ago
sigonasr2 a09ea9a9a7 Merge branch 'master' of http://sig.projectdivar.com/sigonasr2/AdventuresInLestoria 2 months ago
sigonasr2 422c6f02cc Update Chapter 3 maps and entries to be playable. Added Chapter 3 stage and boss music. Pirate Captain base AI completed, mounted parrot AI added. Release Build 11657. 2 months ago
Quapsel 967af0fe5f Added details to the world map. 2 months ago
sigonasr2 3024111b4a Remove unnecessary variables from old monster update code, remove return value, create unnecessary ambiguity with why we would need to return true/false for no purpose. 2 months ago
sigonasr2 970b15ac5d Parrot Monster scripts initialized. 2 months ago
sigonasr2 20f8abf8b5 Fix Parrot spritesheet size. Pirate Captain behavior fixed (cannot directly inherit from Goblin Dagger, copy-paste AI instead due to conflicting states). Release Build 11653. 2 months ago
sigonasr2 5f63f23635 Add missing item icons (placeholders). Add mounted parrot to Pirate Captain. Release Build 11649. 2 months ago
sigonasr2 d432966e3e Worked on Pirate Captain base AI. (Mounted Parrot AI still needs implemented). 2 months ago
sigonasr2 4e1fa8d4e3 Merge branch 'master' of http://sig.projectdivar.com/sigonasr2/AdventuresInLestoria 2 months ago
sigonasr2 c16dccc623 Begin Pirate Captain AI implementation. 2 months ago
Quapsel 888cea1581 „Adventures in Lestoria/assets/config/items/ItemDatabase.txt“ ändern 2 months ago
sigonasr2 da2640fbc1 Health and mana indicators don't get squished strangely anymore due to floating point values. Release Build 11638. 2 months ago
sigonasr2 85ba978793 Fix new crashes pertaining to unchecked 0 distance in line normalizing functions. 2 months ago
sigonasr2 0e3a4c0ba7 Implemented Pirate Buccaneer AI. Remove unnecessary Run Towards overrides in monsters list. Fix Charged Arrow to use proper last graphic when a custom one is specified. Release Build 11621. 2 months ago
sigonasr2 d6c2b6c87f Implement Pirate Marauder AI. Release Build 11609. 2 months ago
sigonasr2 9509f317c3 Implement Pirate strategy. Refactor Dagger Stab/Slash slightly. After loading a resource pack, if it previously failed to load attempt the load again to stop game crashes with modifications to the game pack. Release Build 11602. 2 months ago
sigonasr2 90d9bc86b1 Add in templates, strategies, and setup for Chapter 3 monsters. Fix tile collisions on Chapter 3 tiles. Release Build 11595. 2 months ago
sigonasr2 8f5973d836 Crab and Giant Crab monster entries added. 2 months ago
sigonasr2 21c5af80c2 Pirate Marauder entry added. 2 months ago
sigonasr2 612cda41c3 Pirate Marauder/Pirate Captain data added. 2 months ago
sigonasr2 07f431a5aa Prep Pirate monster entry. 2 months ago
sigonasr2 0f0a70b2b4 Implement new enchant rolling rule: Same enchant may be chosen if the result ends up increasing at least of the supported stats. Release Build 11592. 2 months ago
sigonasr2 880b8e4335 Implement default audio event property to stages. Add fanfare transition and post-boss song event to the game. Add appropriate triggers for when boss fight completes. Addresses Issue #66. Release Build 11576. 2 months ago
sigonasr2 2fe0991920 Push demo fixes into master branch. 2 months ago
sigonasr2 4d74d803b2 Linux fixes. Make sure audio doesn't require loading from filesystem (Use resource packs instead) 2 months ago
sigonasr2 57cc852033 Force all old decal instances to be cleared from the engine when the game is reset. This addresses a very random Linux crash that was occurring enough to be a problem when quitting the game. Release Build 11570. 2 months ago
sigonasr2 ca46f0b92e Update distribution scripts with cleaning out the monsters folder. 2 months ago
sigonasr2 004f896953 MonsterData should be loading graphics from Resource Packs instead. This should solve the issues with Issue #70. Release Build 11566. 2 months ago
sigonasr2 669a267a67 [demo] Remove hash verification functions from the game. See Issue #68. Release Build 11565. 2 months ago
sigonasr2 47ff285bef Remove songs from git index into commercial assets. Update distribution and build scripts to not include songs commercial folder. Fix early destruction of audio buffer data causing a crash. Release Build 11544. 2 months ago
sigonasr2 a16ca296c7 Linux changes and fixes 2 months ago
sigonasr2 d0db3412d9 Added controller compatibility to the refine result and enchant confirmation windows. Modify menu navigation behavior slightly for the three windows with accessory lists. Release Build 11665. 3 months ago
sigonasr2 36b9a071e3 Added controller compatibility to Artificer Enchant Window. Removed menu navigation using right analog stick. Release Build 11662. 3 months ago
sigonasr2 da5d596a37 Implemented controller compatbility for Artificer Disassembly Menu. Release Build 11653. 3 months ago
sigonasr2 86e2976549 Added some menu scrolling helper functions. Implemented Artificer Enchant window. Added controller compatibility to Artificer Refining Window. Release Build 11652. 3 months ago
sigonasr2 a5c10f35e6 Updated MenuItemLabel to accept either a copy or weak reference so that it can hold information not necessarily in our inventory. Release Build 11620. 3 months ago
sigonasr2 d007e2bbf5 Fix key display for Ability 4 to match the keybind of the 4th ability. Release Build 11599. 3 months ago
sigonasr2 1e7345d7b5 Implement skeleton for Enchant Confirm Window. Make original ability functions static for player class ability retrieval. Add private static access internal functions. Remove check for non-existing animations for player (would just not change the animation if it doesn't exist). Release Build 11598. 3 months ago
sigonasr2 0d7759b230 Fix a unit test calling BGM pitch change function (which doesn't need a test). Release Build 11564. 3 months ago
sigonasr2 796c0da6d0 Add back in correct World Map with stage plates for master branch. 3 months ago
sigonasr2 55f4347452 Update master build demo flag (set to false) and admin mode (set to true). Release Build 11562. 3 months ago
sigonasr2 8283147efe [demo] Add congratulatory message for completing the demo. Version updated to 1.3. Release Build 11561. 3 months ago
sigonasr2 fb66fcb04d [demo] Make the 3 class variations unselectable in demo build. Release Build 11558. 3 months ago
sigonasr2 e89dfd168e [demo] Update demo build to newest engine. Block off world map navigation beyond Camp II for demo build. Fix bug with file trimming ignoring escape backslash characters in hash causing invalid file hashes. Disable admin mode for demo release build. Release Build 11555. 3 months ago
  1. 36
      Adventures in Lestoria Tests/BuffTests.cpp
  2. 40
      Adventures in Lestoria Tests/EnchantTests.cpp
  3. 2
      Adventures in Lestoria Tests/GameHelper.h
  4. 62
      Adventures in Lestoria Tests/ItemTests.cpp
  5. 49
      Adventures in Lestoria Tests/MonsterTests.cpp
  6. 20
      Adventures in Lestoria Tests/PlayerTests.cpp
  7. 34
      Adventures in Lestoria/Adventures in Lestoria.tiled-project
  8. 33
      Adventures in Lestoria/Adventures in Lestoria.vcxproj
  9. 53
      Adventures in Lestoria/Adventures in Lestoria.vcxproj.filters
  10. 48
      Adventures in Lestoria/AdventuresInLestoria.cpp
  11. 4
      Adventures in Lestoria/AdventuresInLestoria.h
  12. 11
      Adventures in Lestoria/Animation.cpp
  13. 79
      Adventures in Lestoria/Arc.cpp
  14. 59
      Adventures in Lestoria/Arc.h
  15. 60
      Adventures in Lestoria/ArtificerDisassembleWindow.cpp
  16. 73
      Adventures in Lestoria/ArtificerEnchantConfirmWindow.cpp
  17. 117
      Adventures in Lestoria/ArtificerEnchantWindow.cpp
  18. 7
      Adventures in Lestoria/ArtificerRefineResultWindow.cpp
  19. 56
      Adventures in Lestoria/ArtificerRefineWindow.cpp
  20. 15
      Adventures in Lestoria/ArtificerWindow.cpp
  21. 26
      Adventures in Lestoria/Attributable.h
  22. 3
      Adventures in Lestoria/AttributableStat.h
  23. 9
      Adventures in Lestoria/Audio.cpp
  24. 5
      Adventures in Lestoria/Audio.h
  25. 8
      Adventures in Lestoria/Bear.cpp
  26. 10
      Adventures in Lestoria/Boar.cpp
  27. 2
      Adventures in Lestoria/Bomb.cpp
  28. 4
      Adventures in Lestoria/BreakingPillar.cpp
  29. 2
      Adventures in Lestoria/Bullet.cpp
  30. 2
      Adventures in Lestoria/Bullet.h
  31. 29
      Adventures in Lestoria/BulletTypes.h
  32. 22
      Adventures in Lestoria/CharacterMenuWindow.cpp
  33. 11
      Adventures in Lestoria/ChargedArrow.cpp
  34. 118
      Adventures in Lestoria/Crab.cpp
  35. 2
      Adventures in Lestoria/CraftItemWindow.cpp
  36. 5
      Adventures in Lestoria/DEFINES.h
  37. 4
      Adventures in Lestoria/DaggerSlash.cpp
  38. 4
      Adventures in Lestoria/DaggerStab.cpp
  39. 2
      Adventures in Lestoria/DamageNumber.cpp
  40. 6
      Adventures in Lestoria/Effect.h
  41. 1
      Adventures in Lestoria/EnvironmentalAudio.cpp
  42. 2
      Adventures in Lestoria/EquipSlotButton.h
  43. 12
      Adventures in Lestoria/Frog.cpp
  44. 2
      Adventures in Lestoria/FrogTongue.cpp
  45. 84
      Adventures in Lestoria/GiantCrab.cpp
  46. 85
      Adventures in Lestoria/GiantOctopus.cpp
  47. 9
      Adventures in Lestoria/Goblin_Boar_Rider.cpp
  48. 4
      Adventures in Lestoria/Goblin_Bomb.cpp
  49. 10
      Adventures in Lestoria/Goblin_Bow.cpp
  50. 12
      Adventures in Lestoria/Goblin_Dagger.cpp
  51. 10
      Adventures in Lestoria/Hawk.cpp
  52. 6
      Adventures in Lestoria/IBullet.cpp
  53. 2
      Adventures in Lestoria/IBullet.h
  54. 147
      Adventures in Lestoria/Item.cpp
  55. 35
      Adventures in Lestoria/Item.h
  56. 6
      Adventures in Lestoria/ItemDrop.cpp
  57. 26
      Adventures in Lestoria/ItemEnchant.cpp
  58. 6
      Adventures in Lestoria/ItemEnchant.h
  59. 102
      Adventures in Lestoria/ItemScript.cpp
  60. 38
      Adventures in Lestoria/Menu.cpp
  61. 4
      Adventures in Lestoria/Menu.h
  62. 6
      Adventures in Lestoria/MenuItemButton.h
  63. 8
      Adventures in Lestoria/MenuItemItemButton.h
  64. 40
      Adventures in Lestoria/MenuItemLabel.h
  65. 10
      Adventures in Lestoria/MenuType.h
  66. 4
      Adventures in Lestoria/Merchant.cpp
  67. 133
      Adventures in Lestoria/Monster.cpp
  68. 51
      Adventures in Lestoria/Monster.h
  69. 13
      Adventures in Lestoria/MonsterAttribute.h
  70. 6
      Adventures in Lestoria/MonsterData.cpp
  71. 2
      Adventures in Lestoria/MonsterData.h
  72. 3
      Adventures in Lestoria/MonsterStrategyHelpers.h
  73. 4
      Adventures in Lestoria/NPC.cpp
  74. 179
      Adventures in Lestoria/OctopusArm.cpp
  75. 49
      Adventures in Lestoria/Oktopus boss.txt
  76. 1
      Adventures in Lestoria/Oscillator.h
  77. 74
      Adventures in Lestoria/Parrot.cpp
  78. 118
      Adventures in Lestoria/Pirate_Buccaneer.cpp
  79. 183
      Adventures in Lestoria/Pirate_Captain.cpp
  80. 175
      Adventures in Lestoria/Pirate_Marauder.cpp
  81. 45
      Adventures in Lestoria/Player.cpp
  82. 95
      Adventures in Lestoria/Player.h
  83. 1
      Adventures in Lestoria/PlayerTimerType.h
  84. 30
      Adventures in Lestoria/PoisonBottle.cpp
  85. 8
      Adventures in Lestoria/PoisonPool.cpp
  86. 10
      Adventures in Lestoria/RUN_STRATEGY.cpp
  87. 2
      Adventures in Lestoria/RowItemDisplay.h
  88. 4
      Adventures in Lestoria/RunTowards.cpp
  89. 139
      Adventures in Lestoria/Sandworm.cpp
  90. 32
      Adventures in Lestoria/ScrollableWindowComponent.h
  91. 86
      Adventures in Lestoria/Seagull.cpp
  92. 30
      Adventures in Lestoria/SlimeKing.cpp
  93. 22
      Adventures in Lestoria/SoundEffect.cpp
  94. 5
      Adventures in Lestoria/SoundEffect.h
  95. 2
      Adventures in Lestoria/State_Death.cpp
  96. 38
      Adventures in Lestoria/StoneGolem.cpp
  97. 16
      Adventures in Lestoria/Stone_Elemental.cpp
  98. 8
      Adventures in Lestoria/TMXParser.h
  99. 36
      Adventures in Lestoria/TODO.txt
  100. 95
      Adventures in Lestoria/ThrownProjectile.cpp
  101. Some files were not shown because too many files have changed in this diff Show More

@ -125,5 +125,41 @@ namespace BuffTests
Game::Update(3.f);
Assert::AreEqual(95,game->GetPlayer()->GetHealth(),L"Player should have taken 5 health from the expired callback provided.");
}
TEST_METHOD(MonsterHasBuffFunctionTest){
Monster&m{game->SpawnMonster({},MONSTER_DATA.at("TestName"))};
Game::Update(0.f);
Assert::AreEqual(false,m.HasBuff(BuffType::SPEEDBOOST),L"Monster should not have a speedboost buff before being given one.");
m.AddBuff(BuffType::ADRENALINE_RUSH,1.f,1.f);
Assert::AreEqual(false,m.HasBuff(BuffType::SPEEDBOOST),L"Monster should not have a speedboost buff when given an unrelated buff.");
m.AddBuff(BuffType::SPEEDBOOST,1.f,1.f);
Assert::AreEqual(true,m.HasBuff(BuffType::SPEEDBOOST),L"Monster should now have a speedboost buff.");
m.AddBuff(BuffType::SPEEDBOOST,2.f,1.f);
Assert::AreEqual(true,m.HasBuff(BuffType::SPEEDBOOST),L"Monster should still report having a speedboost buff.");
Game::Update(0.f);
Game::Update(1.f);
Assert::AreEqual(true,m.HasBuff(BuffType::SPEEDBOOST),L"Monster should still have one speedboost buff.");
Game::Update(1.f);
Assert::AreEqual(false,m.HasBuff(BuffType::SPEEDBOOST),L"Monster should no longer have a speedboost buff.");
m.AddBuff(BuffType::SPEEDBOOST,1.f,1.f);
m.RemoveBuff(BuffType::SPEEDBOOST);
Assert::AreEqual(false,m.HasBuff(BuffType::SPEEDBOOST),L"Monster should no longer have a speedboost buff.");
}
TEST_METHOD(PlayerHasBuffFunctionTest){
Assert::AreEqual(false,game->GetPlayer()->HasBuff(BuffType::SPEEDBOOST),L"Player should not have a speedboost buff before being given one.");
game->GetPlayer()->AddBuff(BuffType::ADRENALINE_RUSH,1.f,1.f);
Assert::AreEqual(false,game->GetPlayer()->HasBuff(BuffType::SPEEDBOOST),L"Player should not have a speedboost buff when given an unrelated buff.");
game->GetPlayer()->AddBuff(BuffType::SPEEDBOOST,1.f,1.f);
Assert::AreEqual(true,game->GetPlayer()->HasBuff(BuffType::SPEEDBOOST),L"Player should now have a speedboost buff.");
game->GetPlayer()->AddBuff(BuffType::SPEEDBOOST,2.f,1.f);
Assert::AreEqual(true,game->GetPlayer()->HasBuff(BuffType::SPEEDBOOST),L"Player should still report having a speedboost buff.");
Game::Update(0.f);
Game::Update(1.f);
Assert::AreEqual(true,game->GetPlayer()->HasBuff(BuffType::SPEEDBOOST),L"Player should still have one speedboost buff.");
Game::Update(1.f);
Assert::AreEqual(false,game->GetPlayer()->HasBuff(BuffType::SPEEDBOOST),L"Player should no longer have a speedboost buff.");
game->GetPlayer()->AddBuff(BuffType::SPEEDBOOST,1.f,1.f);
game->GetPlayer()->RemoveBuff(BuffType::SPEEDBOOST);
Assert::AreEqual(false,game->GetPlayer()->HasBuff(BuffType::SPEEDBOOST),L"Player should no longer have a speedboost buff.");
}
};
}

@ -118,13 +118,13 @@ namespace EnchantTests
std::weak_ptr<Item>nullRing{Game::GiveAndEquipEnchantedRing("Aura of the Beast")};
std::unordered_map<int,uint32_t>statDistribution;
for(int i:std::ranges::iota_view(0,1000)){
nullRing.lock()->EnchantItem("Health Boost");
nullRing.lock()->_EnchantItem(ItemEnchant{"Health Boost"});
statDistribution[player->GetMaxHealth()]++;
}
Assert::AreEqual(size_t(3),statDistribution.size(),L"There should be three entries generated. If not, then the RNG picking is likely not working!");
std::weak_ptr<Item>nullRing2{Game::GiveAndEquipEnchantedRing("Aura of the Beast",EquipSlot::RING2)};
for(int i:std::ranges::iota_view(0,1000)){
nullRing2.lock()->EnchantItem("Health Boost");
nullRing2.lock()->_EnchantItem(ItemEnchant{"Health Boost"});
Test::InRange(player->GetMaxHealth(),{106,110},L"Max Health not in expected range with two rings equipped.");
}
}
@ -133,12 +133,12 @@ namespace EnchantTests
Assert::AreEqual(100,player->GetAttack(),L"Player starts with 100 attack.");
std::weak_ptr<Item>nullRing{Game::GiveAndEquipEnchantedRing("Attack Boost")};
for(int i:std::ranges::iota_view(0,1000)){
nullRing.lock()->EnchantItem("Attack Boost");
nullRing.lock()->_EnchantItem(ItemEnchant{"Attack Boost"});
Test::InRange(player->GetAttack(),{103,105},L"Attack not in expected range.");
}
std::weak_ptr<Item>nullRing2{Game::GiveAndEquipEnchantedRing("Attack Boost",EquipSlot::RING2)};
for(int i:std::ranges::iota_view(0,1000)){
nullRing2.lock()->EnchantItem("Attack Boost");
nullRing2.lock()->_EnchantItem(ItemEnchant{"Attack Boost"});
Test::InRange(player->GetAttack(),{106,110},L"Attack not in expected range with two rings equipped.");
}
}
@ -146,12 +146,12 @@ namespace EnchantTests
Assert::AreEqual(100.0_Pct,player->GetMoveSpdMult(),L"Player starts with 100% Movespd.");
std::weak_ptr<Item>nullRing{Game::GiveAndEquipEnchantedRing("Movement Boost")};
for(int i:std::ranges::iota_view(0,1000)){
nullRing.lock()->EnchantItem("Movement Boost");
nullRing.lock()->_EnchantItem(ItemEnchant{"Movement Boost"});
Test::InRange(player->GetMoveSpdMult(),{103.0_Pct,105.0_Pct},L"Move Speed not in expected range.");
}
std::weak_ptr<Item>nullRing2{Game::GiveAndEquipEnchantedRing("Movement Boost",EquipSlot::RING2)};
for(int i:std::ranges::iota_view(0,1000)){
nullRing2.lock()->EnchantItem("Movement Boost");
nullRing2.lock()->_EnchantItem(ItemEnchant{"Movement Boost"});
Test::InRange(player->GetMoveSpdMult(),{106.0_Pct,110.0_Pct},L"Move Speed not in expected range with two rings equipped.");
}
}
@ -159,12 +159,12 @@ namespace EnchantTests
Assert::AreEqual(0.0_Pct,player->GetCooldownReductionPct(),L"Player starts with 0% CDR.");
std::weak_ptr<Item>nullRing{Game::GiveAndEquipEnchantedRing("Ability Haste")};
for(int i:std::ranges::iota_view(0,1000)){
nullRing.lock()->EnchantItem("Ability Haste");
nullRing.lock()->_EnchantItem(ItemEnchant{"Ability Haste"});
Test::InRange(player->GetCooldownReductionPct(),{3.0_Pct,5.0_Pct},L"CDR not in expected range.");
}
std::weak_ptr<Item>nullRing2{Game::GiveAndEquipEnchantedRing("Ability Haste",EquipSlot::RING2)};
for(int i:std::ranges::iota_view(0,1000)){
nullRing2.lock()->EnchantItem("Ability Haste");
nullRing2.lock()->_EnchantItem(ItemEnchant{"Ability Haste"});
Test::InRange(player->GetCooldownReductionPct(),{6.0_Pct,10.0_Pct},L"CDR not in expected range with two rings.");
}
}
@ -172,12 +172,12 @@ namespace EnchantTests
Assert::AreEqual(0.0_Pct,player->GetCritRatePct(),L"Player starts with 0% Crit Rate.");
std::weak_ptr<Item>nullRing{Game::GiveAndEquipEnchantedRing("Crit Rate")};
for(int i:std::ranges::iota_view(0,1000)){
nullRing.lock()->EnchantItem("Crit Rate");
nullRing.lock()->_EnchantItem(ItemEnchant{"Crit Rate"});
Test::InRange(player->GetCritRatePct(),{3.0_Pct,5.0_Pct},L"Crit Rate not in expected range.");
}
std::weak_ptr<Item>nullRing2{Game::GiveAndEquipEnchantedRing("Crit Rate",EquipSlot::RING2)};
for(int i:std::ranges::iota_view(0,1000)){
nullRing2.lock()->EnchantItem("Crit Rate");
nullRing2.lock()->_EnchantItem(ItemEnchant{"Crit Rate"});
Test::InRange(player->GetCritRatePct(),{6.0_Pct,10.0_Pct},L"Crit Rate not in expected range with two rings.");
}
}
@ -185,12 +185,12 @@ namespace EnchantTests
Assert::AreEqual(50.0_Pct,player->GetCritDmgPct(),L"Player starts with 50% Crit Damage.");
std::weak_ptr<Item>nullRing{Game::GiveAndEquipEnchantedRing("Crit Damage")};
for(int i:std::ranges::iota_view(0,1000)){
nullRing.lock()->EnchantItem("Crit Damage");
nullRing.lock()->_EnchantItem(ItemEnchant{"Crit Damage"});
Test::InRange(player->GetCritDmgPct(),{57.0_Pct,60.0_Pct},L"Crit Damage not in expected range.");
}
std::weak_ptr<Item>nullRing2{Game::GiveAndEquipEnchantedRing("Crit Damage",EquipSlot::RING2)};
for(int i:std::ranges::iota_view(0,1000)){
nullRing2.lock()->EnchantItem("Crit Damage");
nullRing2.lock()->_EnchantItem(ItemEnchant{"Crit Damage"});
Test::InRange(player->GetCritDmgPct(),{64.0_Pct,70.0_Pct},L"Crit Damage not in expected range with two rings.");
}
}
@ -198,12 +198,12 @@ namespace EnchantTests
Assert::AreEqual(0.0_Pct,player->GetDamageReductionFromBuffs(),L"Player starts with 0% Damage Reduction.");
std::weak_ptr<Item>nullRing{Game::GiveAndEquipEnchantedRing("Stoneskin")};
for(int i:std::ranges::iota_view(0,1000)){
nullRing.lock()->EnchantItem("Stoneskin");
nullRing.lock()->_EnchantItem(ItemEnchant{"Stoneskin"});
Test::InRange(player->GetDamageReductionFromBuffs(),{3.0_Pct,5.0_Pct},L"Damage Reduction not in expected range.");
}
std::weak_ptr<Item>nullRing2{Game::GiveAndEquipEnchantedRing("Stoneskin",EquipSlot::RING2)};
for(int i:std::ranges::iota_view(0,1000)){
nullRing2.lock()->EnchantItem("Stoneskin");
nullRing2.lock()->_EnchantItem(ItemEnchant{"Stoneskin"});
Test::InRange(player->GetDamageReductionFromBuffs(),{6.0_Pct,10.0_Pct},L"Damage Reduction not in expected range with two rings.");
}
}
@ -211,12 +211,12 @@ namespace EnchantTests
Assert::AreEqual(100,player->GetMaxMana(),L"Player starts with 100 mana.");
std::weak_ptr<Item>nullRing{Game::GiveAndEquipEnchantedRing("Mana Pool")};
for(int i:std::ranges::iota_view(0,1000)){
nullRing.lock()->EnchantItem("Mana Pool");
nullRing.lock()->_EnchantItem(ItemEnchant{"Mana Pool"});
Test::InRange(player->GetMaxMana(),{107,112},L"Mana Pool not in expected range.");
}
std::weak_ptr<Item>nullRing2{Game::GiveAndEquipEnchantedRing("Mana Pool",EquipSlot::RING2)};
for(int i:std::ranges::iota_view(0,1000)){
nullRing2.lock()->EnchantItem("Mana Pool");
nullRing2.lock()->_EnchantItem(ItemEnchant{"Mana Pool"});
Test::InRange(player->GetMaxMana(),{114,124},L"Mana Pool not in expected range with two rings.");
}
}
@ -227,7 +227,7 @@ namespace EnchantTests
Assert::AreEqual(0.0_Pct,player->GetHP6RecoveryPct(),L"Player starts with 0% HP/6 recovery.");
std::weak_ptr<Item>nullRing{Game::GiveAndEquipEnchantedRing("Magical Protection")};
for(int i:std::ranges::iota_view(0,1000)){
nullRing.lock()->EnchantItem("Magical Protection");
nullRing.lock()->_EnchantItem(ItemEnchant{"Magical Protection"});
Test::InRange(player->GetMaxHealth(),{102,103},L"Max Health not in expected range.");
Test::InRange(player->GetDamageReductionFromBuffs(),{2.0_Pct,3.0_Pct},L"Damage Reduction not in expected range.");
Test::InRange(player->GetMoveSpdMult(),{102.0_Pct,103.0_Pct},L"Move Speed % not in expected range.");
@ -235,7 +235,7 @@ namespace EnchantTests
}
std::weak_ptr<Item>nullRing2{Game::GiveAndEquipEnchantedRing("Magical Protection",EquipSlot::RING2)};
for(int i:std::ranges::iota_view(0,1000)){
nullRing2.lock()->EnchantItem("Magical Protection");
nullRing2.lock()->_EnchantItem(ItemEnchant{"Magical Protection"});
Test::InRange(player->GetMaxHealth(),{102,103},L"Max Health not in expected range with two rings.");
Test::InRange(player->GetDamageReductionFromBuffs(),{2.0_Pct,3.0_Pct},L"Damage Reduction not in expected range with two rings.");
Test::InRange(player->GetMoveSpdMult(),{102.0_Pct,103.0_Pct},L"Move Speed % not in expected range with two rings.");
@ -250,7 +250,7 @@ namespace EnchantTests
Assert::AreEqual(50.0_Pct,player->GetCritDmgPct(),L"Player starts with 50% crit rate.");
std::weak_ptr<Item>nullRing{Game::GiveAndEquipEnchantedRing("Aura of the Beast")};
for(int i:std::ranges::iota_view(0,1000)){
nullRing.lock()->EnchantItem("Aura of the Beast");
nullRing.lock()->_EnchantItem(ItemEnchant{"Aura of the Beast"});
Test::InRange(player->GetAttack(),{102,103},L"Attack not in expected range.");
Test::InRange(player->GetCritRatePct(),{2.0_Pct,3.0_Pct},L"Crit Rate not in expected range.");
Test::InRange(player->GetCooldownReductionPct(),{2.0_Pct,3.0_Pct},L"Cooldown Reduction % not in expected range.");
@ -258,7 +258,7 @@ namespace EnchantTests
}
std::weak_ptr<Item>nullRing2{Game::GiveAndEquipEnchantedRing("Aura of the Beast",EquipSlot::RING2)};
for(int i:std::ranges::iota_view(0,1000)){
nullRing2.lock()->EnchantItem("Aura of the Beast");
nullRing2.lock()->_EnchantItem(ItemEnchant{"Aura of the Beast"});
Test::InRange(player->GetAttack(),{102,103},L"Attack not in expected range with two rings.");
Test::InRange(player->GetCritRatePct(),{2.0_Pct,3.0_Pct},L"Crit Rate not in expected range with two rings.");
Test::InRange(player->GetCooldownReductionPct(),{2.0_Pct,3.0_Pct},L"Cooldown Reduction % not in expected range with two rings.");

@ -66,7 +66,7 @@ namespace Game{
inline std::weak_ptr<Item>GiveAndEquipEnchantedRing(const std::string_view enchantName,const EquipSlot slot=EquipSlot::RING1){
std::weak_ptr<Item>nullRing{Inventory::AddItem("Null Ring"s)};
Inventory::EquipItem(nullRing,slot);
nullRing.lock()->EnchantItem(enchantName);
nullRing.lock()->_EnchantItem(enchantName);
return nullRing;
}
}

@ -250,18 +250,54 @@ namespace ItemTests
TEST_METHOD(EnchantTestCheck){
std::weak_ptr<Item>slimeKingRing{Inventory::AddItem("Ring of the Slime King"s)};
Assert::IsFalse(slimeKingRing.lock()->HasEnchant());
bool obtainedDuplicate{false};
for(int i:std::ranges::iota_view(0,1000)){
slimeKingRing.lock()->EnchantItem(ItemEnchant::RollRandomEnchant());
std::optional<ItemEnchant>previousEnchant{slimeKingRing.lock()->GetEnchant()};
std::string previousEnchantName{};
if(slimeKingRing.lock()->HasEnchant())previousEnchantName=slimeKingRing.lock()->GetEnchant().value().Name();
slimeKingRing.lock()->_EnchantItem(ItemEnchant::RollRandomEnchant(previousEnchant));
Assert::IsTrue(slimeKingRing.lock()->HasEnchant());
if(previousEnchantName==slimeKingRing.lock()->GetEnchant().value().Name()){
bool improvementExists{false};
std::string statDowngrades{};
if(previousEnchant.value().HasAttributes()){
for(const auto&[attr,val]:previousEnchant.value()){
if(slimeKingRing.lock()->GetEnchant().value().GetAttribute(attr.ActualName())>val){
improvementExists=true;
break;
}else statDowngrades+=std::format("{} - Previous:{}, New:{}\n",attr.Name(),val,slimeKingRing.lock()->GetEnchant().value().GetAttribute(attr.ActualName()));
}
}else improvementExists=true;
Assert::IsTrue(improvementExists,util::wformat("Could not find a stat improvement for same name stat! {} THIS SHOULD NOT BE ALLOWED!",statDowngrades).c_str());
obtainedDuplicate=true;
}
if(ItemEnchantInfo::ENCHANT_LIST.at(slimeKingRing.lock()->GetEnchant().value().Name()).GetClass().has_value())Assert::AreEqual(int(player->GetClass()),int(ItemEnchantInfo::ENCHANT_LIST.at(slimeKingRing.lock()->GetEnchant().value().Name()).GetClass().value())); //Validate enchant is only for this class if it's a class-based ability.
}
testGame->ChangePlayerClass(WIZARD);
player=testGame->GetPlayer(); //The player pointer has been reassigned...
for(int i:std::ranges::iota_view(0,1000)){
slimeKingRing.lock()->EnchantItem(ItemEnchant::RollRandomEnchant());
std::optional<ItemEnchant> previousEnchant{slimeKingRing.lock()->GetEnchant()};
std::string previousEnchantName{};
if(slimeKingRing.lock()->HasEnchant())previousEnchantName=slimeKingRing.lock()->GetEnchant().value().Name();
slimeKingRing.lock()->_EnchantItem(ItemEnchant::RollRandomEnchant(previousEnchant));
Assert::IsTrue(slimeKingRing.lock()->HasEnchant());
if(previousEnchantName==slimeKingRing.lock()->GetEnchant().value().Name()){
bool improvementExists{false};
std::string statDowngrades{};
if(previousEnchant.value().HasAttributes()){
for(const auto&[attr,val]:previousEnchant.value()){
if(slimeKingRing.lock()->GetEnchant().value().GetAttribute(attr.ActualName())>val){
improvementExists=true;
break;
}else statDowngrades+=std::format("{} - Previous:{}, New:{}\n",attr.Name(),val,slimeKingRing.lock()->GetEnchant().value().GetAttribute(attr.ActualName()));
}
}else improvementExists=true;
Assert::IsTrue(improvementExists,util::wformat("Could not find a stat improvement for same name stat! {} THIS SHOULD NOT BE ALLOWED!",statDowngrades).c_str());
obtainedDuplicate=true;
}
if(ItemEnchantInfo::ENCHANT_LIST.at(slimeKingRing.lock()->GetEnchant().value().Name()).GetClass().has_value())Assert::AreEqual(int(player->GetClass()),int(ItemEnchantInfo::ENCHANT_LIST.at(slimeKingRing.lock()->GetEnchant().value().Name()).GetClass().value())); //Validate enchant is only for this class if it's a class-based ability.
}
Assert::IsTrue(obtainedDuplicate,L"During this test a duplicate enchant was never obtained! THIS SHOULD BE ALLOWED!");
}
TEST_METHOD(AccessoryAntiCompatibilityCheck){
std::weak_ptr<Item>extraRing{Inventory::AddItem("Ring of the Slime King"s)};
@ -277,7 +313,9 @@ namespace ItemTests
std::weak_ptr<Item>extraRing{Inventory::AddItem("Ring of the Slime King"s)};
std::unordered_map<ItemEnchantInfo::ItemEnchantCategory,uint32_t>enchantCounts;
for(int i:std::ranges::iota_view(0,1000)){
ItemEnchantInfo resultEnchant{extraRing.lock()->ApplyRandomEnchant()};
Inventory::AddItem(extraRing.lock()->FragmentName(),"Fragment Enchant Cost"_i[0]);
player->AddMoney("Fragment Enchant Cost"_i[1]);
const ItemEnchant&resultEnchant{extraRing.lock()->ApplyRandomEnchant()};
if(resultEnchant.GetClass().has_value())Assert::AreEqual(int(resultEnchant.GetClass().value()),int(player->GetClass()),L"Player's class matches the class of the enchant.");
enchantCounts[resultEnchant.Category()]++;
Assert::AreEqual(true,extraRing.lock()->GetEnchant().has_value(),L"Ring is expected to be enchanted.");
@ -296,5 +334,23 @@ namespace ItemTests
Assert::AreEqual("Item Enchants.Class Enchants.Enchant Display Color"_Pixel.n,ItemEnchantInfo::GetEnchant("Quickdraw").DisplayCol().n,L"Expecting a class enchant to have the class enchant pixel display color.");
Assert::AreEqual("Item Enchants.Unique Enchants.Enchant Display Color"_Pixel.n,ItemEnchantInfo::GetEnchant("Magical Protection").DisplayCol().n,L"Expecting a unique enchant to have the unique enchant pixel display color.");
}
TEST_METHOD(CanBeEnchantedTest){
std::weak_ptr<Item>extraRing{Inventory::AddItem("Ring of the Slime King"s)};
std::weak_ptr<Item>testArmor{Inventory::AddItem("Test Armor"s)};
Assert::AreEqual(false,extraRing.lock()->CanBeEnchanted(),L"We don't have the money nor required fragments to enchant this item.");
Assert::AreEqual(false,testArmor.lock()->CanBeEnchanted(),L"We can't enchant armor.");
player->SetMoney(2000U);
Assert::AreEqual(false,extraRing.lock()->CanBeEnchanted(),L"We don't have the required fragments to enchant this item.");
player->SetMoney(0U);
Inventory::AddItem(extraRing.lock()->FragmentName(),"Fragment Enchant Cost"_i[0]);
Assert::AreEqual(false,extraRing.lock()->CanBeEnchanted(),L"We don't have the required money to enchant this item.");
player->SetMoney(2000U);
Assert::AreEqual(true,extraRing.lock()->CanBeEnchanted(),L"We don't have the required money to enchant this item.");
extraRing.lock()->ApplyRandomEnchant();
Assert::AreEqual(false,extraRing.lock()->CanBeEnchanted(),L"Ring cannot be enchanted again due to consumption of fragments.");
Inventory::AddItem(extraRing.lock()->FragmentName(),"Fragment Enchant Cost"_i[0]);
Assert::AreEqual(true,extraRing.lock()->CanBeEnchanted(),L"Ring can be enchanted again with the right amount of fragments.");
Assert::AreEqual(uint32_t(2000-"Fragment Enchant Cost"_i[1]),player->GetMoney(),util::wformat("Lost {} money due to enchanting ring.","Fragment Enchant Cost"_i[1]).c_str());
}
};
}

@ -74,6 +74,8 @@ namespace MonsterTests
testGame->InitializeGraphics();
testGame->InitializeClasses();
sig::Animation::InitializeAnimations();
Monster::InitializeStrategies();
MonsterData::InitializeMonsterData();
testGame->InitializeDefaultKeybinds();
testGame->InitializePlayer();
sig::Animation::SetupPlayerAnimations();
@ -506,5 +508,52 @@ namespace MonsterTests
m.Heal(50);
Assert::IsTrue(m._DealTrueDamage(50,HurtFlag::PLAYER_ABILITY),L"When hurting a monster that was alive and takes enough damage to die, it should still take damage and should be triggering like normal.");
}
TEST_METHOD(UnconsciousMonsterTest){
Monster&parrot{game->SpawnMonster({},MONSTER_DATA.at("Parrot"))};
Game::Update(1.f);
Assert::IsFalse(parrot.IsUnconscious(),L"Parrot should not be immediately unconscious.");
parrot.Hurt(1,parrot.OnUpperLevel(),parrot.GetZ());
Assert::IsFalse(parrot.IsUnconscious(),L"Parrot should not be unconscious after taking 1 damage.");
parrot.Hurt(parrot.GetMaxHealth(),parrot.OnUpperLevel(),parrot.GetZ());
Assert::IsTrue(parrot.IsUnconscious(),L"Parrot should now be unconscious.");
Assert::IsTrue(parrot.IsDead(),L"Parrot is considered dead.");
Assert::IsTrue(parrot.InUndamageableState(parrot.OnUpperLevel(),parrot.GetZ()),L"Parrot should be in an undamageable state. Hopefully for obvious reasons.");
Game::Update(MONSTER_DATA.at("Parrot").UnconsciousTime()/2);
Assert::IsTrue(parrot.IsUnconscious(),L"Parrot should still be unconscious.");
Assert::IsTrue(parrot.IsDead(),L"Parrot should still be considered dead.");
Assert::IsTrue(parrot.InUndamageableState(parrot.OnUpperLevel(),parrot.GetZ()),L"Parrot should still be in an undamageable state.");
Game::Update(MONSTER_DATA.at("Parrot").UnconsciousTime()/2);
Assert::IsTrue(!parrot.IsUnconscious(),L"Parrot should no longer be unconscious.");
Assert::IsTrue(parrot.IsAlive(),L"Parrot should be alive.");
Assert::IsTrue(!parrot.InUndamageableState(parrot.OnUpperLevel(),parrot.GetZ()),L"Parrot should not be in an undamageable state.");
}
TEST_METHOD(UnconsciousMonsterHurtTest){
Monster&parrot{game->SpawnMonster({},MONSTER_DATA.at("Parrot"))};
Game::Update(1.f);
parrot.Hurt(parrot.GetMaxHealth(),parrot.OnUpperLevel(),parrot.GetZ());
Assert::IsTrue(parrot.IsUnconscious(),L"Parrot should now be unconscious.");
}
TEST_METHOD(MonsterCollisionRadiusTest){
Monster&parrot{game->SpawnMonster({},MONSTER_DATA.at("Parrot"))};
Game::Update(1.f);
Game::Update(1.f);
Assert::AreEqual(parrot.GetOriginalCollisionRadius(),parrot.GetCollisionRadius(),L"Parrot collision radius should be normal.");
Assert::AreEqual(int(game->GetPlayer()->GetMaxHealth()-parrot.GetCollisionDamage()),game->GetPlayer()->GetHealth(),L"Player should take collision damage from the parrot.");
parrot.SetCollisionRadius(0.f);
Assert::AreEqual(0.f,parrot.GetCollisionRadius(),L"Parrot collision radius should now be zero.");
game->GetPlayer()->Heal(game->GetPlayer()->GetMaxHealth());
game->GetPlayer()->_SetIframes(0.f);
parrot.SetPos({});
game->GetPlayer()->ForceSetPos({});
Game::Update(1.f);
Assert::AreEqual(game->GetPlayer()->GetMaxHealth(),game->GetPlayer()->GetHealth(),L"Player should be full health.");
parrot.SetCollisionRadius(parrot.GetOriginalCollisionRadius());
Game::Update(1.f);
Assert::AreEqual(int(game->GetPlayer()->GetMaxHealth()-parrot.GetCollisionDamage()),game->GetPlayer()->GetHealth(),L"Player should take collision damage from the parrot.");
}
TEST_METHOD(MonsterCollisionRadiusSizeTest){
Monster&parrot{game->SpawnMonster({},MONSTER_DATA.at("Parrot"))};
}
};
}

@ -629,17 +629,17 @@ namespace PlayerTests
TEST_METHOD(HasEnchantCheck){
std::weak_ptr<Item>slimeKingRing{Inventory::AddItem("Ring of the Slime King"s)};
Inventory::EquipItem(slimeKingRing,EquipSlot::RING1);
slimeKingRing.lock()->EnchantItem("Emergency Recovery");
slimeKingRing.lock()->_EnchantItem("Emergency Recovery"sv);
Assert::IsTrue(player->HasEnchant("Emergency Recovery"));
Inventory::EquipItem(slimeKingRing,EquipSlot::RING2);
Assert::IsTrue(player->HasEnchant("Emergency Recovery"));
Inventory::EquipItem(slimeKingRing,EquipSlot::RING1);
slimeKingRing.lock()->EnchantItem("Reaper of Souls");
slimeKingRing.lock()->_EnchantItem("Reaper of Souls"sv);
Assert::IsTrue(player->HasEnchant("Reaper of Souls"));
Inventory::EquipItem(slimeKingRing,EquipSlot::RING2);
Assert::IsTrue(player->HasEnchant("Reaper of Souls"));
Inventory::EquipItem(slimeKingRing,EquipSlot::RING1);
slimeKingRing.lock()->EnchantItem("Attack Boost");
slimeKingRing.lock()->_EnchantItem("Attack Boost"sv);
Assert::IsTrue(player->HasEnchant("Attack Boost"));
Inventory::EquipItem(slimeKingRing,EquipSlot::RING2);
Assert::IsTrue(player->HasEnchant("Attack Boost"));
@ -658,18 +658,18 @@ namespace PlayerTests
Inventory::EquipItem(leatherShoes,EquipSlot::SHOES);
Inventory::EquipItem(woodenSword,EquipSlot::WEAPON);
leatherHelmet.lock()->EnchantItem("Wizard's Soul");
leatherHelmet.lock()->_EnchantItem("Wizard's Soul"sv);
Assert::IsTrue(player->HasEnchant("Wizard's Soul"));
leatherArmor.lock()->EnchantItem("Ability Haste");
leatherArmor.lock()->_EnchantItem("Ability Haste"sv);
Assert::IsTrue(player->HasEnchant("Ability Haste"));
leatherPants.lock()->EnchantItem("Improved Ground Slam");
leatherPants.lock()->_EnchantItem("Improved Ground Slam"sv);
Assert::IsTrue(player->HasEnchant("Improved Ground Slam"));
leatherGloves.lock()->EnchantItem("Battle Shout");
leatherGloves.lock()->_EnchantItem("Battle Shout"sv);
Assert::IsTrue(player->HasEnchant("Battle Shout"));
leatherShoes.lock()->EnchantItem("Attack Boost");
leatherShoes.lock()->_EnchantItem("Attack Boost"sv);
Inventory::UnequipItem(EquipSlot::RING2);
Assert::IsTrue(player->HasEnchant("Attack Boost"));
woodenSword.lock()->EnchantItem("Mana Pool");
woodenSword.lock()->_EnchantItem("Mana Pool"sv);
Assert::IsTrue(player->HasEnchant("Mana Pool"));
Inventory::UnequipItem(EquipSlot::HELMET);
@ -744,7 +744,7 @@ namespace PlayerTests
player->GetAbility3().cooldown=player->GetAbility3().GetCooldownTime();
std::weak_ptr<Item>nullRing{Inventory::AddItem("Null Ring"s)};
Inventory::EquipItem(nullRing,EquipSlot::RING1);
nullRing.lock()->EnchantItem("Multi-Multishot");
nullRing.lock()->_EnchantItem("Multi-Multishot"sv);
testKey->bHeld=true; //Force the key to be held down for testing purposes.
Assert::AreEqual(player->GetAbility3().GetCooldownTime(),oldCooldownTime-oldCooldownTime*"Multi-Multishot"_ENC["COOLDOWN REDUCTION PCT"]/100.f,L"Old cooldown time with multishot cooldown reduction pct matches.");
testGame->SetElapsedTime(0.f);

@ -29,6 +29,24 @@
"object"
]
},
{
"id": 43,
"name": "AudioEvent",
"storageType": "string",
"type": "enum",
"values": [
"LowHealth",
"TitleScreenLoaded",
"BlacksmithUnlock",
"Chapter2Unlock",
"Chapter3Unlock",
"BossFanfare",
"BossDefeated",
"PreBossPhase",
"Default Volume"
],
"valuesAsFlags": false
},
{
"id": 30,
"name": "Backdrop",
@ -54,7 +72,9 @@
"foresty_boss",
"base_camp",
"mountain",
"mountain_boss"
"mountain_boss",
"beach",
"beach_boss"
],
"valuesAsFlags": false
},
@ -177,7 +197,11 @@
"Birds2",
"Birds3",
"Birds4",
"Campfire"
"Campfire",
"Crashing Waves",
"Gull1",
"Gull2",
"Gull3"
],
"valuesAsFlags": false
},
@ -335,6 +359,12 @@
"drawFill": true,
"id": 19,
"members": [
{
"name": "Audio Event",
"propertyType": "AudioEvent",
"type": "string",
"value": "Default Volume"
},
{
"name": "Backdrop",
"propertyType": "Backdrop",

@ -282,7 +282,7 @@
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpplatest</LanguageStandard>
<AdditionalIncludeDirectories>C:\Users\LabUser\source\repos\AdventuresInLestoria\Adventures in Lestoria\steam;C:\Users\LabUser\source\repos\AdventuresInLestoria\Adventures in Lestoria\discord-files;C:\Users\LabUser\Documents\include;C:\Users\niconiconii\source\repos\AdventuresInLestoria\Adventures in Lestoria\discord-files;C:\Users\niconiconii\OneDrive\Documents\include;C:\Users\niconiconii\source\repos\AdventuresInLestoria\Adventures in Lestoria\steam;C:\Users\sigon\source\repos\AdventuresInLestoria\Adventures in Lestoria\steam;C:\Users\sigon\source\repos\AdventuresInLestoria\Adventures in Lestoria\discord-files;C:\Users\sigon\OneDrive\Documents\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>C:\Users\LabUser\source\repos\AdventuresInLestoria\Adventures in Lestoria\steam;$(ProjectDir)steam;$(ProjectDir)discord-files;C:\Users\LabUser\source\repos\AdventuresInLestoria\Adventures in Lestoria\discord-files;C:\Users\LabUser\Documents\include;C:\Users\niconiconii\source\repos\AdventuresInLestoria\Adventures in Lestoria\discord-files;C:\Users\niconiconii\OneDrive\Documents\include;C:\Users\niconiconii\source\repos\AdventuresInLestoria\Adventures in Lestoria\steam;C:\Users\sigon\source\repos\AdventuresInLestoria\Adventures in Lestoria\steam;C:\Users\sigon\source\repos\AdventuresInLestoria\Adventures in Lestoria\discord-files;C:\Users\sigon\OneDrive\Documents\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
</ClCompile>
<Link>
@ -374,6 +374,10 @@
<ClInclude Include="Ability.h" />
<ClInclude Include="AccessoryRowItemDisplay.h" />
<ClInclude Include="Animation.h" />
<ClInclude Include="Arc.h">
<SubType>
</SubType>
</ClInclude>
<ClInclude Include="Attributable.h" />
<ClInclude Include="AttributableStat.h">
<SubType>
@ -764,6 +768,10 @@
<ItemGroup>
<ClCompile Include="Ability.cpp" />
<ClCompile Include="Animation.cpp" />
<ClCompile Include="Arc.cpp">
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="Arrow.cpp" />
<ClCompile Include="ArtificerDisassembleWindow.cpp">
<SubType>
@ -826,6 +834,7 @@
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="Crab.cpp" />
<ClCompile Include="ExplosiveTrap.cpp">
<SubType>
</SubType>
@ -834,6 +843,11 @@
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="GiantCrab.cpp">
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="GiantOctopus.cpp" />
<ClCompile Include="IBullet.cpp" />
<ClCompile Include="BuyItemWindow.cpp">
<SubType>
@ -988,6 +1002,10 @@
</SubType>
</ClCompile>
<ClCompile Include="ItemLoadoutWindow.cpp" />
<ClCompile Include="ItemScript.cpp">
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="Key.cpp" />
<ClCompile Include="LargeStone.cpp" />
<ClCompile Include="LargeTornado.cpp">
@ -1043,6 +1061,7 @@
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="OctopusArm.cpp" />
<ClCompile Include="Overlay.cpp">
<SubType>
</SubType>
@ -1053,15 +1072,23 @@
</SubType>
</ClCompile>
<ClCompile Include="packkey.cpp" />
<ClCompile Include="Parrot.cpp" />
<ClCompile Include="PauseMenu.cpp">
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="Pirate_Buccaneer.cpp" />
<ClCompile Include="Pirate_Captain.cpp" />
<ClCompile Include="Pirate_Marauder.cpp" />
<ClCompile Include="Pixel.cpp" />
<ClCompile Include="PoisonBottle.cpp">
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="ThrownProjectile.cpp">
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="PoisonPool.cpp">
<SubType>
</SubType>
@ -1084,6 +1111,7 @@
<ClCompile Include="PulsatingFire.cpp" />
<ClCompile Include="Ranger.cpp" />
<ClCompile Include="RUN_STRATEGY.cpp" />
<ClCompile Include="Sandworm.cpp" />
<ClCompile Include="SaveFile.cpp">
<SubType>
</SubType>
@ -1092,6 +1120,7 @@
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="Seagull.cpp" />
<ClCompile Include="SellItemWindow.cpp">
<SubType>
</SubType>
@ -1225,6 +1254,7 @@
<None Include="steam\steam_api.json" />
</ItemGroup>
<ItemGroup>
<Text Include="..\x64\Unit Testing\debug.log" />
<Text Include="assets\config\Achievements.txt" />
<Text Include="assets\config\audio\audio.txt" />
<Text Include="assets\config\audio\bgm.txt" />
@ -1286,6 +1316,7 @@
<Text Include="Merchant%27s Items.txt" />
<Text Include="NewClasses.txt" />
<Text Include="InitialConcept.txt" />
<Text Include="Oktopus boss.txt" />
<Text Include="Slime_King_Encounter.txt" />
<Text Include="StatCalculations.txt" />
<Text Include="TODO.txt" />

@ -103,6 +103,9 @@
<Filter Include="Scripts">
<UniqueIdentifier>{eba9dd86-1d5d-4c68-8dcb-760c759099c0}</UniqueIdentifier>
</Filter>
<Filter Include="Debug">
<UniqueIdentifier>{c4119802-3fc8-4555-9013-a7a3ac9b204d}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="olcPixelGameEngine.h">
@ -705,6 +708,9 @@
<ClInclude Include="DynamicMenuLabel.h">
<Filter>Header Files\Interface</Filter>
</ClInclude>
<ClInclude Include="Arc.h">
<Filter>Header Files\Utils</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="Player.cpp">
@ -1187,7 +1193,7 @@
<ClCompile Include="PurpleEnergyBall.cpp">
<Filter>Source Files\Bullet Types</Filter>
</ClCompile>
<ClCompile Include="PoisonBottle.cpp">
<ClCompile Include="ThrownProjectile.cpp">
<Filter>Source Files\Bullet Types</Filter>
</ClCompile>
<ClCompile Include="State_Dialog.cpp">
@ -1226,6 +1232,45 @@
<ClCompile Include="ArtificerRefineResultWindow.cpp">
<Filter>Source Files\Interface</Filter>
</ClCompile>
<ClCompile Include="Pirate_Marauder.cpp">
<Filter>Source Files\Monster Strategies</Filter>
</ClCompile>
<ClCompile Include="Crab.cpp">
<Filter>Source Files\Monster Strategies</Filter>
</ClCompile>
<ClCompile Include="Parrot.cpp">
<Filter>Source Files\Monster Strategies</Filter>
</ClCompile>
<ClCompile Include="Pirate_Buccaneer.cpp">
<Filter>Source Files\Monster Strategies</Filter>
</ClCompile>
<ClCompile Include="Pirate_Captain.cpp">
<Filter>Source Files\Monster Strategies</Filter>
</ClCompile>
<ClCompile Include="Sandworm.cpp">
<Filter>Source Files\Monster Strategies</Filter>
</ClCompile>
<ClCompile Include="Seagull.cpp">
<Filter>Source Files\Monster Strategies</Filter>
</ClCompile>
<ClCompile Include="GiantCrab.cpp">
<Filter>Source Files\Monster Strategies</Filter>
</ClCompile>
<ClCompile Include="ItemScript.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="PoisonBottle.cpp">
<Filter>Source Files\Bullet Types</Filter>
</ClCompile>
<ClCompile Include="GiantOctopus.cpp">
<Filter>Source Files\Monster Strategies</Filter>
</ClCompile>
<ClCompile Include="OctopusArm.cpp">
<Filter>Source Files\Monster Strategies</Filter>
</ClCompile>
<ClCompile Include="Arc.cpp">
<Filter>Source Files\Utils</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="cpp.hint" />
@ -1478,6 +1523,12 @@
<Text Include="ConsoleCommands.txt">
<Filter>Documentation\Admin</Filter>
</Text>
<Text Include="..\x64\Unit Testing\debug.log">
<Filter>Debug</Filter>
</Text>
<Text Include="Oktopus boss.txt">
<Filter>Documentation\Mechanics</Filter>
</Text>
</ItemGroup>
<ItemGroup>
<Image Include="assets\heart.ico">

@ -90,8 +90,8 @@ INCLUDE_FOREDROP_DATA
INCLUDE_MONSTER_DATA
INCLUDE_PACK_KEY
bool DEMO_BUILD = true;
bool ADMIN_MODE = !DEMO_BUILD&&false; //Enables the Unlock All button and admin console.
bool DEMO_BUILD = false;
bool ADMIN_MODE = !DEMO_BUILD&&true; //Enables the Unlock All button and admin console.
bool _DEBUG_MAP_LOAD_INFO = false;
//360x240
vi2d WINDOW_SIZE={24*15,24*10};
@ -281,7 +281,7 @@ void InitializeGameConfigurations(){
}
bool AiL::OnUserCreate(){
if(PACK_KEY=="INSERT_PACK_KEY_HERE")ERR("ERROR! Starting the game with the default pack ID is not allowed!")
if(PACK_KEY=="INSERT_PACK_KEY_HERE")ERR("ERROR! Starting the game with the default pack ID is not allowed!");
gamepack.LoadPack("assets/"+"gamepack_file"_S,PACK_KEY);
GamePad::init();
@ -389,6 +389,7 @@ bool AiL::OnUserCreate(){
if(!gamepack.Loaded()&&"GENERATE_GAMEPACK"_B){
gamepack.SavePack("assets/"+"gamepack_file"_S,PACK_KEY);
LOG("Game Pack has been generated!"<<std::endl<<"========================"<<std::endl);
gamepack.LoadPack("assets/"+"gamepack_file"_S,PACK_KEY);
}
return true;
@ -436,7 +437,7 @@ bool AiL::OnConsoleCommand(const std::string& sCommand){
if(args[0]=="/accessory"){
if(args.size()<2)ConsoleOut()<<"Usage: /accessory <Accessory Name> [Enchant Name]"<<std::endl;
std::weak_ptr<Item>accessory{Inventory::AddItem(args[1])};
if(args.size()>=3)accessory.lock()->EnchantItem(args[2]);
if(args.size()>=3)accessory.lock()->_EnchantItem(ItemEnchant{args[2]});
ConsoleOut()<<"Added "<<args[1]<<" to player's inventory."<<std::endl;
}else{
ConsoleOut()<<"Invalid command! Use /help to see available commands."<<std::endl;
@ -1128,6 +1129,8 @@ void AiL::RenderWorld(float fElapsedTime){
else if(adrenalineRushBuffs.size()>0)playerCol={uint8_t(255*abs(sin(6.f*adrenalineRushBuffs[0].duration))),255,uint8_t(255*abs(sin(6.f*adrenalineRushBuffs[0].duration)))};
else if(movespeedBuffs.size()>0)playerCol={uint8_t(255*abs(sin(2.f*movespeedBuffs[0].duration))),255,uint8_t(255*abs(sin(2.f*movespeedBuffs[0].duration)))};
if(player->HasIframes())playerCol.a*=0.62f;
if(damageReductionBuffs.size()>0)view.DrawPartialSquishedRotatedDecal(pos+vf2d{0,-player->GetZ()*(std::signbit(scale.y)?-1:1)},player->playerOutline.Decal(),player->GetSpinAngle(),{12,12},animationFrame.get().GetSourceRect().pos,animationFrame.get().GetSourceRect().size,playerScale*scale+0.1f,{1.f,player->ySquishFactor},{210,210,210,uint8_t(util::lerp(0,255,abs(sin((PI*GetRunTime())/1.25f))))});
view.DrawPartialSquishedRotatedDecal(pos+vf2d{0,-player->GetZ()*(std::signbit(scale.y)?-1:1)},animationFrame.get().GetSourceImage()->Decal(),player->GetSpinAngle(),{12,12},animationFrame.get().GetSourceRect().pos,animationFrame.get().GetSourceRect().size,playerScale*scale,{1.f,player->ySquishFactor},playerCol);
@ -2035,9 +2038,9 @@ void AiL::RenderHud(){
if(player->GetShield()>0)heartImg=GFX["shield_heart.png"].Decal();
float healthRatio{float(healthCounter.GetDisplayValue())/player->GetMaxHealth()};
float manaRatio{float(manaCounter.GetDisplayValue())/player->GetMaxMana()};
DrawPartialDecal({2,2+(15*(1-healthRatio))},const_cast<Decal*>(heartImg),{0.f,15*(1-healthRatio)},{17,15*healthRatio});
DrawPartialDecal(vf2d{2.f,floor(2+(15*(1-healthRatio)))},const_cast<Decal*>(heartImg),vf2d{0.f,floor(15*(1-healthRatio))},vf2d{17.f,floor(15*healthRatio)});
DrawDecal({2,20},GFX["mana_outline.png"].Decal());
DrawPartialDecal({2,20+(15*(1-manaRatio))},GFX["mana.png"].Decal(),{0.f,15*(1-manaRatio)},{17,15*manaRatio});
DrawPartialDecal(vf2d{2,float(20+(15*(1-manaRatio)))},GFX["mana.png"].Decal(),vf2d{0.f,float(15*(1-manaRatio))},vf2d{17,float(15*manaRatio)});
std::string text=player->GetHealth()>0?std::to_string(healthCounter.GetDisplayValue()+shieldCounter.GetDisplayValue()):"X";
std::string text_mana=std::to_string(manaCounter.GetDisplayValue());
@ -2117,7 +2120,9 @@ void AiL::RenderCooldowns(){
if(loadoutSlot!=-1){
drawOffset.y+=2.f;
}
a.input->DrawPrimaryInput(game,pos+vf2d{14,0.f}+drawOffset,"",255,controlType,{0.75f,0.75f});
InputGroup*input{a.input};
if(a==player->GetAbility4())input=&Player::KEY_ABILITY4;
input->DrawPrimaryInput(game,pos+vf2d{14,0.f}+drawOffset,"",255,controlType,{0.75f,0.75f});
if(a.cooldown>0.1){
vf2d iconScale={1,1};
@ -2447,7 +2452,9 @@ void AiL::_PrepareLevel(MapName map,MusicChange changeMusic){
vf2d spawnerRadius=vf2d{spawnData.ObjectData.GetFloat("width"),spawnData.ObjectData.GetFloat("height")}/2;
for(XMLTag&monster:spawnData.monsters){
std::string monsterName=monster.GetString("value");
monster_list.push_back({monsterName,{monster.GetInteger("x")-spawnData.ObjectData.GetFloat("x"),monster.GetInteger("y")-spawnData.ObjectData.GetFloat("y")}});
const vi2d frameSize{ANIMATION_DATA[std::format("{}_{}",monsterName,MONSTER_DATA[monsterName].GetDefaultIdleAnimation())].GetFrame(0.f).GetSourceRect().size};
const float monsterSizeMult{MONSTER_DATA[monsterName].GetSizeMult()};
monster_list.emplace_back(monsterName,vf2d{monster.GetInteger("x")+(frameSize.x*monsterSizeMult/2)-spawnData.ObjectData.GetFloat("x"),monster.GetInteger("y")-(frameSize.y*monsterSizeMult/2)-spawnData.ObjectData.GetFloat("y")});
}
const int spawnerId=spawnData.ObjectData.GetInteger("id");
@ -2685,7 +2692,7 @@ void AiL::_PrepareLevel(MapName map,MusicChange changeMusic){
LoadingScreen::AddPhase([&](){
for(NPCData data:game->MAP_DATA[game->GetCurrentLevel()].npcs){
if(Unlock::IsUnlocked(data.unlockCondition)){
MONSTER_LIST.push_back(std::make_unique<Monster>(data.spawnPos,MONSTER_DATA[data.name]));
MONSTER_LIST.emplace_back(std::make_unique<Monster>(data.spawnPos,MONSTER_DATA[data.name]));
MONSTER_LIST.back()->ApplyIframes(INFINITE);
MONSTER_LIST.back()->npcData=data;
}
@ -2712,7 +2719,7 @@ void AiL::_PrepareLevel(MapName map,MusicChange changeMusic){
#pragma region Setup Pathfinding (Loading Phase 9)
LoadingScreen::AddPhase([&](){
Audio::SetAudioEvent("Default Volume");
Audio::SetAudioEvent(GetCurrentMap().GetDefaultAudioEvent());
#ifdef __EMSCRIPTEN__
Audio::muted=false;
Audio::UpdateBGMVolume();
@ -2725,7 +2732,6 @@ void AiL::_PrepareLevel(MapName map,MusicChange changeMusic){
if(changeMusic==PLAY_LEVEL_MUSIC){
#pragma region Audio Preparation (Loading Phase 10)
LoadingScreen::AddPhase([&](){
Audio::SetAudioEvent("Default Volume");
game->audioEngine.fullyLoaded=true; //We assume there's no audio to load, so we just set the audio as fully loaded by default.
if(MAP_DATA[GetCurrentLevel()].bgmSongName.length()>0){
Audio::PrepareBGM(MAP_DATA[GetCurrentLevel()].bgmSongName);
@ -3925,28 +3931,28 @@ void AiL::SetLoadoutItem(int slot,std::string itemName){
inputGroup=&Player::KEY_ITEM3;
}break;
}
Ability itemAbility{itemName,"","","Item.Item Cooldown Time"_F,0,inputGroup,"items/"+itemName+".png",VERY_DARK_RED,DARK_RED,PrecastData{GetLoadoutItem(slot).lock()->CastTime(),0,0},true};
Ability itemAbility{itemName,"","","Item.Item Cooldown Time"_F,0,inputGroup,"items/"+itemName+".png",VERY_DARK_RED,DARK_RED,PrecastData{GetLoadoutItem(slot).lock()->CastTime(),GetLoadoutItem(slot).lock()->CastRange(),GetLoadoutItem(slot).lock()->CastSize()},true};
itemAbility.actionPerformedDuringCast=GetLoadoutItem(slot).lock()->UseDuringCast();
itemAbility.itemAbility=true;
switch(slot){
case 0:{
itemAbility.action=[&](Player*p,vf2d pos={}){
return game->UseLoadoutItem(0);
return game->UseLoadoutItem(0,pos);
};
game->GetPlayer()->SetItem1UseFunc(itemAbility);
Component<MenuItemItemButton>(MenuType::ITEM_LOADOUT,"Loadout Item 1")->SetItem(loadout[slot]);
}break;
case 1:{
itemAbility.action=[&](Player*p,vf2d pos={}){
return game->UseLoadoutItem(1);
return game->UseLoadoutItem(1,pos);
};
game->GetPlayer()->SetItem2UseFunc(itemAbility);
Component<MenuItemItemButton>(MenuType::ITEM_LOADOUT,"Loadout Item 2")->SetItem(loadout[slot]);
}break;
case 2:{
itemAbility.action=[&](Player*p,vf2d pos={}){
return game->UseLoadoutItem(2);
return game->UseLoadoutItem(2,pos);
};
game->GetPlayer()->SetItem3UseFunc(itemAbility);
Component<MenuItemItemButton>(MenuType::ITEM_LOADOUT,"Loadout Item 3")->SetItem(loadout[slot]);
@ -3970,11 +3976,11 @@ void AiL::SetLoadoutItem(int slot,std::string itemName){
}
}
bool AiL::UseLoadoutItem(int slot){
bool AiL::UseLoadoutItem(int slot,const std::optional<vf2d>targetingPos){
if(slot<0||slot>GetLoadoutSize()-1)ERR("Invalid inventory slot "+std::to_string(slot)+", please choose a slot in range (0-"+std::to_string(GetLoadoutSize()-1)+").");
if(!ISBLANK(GetLoadoutItem(slot).lock())&&GetLoadoutItem(slot).lock()->Amt()>0){
Tutorial::GetTask(TutorialTaskName::USE_RECOVERY_ITEMS).I(A::ITEM_USE_COUNT)++;
Inventory::UseItem(GetLoadoutItem(slot).lock()->ActualName());
Inventory::UseItem(GetLoadoutItem(slot).lock()->ActualName(),1U,targetingPos);
Inventory::AddLoadoutItemUsed(GetLoadoutItem(slot).lock()->ActualName(),slot);
GetLoadoutItem(slot).lock()->amt--;
if(GetLoadoutItem(slot).lock()->UseSound().length()>0){
@ -4004,7 +4010,7 @@ void AiL::ClearLoadoutItem(int slot){
switch(slot){
case 0:{
itemAbility.action=[&](Player*p,vf2d pos={}){
return game->UseLoadoutItem(0);
return game->UseLoadoutItem(0,pos);
};
game->GetPlayer()->SetItem1UseFunc(itemAbility);
Component<MenuItemItemButton>(MenuType::ITEM_LOADOUT,"Loadout Item 1")->SetItem(Item::BLANK);
@ -4012,7 +4018,7 @@ void AiL::ClearLoadoutItem(int slot){
}break;
case 1:{
itemAbility.action=[&](Player*p,vf2d pos={}){
return game->UseLoadoutItem(1);
return game->UseLoadoutItem(1,pos);
};
game->GetPlayer()->SetItem2UseFunc(itemAbility);
Component<MenuItemItemButton>(MenuType::ITEM_LOADOUT,"Loadout Item 2")->SetItem(Item::BLANK);
@ -4020,7 +4026,7 @@ void AiL::ClearLoadoutItem(int slot){
}break;
case 2:{
itemAbility.action=[&](Player*p,vf2d pos={}){
return game->UseLoadoutItem(2);
return game->UseLoadoutItem(2,pos);
};
game->GetPlayer()->SetItem3UseFunc(itemAbility);
Component<MenuItemItemButton>(MenuType::ITEM_LOADOUT,"Loadout Item 3")->SetItem(Item::BLANK);
@ -4040,7 +4046,7 @@ void AiL::RenderFadeout(){
SetMosaicEffect(1U);
GameState::_ChangeState(transitionState);
}else{
SetMosaicEffect(util::lerp(1U,mosaicEffectTransition,1-(fadeOutDuration/fadeOutTotalTime)));
SetMosaicEffect(util::lerp(uint8_t(1),mosaicEffectTransition,1-(fadeOutDuration/fadeOutTotalTime)));
alpha=uint8_t(util::lerp(0,255,1-(fadeOutDuration/fadeOutTotalTime)));
}
}else

@ -102,6 +102,7 @@ class AiL : public olc::PixelGameEngine
friend class Audio;
friend class Minimap;
friend class MiniAudio;
friend class Arc;
std::unique_ptr<Player>player;
SplashScreen splash;
public:
@ -167,6 +168,7 @@ public:
double levelTime=0.;
Camera2D camera;
std::map<MapName,Map>MAP_DATA;
ResourcePack gamepack;
private:
std::vector<std::unique_ptr<Effect>>foregroundEffects,backgroundEffects,foregroundEffectsToBeInserted,backgroundEffectsToBeInserted;
@ -362,7 +364,7 @@ public:
int GetLoadoutSize()const;
void RestockLoadoutItems();
//Returns true if the item can be used (we have >0 of it)
bool UseLoadoutItem(int slot);
bool UseLoadoutItem(int slot,const std::optional<vf2d>targetingPos);
//Blanks out this loadout item.
void ClearLoadoutItem(int slot);
void RenderFadeout();

@ -401,6 +401,8 @@ void sig::Animation::InitializeAnimations(){
CreateHorizontalAnimationSequence("bomb_boom.png",5,{36,36},AnimationData{.frameDuration{0.2f},.style{Animate2D::Style::OneShot}});
CreateHorizontalAnimationSequence("tornado2.png",4,{24,48},AnimationData{.frameDuration{0.1f},.style{Animate2D::Style::Repeat}});
CreateHorizontalAnimationSequence("large_tornado.png",4,{72,144},AnimationData{.frameDuration{0.1f},.style{Animate2D::Style::Repeat}});
CreateHorizontalAnimationSequence("sand_suction.png",4,{72,72},AnimationData{.frameDuration{0.2f},.style{Animate2D::Style::Repeat}});
CreateHorizontalAnimationSequence("bomb.png",4,{24,24},AnimationData{.frameDuration{0.2f},.style{Animate2D::Style::OneShot}});
CreateStillAnimation("meteor.png",{192,192});
@ -433,6 +435,14 @@ void sig::Animation::InitializeAnimations(){
ANIMATION_DATA[std::format("GOBLIN_BOW_ATTACK_{}",animationIndex)]=mountShootAnimation;
}
Animate2D::FrameSequence parrot_sit_n,parrot_sit_e,parrot_sit_s,parrot_sit_w;
//Idle sequences for the sitting parrot.
for(int animationIndex=0;animationIndex<4;animationIndex++){
Animate2D::FrameSequence mountAnimation{0.6f};
mountAnimation.AddFrame(Animate2D::Frame{&GFX["monsters/commercial_assets/Parrot_foreground.png"],{{0,animationIndex*48},{48,48}}});
ANIMATION_DATA[std::format("PARROT_MOUNTED_{}",animationIndex)]=mountAnimation;
}
#pragma region Trapper Target Mark Debuff
AnimationData targetAnimData{.frameDuration{0.1f},.style{Animate2D::Style::Repeat}};
Animate2D::FrameSequence targetAnim(targetAnimData.frameDuration,targetAnimData.style);
@ -451,6 +461,7 @@ void sig::Animation::InitializeAnimations(){
CreateHorizontalAnimationSequence("bear_trap.png",3,{24,24},AnimationData{.frameDuration{0.1f},.style{Animate2D::Style::PingPong}});
CreateHorizontalAnimationSequence("explosive_trap.png",4,{48,48},AnimationData{.frameDuration{0.06f},.style{Animate2D::Style::PingPong}});
CreateHorizontalAnimationSequence("explosionframes.png",21,{24,24},AnimationData{.frameDuration{0.02f},.style{Animate2D::Style::OneShot}});
CreateHorizontalAnimationSequence("fire_ring.png",5,{60,60},AnimationData{.frameDuration{0.1f},.style{Animate2D::Style::Repeat}});
CreateHorizontalAnimationSequence("portal.png",8,{24,24},AnimationData{.frameDuration{0.1f},.style{Animate2D::Style::Repeat}});

@ -0,0 +1,79 @@
#pragma region License
/*
License (OLC-3)
~~~~~~~~~~~~~~~
Copyright 2024 Joshua Sigona <sigonasr2@gmail.com>
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions or derivations of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions or derivative works in binary form must reproduce the above
copyright notice. This list of conditions and the following disclaimer must be
reproduced in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may
be used to endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
Portions of this software are copyright © 2024 The FreeType
Project (www.freetype.org). Please see LICENSE_FT.txt for more information.
All rights reserved.
*/
#pragma endregion
#include "Arc.h"
#include "AdventuresInLestoria.h"
INCLUDE_game
Arc::Arc(const vf2d pos,const float radius,const float pointingAngle,const float sweepAngle)
:pos(pos),radius(radius),pointingAngle(pointingAngle),sweepAngle(sweepAngle){
if(sweepAngle<0.f)ERR(std::format("WARNING! Sweep angle must be greater than or equal to 0! Provided Sweep Angle: {}",sweepAngle));
GenerateArc();
}
void Arc::Draw(AiL*game,const Pixel col){
game->SetDecalStructure(DecalStructure::FAN);
game->view.DrawPolygonDecal(nullptr,poly.pos,poly.pos,col);
}
const bool Arc::overlaps(const vf2d checkPos)const{
return geom2d::overlaps(checkPos,poly);
}
void Arc::GrowRadius(const float growAmt){
radius+=growAmt;
GenerateArc();
}
void Arc::GenerateArc(){
poly.pos.clear();
//Use cut-off point between two angles
poly.pos.emplace_back(pos); //Always add 0,0
float smallestAng{util::radToDeg(pointingAngle-sweepAngle)+90};
float largestAng{util::radToDeg(pointingAngle+sweepAngle)+90};
while(smallestAng<0.f)smallestAng+=360;
while(largestAng<0.f)largestAng+=360;
int startInd{int(std::fmod(smallestAng/4+1,90))};
int endInd{int(std::fmod(largestAng/4+1,90))};
if(startInd>endInd){
startInd=(startInd+3)%90;
}else{
endInd=(endInd+3)%90;
}
for(int i=startInd;i!=endInd;i=(i+1)%game->circleCooldownPoints.size()){
poly.pos.emplace_back(pos+game->circleCooldownPoints[i]*radius);
}
poly.pos.emplace_back(pos); //Connect back to itself.
}

@ -0,0 +1,59 @@
#pragma region License
/*
License (OLC-3)
~~~~~~~~~~~~~~~
Copyright 2024 Joshua Sigona <sigonasr2@gmail.com>
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions or derivations of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions or derivative works in binary form must reproduce the above
copyright notice. This list of conditions and the following disclaimer must be
reproduced in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may
be used to endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
Portions of this software are copyright © 2024 The FreeType
Project (www.freetype.org). Please see LICENSE_FT.txt for more information.
All rights reserved.
*/
#pragma endregion
#pragma once
#include "olcUTIL_Geometry2D.h"
#include "Pixel.h"
class AiL;
class Arc{
public:
//Define a sweep angle such that each direction will arc that way. Example: PI/2 means a sweep angle from -PI/2 to PI/2. MUST BE POSITIVE
Arc(const vf2d pos,const float radius,const float pointingAngle,const float sweepAngle);
void Draw(AiL*game,const Pixel col);
const bool overlaps(const vf2d checkPos)const;
void GrowRadius(const float growAmt);
const vf2d pos;
const float pointingAngle;
const float sweepAngle;
float radius;
geom2d::polygon<float>poly;
private:
void GenerateArc();
};

@ -109,7 +109,7 @@ void Menu::InitializeArtificerDisassembleWindow(){
EnableDisassemblyDisplay();
RowItemDisplay&item{*DYNAMIC_POINTER_CAST<RowItemDisplay>(data.component)};
Component<MenuItemItemButton>(data.menu.type,"Item Icon")->SetItem(item.GetItem().lock());
Component<MenuIconButton>(data.menu.type,"Disassembly Result")->SetIcon(GFX.at(item.GetItem().lock()->FragmentName()).Decal());
Component<MenuIconButton>(data.menu.type,"Disassembly Result")->SetIcon(item.GetItem().lock()->Icon().Decal());
Component<MenuLabel>(data.menu.type,"Disassembly Result Title")->SetLabel(item.GetItem().lock()->FragmentName());
Component<MenuLabel>(data.menu.type,"Fragment Total Count")->SetLabel(std::format("Currently Owned: {}",Inventory::GetItemCount(item.GetItem().lock()->FragmentName())));
return true;
@ -119,7 +119,7 @@ void Menu::InitializeArtificerDisassembleWindow(){
if(childComponent){
RowItemDisplay&item{childComponent.value().get()};
Component<MenuItemItemButton>(data.menu.type,"Item Icon")->SetItem(item.GetItem().lock());
Component<MenuIconButton>(data.menu.type,"Disassembly Result")->SetIcon(GFX.at(item.GetItem().lock()->FragmentName()).Decal());
Component<MenuIconButton>(data.menu.type,"Disassembly Result")->SetIcon(item.GetItem().lock()->Icon().Decal());
Component<MenuLabel>(data.menu.type,"Disassembly Result Title")->SetLabel(item.GetItem().lock()->FragmentName());
Component<MenuLabel>(data.menu.type,"Fragment Total Count")->SetLabel(std::format("Currently Owned: {}",Inventory::GetItemCount(item.GetItem().lock()->FragmentName())));
EnableDisassemblyDisplay();
@ -136,9 +136,14 @@ void Menu::InitializeArtificerDisassembleWindow(){
Menu::AddInventoryListener(inventoryDisplay,"Accessories");
ResetDisassemblyDisplay();
artificerDisassembleWindow->SetupKeyboardNavigation(
[](MenuType type,Data&returnData){ //On Open
returnData="";
auto&items{Component<RowInventoryScrollableWindowComponent>(type,"Accessory List")->GetComponents()};
if(Component<RowInventoryScrollableWindowComponent>(type,"Accessory List")->GetSelectedChild())returnData=Component<RowInventoryScrollableWindowComponent>(type,"Accessory List")->GetSelectedChild().value().get().GetName();
else if(items.size()>0)returnData=items[0];
else returnData="Back";
},
{ //Button Key
{game->KEY_SCROLL,{"Navigate",[](MenuType type){}}},
@ -146,12 +151,49 @@ void Menu::InitializeArtificerDisassembleWindow(){
Menu::CloseMenu();
}}},
{game->KEY_CONFIRM,{"Select",[](MenuType type){}}},
{{game->KEY_SHOULDER2,Pressed},{"Scroll Up/Down",[](MenuType type){}}},
{{game->KEY_SHOULDER,Pressed},{"Scroll Up/Down",[](MenuType type){}}},
{{game->KEY_FASTSCROLLUP,PressedDAS},{"Scroll",[](MenuType type){
Menu::menus[type]->GetSelection().lock()->parentComponent.lock()->DecreaseSelectionIndex(3.f);
}}},
{{game->KEY_FASTSCROLLDOWN,PressedDAS},{"Scroll",[](MenuType type){
Menu::menus[type]->GetSelection().lock()->parentComponent.lock()->IncreaseSelectionIndex(3.f);
}}},
}
,{ //Button Navigation Rules
{"Sample Button",{
.up="",
.down="",
.left="",
.right="",}},
});
{"Accessory List",{
.up=[&](MenuType type,Data&returnData){
Menu::ScrollUp(type,"Accessory List",returnData,"Back");
},
.down=[&](MenuType type,Data&returnData){
Menu::ScrollDown(type,"Accessory List",returnData,"Back");
},
.left="Back",
.right=[&](MenuType type,Data&returnData){
Menu::menus[type]->GetSelection().lock()->Click();
returnData="Disassemble Button";
},}},
{"Back",{
.up=[&](MenuType type,Data&returnData){
returnData=Component<RowInventoryScrollableWindowComponent>(type,"Accessory List")->GetComponents().back().lock()->GetName();
},
.down=[&](MenuType type,Data&returnData){
returnData=Component<RowInventoryScrollableWindowComponent>(type,"Accessory List")->GetComponents().front().lock()->GetName();
},
.left="Disassemble Button",
.right="Disassemble Button",}},
{"Disassemble Button",{
.up=[&](MenuType type,Data&returnData){
returnData=Component<RowInventoryScrollableWindowComponent>(type,"Accessory List")->GetComponents().back().lock()->GetName();
},
.down=[&](MenuType type,Data&returnData){
returnData=Component<RowInventoryScrollableWindowComponent>(type,"Accessory List")->GetComponents().front().lock()->GetName();
},
.left=[&](MenuType type,Data&returnData){
if(Component<RowInventoryScrollableWindowComponent>(type,"Accessory List")->GetSelectedChild())returnData=Component<RowInventoryScrollableWindowComponent>(type,"Accessory List")->GetSelectedChild().value().get().GetName();
else returnData="Back";
},
.right="Back",}},
}
);
}

@ -38,28 +38,81 @@ All rights reserved.
#include "Menu.h"
#include "AdventuresInLestoria.h"
#include "MenuIconButton.h"
#include "MenuItemLabel.h"
#include "SoundEffect.h"
INCLUDE_game
void Menu::InitializeArtificerEnchantConfirmWindow(){
Menu*artificerEnchantConfirmWindow=CreateMenu(ARTIFICER_ENCHANT_CONFIRM,CENTERED,vi2d{144,144});
Menu*artificerEnchantConfirmWindow=CreateMenu(ARTIFICER_ENCHANT_CONFIRM,CENTERED,vi2d{240,144});
auto enchantSuccessLabel{artificerEnchantConfirmWindow->ADD("Enchant Success Label",MenuLabel)(geom2d::rect<float>{{0.f,-14.f},vf2d{artificerEnchantConfirmWindow->size.x,12.f}},"Enchantment Success!",1.f,ComponentAttr::SHADOW|ComponentAttr::OUTLINE|ComponentAttr::BACKGROUND)END};
auto chooseResultLabel{artificerEnchantConfirmWindow->ADD("Choose Result Label",MenuLabel)(geom2d::rect<float>{vf2d{0.f,0.f},vf2d{artificerEnchantConfirmWindow->size.x,12.f}},"Choose a Result",1.f,ComponentAttr::SHADOW)END};
auto backgroundOld{artificerEnchantConfirmWindow->ADD("Old Background",MenuLabel)(geom2d::rect<float>{{-2.f,12.f},{artificerEnchantConfirmWindow->size.x/2.f-4.f,124.f}},"",1.f,ComponentAttr::BACKGROUND|ComponentAttr::OUTLINE)END};
auto backgroundNew{artificerEnchantConfirmWindow->ADD("New Background",MenuLabel)(geom2d::rect<float>{{artificerEnchantConfirmWindow->size.x/2.f-2.f+8.f,12.f},{artificerEnchantConfirmWindow->size.x/2.f-4.f,124.f}},"",1.f,ComponentAttr::BACKGROUND|ComponentAttr::OUTLINE)END};
const float oldLabelTextWidth{game->GetTextSize("OLD").x*2.f};
auto oldLabel{artificerEnchantConfirmWindow->ADD("Old Item Label",MenuLabel)(geom2d::rect<float>{{artificerEnchantConfirmWindow->size.x/4.f-oldLabelTextWidth/2-4.f,14.f},vf2d{oldLabelTextWidth+8.f,24.f}},"OLD",2.f,ComponentAttr::SHADOW|ComponentAttr::BACKGROUND)END};
const float newLabelTextWidth{game->GetTextSize("NEW").x*2.f};
auto newLabel{artificerEnchantConfirmWindow->ADD("New Item Label",MenuLabel)(geom2d::rect<float>{{artificerEnchantConfirmWindow->size.x/2.f+artificerEnchantConfirmWindow->size.x/4.f-oldLabelTextWidth/2+2.f,14.f},vf2d{oldLabelTextWidth+8.f,24.f}},"NEW",2.f,ComponentAttr::SHADOW|ComponentAttr::BACKGROUND)END};
auto oldIcon{artificerEnchantConfirmWindow->ADD("Old Item Icon",MenuIconButton)(geom2d::rect<float>{{artificerEnchantConfirmWindow->size.x/4.f-16.f,42.f},{24,24}},nullptr,DO_NOTHING,IconButtonAttr::NOT_SELECTABLE|IconButtonAttr::NO_OUTLINE)END};
auto newIcon{artificerEnchantConfirmWindow->ADD("New Item Icon",MenuIconButton)(geom2d::rect<float>{{artificerEnchantConfirmWindow->size.x/2.f+artificerEnchantConfirmWindow->size.x/4.f-16.f+4.f,42.f},{24,24}},nullptr,DO_NOTHING,IconButtonAttr::NOT_SELECTABLE|IconButtonAttr::NO_OUTLINE)END};
auto oldItemDescription{artificerEnchantConfirmWindow->ADD("Old Item Description",MenuItemLabel)(geom2d::rect<float>{{0.f,74.f},{artificerEnchantConfirmWindow->size.x/2.f-8.f,60.f}},Item::BLANK,ItemLabelDescriptionType::ITEM_DESCRIPTION,0.5f,ComponentAttr::SHADOW|ComponentAttr::BACKGROUND|ComponentAttr::OUTLINE|ComponentAttr::LEFT_ALIGN)END};
auto newItemDescription{artificerEnchantConfirmWindow->ADD("New Item Description",MenuItemLabel)(geom2d::rect<float>{{artificerEnchantConfirmWindow->size.x/2.f+8.f,74.f},{artificerEnchantConfirmWindow->size.x/2.f-8.f,60.f}},Item::BLANK,ItemLabelDescriptionType::ITEM_DESCRIPTION,0.5f,ComponentAttr::SHADOW|ComponentAttr::BACKGROUND|ComponentAttr::OUTLINE|ComponentAttr::LEFT_ALIGN)END};
const float takeOldTextWidth{float(game->GetTextSize("Take Old").x)*2.f};
auto takeOldButton{artificerEnchantConfirmWindow->ADD("Take Old Button",MenuComponent)(geom2d::rect<float>{{artificerEnchantConfirmWindow->size.x/4.f-takeOldTextWidth/2.f,138.f},{takeOldTextWidth-8.f,20.f}},"Take Old",[](MenuFuncData data){
onClick:
std::any&oldEnchant{Menu::menus[ARTIFICER_ENCHANT]->ANY(A::ENCHANT)};
std::weak_ptr<Item>newItem{std::get<std::weak_ptr<Item>>(Component<MenuItemLabel>(data.menu.GetType(),"New Item Description")->GetItem())}; //NOTE: We're making an assumption here that the new item description holds a weak pointer. This should be true because the only way to get here was to set it up through clicking the Enchant button in Artificer Enchant Window.
if(oldEnchant.has_value())newItem.lock()->_EnchantItem(std::any_cast<ItemEnchant&>(oldEnchant));
else newItem.lock()->RemoveEnchant();
Component<MenuComponent>(ARTIFICER_ENCHANT,"Fragment Enchant Button")->SetGrayedOut(!newItem.lock()->CanBeEnchanted());
const std::string_view fragmentName{newItem.lock()->FragmentName()};
const Pixel fragmentItemDisplayCol{Inventory::GetItemCount(fragmentName)>="Fragment Enchant Cost"_i[0]?WHITE:RED};
const Pixel moneyCostDisplayCol{game->GetPlayer()->GetMoney()>="Fragment Enchant Cost"_i[1]?WHITE:RED};
Component<MenuLabel>(ARTIFICER_ENCHANT,"Fragment Label")->SetLabel(std::format("{}{} x{} ({})",fragmentItemDisplayCol.toHTMLColorCode(),fragmentName,"Fragment Enchant Cost"_i[0],Inventory::GetItemCount(fragmentName)));
Component<MenuLabel>(ARTIFICER_ENCHANT,"Fragment Money Cost Label")->SetLabel(std::format("{}{} gold",moneyCostDisplayCol.toHTMLColorCode(),"Fragment Enchant Cost"_i[1]));
SoundEffect::PlaySFX("Take Enchant",SoundEffect::CENTERED);
Menu::CloseMenu();
return true;
},vf2d{2.f,2.f})END};
const float takeNewTextWidth{float(game->GetTextSize("Take New").x)*2.f};
auto takeNewButton{artificerEnchantConfirmWindow->ADD("Take New Button",MenuComponent)(geom2d::rect<float>{vf2d{artificerEnchantConfirmWindow->size.x/2.f+8.f+artificerEnchantConfirmWindow->size.x/4.f-takeNewTextWidth/2.f,138.f},{takeNewTextWidth-8.f,20.f}},"Take New",[](MenuFuncData data){
onClick:
std::weak_ptr<Item>newItem{std::get<std::weak_ptr<Item>>(Component<MenuItemLabel>(data.menu.GetType(),"New Item Description")->GetItem())};
Component<MenuComponent>(ARTIFICER_ENCHANT,"Fragment Enchant Button")->SetGrayedOut(!newItem.lock()->CanBeEnchanted());
const std::string_view fragmentName{newItem.lock()->FragmentName()};
const Pixel fragmentItemDisplayCol{Inventory::GetItemCount(fragmentName)>="Fragment Enchant Cost"_i[0]?WHITE:RED};
const Pixel moneyCostDisplayCol{game->GetPlayer()->GetMoney()>="Fragment Enchant Cost"_i[1]?WHITE:RED};
Component<MenuLabel>(ARTIFICER_ENCHANT,"Fragment Label")->SetLabel(std::format("{}{} x{} ({})",fragmentItemDisplayCol.toHTMLColorCode(),fragmentName,"Fragment Enchant Cost"_i[0],Inventory::GetItemCount(fragmentName)));
Component<MenuLabel>(ARTIFICER_ENCHANT,"Fragment Money Cost Label")->SetLabel(std::format("{}{} gold",moneyCostDisplayCol.toHTMLColorCode(),"Fragment Enchant Cost"_i[1]));
SoundEffect::PlaySFX("Take Enchant",SoundEffect::CENTERED);
Menu::CloseMenu();
return true;
},vf2d{2.f,2.f})END};
artificerEnchantConfirmWindow->SetupKeyboardNavigation(
[](MenuType type,Data&returnData){ //On Open
returnData="";
returnData="Take New Button";
},
{ //Button Key
{game->KEY_SCROLL,{"Navigate",[](MenuType type){}}},
{game->KEY_BACK,{"Stay",[](MenuType type){
Menu::CloseMenu();
}}},
{game->KEY_CONFIRM,{"Select",[](MenuType type){}}},
}
,{ //Button Navigation Rules
{"Sample Button",{
.up="",
.down="",
.left="",
.right="",}},
{"Take Old Button",{
.left="Take New Button",
.right="Take New Button",}},
{"Take New Button",{
.left="Take Old Button",
.right="Take Old Button",}},
});
}

@ -40,9 +40,10 @@ All rights reserved.
#include "AdventuresInLestoria.h"
#include "RowInventoryScrollableWindowComponent.h"
#include "MenuItemItemButton.h"
#include "MenuLabel.h"
#include "MenuItemLabel.h"
#include "MenuDecal.h"
#include "PlayerMoneyLabel.h"
#include "SoundEffect.h"
INCLUDE_game
@ -59,18 +60,43 @@ void Menu::InitializeArtificerEnchantWindow(){
auto accessoryDescription{artificerEnchantWindow->ADD("Item Description",MenuLabel)(geom2d::rect<float>{{artificerEnchantWindow->size.x/2+56.f,16.f},{artificerEnchantWindow->size.x/2-40.f,72.f}},"",0.5f,ComponentAttr::BACKGROUND|ComponentAttr::OUTLINE|ComponentAttr::SHADOW|ComponentAttr::LEFT_ALIGN)END};
const auto ResetRefineDisplay{[artificerEnchantWindow](){
const auto ResetEnchantDisplay{[artificerEnchantWindow](){
MenuType menuType{artificerEnchantWindow->GetType()};
Component<MenuItemItemButton>(menuType,"Item Icon")->SetItem(Item::BLANK);
Component<ScrollableWindowComponent>(menuType,"Enchant Container")->Disable();
Component<MenuLabel>(menuType,"Enchant List Header")->Disable();
Component<MenuLabel>(menuType,"Enchant Cost Label")->Disable();
Component<MenuDecal>(menuType,"Fragment Cost Icon")->Disable();
Component<MenuLabel>(menuType,"Fragment Label")->Disable();
Component<MenuLabel>(menuType,"Fragment Money Cost Label")->Disable();
Component<MenuComponent>(menuType,"Fragment Enchant Button")->Disable();
}};
const auto EnableRefineDisplay{[artificerEnchantWindow](){
const auto EnableEnchantDisplay{[artificerEnchantWindow](){
MenuType menuType{artificerEnchantWindow->GetType()};
const std::weak_ptr<Item>&selectedItem{Component<MenuItemItemButton>(menuType,"Item Icon")->GetItem()};
std::vector<ItemEnchantInfo>availableEnchants{ItemEnchant::GetAvailableEnchants()};
std::sort(availableEnchants.begin(),availableEnchants.end(),[](const ItemEnchantInfo&enchant1,const ItemEnchantInfo&enchant2){
return enchant1.Category()!=enchant2.Category()?int(enchant1.Category())<int(enchant2.Category()):enchant1.Name()<enchant2.Name();
});
std::string enchantList{std::accumulate(availableEnchants.begin(),availableEnchants.end(),""s,[](const std::string&acc,const ItemEnchantInfo&enchant){
return std::format("{}{}{}#FFFFFF\n",acc,enchant.DisplayCol().toHTMLColorCode(),enchant.Name());
})};
Component<ScrollableWindowComponent>(menuType,"Enchant Container")->Enable();
Component<MenuLabel>(menuType,"Enchant List")->SetLabel(enchantList);
Component<MenuLabel>(menuType,"Enchant List Header")->Enable();
Component<MenuLabel>(menuType,"Enchant Cost Label")->Enable();
Component<MenuDecal>(menuType,"Fragment Cost Icon")->Enable();
Component<MenuDecal>(menuType,"Fragment Cost Icon")->SetImage(GFX.at(selectedItem.lock()->FragmentIcon().value()));
Component<MenuLabel>(menuType,"Fragment Label")->Enable();
Component<MenuLabel>(menuType,"Fragment Money Cost Label")->Enable();
Component<MenuComponent>(menuType,"Fragment Enchant Button")->Enable();
Component<MenuComponent>(menuType,"Fragment Enchant Button")->SetGrayedOut(!selectedItem.lock()->CanBeEnchanted());
const std::string_view fragmentName{selectedItem.lock()->FragmentName()};
const Pixel fragmentItemDisplayCol{Inventory::GetItemCount(fragmentName)>="Fragment Enchant Cost"_i[0]?WHITE:RED};
const Pixel moneyCostDisplayCol{game->GetPlayer()->GetMoney()>="Fragment Enchant Cost"_i[1]?WHITE:RED};
Component<MenuLabel>(menuType,"Fragment Label")->SetLabel(std::format("{}{} x{} ({})",fragmentItemDisplayCol.toHTMLColorCode(),fragmentName,"Fragment Enchant Cost"_i[0],Inventory::GetItemCount(fragmentName)));
Component<MenuLabel>(menuType,"Fragment Money Cost Label")->SetLabel(std::format("{}{} gold",moneyCostDisplayCol.toHTMLColorCode(),"Fragment Enchant Cost"_i[1]));
}};
auto inventoryDisplay{artificerEnchantWindow->ADD("Accessory List",RowInventoryScrollableWindowComponent)(geom2d::rect<float>{{0.f,28.f},{artificerEnchantWindow->size.x/2-4.f,artificerEnchantWindow->size.y-44}},"","",[](MenuFuncData data){
@ -78,18 +104,18 @@ void Menu::InitializeArtificerEnchantWindow(){
RowItemDisplay&item{*DYNAMIC_POINTER_CAST<RowItemDisplay>(data.component)};
DYNAMIC_POINTER_CAST<RowInventoryScrollableWindowComponent>(data.parentComponent.lock())->SelectChild(DYNAMIC_POINTER_CAST<RowItemDisplay>(data.component));
return true;
},[EnableRefineDisplay](MenuFuncData data){OnHover:
},[EnableEnchantDisplay](MenuFuncData data){OnHover:
RowItemDisplay&item{*DYNAMIC_POINTER_CAST<RowItemDisplay>(data.component)};
Component<MenuItemItemButton>(data.menu.type,"Item Icon")->SetItem(item.GetItem());
EnableRefineDisplay();
EnableEnchantDisplay();
return true;
},[ResetRefineDisplay,EnableRefineDisplay](MenuFuncData data){OnMouseOut:
ResetRefineDisplay();
},[ResetEnchantDisplay,EnableEnchantDisplay](MenuFuncData data){OnMouseOut:
ResetEnchantDisplay();
auto childComponent{DYNAMIC_POINTER_CAST<RowInventoryScrollableWindowComponent>(data.parentComponent.lock())->GetSelectedChild()};
if(childComponent){
RowItemDisplay&item{childComponent.value().get()};
Component<MenuItemItemButton>(data.menu.type,"Item Icon")->SetItem(item.GetItem());
EnableRefineDisplay();
EnableEnchantDisplay();
}
return true;
},
@ -107,9 +133,22 @@ void Menu::InitializeArtificerEnchantWindow(){
auto fragmentDisplayLabel{artificerEnchantWindow->ADD("Fragment Label",MenuLabel)(geom2d::rect<float>{{artificerEnchantWindow->size.x/2+80.f,152.f},{artificerEnchantWindow->size.x/2-60.f,12.f}},"",1.f,ComponentAttr::SHADOW|ComponentAttr::FIT_TO_LABEL|ComponentAttr::LEFT_ALIGN)END};
auto fragmentMoneyCostLabel{artificerEnchantWindow->ADD("Fragment Money Cost Label",MenuLabel)(geom2d::rect<float>{{artificerEnchantWindow->size.x/2+80.f,164.f},{artificerEnchantWindow->size.x/2-60.f,12.f}},"",1.f,ComponentAttr::SHADOW|ComponentAttr::LEFT_ALIGN)END};
auto fragmentEnchantButton{artificerEnchantWindow->ADD("Fragment Enchant Button",MenuComponent)(geom2d::rect<float>{{artificerEnchantWindow->size.x/2+96.f,180.f},{artificerEnchantWindow->size.x/2-80.f,12.f}},"Enchant",[EnableRefineDisplay](MenuFuncData data){
auto fragmentEnchantButton{artificerEnchantWindow->ADD("Fragment Enchant Button",MenuComponent)(geom2d::rect<float>{{artificerEnchantWindow->size.x/2+96.f,180.f},{artificerEnchantWindow->size.x/2-80.f,12.f}},"Enchant",[EnableEnchantDisplay](MenuFuncData data){
onClick:
EnableRefineDisplay(); //Refresh the current display contents.
EnableEnchantDisplay(); //Refresh the current display contents.
SoundEffect::PlaySFX("Enchant Item",SoundEffect::CENTERED);
std::weak_ptr<Item>item{Component<MenuItemItemButton>(data.menu.type,"Item Icon")->GetItem()};
std::optional<ItemEnchant>previousEnchant{item.lock()->GetEnchant()};
data.menu.ANY(A::ENCHANT)={};
if(previousEnchant.has_value())data.menu.ANY(A::ENCHANT)=previousEnchant.value();
Component<MenuIconButton>(ARTIFICER_ENCHANT_CONFIRM,"Old Item Icon")->SetIcon(item.lock()->Icon().Decal());
Component<MenuItemLabel>(ARTIFICER_ENCHANT_CONFIRM,"Old Item Description")->SetItem(*item.lock());
item.lock()->ApplyRandomEnchant();
const ItemEnchant&newEnchant{item.lock()->GetEnchant().value()};
Component<MenuIconButton>(ARTIFICER_ENCHANT_CONFIRM,"New Item Icon")->SetIcon(item.lock()->Icon().Decal());
Component<MenuItemLabel>(ARTIFICER_ENCHANT_CONFIRM,"New Item Description")->SetItem(item.lock());
Menu::OpenMenu(ARTIFICER_ENCHANT_CONFIRM,true);
return true;
})END};
@ -128,11 +167,15 @@ void Menu::InitializeArtificerEnchantWindow(){
return true;
})END};
ResetEnchantDisplay();
Menu::AddInventoryListener(inventoryDisplay,"Accessories");
artificerEnchantWindow->SetupKeyboardNavigation(
[](MenuType type,Data&returnData){ //On Open
returnData="";
auto&items{Component<RowInventoryScrollableWindowComponent>(type,"Accessory List")->GetComponents()};
if(Component<RowInventoryScrollableWindowComponent>(type,"Accessory List")->GetSelectedChild())returnData=Component<RowInventoryScrollableWindowComponent>(type,"Accessory List")->GetSelectedChild().value().get().GetName();
else if(items.size()>0)returnData=items[0];
else returnData="Back";
},
{ //Button Key
{game->KEY_SCROLL,{"Navigate",[](MenuType type){}}},
@ -140,12 +183,52 @@ void Menu::InitializeArtificerEnchantWindow(){
Menu::CloseMenu();
}}},
{game->KEY_CONFIRM,{"Select",[](MenuType type){}}},
{{game->KEY_SHOULDER2,Pressed},{"Scroll Up/Down",[](MenuType type){}}},
{{game->KEY_SHOULDER,Pressed},{"Scroll Up/Down",[](MenuType type){}}},
{{game->KEY_FASTSCROLLUP,PressedDAS},{"Scroll",[](MenuType type){
Menu::menus[type]->GetSelection().lock()->parentComponent.lock()->DecreaseSelectionIndex(3.f);
}}},
{{game->KEY_FASTSCROLLDOWN,PressedDAS},{"Scroll",[](MenuType type){
Menu::menus[type]->GetSelection().lock()->parentComponent.lock()->IncreaseSelectionIndex(3.f);
}}},
{{game->KEY_SCROLLVERT_R,Analog,InputEngageGroup::NOT_VISIBLE},{"Scroll Enchants",[](MenuType type){
Component<ScrollableWindowComponent>(type,"Enchant Container")->Scroll(game->KEY_SCROLLVERT.Analog());
}}},
}
,{ //Button Navigation Rules
{"Sample Button",{
.up="",
.down="",
.left="",
.right="",}},
});
{"Accessory List",{
.up=[&](MenuType type,Data&returnData){
Menu::ScrollUp(type,"Accessory List",returnData,"Back");
},
.down=[&](MenuType type,Data&returnData){
Menu::ScrollDown(type,"Accessory List",returnData,"Back");
},
.left="Back",
.right=[&](MenuType type,Data&returnData){
Menu::menus[type]->GetSelection().lock()->Click();
returnData="Fragment Enchant Button";
},}},
{"Back",{
.up=[&](MenuType type,Data&returnData){
returnData=Component<RowInventoryScrollableWindowComponent>(type,"Accessory List")->GetComponents().back().lock()->GetName();
},
.down=[&](MenuType type,Data&returnData){
returnData=Component<RowInventoryScrollableWindowComponent>(type,"Accessory List")->GetComponents().front().lock()->GetName();
},
.left="Fragment Enchant Button",
.right="Fragment Enchant Button",}},
{"Fragment Enchant Button",{
.up=[&](MenuType type,Data&returnData){
returnData=Component<RowInventoryScrollableWindowComponent>(type,"Accessory List")->GetComponents().back().lock()->GetName();
},
.down=[&](MenuType type,Data&returnData){
returnData=Component<RowInventoryScrollableWindowComponent>(type,"Accessory List")->GetComponents().front().lock()->GetName();
},
.left=[&](MenuType type,Data&returnData){
if(Component<RowInventoryScrollableWindowComponent>(type,"Accessory List")->GetSelectedChild())returnData=Component<RowInventoryScrollableWindowComponent>(type,"Accessory List")->GetSelectedChild().value().get().GetName();
else returnData="Back";
},
.right="Back",}},
}
);
}

@ -61,7 +61,7 @@ void Menu::InitializeArtificerRefineResultWindow(){
artificerRefineResultWindow->SetupKeyboardNavigation(
[](MenuType type,Data&returnData){ //On Open
returnData="";
returnData="Continue Button";
},
{ //Button Key
{game->KEY_SCROLL,{"Navigate",[](MenuType type){}}},
@ -71,10 +71,5 @@ void Menu::InitializeArtificerRefineResultWindow(){
{game->KEY_CONFIRM,{"Select",[](MenuType type){}}},
}
,{ //Button Navigation Rules
{"Sample Button",{
.up="",
.down="",
.left="",
.right="",}},
});
}

@ -124,7 +124,7 @@ void Menu::InitializeArtificerRefineWindow(){
auto fragmentRefineButton{artificerRefineWindow->ADD("Fragment Refine Button",MenuComponent)(geom2d::rect<float>{{artificerRefineWindow->size.x/2+96.f,180.f},{artificerRefineWindow->size.x/2-80.f,12.f}},"Refine",[EnableRefineDisplay](MenuFuncData data){
onClick:
SoundEffect::PlaySFX("Sprint",SoundEffect::CENTERED);
SoundEffect::PlaySFX("Refine Item",SoundEffect::CENTERED);
std::weak_ptr<Item>item{Component<MenuItemItemButton>(data.menu.type,"Item Icon")->GetItem()};
RefineResult result{item.lock()->Refine()};
Component<MenuItemItemButton>(ARTIFICER_REFINE_RESULT,"Item Icon")->SetItem(item);
@ -157,7 +157,10 @@ void Menu::InitializeArtificerRefineWindow(){
artificerRefineWindow->SetupKeyboardNavigation(
[](MenuType type,Data&returnData){ //On Open
returnData="";
auto&items{Component<RowInventoryScrollableWindowComponent>(type,"Accessory List")->GetComponents()};
if(Component<RowInventoryScrollableWindowComponent>(type,"Accessory List")->GetSelectedChild())returnData=Component<RowInventoryScrollableWindowComponent>(type,"Accessory List")->GetSelectedChild().value().get().GetName();
else if(items.size()>0)returnData=items[0];
else returnData="Back";
},
{ //Button Key
{game->KEY_SCROLL,{"Navigate",[](MenuType type){}}},
@ -165,12 +168,49 @@ void Menu::InitializeArtificerRefineWindow(){
Menu::CloseMenu();
}}},
{game->KEY_CONFIRM,{"Select",[](MenuType type){}}},
{{game->KEY_SHOULDER2,Pressed},{"Scroll Up/Down",[](MenuType type){}}},
{{game->KEY_SHOULDER,Pressed},{"Scroll Up/Down",[](MenuType type){}}},
{{game->KEY_FASTSCROLLUP,PressedDAS},{"Scroll",[](MenuType type){
Menu::menus[type]->GetSelection().lock()->parentComponent.lock()->DecreaseSelectionIndex(3.f);
}}},
{{game->KEY_FASTSCROLLDOWN,PressedDAS},{"Scroll",[](MenuType type){
Menu::menus[type]->GetSelection().lock()->parentComponent.lock()->IncreaseSelectionIndex(3.f);
}}},
}
,{ //Button Navigation Rules
{"Sample Button",{
.up="",
.down="",
.left="",
.right="",}},
});
{"Accessory List",{
.up=[&](MenuType type,Data&returnData){
Menu::ScrollUp(type,"Accessory List",returnData,"Back");
},
.down=[&](MenuType type,Data&returnData){
Menu::ScrollDown(type,"Accessory List",returnData,"Back");
},
.left="Back",
.right=[&](MenuType type,Data&returnData){
Menu::menus[type]->GetSelection().lock()->Click();
returnData="Fragment Refine Button";
},}},
{"Back",{
.up=[&](MenuType type,Data&returnData){
returnData=Component<RowInventoryScrollableWindowComponent>(type,"Accessory List")->GetComponents().back().lock()->GetName();
},
.down=[&](MenuType type,Data&returnData){
returnData=Component<RowInventoryScrollableWindowComponent>(type,"Accessory List")->GetComponents().front().lock()->GetName();
},
.left="Fragment Refine Button",
.right="Fragment Refine Button",}},
{"Fragment Refine Button",{
.up=[&](MenuType type,Data&returnData){
returnData=Component<RowInventoryScrollableWindowComponent>(type,"Accessory List")->GetComponents().back().lock()->GetName();
},
.down=[&](MenuType type,Data&returnData){
returnData=Component<RowInventoryScrollableWindowComponent>(type,"Accessory List")->GetComponents().front().lock()->GetName();
},
.left=[&](MenuType type,Data&returnData){
if(Component<RowInventoryScrollableWindowComponent>(type,"Accessory List")->GetSelectedChild())returnData=Component<RowInventoryScrollableWindowComponent>(type,"Accessory List")->GetSelectedChild().value().get().GetName();
else returnData="Back";
},
.right="Back",}},
}
);
}

@ -62,6 +62,7 @@ void Menu::InitializeArtificerWindow(){
},vf2d{2.f,2.f},ButtonAttr::FIT_TO_LABEL)END;
artificerWindow->ADD("Enchant Button",MenuComponent)(geom2d::rect<float>{{0.f,60.f},{144.f,24.f}},"Enchant",[](MenuFuncData data){
Menu::OpenMenu(MenuType::ARTIFICER_ENCHANT);
Component<RowInventoryScrollableWindowComponent>(MenuType::ARTIFICER_ENCHANT,"Accessory List")->ClearSelectedChild();
return true;
},vf2d{2.f,2.f},ButtonAttr::FIT_TO_LABEL)END;
artificerWindow->ADD("Help Button",MenuComponent)(geom2d::rect<float>{{0.f,88.f},{144.f,24.f}},"Help",[](MenuFuncData data){
@ -75,7 +76,7 @@ void Menu::InitializeArtificerWindow(){
artificerWindow->SetupKeyboardNavigation(
[](MenuType type,Data&returnData){ //On Open
returnData="Refine Button";
returnData="Disassemble Button";
},
{ //Button Key
{game->KEY_SCROLL,{"Navigate",[](MenuType type){}}},
@ -85,20 +86,20 @@ void Menu::InitializeArtificerWindow(){
{game->KEY_CONFIRM,{"Select",[](MenuType type){}}},
}
,{ //Button Navigation Rules
{"Refine Button",{
.up="Leave Button",
.down="Disassemble Button",}},
{"Disassemble Button",{
.up="Refine Button",
.up="Leave Button",
.down="Refine Button",}},
{"Refine Button",{
.up="Disassemble Button",
.down="Enchant Button",}},
{"Enchant Button",{
.up="Disassemble Button",
.up="Refine Button",
.down="Help Button",}},
{"Help Button",{
.up="Enchant Button",
.down="Leave Button",}},
{"Leave Button",{
.up="Help Button",
.down="Refine Button",}},
.down="Disassemble Button",}},
});
}

@ -40,50 +40,58 @@ All rights reserved.
#include "DEFINES.h"
#include <variant>
using namespace std::literals;
class IAttributable{
public:
inline virtual ~IAttributable(){};
std::map<Attribute,std::variant<VARIANTS>>attributes;
std::map<Attribute,std::any>attributes;
inline float&GetFloat(Attribute a){
if(attributes.count(a)==0){
attributes[a]=0.f;
}
return std::get<float>(attributes[a]);
return std::any_cast<float&>(attributes[a]);
};
inline int&GetInt(Attribute a){
if(attributes.count(a)==0){
attributes[a]=0;
}
return std::get<int>(attributes[a]);
return std::any_cast<int&>(attributes[a]);
};
inline std::string&GetString(Attribute a){
if(attributes.count(a)==0){
attributes[a]="";
attributes[a]=""s;
}
return std::get<std::string>(attributes[a]);
return std::any_cast<std::string&>(attributes[a]);
};
inline bool&GetBool(Attribute a){
if(attributes.count(a)==0){
attributes[a]=false;
}
return std::get<bool>(attributes[a]);
return std::any_cast<bool&>(attributes[a]);
};
inline vf2d&GetVf2d(Attribute a){
if(attributes.count(a)==0){
attributes[a]=vf2d{};
}
return std::get<vf2d>(attributes[a]);
return std::any_cast<vf2d&>(attributes[a]);
};
inline std::vector<std::any>&GetVec(Attribute a){
if(attributes.count(a)==0){
attributes[a]=std::vector<std::any>{};
}
return std::get<std::vector<std::any>>(attributes[a]);
return std::any_cast<std::vector<std::any>&>(attributes[a]);
};
inline std::any&GetAny(Attribute a){
if(attributes.count(a)==0){
attributes[a]=std::any{};
}
return attributes[a];
};
inline size_t&GetSizeT(Attribute a){
if(attributes.count(a)==0){
attributes[a]=size_t(0);
}
return std::get<size_t>(attributes[a]);
return std::any_cast<size_t&>(attributes[a]);
};
};

@ -113,6 +113,9 @@ public:
inline auto end()const{
return attributes.end();
}
inline auto size()const{
return attributes.size();
}
inline void clear(){
attributes.clear();
}

@ -41,6 +41,7 @@ All rights reserved.
#include "util.h"
#include "LoadingScreen.h"
#include "Menu.h"
#include "SoundEffect.h"
INCLUDE_game
INCLUDE_DATA
@ -384,6 +385,7 @@ void Audio::SetBGMVolume(float vol){
UpdateBGMVolume();
}
void Audio::SetBGMPitch(float pitch){
if(game->TestingModeEnabled())return;
BGM&track=Self().bgm[Self().playParams.sound];
for(int channelListIndex=0;int trackID:track.GetChannelIDs()){
Engine().SetPitch(trackID,pitch);
@ -415,8 +417,11 @@ float Audio::GetCalculatedBGMVolume(const float channelVol){
}
return channelVol*GetBGMVolume()*GetMuteMult()*pauseMult;
}
float Audio::GetCalculatedSFXVolume(const float vol){
return vol*GetSFXVolume()*GetMuteMult();
float Audio::GetCalculatedSFXVolume(const SoundEffect&sfx){
return sfx.TreatAsBPM()?Audio::GetCalculatedBGMVolume(sfx.GetVolume()):sfx.GetVolume()*GetSFXVolume()*GetMuteMult();
}
float Audio::GetCalculatedSFXVolume(const float sfxVol){
return sfxVol*GetSFXVolume()*GetMuteMult();
}
float Audio::GetMuteMult(){
if(muted)return 0.f;

@ -49,6 +49,8 @@ using ChannelIDList=std::vector<ChannelID>;
using Volume=float;
using VolumeList=std::vector<Volume>;
class SoundEffect;
class Audio{
friend class AiL;
public:
@ -80,7 +82,8 @@ public:
//This will get a prepared BGM loop iteration count which is useful for loading stages.
static int GetPrepareBGMLoopIterations(std::string_view sound);
static float GetCalculatedBGMVolume(const float channelVol);
static float GetCalculatedSFXVolume(const float vol);
static float GetCalculatedSFXVolume(const SoundEffect&sfx);
static float GetCalculatedSFXVolume(const float sfxVol); //NOTE: This is a more manually invoked function! If you are trying to play a specific sound effect from an event, use the SoundEffect version instead!! This accounts for any additional flags related to volume.
private:
bool trackLoadStarted=false;
bool trackLoadComplete=false;

@ -52,11 +52,11 @@ INCLUDE_MONSTER_DATA
using A=Attribute;
void Monster::STRATEGY::BEAR(Monster&m,float fElapsedTime,std::string strategy){
switch(m.I(A::PHASE)){
switch(PHASE()){
case 0:{
float distToPlayer=geom2d::line<float>(m.GetPos(),game->GetPlayer()->GetPos()).length();
if(distToPlayer<operator""_Pixels(ConfigFloat("Attack Range"))){
m.I(A::PHASE)=1;
SETPHASE(1);
m.PerformShootAnimation();
m.F(A::CASTING_TIMER)=ConfigFloat("Chargeup Time");
//The bear slam attack indicator will move with the bear, and the LOCKON_POS variable will hold a polar coordinate indicating distance and angle for where it should be attacking relative to its position.
@ -75,7 +75,7 @@ void Monster::STRATEGY::BEAR(Monster&m,float fElapsedTime,std::string strategy){
case 1:{
m.F(A::CASTING_TIMER)=std::max(0.f,m.F(A::CASTING_TIMER)-fElapsedTime);
if(m.F(A::CASTING_TIMER)==0.f){
m.I(A::PHASE)=2;
SETPHASE(2);
m.F(A::CASTING_TIMER)=ConfigFloat("Attack Animation Wait Time");
m.PerformAnimation("SLAM");
}
@ -85,7 +85,7 @@ void Monster::STRATEGY::BEAR(Monster&m,float fElapsedTime,std::string strategy){
if(m.F(A::CASTING_TIMER)==0.f){
float distToPlayer=geom2d::line<float>(m.GetPos(),game->GetPlayer()->GetPos()).length();
SoundEffect::PlaySFX("Bear Slam Attack",m.GetPos()+m.V(A::LOCKON_POS).cart());
m.I(A::PHASE)=0;
SETPHASE(0);
m.I(A::BEAR_STOMP_COUNT)++;
geom2d::circle<float>attackCircle={m.GetPos()+m.V(A::LOCKON_POS).cart(),float(operator""_Pixels(ConfigFloat("Smash Attack Diameter"))/2.f)};
if(geom2d::overlaps(attackCircle,game->GetPlayer()->Hitbox())){

@ -57,7 +57,7 @@ void Monster::STRATEGY::BOAR(Monster&m,float fElapsedTime,std::string strategy){
RECOVERY,
};
switch(m.phase){
switch(PHASE()){
case PhaseName::MOVE:{
float distToPlayer=m.GetDistanceFrom(game->GetPlayer()->GetPos());
if(m.canMove&&distToPlayer>=ConfigInt("Closein Range")/100.f*24){
@ -75,7 +75,7 @@ void Monster::STRATEGY::BOAR(Monster&m,float fElapsedTime,std::string strategy){
ScratchPhaseTransition:
m.PerformAnimation("SCRATCH");
m.F(A::CASTING_TIMER)=ConfigInt("Ground Scratch Count")*m.GetCurrentAnimation().GetTotalAnimationDuration();
m.phase=PhaseName::SCRATCH;
SETPHASE(PhaseName::SCRATCH);
vf2d chargeTargetPoint=geom2d::line<float>(m.GetPos(),game->GetPlayer()->GetPos()).rpoint(ConfigFloat("Charge Distance")/100.f*24);
float distanceToChargePoint=geom2d::line<float>(m.GetPos(),chargeTargetPoint).length();
@ -89,7 +89,7 @@ void Monster::STRATEGY::BOAR(Monster&m,float fElapsedTime,std::string strategy){
m.F(A::CASTING_TIMER)-=fElapsedTime;
if(m.F(A::CASTING_TIMER)<=0.f){
m.PerformShootAnimation();
m.phase=PhaseName::CHARGE;
SETPHASE(PhaseName::CHARGE);
m.AddBuff(BuffType::SPEEDBOOST,INFINITE,ConfigFloat("Charge Movespeed")/100.f-1);
@ -102,7 +102,7 @@ void Monster::STRATEGY::BOAR(Monster&m,float fElapsedTime,std::string strategy){
float distToTarget=geom2d::line<float>(m.GetPos(),m.target).length();
auto TransitionToRecoveryPhase=[&](){
m.phase=PhaseName::RECOVERY;
SETPHASE(PhaseName::RECOVERY);
m.F(A::CHARGE_COOLDOWN)=ConfigFloat("Charge Recovery Time");
m.PerformIdleAnimation();
};
@ -120,7 +120,7 @@ void Monster::STRATEGY::BOAR(Monster&m,float fElapsedTime,std::string strategy){
m.F(A::CHARGE_COOLDOWN)-=fElapsedTime;
m.targetAcquireTimer=0.f;
m.RemoveBuff(BuffType::SPEEDBOOST);
if(m.F(A::CHARGE_COOLDOWN)<=0)m.phase=PhaseName::MOVE;
if(m.F(A::CHARGE_COOLDOWN)<=0)SETPHASE(PhaseName::MOVE);
}break;
}
}

@ -103,7 +103,7 @@ BulletDestroyState Bomb::MonsterHit(Monster&monster,const uint8_t markStacksBefo
void Bomb::Draw(const Pixel blendCol)const{
Bullet::Draw(blendCol);
game->view.DrawPartialRotatedDecal(pos-vf2d{0,GetZ()},bomb_animation.GetFrame(animation).GetSourceImage()->Decal(),rotates?atan2(vel.y,vel.x)-PI/2:0,bomb_animation.GetFrame(animation).GetSourceRect().size/2,bomb_animation.GetFrame(animation).GetSourceRect().pos,bomb_animation.GetFrame(animation).GetSourceRect().size,scale,fadeOutTime==0?col:Pixel{col.r,col.g,col.b,uint8_t(util::lerp(col.a,0,1-((fadeOutTime-GetFadeoutTimer())/fadeOutTime)))});
game->view.DrawPartialRotatedDecal(pos-vf2d{0,GetZ()},bomb_animation.GetFrame(animation).GetSourceImage()->Decal(),rotates?atan2(vel.y,vel.x)-PI/2:0,bomb_animation.GetFrame(animation).GetSourceRect().size/2,bomb_animation.GetFrame(animation).GetSourceRect().pos,bomb_animation.GetFrame(animation).GetSourceRect().size,scale,fadeOutTime==0?col:Pixel{col.r,col.g,col.b,uint8_t(util::lerp(col.a,uint8_t(0),1-((fadeOutTime-GetFadeoutTimer())/fadeOutTime)))});
}
void Bomb::ModifyOutgoingDamageData(HurtDamageInfo&data){}

@ -56,11 +56,11 @@ void Monster::STRATEGY::BREAKING_PILLAR(Monster&m,float fElapsedTime,std::string
m.animation.ModifyDisplaySprite(m.internal_animState,ConfigString("Break Phase 1 Animation Name"));
}else m.animation.ModifyDisplaySprite(m.internal_animState,ConfigString("Break Phase 2 Animation Name"));
switch(m.phase){
switch(PHASE()){
case INITIALIZE:{
m.F(A::BREAK_TIME)=ConfigFloat("Break Time");
m.F(A::SHAKE_TIMER)=0.2f;
m.phase=RUN;
SETPHASE(RUN);
m.SetStrategyDeathFunction([&](GameEvent&deathEvent,Monster&m,const std::string&strategy){
m.lifetime=0.01f;

@ -41,6 +41,6 @@ All rights reserved.
Bullet::Bullet(vf2d pos,vf2d vel,float radius,int damage,bool upperLevel,bool friendly,Pixel col,vf2d scale,float image_angle)
:IBullet(pos,vel,radius,damage,upperLevel,friendly,col,scale,image_angle){}
//Initializes a bullet with an animation.
Bullet::Bullet(vf2d pos,vf2d vel,float radius,int damage,std::string animation,bool upperLevel,bool hitsMultiple,float lifetime,bool rotatesWithAngle,bool friendly,Pixel col,vf2d scale,float image_angle,std::string_view hitSound)
Bullet::Bullet(vf2d pos,vf2d vel,float radius,int damage,const std::string&animation,bool upperLevel,bool hitsMultiple,float lifetime,bool rotatesWithAngle,bool friendly,Pixel col,vf2d scale,float image_angle,std::string_view hitSound)
:IBullet(pos,vel,radius,damage,animation,upperLevel,hitsMultiple,lifetime,rotatesWithAngle,friendly,col,scale,image_angle,hitSound){}
void Bullet::ModifyOutgoingDamageData(HurtDamageInfo&data){}

@ -43,7 +43,7 @@ class Bullet:public IBullet{
public:
Bullet(vf2d pos,vf2d vel,float radius,int damage,bool upperLevel,bool friendly=false,Pixel col=WHITE,vf2d scale={1,1},float image_angle=0.f);
//Initializes a bullet with an animation.
Bullet(vf2d pos,vf2d vel,float radius,int damage,std::string animation,bool upperLevel,bool hitsMultiple=false,float lifetime=INFINITE,bool rotatesWithAngle=false,bool friendly=false,Pixel col=WHITE,vf2d scale={1,1},float image_angle=0.f,std::string_view hitSound="");
Bullet(vf2d pos,vf2d vel,float radius,int damage,const std::string&animation,bool upperLevel,bool hitsMultiple=false,float lifetime=INFINITE,bool rotatesWithAngle=false,bool friendly=false,Pixel col=WHITE,vf2d scale={1,1},float image_angle=0.f,std::string_view hitSound="");
protected:
virtual void ModifyOutgoingDamageData(HurtDamageInfo&data);
};

@ -98,10 +98,13 @@ struct Arrow:public Bullet{
struct ChargedArrow:public Bullet{
vf2d lastLaserPos;
ChargedArrow(vf2d pos,vf2d vel,float radius,int damage,bool upperLevel,bool friendly=false,Pixel col=WHITE);
ChargedArrow(const std::string&shotArrowGraphic,const std::string&laserGraphic,vf2d pos,vf2d vel,float radius,int damage,bool upperLevel,bool friendly=false,Pixel col=WHITE);
void Update(float fElapsedTime)override;
BulletDestroyState PlayerHit(Player*player)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _PlayerHit()!!
BulletDestroyState MonsterHit(Monster&monster,const uint8_t markStacksBeforeHit)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _MonsterHit()!!
void ModifyOutgoingDamageData(HurtDamageInfo&data);
private:
std::string laserGraphic;
};
struct FrogTongue:public Bullet{
@ -148,7 +151,7 @@ struct DaggerStab:public Bullet{
float daggerStabDistance;
float knockbackAmt;
DirectionOffsets daggerPositionOffsets;
DaggerStab(Monster&sourceMonster,float radius,int damage,const float knockbackAmt,bool upperLevel,const Direction facingDir,const float daggerFrameDuration,const float daggerStabDistance,const DirectionOffsets offsets,bool friendly=false,Pixel col=WHITE);
DaggerStab(Monster&sourceMonster,const std::string&image,float radius,int damage,const float knockbackAmt,bool upperLevel,const Direction facingDir,const float daggerFrameDuration,const float daggerStabDistance,const DirectionOffsets offsets,bool friendly=false,Pixel col=WHITE);
void Update(float fElapsedTime)override;
BulletDestroyState PlayerHit(Player*player)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _PlayerHit()!!
BulletDestroyState MonsterHit(Monster&monster,const uint8_t markStacksBeforeHit)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _MonsterHit()!!
@ -161,7 +164,7 @@ struct DaggerSlash:public Bullet{
float frameDuration;
float daggerSlashDistance;
float knockbackAmt;
DaggerSlash(Monster&sourceMonster,float radius,int damage,const float knockbackAmt,bool upperLevel,const Direction facingDir,const float daggerFrameDuration,const float daggerSlashDistance,bool friendly=false,Pixel col=WHITE);
DaggerSlash(Monster&sourceMonster,const std::string&image,float radius,int damage,const float knockbackAmt,bool upperLevel,const Direction facingDir,const float daggerFrameDuration,const float daggerSlashDistance,bool friendly=false,Pixel col=WHITE);
void Update(float fElapsedTime)override;
BulletDestroyState PlayerHit(Player*player)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _PlayerHit()!!
BulletDestroyState MonsterHit(Monster&monster,const uint8_t markStacksBeforeHit)override;//DO NOT CALL THIS DIRECTLY! INSTEAD USE _MonsterHit()!!
@ -348,19 +351,33 @@ private:
std::optional<std::weak_ptr<Monster>>homingTarget;
};
struct PoisonBottle:public Bullet{
PoisonBottle(vf2d pos,vf2d targetPos,float explodeRadius,float bounceExplodeRadius,float z,float totalFallTime,float totalRiseZAmt,int damage,int additionalBounceCount,bool upperLevel,bool hitsMultiple=false,float lifetime=INFINITE,bool friendly=false,Pixel col=WHITE,vf2d scale={1,1},float image_angle={0.f});
struct ThrownProjectile:public Bullet{
ThrownProjectile(vf2d pos,vf2d targetPos,const std::string&img,float explodeRadius,float z,float totalFallTime,float totalRiseZAmt,int damage,bool upperLevel,bool hitsMultiple=false,float lifetime=INFINITE,bool friendly=false,Pixel col=WHITE,vf2d scale={1,1},float image_angle={0.f},const std::optional<Effect>explodeEffect={},const std::optional<std::string>explodeSoundEffect={},const std::optional<LingeringEffect>lingeringEffect={});
void Update(float fElapsedTime)override;
void ModifyOutgoingDamageData(HurtDamageInfo&data);
private:
void _OnGroundLand();
virtual void OnGroundLand(); //This is called when the projectile lands, damage is not dealt yet, default behavior just deals damage in the area.
protected:
const vf2d targetPos;
const vf2d startingPos;
const float totalFallTime;
const float originalRisingTime,originalFallingTime;
float originalRisingTime;
float originalFallingTime;
float risingTime,fallingTime;
const float initialZ;
const float totalRiseZAmt;
const float explodeRadius;
const std::optional<Effect>explodeEffect;
const std::optional<std::string>explodeSoundEffect;
const std::optional<LingeringEffect>lingeringEffect;
const std::string img;
};
struct PoisonBottle:public ThrownProjectile{
PoisonBottle(vf2d pos,vf2d targetPos,const std::string&img,float explodeRadius,float bounceExplodeRadius,float z,float totalFallTime,float totalRiseZAmt,int damage,int additionalBounceCount,bool upperLevel,bool hitsMultiple=false,float lifetime=INFINITE,bool friendly=false,Pixel col=WHITE,vf2d scale={1,1},float image_angle={0.f});
void ModifyOutgoingDamageData(HurtDamageInfo&data);
virtual void OnGroundLand()override final; //This is called when the projectile lands, damage is not dealt yet, default behavior just deals damage in the area.
private:
const float bounceExplodeRadius;
int additionalBounceCount;
};

@ -476,28 +476,10 @@ void Menu::InitializeCharacterMenuWindow(){
,{ //Button Navigation Rules
{"Equip List",{
.up=[](MenuType type,Data&returnData){
if(!Menu::menus[type]->GetSelection().expired()){
auto selection=Menu::menus[type]->GetSelection().lock();
size_t index=Component<ScrollableWindowComponent>(type,"Equip List")->GetComponentIndex(selection);
index--;
if(index>=Component<ScrollableWindowComponent>(type,"Equip List")->GetComponents().size()){
returnData="Equip Selection Select Button";
}else{
returnData=Component<ScrollableWindowComponent>(type,"Equip List")->GetComponents()[index];
}
}
Menu::ScrollUp(type,"Equip List",returnData,"Equip Selection Select Button");
},
.down=[](MenuType type,Data&returnData){
if(!Menu::menus[type]->GetSelection().expired()){
auto selection=Menu::menus[type]->GetSelection().lock();
size_t index=Component<ScrollableWindowComponent>(type,"Equip List")->GetComponentIndex(selection);
index++;
if(index>=Component<ScrollableWindowComponent>(type,"Equip List")->GetComponents().size()){
returnData="Equip Selection Select Button";
}else{
returnData=Component<ScrollableWindowComponent>(type,"Equip List")->GetComponents()[index];
}
}
Menu::ScrollDown(type,"Equip List",returnData,"Equip Selection Select Button");
},
.left=[](MenuType type,Data&returnData){
auto equipList=Component<ScrollableWindowComponent>(type,"Equip List")->GetComponents();

@ -45,9 +45,12 @@ All rights reserved.
INCLUDE_game
ChargedArrow::ChargedArrow(vf2d pos,vf2d vel,float radius,int damage,bool upperLevel,bool friendly,Pixel col)
:lastLaserPos(pos),
Bullet(pos,vel,radius,damage,
"charged_shot_arrow.png",upperLevel,true,INFINITE,true,friendly,col){}
:lastLaserPos(pos),laserGraphic("laser.png"),
Bullet(pos,vel,radius,damage,"charged_shot_arrow.png",upperLevel,true,INFINITE,true,friendly,col){}
ChargedArrow::ChargedArrow(const std::string&shotArrowGraphic,const std::string&laserGraphic,vf2d pos,vf2d vel,float radius,int damage,bool upperLevel,bool friendly,Pixel col)
:lastLaserPos(pos),laserGraphic(laserGraphic),
Bullet(pos,vel,radius,damage,shotArrowGraphic,upperLevel,true,INFINITE,true,friendly,col){}
void ChargedArrow::Update(float fElapsedTime){
geom2d::line lineToCurrentPos(geom2d::line(lastLaserPos,pos));
@ -58,7 +61,7 @@ void ChargedArrow::Update(float fElapsedTime){
const float normalBeamRadius{12*"Ranger.Ability 2.Radius"_F/100/5};
float laserWidth{radius/normalBeamRadius};
game->AddEffect(std::make_unique<Effect>(midpoint,0.1f,"laser.png",upperLevel,vf2d{laserWidth,dist*2},0.3f,vf2d{},Pixel{192,128,238},atan2(pos.y-lastLaserPos.y,pos.x-lastLaserPos.x)+PI/2,0,true));
game->AddEffect(std::make_unique<Effect>(midpoint,0.1f,laserGraphic,upperLevel,vf2d{laserWidth,dist*2},0.3f,vf2d{},Pixel{192,128,238},atan2(pos.y-lastLaserPos.y,pos.x-lastLaserPos.x)+PI/2,0,true));
lastLaserPos=pos;
}
}

@ -0,0 +1,118 @@
#pragma region License
/*
License (OLC-3)
~~~~~~~~~~~~~~~
Copyright 2024 Joshua Sigona <sigonasr2@gmail.com>
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions or derivations of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions or derivative works in binary form must reproduce the above
copyright notice. This list of conditions and the following disclaimer must be
reproduced in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may
be used to endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
Portions of this software are copyright © 2024 The FreeType
Project (www.freetype.org). Please see LICENSE_FT.txt for more information.
All rights reserved.
*/
#pragma endregion
#include "MonsterStrategyHelpers.h"
#include "util.h"
#include "AdventuresInLestoria.h"
#include "SoundEffect.h"
#include "BulletTypes.h"
using A=Attribute;
INCLUDE_game
void Monster::STRATEGY::CRAB(Monster&m,float fElapsedTime,std::string strategy){
enum PhaseName{
INIT,
MOVE,
PREPARE_CHARGE,
CHARGE,
};
switch(PHASE()){
case INIT:{
m.B(A::RANDOM_DIRECTION)=util::random()%2;
m.F(A::RANDOM_RANGE)=util::random_range(ConfigPixelsArr("Random Direction Range",0),ConfigPixelsArr("Random Direction Range",1));
SETPHASE(MOVE);
}break;
case MOVE:{
m.F(A::ATTACK_COOLDOWN)+=fElapsedTime;
m.F(A::LAST_JUMP_TIMER)+=fElapsedTime;
float distToPlayer=m.GetDistanceFrom(game->GetPlayer()->GetPos());
const bool outsideMaxShootingRange=distToPlayer>=ConfigPixelsArr("Stand Still and Shoot Range",1);
if(m.F(A::LAST_JUMP_TIMER)>=ConfigFloat("Stop Check Interval")){
if(util::random(100)<=ConfigFloat("Stop Percent")){
SETPHASE(PREPARE_CHARGE);
m.PerformAnimation("CHARGEUP",m.GetFacingDirectionToTarget(game->GetPlayer()->GetPos()));
m.F(A::CASTING_TIMER)=ConfigFloat("Charge Wait Time");
m.target=game->GetPlayer()->GetPos()+util::distance(m.GetPos(),game->GetPlayer()->GetPos())*util::pointTo(m.GetPos(),game->GetPlayer()->GetPos());
}else
if(util::random(100)<=ConfigFloat("Change Direction Chance"))m.B(A::RANDOM_DIRECTION)=!m.B(A::RANDOM_DIRECTION);
m.F(A::LAST_JUMP_TIMER)=0.f;
}else
if(outsideMaxShootingRange){
m.target=game->GetPlayer()->GetPos();
RUN_TOWARDS(m,fElapsedTime,"Run Towards");
}else
if(distToPlayer<ConfigPixels("Run Away Range")){
m.target=geom2d::line<float>(m.GetPos(),game->GetPlayer()->GetPos()).upoint(-1);
RUN_TOWARDS(m,fElapsedTime,"Run Towards");
}else
if(distToPlayer>=ConfigPixelsArr("Random Direction Range",0)&&distToPlayer<ConfigPixelsArr("Random Direction Range",1)){
#define CW true
#define CCW false
//We are going to walk in a circular direction either CW or CCW (determined in windup phase)
float dirFromPlayer=util::angleTo(game->GetPlayer()->GetPos(),m.GetPos());
float targetDir=m.B(A::RANDOM_DIRECTION)==CW?dirFromPlayer+PI/4:dirFromPlayer-PI/4;
m.target=game->GetPlayer()->GetPos()+vf2d{m.F(A::RANDOM_RANGE),targetDir}.cart();
RUN_TOWARDS(m,fElapsedTime,"Run Towards");
m.F(A::CHASE_TIMER)=1.f;
}
}break;
case PREPARE_CHARGE:{
m.F(A::CASTING_TIMER)-=fElapsedTime;
if(m.F(A::CASTING_TIMER)<=0.f){
SETPHASE(CHARGE);
m.F(A::CHASE_TIMER)=ConfigFloat("Charge Max Time");
m.PerformAnimation("PINCER");
}
}break;
case CHARGE:{
m.F(A::CHASE_TIMER)-=fElapsedTime;
RUN_TOWARDS(m,fElapsedTime,"Run Towards");
if(m.F(A::CHASE_TIMER)<=0.f||m.ReachedTargetPos()){
SETPHASE(MOVE);
m.B(A::RANDOM_DIRECTION)=util::random()%2;
m.F(A::RANDOM_RANGE)=util::random_range(ConfigPixelsArr("Random Direction Range",0),ConfigPixelsArr("Random Direction Range",1));
}
}break;
}
}

@ -70,7 +70,7 @@ void Menu::InitializeCraftItemWindow(){
for(const auto&[name,amt]:consumedResources.GetItems()){
Inventory::RemoveItem(Inventory::GetItem(name)[0],amt);
}
game->GetPlayer()->SetMoney(game->GetPlayer()->GetMoney()-consumedResources.GetCost());
game->GetPlayer()->RemoveMoney(consumedResources.GetCost());
Inventory::UpdateBlacksmithInventoryLists();
SoundEffect::PlaySFX("Craft Equip",SoundEffect::CENTERED);

@ -63,6 +63,7 @@ using MonsterSpawnerID=int;
#define INCLUDE_ITEM_CONVERSIONS extern safemap<std::string,IT>ITEM_CONVERSIONS;
#define INCLUDE_ADMIN_MODE extern bool ADMIN_MODE;
#define INCLUDE_DEMO_BUILD extern bool DEMO_BUILD;
#define INCLUDE_ITEM_SCRIPTS extern safemap<std::string,ItemScript>ITEM_SCRIPTS;
#define INCLUDE_PACK_KEY extern std::string PACK_KEY;
@ -121,6 +122,10 @@ Ability&class::GetAbility1(){return ability1;}; \
Ability&class::GetAbility2(){return ability2;}; \
Ability&class::GetAbility3(){return ability3;}; \
Ability&class::GetAbility4(){return ability4;}; \
Ability&class::_GetOriginalAbility1(){return GetOriginalAbility1();}; \
Ability&class::_GetOriginalAbility2(){return GetOriginalAbility2();}; \
Ability&class::_GetOriginalAbility3(){return GetOriginalAbility2();}; \
Ability&class::_GetOriginalRightClickAbility(){return GetOriginalRightClickAbility();}; \
Ability&class::GetOriginalAbility1(){return original_ability1;}; \
Ability&class::GetOriginalAbility2(){return original_ability2;}; \
Ability&class::GetOriginalAbility3(){return original_ability3;}; \

@ -45,8 +45,8 @@ All rights reserved.
INCLUDE_game
INCLUDE_ANIMATION_DATA
DaggerSlash::DaggerSlash(Monster&sourceMonster,float radius,int damage,const float knockbackAmt,bool upperLevel,const Direction facingDir,const float daggerFrameDuration,const float daggerSlashDistance,bool friendly,Pixel col)
:Bullet(sourceMonster.GetPos(),{},radius,damage,"goblin_sword_slash.png",upperLevel,false,daggerFrameDuration*ANIMATION_DATA["goblin_sword_slash.png"].GetFrameCountBasedOnAnimationStyle(),true,friendly,col),
DaggerSlash::DaggerSlash(Monster&sourceMonster,const std::string&image,float radius,int damage,const float knockbackAmt,bool upperLevel,const Direction facingDir,const float daggerFrameDuration,const float daggerSlashDistance,bool friendly,Pixel col)
:Bullet(sourceMonster.GetPos(),{},radius,damage,image,upperLevel,false,daggerFrameDuration*ANIMATION_DATA["goblin_sword_slash.png"].GetFrameCountBasedOnAnimationStyle(),true,friendly,col),
sourceMonster(sourceMonster),frameDuration(daggerFrameDuration),daggerSlashDistance(daggerSlashDistance),facingDir(facingDir),knockbackAmt(knockbackAmt){}
void DaggerSlash::Update(float fElapsedTime){
ANIMATION_DATA["goblin_sword_slash.png"].ChangeFrameDuration(frameDuration);

@ -45,8 +45,8 @@ All rights reserved.
INCLUDE_game
INCLUDE_ANIMATION_DATA
DaggerStab::DaggerStab(Monster&sourceMonster,float radius,int damage,const float knockbackAmt,bool upperLevel,const Direction facingDir,const float daggerFrameDuration,const float daggerStabDistance,const DirectionOffsets offsets,bool friendly,Pixel col)
:Bullet(sourceMonster.GetPos(),{},radius,damage,"dagger_stab.png",upperLevel,false,daggerFrameDuration*ANIMATION_DATA["dagger_stab.png"].GetFrameCountBasedOnAnimationStyle(),true,friendly,col),
DaggerStab::DaggerStab(Monster&sourceMonster,const std::string&image,float radius,int damage,const float knockbackAmt,bool upperLevel,const Direction facingDir,const float daggerFrameDuration,const float daggerStabDistance,const DirectionOffsets offsets,bool friendly,Pixel col)
:Bullet(sourceMonster.GetPos(),{},radius,damage,image,upperLevel,false,daggerFrameDuration*ANIMATION_DATA["dagger_stab.png"].GetFrameCountBasedOnAnimationStyle(),true,friendly,col),
sourceMonster(sourceMonster),frameDuration(daggerFrameDuration),daggerStabDistance(daggerStabDistance),facingDir(facingDir),daggerPositionOffsets(offsets),knockbackAmt(knockbackAmt){}
void DaggerStab::Update(float fElapsedTime){
ANIMATION_DATA["dagger_stab.png"].ChangeFrameDuration(frameDuration);

@ -143,7 +143,7 @@ void DamageNumber::Draw(){
float DamageNumber::GetOriginalRiseSpd(){
float riseSpd{20.f};
if(type==INTERRUPT||type==MANA_GAIN)riseSpd=40.f;
if(type==INTERRUPT||type==MANA_GAIN||type==HEALTH_GAIN)riseSpd=40.f;
if(type==DOT)riseSpd=-10.f;
return riseSpd;
}

@ -204,13 +204,13 @@ struct FadeInOutEffect:Effect{
const float originalParticleSpawnTimer{};
};
struct PoisonPool:FadeInOutEffect{
PoisonPool(vf2d pos,const std::string&img,float radius,int damage,float damageFreq,const HurtType friendly,float lifetime,float cycleSpd,bool onUpperLevel,float size,vf2d spd,Pixel col,float rotation,float rotationSpd,bool additiveBlending=false,float particleSpawnFreq=0.f,const std::function<Effect(const Effect&self)>&particleGenerator={});
struct LingeringEffect:FadeInOutEffect{
LingeringEffect(vf2d pos,const std::string&img,const std::string&soundEffect,float radius,int damage,float damageFreq,const HurtType friendly,float lifetime,float cycleSpd,bool onUpperLevel,float size,vf2d spd,Pixel col,float rotation,float rotationSpd,bool additiveBlending=false,float particleSpawnFreq=0.f,const std::function<Effect(const Effect&self)>&particleGenerator={});
virtual bool Update(float fElapsedTime)override final;
const int damage;
float damageTimer{};
const float originalDamageTimer{};
const float radius;
HurtType friendly;
const size_t poisonPoolSFXID{};
const size_t sfxID{};
};

@ -68,6 +68,7 @@ void EnvironmentalAudio::SetAudioName(const std::string_view audioName){
void EnvironmentalAudio::Activate(){
if(activated)return;
soundInstance=Audio::LoadAndPlaySFX(operator""_SFX(SOUND_DATA[audioName].file.c_str(),SOUND_DATA[audioName].file.length()),true);
Audio::Engine().SetPitch(soundInstance,util::random_range(0.9f,1.1f));
activated=true;
}
void EnvironmentalAudio::Deactivate(){

@ -52,7 +52,7 @@ public:
inline void OnEquipStatsUpdate()override{
const std::weak_ptr<Item>equip=Inventory::GetEquip(slot);
if(!ISBLANK(equip)){
icon=const_cast<Decal*>(equip.lock()->Decal());
icon=const_cast<Decal*>(equip.lock()->Icon().Decal());
itemRef=equip;
}else{
icon=nullptr;

@ -51,11 +51,11 @@ using A=Attribute;
void Monster::STRATEGY::FROG(Monster&m,float fElapsedTime,std::string strategy){
m.F(A::LOCKON_WAITTIME)=std::max(0.0f,m.F(A::LOCKON_WAITTIME)-fElapsedTime);
phase:
switch(m.I(A::PHASE)){
switch(PHASE()){
case 0:{ //Move towards phase.
float distToPlayer=geom2d::line<float>(m.GetPos(),game->GetPlayer()->GetPos()).length();
if(distToPlayer<24*ConfigInt("Range")/100.f){
m.I(A::PHASE)++;
SETPHASE(PHASE()+1);
m.F(A::LOCKON_WAITTIME)=ConfigFloat("Lockon Wait Time");
m.V(A::LOCKON_POS)=game->GetPlayer()->GetPos();
m.RotateTowardsPos(m.V(A::LOCKON_POS));
@ -72,23 +72,23 @@ void Monster::STRATEGY::FROG(Monster&m,float fElapsedTime,std::string strategy){
SoundEffect::PlaySFX("Slime Shoot",m.pos);
CreateBullet(FrogTongue)(m,tongueMaxRangePos,ConfigFloat("Attack Duration"),m.GetAttack(),m.OnUpperLevel(),ConfigFloat("Tongue Knockback Strength"),false,ConfigPixel("Tongue Color"))EndBullet;
m.PerformShootAnimation();
m.I(A::PHASE)=2;
SETPHASE(2);
}
m.PerformIdleAnimation();
}break;
case 2:{
if(m.F(A::LOCKON_WAITTIME)==0.0f){
m.F(A::LOCKON_WAITTIME)=ConfigFloat("Attack Recovery Time");
m.I(A::PHASE)=3;
SETPHASE(3);
}
}break;
case 3:{
if(m.F(A::LOCKON_WAITTIME)==0.0f){
m.I(A::PHASE)=0;
SETPHASE(0);
}
}break;
default:{
ERR(std::format("Unhandled phase {} for {} strategy!",m.I(A::PHASE),strategy));
ERR(std::format("Unhandled phase {} for {} strategy!",PHASE(),strategy));
}
}
}

@ -56,7 +56,7 @@ void FrogTongue::Update(float fElapsedTime){
geom2d::line<float>lineToTarget(pos,targetPos);
vf2d drawVec=lineToTarget.vector().norm()*3;
tongueLength=util::lerp(0,lineToTarget.length(),pow(sin((lifetime*PI)/duration),20.f));
tongueLength=util::lerp(0.f,lineToTarget.length(),pow(sin((lifetime*PI)/duration),20.f));
vf2d tongueEndPos=geom2d::line<float>(pos+drawVec,targetPos).upoint(pow(sin((lifetime*PI)/duration),20.f));
geom2d::line<float>tongueLine(pos+drawVec,tongueEndPos);

@ -0,0 +1,84 @@
#pragma region License
/*
License (OLC-3)
~~~~~~~~~~~~~~~
Copyright 2024 Joshua Sigona <sigonasr2@gmail.com>
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions or derivations of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions or derivative works in binary form must reproduce the above
copyright notice. This list of conditions and the following disclaimer must be
reproduced in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may
be used to endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
Portions of this software are copyright © 2024 The FreeType
Project (www.freetype.org). Please see LICENSE_FT.txt for more information.
All rights reserved.
*/
#pragma endregion
#include "MonsterStrategyHelpers.h"
#include "util.h"
#include "AdventuresInLestoria.h"
#include "SoundEffect.h"
#include "BulletTypes.h"
using A=Attribute;
INCLUDE_game
void Monster::STRATEGY::GIANT_CRAB(Monster&m,float fElapsedTime,std::string strategy){
enum PhaseName{
PREPARE_CHARGE,
CHARGE,
};
switch(PHASE()){
case PREPARE_CHARGE:{
m.F(A::CASTING_TIMER)-=fElapsedTime;
if(m.F(A::CASTING_TIMER)<=0.f){
m.AddBuff(BuffType::SPEEDBOOST,ConfigFloat("Charge Time"),0.f);
m.F(A::SPEED_RAMPUP_TIMER)=0.1f;
SETPHASE(CHARGE);
}
}break;
case CHARGE:{
m.F(A::CHASE_TIMER)+=fElapsedTime;
m.F(A::SPEED_RAMPUP_TIMER)-=fElapsedTime;
if(m.F(A::CHASE_TIMER)>=ConfigFloat("Charge Time")||m.ReachedTargetPos()){
m.F(A::CHASE_TIMER)=0.f;
m.target=geom2d::line<float>(m.GetPos(),game->GetPlayer()->GetPos()).rpoint(util::distance(m.GetPos(),game->GetPlayer()->GetPos())+ConfigPixels("Charge Extend Distance"));
m.RemoveBuff(BuffType::SPEEDBOOST);
m.F(A::CASTING_TIMER)=ConfigFloat("Charge Wait Time");
SETPHASE(PREPARE_CHARGE);
}
if(m.F(A::SPEED_RAMPUP_TIMER)<=0.f){
m.F(A::SPEED_RAMPUP_TIMER)=0.1f;
if(m.HasBuff(BuffType::SPEEDBOOST)){
const float buffIntensity{m.GetBuffs(BuffType::SPEEDBOOST)[0].intensity};
m.EditBuff(BuffType::SPEEDBOOST,0).intensity=std::min(buffIntensity+ConfigFloat("Movespeed Rampup Final Amount")/100.f/(ConfigFloat("Movespeed Rampup Time")/0.1f),ConfigFloat("Movespeed Rampup Final Amount")/100.f);
}
}
RUN_TOWARDS(m,fElapsedTime,"Run Towards");
}break;
}
}

@ -0,0 +1,85 @@
#pragma region License
/*
License (OLC-3)
~~~~~~~~~~~~~~~
Copyright 2024 Joshua Sigona <sigonasr2@gmail.com>
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions or derivations of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions or derivative works in binary form must reproduce the above
copyright notice. This list of conditions and the following disclaimer must be
reproduced in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may
be used to endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
Portions of this software are copyright © 2024 The FreeType
Project (www.freetype.org). Please see LICENSE_FT.txt for more information.
All rights reserved.
*/
#pragma endregion
#include "MonsterStrategyHelpers.h"
#include "util.h"
#include "AdventuresInLestoria.h"
#include "SoundEffect.h"
#include "BulletTypes.h"
using A=Attribute;
INCLUDE_game
INCLUDE_MONSTER_LIST
void Monster::STRATEGY::GIANT_OCTOPUS(Monster&m,float fElapsedTime,std::string strategy){
enum PhaseName{
IDENTIFY_ARMS,
NORMAL,
};
switch(PHASE()){
case IDENTIFY_ARMS:{
m.F(A::CASTING_TIMER)=util::random_range(ConfigFloatArr("Tentacle Move Timer",0),ConfigFloatArr("Tentacle Move Timer",1));
for(std::shared_ptr<Monster>&arm:MONSTER_LIST){
const std::string OCTOPUS_ARM_NAME{"Octopus Arm"};
if(arm->GetName()==OCTOPUS_ARM_NAME){
std::weak_ptr<Monster>armPtr{arm};
m.VEC(A::ARM_LIST).emplace_back(armPtr);
m.VEC(A::ARM_LOCATIONS).emplace_back(armPtr.lock()->GetPos());
}
}
}break;
case NORMAL:{
m.F(A::CASTING_TIMER)-=fElapsedTime;
if(m.F(A::CASTING_TIMER)<=0.f){
int deadMonsterCount{0};
std::vector<vf2d>tempArmLocs;
for(size_t i=0U;std::any&arm:m.VEC(A::ARM_LIST)){
const std::weak_ptr<Monster>&m{std::any_cast<std::weak_ptr<Monster>>(arm)};
if(m.expired()||m.lock()->IsDead()){
deadMonsterCount++;
tempArmLocs.emplace_back(std::any_cast<vf2d>(m.lock()->VEC(A::ARM_LOCATIONS)));
}
}
if(deadMonsterCount>0){
}
}
}break;
}
}

@ -52,17 +52,16 @@ void Monster::STRATEGY::GOBLIN_BOAR_RIDER(Monster&m,float fElapsedTime,std::stri
if(!m.B(A::INITIALIZED_MOUNTED_MONSTER)){
m.B(A::INITIALIZED_MOUNTED_MONSTER)=true;
m.F(A::PERCEPTION_LEVEL)=ConfigFloat("Starting Perception Level");
m.internal_mounted_animState=Animate2D::AnimationState{};
m.mounted_animation=Animate2D::Animation<std::string>{};
for(bool firstAnimation=true;const std::string&animation:Config("Imposed Monster Animations").GetValues()){
m.mounted_animation.value().AddState(animation,ANIMATION_DATA.at(animation));
if(firstAnimation)m.mounted_animation.value().ChangeState(m.internal_mounted_animState.value(),animation);
if(firstAnimation)m.mounted_animation.value().ChangeState(m.internal_mounted_animState,animation);
firstAnimation=false;
}
m.mountedSprOffset=ConfigVec("Imposed Monster Offset");
m.deathData.push_back(DeathSpawnInfo{ConfigString("Spawned Monster"),1U});
m.deathData.emplace_back(ConfigString("Spawned Monster"),1U);
}
BOAR(m,fElapsedTime,"Boar");
@ -79,11 +78,11 @@ void Monster::STRATEGY::GOBLIN_BOAR_RIDER(Monster&m,float fElapsedTime,std::stri
m.F(A::ATTACK_COOLDOWN)=0.f;
m.F(A::PERCEPTION_LEVEL)=std::min(ConfigFloat("Maximum Perception Level"),m.F(A::PERCEPTION_LEVEL)+ConfigFloat("Perception Level Increase"));
m.mounted_animation.value().ChangeState(m.internal_mounted_animState.value(),std::format("GOBLIN_BOW_MOUNTED_{}",int(m.GetFacingDirection())));
m.mounted_animation.value().ChangeState(m.internal_mounted_animState,std::format("GOBLIN_BOW_MOUNTED_{}",int(m.GetFacingDirection())));
}
}else
if(m.F(A::ATTACK_COOLDOWN)>=ConfigFloat("Attack Reload Time")){
m.F(A::SHOOT_TIMER)=ConfigFloat("Attack Windup Time");
m.mounted_animation.value().ChangeState(m.internal_mounted_animState.value(),std::format("GOBLIN_BOW_ATTACK_{}",int(m.GetFacingDirection())));
m.mounted_animation.value().ChangeState(m.internal_mounted_animState,std::format("GOBLIN_BOW_ATTACK_{}",int(m.GetFacingDirection())));
}
}

@ -60,10 +60,10 @@ void Monster::STRATEGY::GOBLIN_BOMB(Monster&m,float fElapsedTime,std::string str
RUN,
};
switch(m.phase){
switch(PHASE()){
case INITIALIZE:{
m.F(A::SHOOT_TIMER)=m.randomFrameOffset;
m.phase=RUN;
SETPHASE(RUN);
}break;
case RUN:{
m.F(A::SHOOT_TIMER)+=fElapsedTime;

@ -66,10 +66,10 @@ void Monster::STRATEGY::GOBLIN_BOW(Monster&m,float fElapsedTime,std::string stra
};
#pragma endregion
switch(m.phase){
switch(PHASE()){
case INITIALIZE_PERCEPTION:{
m.F(A::PERCEPTION_LEVEL)=ConfigFloat("Starting Perception Level");
m.phase=MOVE;
SETPHASE(MOVE);
}break;
case MOVE:{
m.F(A::ATTACK_COOLDOWN)+=fElapsedTime;
@ -79,7 +79,7 @@ void Monster::STRATEGY::GOBLIN_BOW(Monster&m,float fElapsedTime,std::string stra
const bool outsideMaxShootingRange=distToPlayer>=ConfigPixelsArr("Stand Still and Shoot Range",1);
auto PrepareToShoot=[&](){
m.phase=LOCKON;
SETPHASE(LOCKON);
m.F(A::SHOOT_TIMER)=ConfigFloat("Attack Windup Time");
m.PerformAnimation("SHOOT",m.GetFacingDirectionToTarget(game->GetPlayer()->GetPos()));
@ -118,7 +118,7 @@ void Monster::STRATEGY::GOBLIN_BOW(Monster&m,float fElapsedTime,std::string stra
m.V(A::EXTENDED_LINE)=extendedLine;
Arrow tempArrow{m.GetPos(),extendedLine,pointTowardsPlayer.vector().norm()*ConfigFloat("Arrow Spd"),"goblin_arrow.png",ConfigFloat("Arrow Hitbox Radius"),m.GetAttack(),m.OnUpperLevel()};
m.V(A::FIRE_VELOCITY)=tempArrow.PointToBestTargetPath(m.F(A::PERCEPTION_LEVEL));
m.phase=WINDUP;
SETPHASE(WINDUP);
}
}break;
case WINDUP:{
@ -128,7 +128,7 @@ void Monster::STRATEGY::GOBLIN_BOW(Monster&m,float fElapsedTime,std::string stra
CreateBullet(Arrow)(m.GetPos(),m.V(A::EXTENDED_LINE),m.V(A::FIRE_VELOCITY),"goblin_arrow.png",ConfigFloat("Arrow Hitbox Radius"),m.GetAttack(),m.OnUpperLevel())EndBullet;
m.F(A::PERCEPTION_LEVEL)=std::min(ConfigFloat("Maximum Perception Level"),m.F(A::PERCEPTION_LEVEL)+ConfigFloat("Perception Level Increase"));
m.PerformAnimation("IDLE",m.GetFacingDirectionToTarget(game->GetPlayer()->GetPos()));
m.phase=MOVE;
SETPHASE(MOVE);
}
m.B(A::RANDOM_DIRECTION)=util::random()%2;
m.F(A::RANDOM_RANGE)=util::random_range(ConfigPixelsArr("Random Direction Range",0),ConfigPixelsArr("Random Direction Range",1));

@ -66,13 +66,13 @@ void Monster::STRATEGY::GOBLIN_DAGGER(Monster&m,float fElapsedTime,std::string s
SLASH
};
switch(m.phase){
switch(PHASE()){
case MOVE:{
float distToPlayer=m.GetDistanceFrom(game->GetPlayer()->GetPos());
if(distToPlayer>ConfigFloat("Attack Spacing")/100.f*24){
RUN_TOWARDS(m,fElapsedTime,"Run Towards");
}else{
m.phase=WINDUP;
SETPHASE(WINDUP);
m.I(A::ATTACK_TYPE)=util::random()%2; //Choose randomly between stab or slash.
switch(m.I(A::ATTACK_TYPE)){
case STAB:{
@ -90,18 +90,18 @@ void Monster::STRATEGY::GOBLIN_DAGGER(Monster&m,float fElapsedTime,std::string s
case WINDUP:{
m.F(A::CASTING_TIMER)-=fElapsedTime;
if(m.F(A::CASTING_TIMER)<=0){
m.phase=RECOVERY;
SETPHASE(RECOVERY);
switch(m.I(A::ATTACK_TYPE)){
case STAB:{
vf2d stabTarget=game->GetPlayer()->GetPos();
m.PerformAnimation("STABBING",m.GetFacingDirectionToTarget(game->GetPlayer()->GetPos()));
CreateBullet(DaggerStab)(m,ConfigFloat("Dagger Hit Radius"),m.GetAttack(),ConfigFloat("Dagger Stab Knockback"),m.OnUpperLevel(),m.GetFacingDirectionToTarget(stabTarget),ConfigFloat("Dagger Frame Duration"),ConfigFloat("Dagger Stab Distance"),
CreateBullet(DaggerStab)(m,ConfigString("Dagger Stab Image"),ConfigFloat("Dagger Hit Radius"),m.GetAttack(),ConfigFloat("Dagger Stab Knockback"),m.OnUpperLevel(),m.GetFacingDirectionToTarget(stabTarget),ConfigFloat("Dagger Frame Duration"),ConfigFloat("Dagger Stab Distance"),
DaggerStab::DirectionOffsets{ConfigVec("Dagger Up Offset"),ConfigVec("Dagger Down Offset"),ConfigVec("Dagger Right Offset"),ConfigVec("Dagger Left Offset")})EndBullet;
}break;
case SLASH:{
vf2d slashTarget=game->GetPlayer()->GetPos();
m.PerformAnimation("SLASHING",m.GetFacingDirectionToTarget(game->GetPlayer()->GetPos()));
CreateBullet(DaggerSlash)(m,ConfigFloat("Dagger Hit Radius"),m.GetAttack(),ConfigFloat("Dagger Slash Knockback"),m.OnUpperLevel(),m.GetFacingDirectionToTarget(slashTarget),ConfigFloat("Dagger Frame Duration"),ConfigFloat("Dagger Slash Distance"))EndBullet;
CreateBullet(DaggerSlash)(m,ConfigString("Dagger Slash Image"),ConfigFloat("Dagger Hit Radius"),m.GetAttack(),ConfigFloat("Dagger Slash Knockback"),m.OnUpperLevel(),m.GetFacingDirectionToTarget(slashTarget),ConfigFloat("Dagger Frame Duration"),ConfigFloat("Dagger Slash Distance"))EndBullet;
}break;
default:ERR(std::format("WARNING! Invalid Attack type {} provided. THIS SHOULD NOT BE HAPPENING!",m.I(A::ATTACK_TYPE)));
}
@ -113,7 +113,7 @@ void Monster::STRATEGY::GOBLIN_DAGGER(Monster&m,float fElapsedTime,std::string s
m.F(A::CASTING_TIMER)-=fElapsedTime;
m.F(A::RECOVERY_TIME)-=fElapsedTime;
if(m.F(A::CASTING_TIMER)<=0){m.PerformIdleAnimation(m.GetFacingDirectionToTarget(game->GetPlayer()->GetPos()));}
if(m.F(A::RECOVERY_TIME)<=0)m.phase=MOVE;
if(m.F(A::RECOVERY_TIME)<=0)SETPHASE(MOVE);
}break;
}
}

@ -63,11 +63,11 @@ void Monster::STRATEGY::HAWK(Monster&m,float fElapsedTime,std::string strategy){
m.SetZ(std::max(float(m.F(A::FLYING_HEIGHT)+ConfigFloat("Flight Oscillation Amount")*sin((PI*m.TimeSpentAlive())/1.5f)),0.f));
#pragma endregion
switch(m.phase){
switch(PHASE()){
case INITIALIZE:{
m.F(A::TARGET_FLYING_HEIGHT)=m.F(A::FLYING_HEIGHT)=ConfigFloat("Flight Height")-ConfigFloat("Flight Height Variance")+util::random_range(0,ConfigFloat("Flight Height Variance")*2);
m.B(A::RANDOM_DIRECTION)=int(util::random_range(0,2));
m.phase=FLYING;
SETPHASE(FLYING);
m.AddBuff(BuffType::SELF_INFLICTED_SLOWDOWN,INFINITE,util::random_range(0,ConfigFloat("Flight Speed Variance")/100));
m.F(A::ATTACK_COOLDOWN)=util::random_range(1.f,ConfigFloat("Flight Charge Cooldown"));
}break;
@ -76,7 +76,7 @@ void Monster::STRATEGY::HAWK(Monster&m,float fElapsedTime,std::string strategy){
if(m.F(A::FLYING_HEIGHT)<m.F(A::TARGET_FLYING_HEIGHT))m.F(A::FLYING_HEIGHT)=std::min(m.F(A::TARGET_FLYING_HEIGHT),m.F(A::FLYING_HEIGHT)+ConfigFloat("Attack Z Speed")*fElapsedTime);
if(m.F(A::ATTACK_COOLDOWN)<=0){
m.F(A::CASTING_TIMER)=ConfigFloat("Attack Wait Time");
m.phase=PREPARE_CHARGE;
SETPHASE(PREPARE_CHARGE);
}else{
float dirToPlayer{util::pointTo(game->GetPlayer()->GetPos(),m.GetPos()).polar().y};
dirToPlayer+=m.B(A::RANDOM_DIRECTION)?0.25*PI:-0.25*PI;
@ -89,7 +89,7 @@ void Monster::STRATEGY::HAWK(Monster&m,float fElapsedTime,std::string strategy){
m.UpdateFacingDirection(game->GetPlayer()->GetPos());
m.PerformAnimation("ATTACK");
if(m.F(A::CASTING_TIMER)<=0){
m.phase=CHARGE;
SETPHASE(CHARGE);
m.PerformAnimation("ATTACKING");
m.target=geom2d::line<float>(m.GetPos(),game->GetPlayer()->GetPos()).rpoint(ConfigFloat("Flight Distance")*2.f);
m.UpdateFacingDirection(m.target);
@ -103,7 +103,7 @@ void Monster::STRATEGY::HAWK(Monster&m,float fElapsedTime,std::string strategy){
float distToTarget=geom2d::line<float>(m.GetPos(),m.target).length();
if(distToTarget<12.f){
m.F(A::TARGET_FLYING_HEIGHT)=ConfigFloat("Flight Height")-ConfigFloat("Flight Height Variance")+util::random_range(0,ConfigFloat("Flight Height Variance")*2);
m.phase=FLYING;
SETPHASE(FLYING);
m.F(A::ATTACK_COOLDOWN)=ConfigFloat("Flight Charge Cooldown");
m.PerformJumpAnimation();
}

@ -51,7 +51,7 @@ INCLUDE_WINDOW_SIZE
IBullet::IBullet(vf2d pos,vf2d vel,float radius,int damage,bool upperLevel,bool friendly,Pixel col,vf2d scale,float image_angle)
:pos(pos),vel(vel),radius(radius),damage(damage),col(col),friendly(friendly),upperLevel(upperLevel),scale(scale),image_angle(image_angle){};
IBullet::IBullet(vf2d pos,vf2d vel,float radius,int damage,std::string animation,bool upperLevel,bool hitsMultiple,float lifetime,bool rotatesWithAngle,bool friendly,Pixel col,vf2d scale,float image_angle,std::string_view hitSound)
IBullet::IBullet(vf2d pos,vf2d vel,float radius,int damage,const std::string&animation,bool upperLevel,bool hitsMultiple,float lifetime,bool rotatesWithAngle,bool friendly,Pixel col,vf2d scale,float image_angle,std::string_view hitSound)
:pos(pos),vel(vel),radius(radius),damage(damage),col(col),animated(true),rotates(rotatesWithAngle),lifetime(lifetime),hitsMultiple(hitsMultiple),friendly(friendly),upperLevel(upperLevel),scale(scale),image_angle(image_angle),hitSound(std::string(hitSound)){
this->animation.AddState(animation,ANIMATION_DATA.at(animation));
this->animation.ChangeState(internal_animState,animation);
@ -173,8 +173,8 @@ void IBullet::_Draw()const{
Pixel blendCol=col;
if(fadeInTime==0&&fadeOutTime==0)blendCol.a=col.a;
else if(fadeOutTime>0)blendCol.a=uint8_t(util::lerp(col.a,0,1-((fadeOutTime-fadeOutTimer)/fadeOutTime)));
else if(fadeInTime>0)blendCol.a=uint8_t(util::lerp(col.a,0,((fadeInTime-fadeInTimer)/fadeInTime)));
else if(fadeOutTime>0)blendCol.a=uint8_t(util::lerp(col.a,uint8_t(0),1-((fadeOutTime-fadeOutTimer)/fadeOutTime)));
else if(fadeInTime>0)blendCol.a=uint8_t(util::lerp(col.a,uint8_t(0),((fadeInTime-fadeInTimer)/fadeInTime)));
if(GetZ()>0){
vf2d shadowScale=vf2d{8*scale.x/3.f,1}/std::max(1.f,GetZ()/8);

@ -110,7 +110,7 @@ public:
virtual ~IBullet()=default;
IBullet(vf2d pos,vf2d vel,float radius,int damage,bool upperLevel,bool friendly=false,Pixel col=WHITE,vf2d scale={1,1},float image_angle=0.f);
//Initializes a bullet with an animation.
IBullet(vf2d pos,vf2d vel,float radius,int damage,std::string animation,bool upperLevel,bool hitsMultiple=false,float lifetime=INFINITE,bool rotatesWithAngle=false,bool friendly=false,Pixel col=WHITE,vf2d scale={1,1},float image_angle=0.f,std::string_view hitSound="");
IBullet(vf2d pos,vf2d vel,float radius,int damage,const std::string&animation,bool upperLevel,bool hitsMultiple=false,float lifetime=INFINITE,bool rotatesWithAngle=false,bool friendly=false,Pixel col=WHITE,vf2d scale={1,1},float image_angle=0.f,std::string_view hitSound="");
public:
void SimulateUpdate(const float fElapsedTime);

@ -80,7 +80,7 @@ std::vector<std::string>ItemSortRules::secondarySort;
Stats Stats::NO_MAX_HIGHLIGHT;
ItemInfo::ItemInfo()
:customProps({nullptr,nullptr}),img(nullptr){}
:customProps({nullptr,nullptr}){}
void ItemInfo::InitializeItems(){
Stats::NO_MAX_HIGHLIGHT={};
@ -142,6 +142,8 @@ void ItemInfo::InitializeItems(){
std::string scriptName="",description="",category="";
std::string setName="";
float castTime=0;
float castRange=0;
float castSize=0;
std::vector<std::string> slot;
float cooldownTime="Item.Item Cooldown Time"_F;
std::vector<ItemAttribute>statValueList;
@ -167,6 +169,12 @@ void ItemInfo::InitializeItems(){
}else
if(keyName=="Cast Time"){
castTime=float(data[key][keyName].GetReal());
}
if(keyName=="Cast Range"){
castRange=float(data[key][keyName].GetReal());
}else
if(keyName=="Cast Size"){
castSize=float(data[key][keyName].GetReal())/100.f*24;
}else
if(keyName=="Cooldown Time"){
cooldownTime=float(data[key][keyName].GetReal());
@ -302,6 +310,8 @@ void ItemInfo::InitializeItems(){
it.description=description;
it.category=category;
it.castTime=castTime;
it.castRange=castRange;
it.castSize=castSize;
it.cooldownTime=cooldownTime;
it.slot=EquipSlot::NONE;
it.set=setName;
@ -320,7 +330,7 @@ void ItemInfo::InitializeItems(){
ERR("WARNING! Tried to add item "<<it.name<<" to category "<<it.category<<" which does not exist!")
}
ITEM_CATEGORIES.at(it.category).insert(it.name);
it.img=img.Decal();
it.img=img;
ItemProps&props=it.customProps;
if(scriptName!=""){
props.scriptProps=&DATA["ItemScript"][scriptName];
@ -371,27 +381,28 @@ void ItemInfo::InitializeItems(){
for(const auto&itemName:ITEM_CATEGORIES.at("Accessories")){
const std::string fragmentName{ITEM_DATA.at(itemName).fragmentName.value()};
ItemInfo&it{ITEM_DATA[fragmentName]};
GFX[fragmentName].Create(24,24);
GFX["item_img_directory"_S+fragmentName].Create(24,24);
it.img=GFX.at("item_img_directory"_S+fragmentName);
if(!game->TestingModeEnabled()){
if(it.FragmentIcon()){
game->SetDrawTarget(GFX.at(fragmentName).Sprite());
game->SetDrawTarget(GFX.at("item_img_directory"_S+fragmentName).Sprite());
game->Clear(BLANK);
game->SetPixelMode(Pixel::ALPHA);
game->DrawSprite({},GFX.at(it.FragmentIcon().value()).Sprite());
game->SetDrawTarget(nullptr);
GFX.at(fragmentName).Decal()->Update();
GFX.at("item_img_directory"_S+fragmentName).Decal()->Update();
}else{
#pragma region Collect colors from source ring image
std::set<Pixel>colors;
for(int y=0;y<24;y++){
for(int x=0;x<24;x++){
colors.insert(ITEM_DATA.at(itemName).img->sprite->GetPixel(x,y));
colors.insert(ITEM_DATA.at(itemName).Icon().Sprite()->GetPixel(x,y));
}
}
colors.erase(BLANK);
#pragma endregion
#pragma region Generate fragment with randomly sampled pixels from the source ring image
game->SetDrawTarget(GFX.at(fragmentName).Sprite());
game->SetDrawTarget(GFX.at("item_img_directory"_S+fragmentName).Sprite());
game->DrawSprite({},GFX.at("items/Fragment.png").Sprite(),1U,0U,[colors](const Pixel&in){
if(in==BLANK)return in;
for(const Pixel&p:colors){
@ -400,12 +411,12 @@ void ItemInfo::InitializeItems(){
return in;
});
game->SetDrawTarget(nullptr);
GFX.at(fragmentName).Decal()->Update();
GFX.at("item_img_directory"_S+fragmentName).Decal()->Update();
#pragma endregion
}
}
it.img=GFX.at(fragmentName).Decal();
it.fragmentIcon=it.name=fragmentName;
it.name=fragmentName;
it.fragmentIcon="item_img_directory"_S+fragmentName;
it.description="Fragment Description"_S;
it.category="Materials";
LOG(std::format("Item Fragment {} generated...",fragmentName));
@ -426,15 +437,15 @@ void ItemInfo::InitializeItems(){
ItemProps::ItemProps(utils::datafile*scriptProps,utils::datafile*customProps)
:scriptProps(scriptProps),customProps(customProps){}
int ItemProps::GetIntProp(const std::string&prop,size_t index)const{
int ItemProps::GetInt(const std::string&prop,size_t index)const{
if(customProps->HasProperty(prop)) return (*customProps)[prop].GetInt(index);
else return (*scriptProps)[prop].GetInt(index);
};
float ItemProps::GetFloatProp(const std::string&prop,size_t index)const{
float ItemProps::GetFloat(const std::string&prop,size_t index)const{
if(customProps->HasProperty(prop)) return float((*customProps)[prop].GetReal(index));
else return float((*scriptProps)[prop].GetReal(index));
};
std::string ItemProps::GetStringProp(const std::string&prop,size_t index)const{
std::string ItemProps::GetString(const std::string&prop,size_t index)const{
if(customProps->HasProperty(prop)) return (*customProps)[prop].GetString(index);
else return (*scriptProps)[prop].GetString(index);
};
@ -451,51 +462,6 @@ const std::unordered_map<std::string,BuffOverTimeType::BuffOverTimeType>ItemInfo
{"MP % Restore",BuffOverTimeType::MP_PCT_RESTORATION},
};
void ItemInfo::InitializeScripts(){
ITEM_SCRIPTS["Restore"]=[](AiL*game,ItemProps props){
for(const auto&[propName,buffType]:NameToBuffType){
int restoreAmt=props.GetIntProp(propName);
game->GetPlayer()->AddBuff(BuffRestorationType::ONE_OFF,NameToBuffType.at(propName),0.01f,float(restoreAmt),0.0f);
if(restoreAmt>0&&props.PropCount(propName)==3){
game->GetPlayer()->AddBuff(BuffRestorationType::OVER_TIME,NameToBuffType.at(propName),props.GetFloatProp(propName,2),float(restoreAmt),props.GetFloatProp(propName,1));
}
}
return true;
};
for(auto&[key,value]:ItemAttribute::attributes){
if(!DATA.GetProperty("ItemScript.Buff").HasProperty(key)){
ERR("WARNING! Buff Item Script does not support Buff "<<std::quoted(value.Name())<<"!");
}
}
ITEM_SCRIPTS["Buff"]=[](AiL*game,ItemProps props){
for(auto&[key,value]:ItemAttribute::attributes){
float intensity=props.GetFloatProp(key,0);
if(intensity==0.f)continue;
if(ItemAttribute::Get(key).DisplayAsPercent())intensity/=100;
game->GetPlayer()->AddBuff(BuffType::STAT_UP,props.GetFloatProp(key,1),intensity,{ItemAttribute::Get(key)});
}
return true;
};
ITEM_SCRIPTS["RestoreDuringCast"]=[](AiL*game,ItemProps props){
for(const auto&[propName,buffType]:NameToBuffType){
int restoreAmt=props.GetIntProp(propName);
game->GetPlayer()->AddBuff(BuffRestorationType::ONE_OFF,NameToBuffType.at(propName),0.01f,float(restoreAmt),0.0f);
if(restoreAmt>0&&props.PropCount(propName)==3){
game->GetPlayer()->AddBuff(BuffRestorationType::OVER_TIME_DURING_CAST,NameToBuffType.at(propName),props.GetFloatProp(propName,2),float(restoreAmt),props.GetFloatProp(propName,1));
}
}
return true;
};
ITEM_SCRIPTS.SetInitialized();
LOG(ITEM_SCRIPTS.size()<<" item scripts have been loaded.");
}
Item::Item()
:amt(0),it(nullptr),enhancementLevel(0){}
@ -560,11 +526,11 @@ uint32_t Inventory::GetItemCount(IT it){
}
//Returns true if the item has been consumed completely and there are 0 remaining of that type in our inventory.
bool Inventory::UseItem(IT it,uint32_t amt){
bool Inventory::UseItem(IT it,uint32_t amt,const std::optional<vf2d>targetingPos){
if(!_inventory.count(it))return false;
//There are two places to manipulate items in (Both the sorted inventory and the actual inventory)
for(uint32_t i=0;i<amt;i++){
if(ExecuteAction(it)){
if(ExecuteAction(it,targetingPos)){
return RemoveItem(GetItem(it)[0]);
}
}
@ -680,9 +646,9 @@ void Inventory::InsertIntoStageInventoryCategory(std::shared_ptr<Item>itemRef,co
Menu::InventorySlotsUpdated(stageInventoryCategory);
}
bool Inventory::ExecuteAction(IT item){
bool Inventory::ExecuteAction(IT item,const std::optional<vf2d>targetingPos){
if(ITEM_SCRIPTS.count(ITEM_DATA.at(item).useFunc)){
return ITEM_SCRIPTS.at(ITEM_DATA.at(item).useFunc)(game,ITEM_DATA[item].customProps);
return ITEM_SCRIPTS.at(ITEM_DATA.at(item).useFunc)(game,targetingPos,ITEM_DATA[item].customProps);
}else{
return false;
}
@ -790,8 +756,8 @@ const std::string Item::Description(CompactText compact)const{
const ITCategory Item::Category()const{
return it->Category();
}
const::Decal*const Item::Decal()const{
return it->Decal();
const::Renderable&Item::Icon()const{
return it->Icon();
}
const ItemScript&Item::OnUseAction()const{
return it->OnUseAction();
@ -806,8 +772,8 @@ const std::string&ItemInfo::Description()const{
const ITCategory ItemInfo::Category()const{
return category;
}
const::Decal*const ItemInfo::Decal()const{
return img;
const::Renderable&ItemInfo::Icon()const{
return img.value().get();
}
const ItemScript&ItemInfo::OnUseAction()const{
return ITEM_SCRIPTS.at(useFunc);
@ -854,7 +820,7 @@ void ItemOverlay::Draw(){
Pixel lightCol=Menu::GetCurrentTheme().GetButtonCol()*1.2f;
game->GradientFillRectDecal(pos,{item.width,8},darkCol,darkCol,darkCol,lightCol);
game->DrawRectDecal(pos,{item.width,8},Menu::GetCurrentTheme().GetHighlightCol());
game->DrawDecal(pos,const_cast<Decal*>(item.it.Decal()),{itemScale,itemScale});
game->DrawDecal(pos,const_cast<Decal*>(item.it.Icon().Decal()),{itemScale,itemScale});
game->DrawShadowStringPropDecal(pos+vf2d{itemScale*24+2,2},item.it.Name(),WHITE,BLACK,{0.5f,0.7f});
counter++;
}
@ -1034,7 +1000,7 @@ void Item::EnhanceItem(uint8_t qty){
for(const auto&[name,amt]:consumedResources.GetItems()){
Inventory::RemoveItem(Inventory::GetItem(name)[0],amt);
}
game->GetPlayer()->SetMoney(game->GetPlayer()->GetMoney()-consumedResources.GetCost());
game->GetPlayer()->RemoveMoney(consumedResources.GetCost());
SoundEffect::PlaySFX("Enhance Item",SoundEffect::CENTERED);
@ -1047,7 +1013,7 @@ void Item::EnhanceItem(uint8_t qty){
for(const auto&[name,amt]:consumedResources.GetItems()){
Inventory::RemoveItem(Inventory::GetItem(name)[0],amt);
}
game->GetPlayer()->SetMoney(game->GetPlayer()->GetMoney()-consumedResources.GetCost());
game->GetPlayer()->RemoveMoney(consumedResources.GetCost());
}
}
SoundEffect::PlaySFX("Craft Item",SoundEffect::CENTERED);
@ -1441,10 +1407,14 @@ const bool Item::CanBeRefined()const{
return false; //Maxed out, so cannot be refined.
}
const bool Item::CanBeEnchanted()const{
return IsAccessory()&&Inventory::GetItemCount(FragmentName())>="Fragment Enchant Cost"_i[0]&&game->GetPlayer()->GetMoney()>="Fragment Enchant Cost"_i[1];
}
RefineResult Item::Refine(){
if(!CanBeRefined())ERR("WARNING! Trying to refine an item that cannot be refined! Make sure you are checking with CanBeRefined() before calling this function!");
Inventory::RemoveItem(FragmentName(),"Fragment Refine Cost"_i[0]);
game->GetPlayer()->SetMoney(game->GetPlayer()->GetMoney()-"Fragment Refine Cost"_i[1]);
game->GetPlayer()->RemoveMoney("Fragment Refine Cost"_i[1]);
std::vector<ItemAttribute>statsAvailableForRefining;
for(const auto&[attr,val]:randomizedStats){
float maxVal{ITEM_DATA[ActualName()].GetMaxStats().A_Read(attr)};
@ -1461,13 +1431,13 @@ RefineResult Item::Refine(){
return RefineResult{chosenAttr,newAmt-oldAmt};
}
void Item::EnchantItem(const std::string_view enchantName){
enchant=ItemEnchant{enchantName};
void Item::_EnchantItem(const ItemEnchant&newEnchant){
enchant=newEnchant;
game->GetPlayer()->RecalculateEquipStats();
}
std::optional<ItemEnchant>Item::GetEnchant()const{
const std::optional<ItemEnchant>&Item::GetEnchant()const{
return enchant;
}
const bool Item::HasEnchant()const{
@ -1523,8 +1493,31 @@ const std::optional<std::string>&Item::FragmentIcon()const{
return ITEM_DATA.at(FragmentName()).FragmentIcon();
}
const ItemEnchantInfo Item::ApplyRandomEnchant(){
EnchantName randomEnchantName{ItemEnchant::RollRandomEnchant()};
EnchantItem(randomEnchantName);
return ItemEnchantInfo::GetEnchant(randomEnchantName);
const ItemEnchant&Item::ApplyRandomEnchant(){
if(!CanBeEnchanted())ERR("WARNING! Trying to enchant an item that cannot be enchanted! Make sure you are checking with CanBeEnchanted() before calling this function!");
ItemEnchant randomEnchant{ItemEnchant::RollRandomEnchant(GetEnchant())};
_EnchantItem(randomEnchant);
Inventory::RemoveItem(FragmentName(),"Fragment Enchant Cost"_i[0]);
game->GetPlayer()->RemoveMoney("Fragment Enchant Cost"_i[1]);
return GetEnchant().value();
}
void Item::RemoveEnchant(){
enchant.reset();
}
const float Item::CastRange()const{
return ITEM_DATA.at(ActualName()).CastRange();
}
const float ItemInfo::CastRange()const{
return castRange;
}
const float Item::CastSize()const{
return ITEM_DATA.at(ActualName()).CastSize();
}
const float ItemInfo::CastSize()const{
return castSize;
}

@ -59,7 +59,7 @@ using ITCategory=std::string;
using EventName=std::string;
using EnchantName=std::string;
using ItemScript=std::function<bool(AiL*,ItemProps)>;
using ItemScript=std::function<bool(AiL*game,std::optional<vf2d>targetingPos,ItemProps props)>;
enum class EquipSlot{
HELMET= 0b0000'0001,
@ -213,10 +213,12 @@ public:
const EventName&UseSound()const;
const ITCategory Category()const;
const EquipSlot GetEquipSlot()const;
const::Decal*const Decal()const;
const::Renderable&Icon()const;
const Stats GetStats()const;
const ItemScript&OnUseAction()const;
const float CastTime()const;
const float CastRange()const;
const float CastSize()const;
const bool UseDuringCast()const; //When true, the item is activated during the cast instead of after the cast.
const float CooldownTime()const;
//Use ISBLANK macro instead!! This should not be called directly!!
@ -254,11 +256,16 @@ public:
//Assumes an item can be refined. CHECK WITH CanBeRefined() First!!!
//Refines a random stat by the parameters defined in the Accessories.txt configuration file. Also takes away the costs required to refine the item.
RefineResult Refine();
const bool CanBeEnchanted()const;
void Lock();
void Unlock();
void EnchantItem(const std::string_view enchantName);
const ItemEnchantInfo ApplyRandomEnchant();
std::optional<ItemEnchant>GetEnchant()const;
void RemoveEnchant();
void _EnchantItem(const ItemEnchant&newEnchant);
//Applies a random valid enchant to an item and consumes the materials in the process.
//Assumes an item can be enchanted. CHECK WITH CanBeEnchanted() First!!!
//Enchants the item based on its class parameters defined in the ItemEnchants.txt configuration file. Also takes away the costs required to refine the item (Located in Accessories.txt).
const ItemEnchant&ApplyRandomEnchant();
const std::optional<ItemEnchant>&GetEnchant()const;
const bool HasEnchant()const;
friend const bool operator==(std::shared_ptr<Item>lhs,std::shared_ptr<Item>rhs){return lhs->it==rhs->it&&lhs->randomizedStats==rhs->randomizedStats;};
friend const bool operator==(std::shared_ptr<Item>lhs,const IT&rhs){return lhs->ActualName()==const_cast<IT&>(rhs);};
@ -290,7 +297,7 @@ public:
static std::vector<std::shared_ptr<Item>>CopyItem(IT it);
static std::vector<std::weak_ptr<Item>>GetItem(IT it);
//Auto-executes its use function and removes the amt specified from the inventory. Multiple amounts will cause the item to execute its useFunc multiple times.
static bool UseItem(IT it,uint32_t amt=1);
static bool UseItem(IT it,uint32_t amt=1,const std::optional<vf2d>targetingPos={});
//Returns true if the item has been consumed completely and there are 0 remaining of that type in our inventory.
static bool RemoveItem(std::weak_ptr<Item>itemRef,ITCategory inventory,uint32_t amt = 1);
//Returns true if the item has been consumed completely and there are 0 remaining of that type in our inventory.
@ -323,7 +330,7 @@ public:
private:
static void InsertIntoSortedInv(std::shared_ptr<Item>itemRef);
static void InsertIntoStageInventoryCategory(std::shared_ptr<Item>itemRef,const bool monsterDrop);
static bool ExecuteAction(IT item);
static bool ExecuteAction(IT item,const std::optional<vf2d>targetingPos);
static std::multimap<IT,std::shared_ptr<Item>>_inventory;
static std::vector<std::shared_ptr<Item>>blacksmithInventory;
static std::map<EquipSlot,std::weak_ptr<Item>>equipment;
@ -338,9 +345,9 @@ class ItemProps{
utils::datafile*customProps;
public:
ItemProps(utils::datafile*scriptProps,utils::datafile*customProps);
int GetIntProp(const std::string&prop,size_t index=0)const;
float GetFloatProp(const std::string&prop,size_t index=0)const;
std::string GetStringProp(const std::string&prop,size_t index=0)const;
int GetInt(const std::string&prop,size_t index=0)const;
float GetFloat(const std::string&prop,size_t index=0)const;
std::string GetString(const std::string&prop,size_t index=0)const;
const uint32_t PropCount(const std::string&prop)const;
};
@ -351,10 +358,12 @@ class ItemInfo{
std::string description;
std::string category;
float castTime=0;
float castRange=0;
float castSize=0;
float cooldownTime=0;
EquipSlot slot=EquipSlot::NONE;
EnhancementInfo enhancement;
::Decal*img;
std::optional<std::reference_wrapper<Renderable>>img;
EventName useSound="";
std::string set="";
//Returns true if the item can be used, false otherwise
@ -384,7 +393,7 @@ public:
const std::string&Name()const;
const std::string&Description()const;
const ITCategory Category()const;
const::Decal*const Decal()const;
const::Renderable& Icon()const;
static const EquipSlot StringToEquipSlot(std::string_view slotName);
/*
For the useFunc, return true if the item can be used, false otherwise.
@ -393,6 +402,8 @@ public:
const ItemScript&OnUseAction()const;
const Stats GetStats(int enhancementLevel)const;
const float CastTime()const;
const float CastRange()const;
const float CastSize()const;
const float CooldownTime()const;
const EquipSlot Slot()const;
const std::optional<const::ItemSet*const>ItemSet()const;

@ -86,9 +86,9 @@ void ItemDrop::Draw()const{
yOffset=sin((game->levelTime+randomSpinOffset)*3)*0.5f;
}
game->view.DrawRotatedDecal(pos-vf2d{0,GetZ()+yOffset},GFX["skill_overlay_icon_overlay.png"].Decal(),0,GFX["skill_overlay_icon_overlay.png"].Decal()->sprite->Size()/2,{"ItemDrop.Item Drop Scale"_F,"ItemDrop.Item Drop Scale"_F},YELLOW);
game->view.DrawRotatedDecal(pos-vf2d{0,GetZ()+yOffset},const_cast<Decal*>(item->Decal()),0,item->Decal()->sprite->Size()/2,{"ItemDrop.Item Drop Scale"_F,"ItemDrop.Item Drop Scale"_F},{255,255,255,128});
game->view.DrawRotatedDecal(pos-vf2d{0,GetZ()+yOffset},const_cast<Decal*>(item->Icon().Decal()),0,item->Icon().Sprite()->Size()/2,{"ItemDrop.Item Drop Scale"_F,"ItemDrop.Item Drop Scale"_F},{255,255,255,128});
game->SetDecalMode(DecalMode::ADDITIVE);
game->view.DrawRotatedDecal(pos-vf2d{0,GetZ()+yOffset},const_cast<Decal*>(item->Decal()),0,item->Decal()->sprite->Size()/2,{"ItemDrop.Item Drop Scale"_F,"ItemDrop.Item Drop Scale"_F},{uint8_t(abs(sin(game->levelTime*1.5)*255.f)),uint8_t(abs(sin(game->levelTime*1.5)*255.f)),uint8_t(abs(sin(game->levelTime*1.5)*255.f)),128});
game->view.DrawRotatedDecal(pos-vf2d{0,GetZ()+yOffset},const_cast<Decal*>(item->Icon().Decal()),0,item->Icon().Sprite()->Size()/2,{"ItemDrop.Item Drop Scale"_F,"ItemDrop.Item Drop Scale"_F},{uint8_t(abs(sin(game->levelTime*1.5)*255.f)),uint8_t(abs(sin(game->levelTime*1.5)*255.f)),uint8_t(abs(sin(game->levelTime*1.5)*255.f)),128});
game->SetDecalMode(DecalMode::NORMAL);
}
@ -110,7 +110,7 @@ void ItemDrop::UpdateDrops(float fElapsedTime){
if(drop.zSpeed==0&&drop.OnUpperLevel()==game->GetPlayer()->OnUpperLevel()){
geom2d::line<float>lineTo=geom2d::line<float>(drop.pos,game->GetPlayer()->GetPos());
float dist=lineTo.length();
if(dist<="ItemDrop.Item Drop Suction Range"_F){
if(dist<="ItemDrop.Item Drop Suction Range"_F&&dist>0){
vf2d pointVel=lineTo.vector().norm();
float moveDistance=(1.f/std::min(48.f,dist))*"ItemDrop.Item Drop Suction Strength"_F*fElapsedTime;
if(moveDistance>dist){

@ -246,7 +246,7 @@ const std::vector<ItemEnchantInfo>ItemEnchant::GetAvailableEnchants(){
return filteredEnchants;
}
const std::string ItemEnchant::RollRandomEnchant(){
const ItemEnchant ItemEnchant::RollRandomEnchant(const std::optional<ItemEnchant>previousEnchant){
const std::vector<ItemEnchantInfo>filteredEnchants{GetAvailableEnchants()};
int randomRoll{int(util::random_range(0,100))};
@ -263,7 +263,21 @@ const std::string ItemEnchant::RollRandomEnchant(){
std::copy_if(filteredEnchants.begin(),filteredEnchants.end(),std::back_inserter(remainingAvailableEnchants),[](const ItemEnchantInfo&enchant){return enchant.Category()==ItemEnchantInfo::ItemEnchantCategory::UNIQUE;});
}else ERR(std::format("WARNING! Somehow did not pick a value in any of the given ranges. Rolled value was: {}. Ranges were {}-{}, {}-{}, {}-{}",randomRoll,generalEnchantRange.first,generalEnchantRange.second,classEnchantRange.first,classEnchantRange.second,uniqueEnchantRange.first,uniqueEnchantRange.second));
return remainingAvailableEnchants[util::random()%remainingAvailableEnchants.size()].Name();
const auto AtLeastOneAttributeIncreased=[](const ItemEnchant&oldEnchant,const ItemEnchant&newEnchant){
if(oldEnchant.Name()!=newEnchant.Name())ERR(std::format("WARNING! Not intended to be used with enchants of varying names!! Old:{} New:{}",oldEnchant.Name(),newEnchant.Name()));
for(const auto&[attr,val]:oldEnchant){
if(newEnchant.GetAttribute(attr.ActualName())>val)return true;
}
return false;
};
for(;;){
ItemEnchantInfo&randomEnchant{remainingAvailableEnchants[util::random()%remainingAvailableEnchants.size()]};
ItemEnchant newEnchant{randomEnchant.Name()};
if(!previousEnchant||
previousEnchant.value().Name()!=newEnchant.Name()||
AtLeastOneAttributeIncreased(previousEnchant.value(),newEnchant))return newEnchant;
}
}
void ItemEnchant::UpdateDescription(){
@ -329,3 +343,11 @@ const std::optional<std::reference_wrapper<Ability>>ItemEnchant::Ability()const{
const Pixel&ItemEnchantInfo::DisplayCol()const{
return ItemEnchantInfo::enchantTextDisplayCol.at(Category());
}
const bool ItemEnchant::HasAttributes()const{
return stats.size()>0;
}
const std::optional<Class>&ItemEnchant::GetClass()const{
return ItemEnchantInfo::GetEnchant(Name()).GetClass();
}

@ -100,6 +100,8 @@ private:
static std::unordered_map<ItemEnchantCategory,ItemEnchantCategoryData>ENCHANT_CATEGORIES;
};
class Item;
class ItemEnchant{
public:
ItemEnchant(const std::string_view enchantName);
@ -109,13 +111,15 @@ public:
const std::optional<ItemEnchantInfo::AbilitySlot>&AbilitySlot()const;
const static std::vector<ItemEnchantInfo>GetAvailableEnchants();
//Rolls a class-appropriate random enchant.
const static std::string RollRandomEnchant();
const static ItemEnchant RollRandomEnchant(const std::optional<ItemEnchant>previousEnchant={});
void SetAttribute(const std::string_view attr,const float val);
const float&GetAttribute(const std::string_view attr)const;
std::map<ItemAttribute,float>::const_iterator begin()const;
std::map<ItemAttribute,float>::const_iterator end()const;
const bool HasAttributes()const;
const Pixel&DisplayCol()const;
const std::optional<std::reference_wrapper<::Ability>>Ability()const;
const std::optional<Class>&GetClass()const;
private:
void UpdateDescription();
const ItemEnchantInfo&GetEnchantInfo()const;

@ -0,0 +1,102 @@
#pragma region License
/*
License (OLC-3)
~~~~~~~~~~~~~~~
Copyright 2024 Joshua Sigona <sigonasr2@gmail.com>
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions or derivations of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions or derivative works in binary form must reproduce the above
copyright notice. This list of conditions and the following disclaimer must be
reproduced in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may
be used to endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
Portions of this software are copyright © 2024 The FreeType
Project (www.freetype.org). Please see LICENSE_FT.txt for more information.
All rights reserved.
*/
#pragma endregion
#include "AdventuresInLestoria.h"
#include "BulletTypes.h"
INCLUDE_ITEM_SCRIPTS
INCLUDE_game
INCLUDE_ANIMATION_DATA
void ItemInfo::InitializeScripts(){
ITEM_SCRIPTS["Restore"]=[](AiL*game,std::optional<vf2d>targetingPos,ItemProps props){
for(const auto&[propName,buffType]:NameToBuffType){
int restoreAmt=props.GetInt(propName);
game->GetPlayer()->AddBuff(BuffRestorationType::ONE_OFF,NameToBuffType.at(propName),0.01f,float(restoreAmt),0.0f);
if(restoreAmt>0&&props.PropCount(propName)==3){
game->GetPlayer()->AddBuff(BuffRestorationType::OVER_TIME,NameToBuffType.at(propName),props.GetFloat(propName,2),float(restoreAmt),props.GetFloat(propName,1));
}
}
return true;
};
for(auto&[key,value]:ItemAttribute::attributes){
if(!DATA.GetProperty("ItemScript.Buff").HasProperty(key)){
ERR("WARNING! Buff Item Script does not support Buff "<<std::quoted(value.Name())<<"!");
}
}
ITEM_SCRIPTS["Buff"]=[](AiL*game,std::optional<vf2d>targetingPos,ItemProps props){
for(auto&[key,value]:ItemAttribute::attributes){
float intensity=props.GetFloat(key,0);
if(intensity==0.f)continue;
if(ItemAttribute::Get(key).DisplayAsPercent())intensity/=100;
game->GetPlayer()->AddBuff(BuffType::STAT_UP,props.GetFloat(key,1),intensity,{ItemAttribute::Get(key)});
}
return true;
};
ITEM_SCRIPTS["RestoreDuringCast"]=[](AiL*game,std::optional<vf2d>targetingPos,ItemProps props){
for(const auto&[propName,buffType]:NameToBuffType){
int restoreAmt=props.GetInt(propName);
game->GetPlayer()->AddBuff(BuffRestorationType::ONE_OFF,NameToBuffType.at(propName),0.01f,float(restoreAmt),0.0f);
if(restoreAmt>0&&props.PropCount(propName)==3){
game->GetPlayer()->AddBuff(BuffRestorationType::OVER_TIME_DURING_CAST,NameToBuffType.at(propName),props.GetFloat(propName,2),float(restoreAmt),props.GetFloat(propName,1));
}
}
return true;
};
ITEM_SCRIPTS["Projectile"]=[](AiL*game,std::optional<vf2d>targetingPos,ItemProps props){
const int projectileDamage{props.GetInt("Base Damage")+int(game->GetPlayer()->GetAttack()*props.GetFloat("Player Damage Mult"))};
std::optional<LingeringEffect>lingeringEffect{};
if(props.GetFloat("Linger Time")>0.f){
const int damage{props.GetInt("Tick Base Damage")+int(props.GetFloat("Tick Player Damage Mult")*game->GetPlayer()->GetAttack())};
lingeringEffect.emplace(vf2d{},props.GetString("Lingering Effect"),props.GetString("Lingering Sound"),props.GetFloat("Linger Radius")/100.f*24,damage,props.GetFloat("Tick Rate"),HurtType::MONSTER,props.GetFloat("Linger Time"),5.f,game->GetPlayer()->OnUpperLevel(),1.f,vf2d{},DARK_RED,util::random(2*PI),0.f,false,0.2f,
[](const Effect&self){
return Effect{self.pos,0.5f,"fire_ring.png",self.OnUpperLevel(),util::random_range(0.7f,1.f),0.1f,{},PixelLerp(Pixel(0xF7B752),Pixel(0xE74F30),util::random(1.f)),util::random(2*PI),0.f,true};
});
}
CreateBullet(ThrownProjectile)(game->GetPlayer()->GetPos(),targetingPos.value(),props.GetString("Image"),props.GetFloat("Cast Size")/100.f*24,game->GetPlayer()->GetZ(),0.3f,8.f,projectileDamage,game->GetPlayer()->OnUpperLevel(),false,INFINITE,true,WHITE,props.GetFloat("Cast Size")/100.f*vf2d{1.f,1.f},0.f,
Effect{vf2d{},ANIMATION_DATA.at("explosionframes.png").GetTotalAnimationDuration(),"explosionframes.png",game->GetPlayer()->OnUpperLevel(),props.GetFloat("Cast Size")/100.f*vf2d{1.f,1.f}},props.GetString("Explode Sound Effect"),lingeringEffect)EndBullet;
return true;
};
ITEM_SCRIPTS.SetInitialized();
LOG(ITEM_SCRIPTS.size()<<" item scripts have been loaded.");
}

@ -409,8 +409,8 @@ void Menu::KeyboardButtonNavigation(AiL*game,vf2d menuPos){
}
if(navigationGroups.count(selectionButtonName)){
Navigation nav=navigationGroups[selectionButtonName];
if(game->KEY_UP.PressedDAS()||game->KEY_SCROLLUP.AnalogDAS(0.5f)<-0.5f){
const float scrollVertAnalogDAS{game->KEY_SCROLLVERT_L.AnalogDAS(0.5f)};
if(game->KEY_UP.PressedDAS()||scrollVertAnalogDAS<-0.5f){
SoundEffect::PlaySFX("Menu Navigate",SoundEffect::CENTERED);
SetMouseNavigation(false);
if(std::holds_alternative<std::string>(nav.up)&&std::get<std::string>(nav.up).length()>0)SetSelection(std::string_view(std::get<std::string>(nav.up)));
@ -418,10 +418,11 @@ void Menu::KeyboardButtonNavigation(AiL*game,vf2d menuPos){
if(std::holds_alternative<MenuDataFunc>(nav.up)){
Data returnData;
std::get<MenuDataFunc>(nav.up)(type,returnData);
if(!std::holds_alternative<ButtonName>(returnData)&&!std::holds_alternative<std::weak_ptr<MenuComponent>>(returnData))ERR(std::format("WARNING! No valid return type set for up button navigation in menu {}",int(type)));
SetSelection(returnData);
}
}
if(game->KEY_DOWN.PressedDAS()||game->KEY_SCROLLDOWN.AnalogDAS(0.5f)>0.5f){
if(game->KEY_DOWN.PressedDAS()||scrollVertAnalogDAS>0.5f){
SoundEffect::PlaySFX("Menu Navigate",SoundEffect::CENTERED);
SetMouseNavigation(false);
if(std::holds_alternative<std::string>(nav.down)&&std::get<std::string>(nav.down).length()>0)SetSelection(std::string_view(std::get<std::string>(nav.down)));
@ -429,6 +430,7 @@ void Menu::KeyboardButtonNavigation(AiL*game,vf2d menuPos){
if(std::holds_alternative<MenuDataFunc>(nav.down)){
Data returnData;
std::get<MenuDataFunc>(nav.down)(type,returnData);
if(!std::holds_alternative<ButtonName>(returnData)&&!std::holds_alternative<std::weak_ptr<MenuComponent>>(returnData))ERR(std::format("WARNING! No valid return type set for down button navigation in menu {}",int(type)));
SetSelection(returnData);
}
}
@ -440,6 +442,7 @@ void Menu::KeyboardButtonNavigation(AiL*game,vf2d menuPos){
if(std::holds_alternative<MenuDataFunc>(nav.left)){
Data returnData;
std::get<MenuDataFunc>(nav.left)(type,returnData);
if(!std::holds_alternative<ButtonName>(returnData)&&!std::holds_alternative<std::weak_ptr<MenuComponent>>(returnData))ERR(std::format("WARNING! No valid return type set for left button navigation in menu {}",int(type)));
SetSelection(returnData);
}
}
@ -451,6 +454,7 @@ void Menu::KeyboardButtonNavigation(AiL*game,vf2d menuPos){
if(std::holds_alternative<MenuDataFunc>(nav.right)){
Data returnData;
std::get<MenuDataFunc>(nav.right)(type,returnData);
if(!std::holds_alternative<ButtonName>(returnData)&&!std::holds_alternative<std::weak_ptr<MenuComponent>>(returnData))ERR(std::format("WARNING! No valid return type set for right button navigation in menu {}",int(type)));
SetSelection(returnData);
}
}
@ -815,3 +819,31 @@ const bool Menu::IsMouseOverMenu(){
if(Menu::stack.size()==0)return false;
return geom2d::overlaps(game->GetMousePos(),geom2d::rect<float>{Menu::stack.back()->pos-"Interface.9PatchSize"_V.x,Menu::stack.back()->size+"Interface.9PatchSize"_V.y*2});
}
void Menu::ScrollUp(const MenuType type,const std::string&componentName,Data&returnData,const std::optional<std::string>wrapDestination){
if(!Menu::menus[type]->GetSelection().expired()){
auto selection=Menu::menus[type]->GetSelection().lock();
size_t index=Component<ScrollableWindowComponent>(type,componentName)->GetComponentIndex(selection);
index--;
if(index>=Component<ScrollableWindowComponent>(type,componentName)->GetComponents().size()){
if(wrapDestination)returnData=wrapDestination.value();
else returnData=Component<ScrollableWindowComponent>(type,componentName)->GetComponents().back();
}else{
returnData=Component<ScrollableWindowComponent>(type,componentName)->GetComponents()[index];
}
}
}
void Menu::ScrollDown(const MenuType type,const std::string&componentName,Data&returnData,const std::optional<std::string>wrapDestination){
if(!Menu::menus[type]->GetSelection().expired()){
auto selection=Menu::menus[type]->GetSelection().lock();
size_t index=Component<ScrollableWindowComponent>(type,componentName)->GetComponentIndex(selection);
index++;
if(index>=Component<ScrollableWindowComponent>(type,componentName)->GetComponents().size()){
if(wrapDestination)returnData=wrapDestination.value();
else returnData=Component<ScrollableWindowComponent>(type,componentName)->GetComponents().front();
}else{
returnData=Component<ScrollableWindowComponent>(type,componentName)->GetComponents()[index];
}
}
}

@ -208,6 +208,10 @@ public:
//Returns whether or not this menu type is currently in the foreground of the game, and thus being interacted with by the user.
static bool IsCurrentlyActive(MenuType type);
static const bool IsMouseOverMenu(); //Returns whether the mouse is hovering over any portion of the menu, thus can be used if a menu is not supposed to cover up everything to determine mouse interactions.
//If a wrap destination is specified, will resolve the selection to that button when the top is reached, otherwise will default to wrapping around.
static void ScrollUp(const MenuType type,const std::string&componentName,Data&returnData,const std::optional<std::string>wrapDestination="");
//If a wrap destination is specified, will resolve the selection to that button when the bottom is reached, otherwise will default to wrapping around.
static void ScrollDown(const MenuType type,const std::string&componentName,Data&returnData,const std::optional<std::string>wrapDestination="");
private:
Menu(vf2d pos,vf2d size);
static MenuType lastMenuTypeCreated;

@ -59,13 +59,13 @@ private:
public:
int selected=-1; //0-2 representing which loadout slot this item consumes. -1 means not selected.
inline MenuItemButton(geom2d::rect<float>rect,const std::vector<std::shared_ptr<Item>>&invRef,int invIndex,MenuFunc onClick,MenuType itemDescriptionMenu,std::string itemNameLabelName,std::string itemDescriptionLabelName,IconButtonAttr attributes=IconButtonAttr::SELECTABLE)
:MenuIconButton(rect,invRef.size()>invIndex?const_cast<Decal*>(invRef[invIndex]->Decal()):nullptr,onClick,attributes),invRef(invRef),itemDescriptionMenu(itemDescriptionMenu),itemNameLabelName(itemNameLabelName),itemDescriptionLabelName(itemDescriptionLabelName){
:MenuIconButton(rect,invRef.size()>invIndex?const_cast<Decal*>(invRef[invIndex]->Icon().Decal()):nullptr,onClick,attributes),invRef(invRef),itemDescriptionMenu(itemDescriptionMenu),itemNameLabelName(itemNameLabelName),itemDescriptionLabelName(itemDescriptionLabelName){
draggable=false;
inventoryIndex=invIndex;
valid=invRef.size()>invIndex;
}
inline MenuItemButton(geom2d::rect<float>rect,const std::vector<std::shared_ptr<Item>>&invRef,int invIndex,MenuFunc onClick,MenuFunc onHover,MenuFunc onMouseOut,MenuType itemDescriptionMenu,std::string itemNameLabelName,std::string itemDescriptionLabelName,IconButtonAttr attributes=IconButtonAttr::SELECTABLE)
:MenuIconButton(rect,invRef.size()>invIndex?const_cast<Decal*>(invRef[invIndex]->Decal()):nullptr,onClick,attributes),invRef(invRef),itemDescriptionMenu(itemDescriptionMenu),itemNameLabelName(itemNameLabelName),itemDescriptionLabelName(itemDescriptionLabelName){
:MenuIconButton(rect,invRef.size()>invIndex?const_cast<Decal*>(invRef[invIndex]->Icon().Decal()):nullptr,onClick,attributes),invRef(invRef),itemDescriptionMenu(itemDescriptionMenu),itemNameLabelName(itemNameLabelName),itemDescriptionLabelName(itemDescriptionLabelName){
draggable=false;
inventoryIndex=invIndex;
runHoverFunctions=true;
@ -108,7 +108,7 @@ protected:
std::string labelNameText;
std::string labelDescriptionText;
if(valid){
icon=const_cast<Decal*>(invRef[inventoryIndex]->Decal());
icon=const_cast<Decal*>(invRef[inventoryIndex]->Icon().Decal());
labelNameText=invRef[inventoryIndex]->DisplayName();
labelDescriptionText=invRef[inventoryIndex]->Description(compact);
}else{

@ -58,12 +58,12 @@ protected:
std::weak_ptr<Item>itemRef;
public:
inline MenuItemItemButton(geom2d::rect<float>rect,const std::weak_ptr<Item>itemRef,MenuFunc onClick,std::string itemNameLabelName,std::string itemDescriptionLabelName,IconButtonAttr attributes=IconButtonAttr::SELECTABLE)
:MenuIconButton(rect,!ISBLANK(itemRef)?const_cast<Decal*>(itemRef.lock()->Decal()):nullptr,onClick,attributes),itemRef(itemRef),itemNameLabelName(itemNameLabelName),itemDescriptionLabelName(itemDescriptionLabelName){
:MenuIconButton(rect,!ISBLANK(itemRef)?const_cast<Decal*>(itemRef.lock()->Icon().Decal()):nullptr,onClick,attributes),itemRef(itemRef),itemNameLabelName(itemNameLabelName),itemDescriptionLabelName(itemDescriptionLabelName){
draggable=false;
valid=!ISBLANK(itemRef);
}
inline MenuItemItemButton(geom2d::rect<float>rect,const std::weak_ptr<Item>itemRef,MenuFunc onClick,MenuFunc onHover,MenuFunc onMouseOut,std::string itemNameLabelName="",std::string itemDescriptionLabelName="",IconButtonAttr attributes=IconButtonAttr::SELECTABLE)
:MenuIconButton(rect,!ISBLANK(itemRef)?const_cast<Decal*>(itemRef.lock()->Decal()):nullptr,onClick,attributes),itemRef(itemRef),itemNameLabelName(itemNameLabelName),itemDescriptionLabelName(itemDescriptionLabelName){
:MenuIconButton(rect,!ISBLANK(itemRef)?const_cast<Decal*>(itemRef.lock()->Icon().Decal()):nullptr,onClick,attributes),itemRef(itemRef),itemNameLabelName(itemNameLabelName),itemDescriptionLabelName(itemDescriptionLabelName){
runHoverFunctions=true;
draggable=false;
valid=!ISBLANK(itemRef);
@ -94,7 +94,7 @@ public:
icon=nullptr;
return;
}
icon=const_cast<Decal*>(itemRef.lock()->Decal());
icon=const_cast<Decal*>(itemRef.lock()->Icon().Decal());
}
inline void SetHideDetails(bool hideDetails){
this->hideDetails=hideDetails;
@ -125,7 +125,7 @@ protected:
labelNameText="";
labelDescriptionText="";
}else{
icon=const_cast<Decal*>(itemRef.lock()->Decal());
icon=const_cast<Decal*>(itemRef.lock()->Icon().Decal());
labelNameText=itemRef.lock()->DisplayName();
labelDescriptionText=itemRef.lock()->Description(compact);
}

@ -50,26 +50,52 @@ namespace ItemLabelDescriptionType{
using namespace ItemLabelDescriptionType;
class MenuItemLabel:public MenuLabel{
std::weak_ptr<Item>itemRef;
std::variant<Item,std::weak_ptr<Item>>itemRef;
ItemDescriptionType labelType;
public:
inline MenuItemLabel(geom2d::rect<float>rect,std::weak_ptr<Item>itemRef,ItemDescriptionType labelType=ITEM_DESCRIPTION,float scale=1,ComponentAttr attributes=ComponentAttr::NONE)
:MenuLabel(rect,"",scale,attributes),itemRef(itemRef),labelType(labelType){}
inline MenuItemLabel(geom2d::rect<float>rect,std::weak_ptr<Item>weakItemRef,ItemDescriptionType labelType=ITEM_DESCRIPTION,float scale=1,ComponentAttr attributes=ComponentAttr::NONE)
:MenuLabel(rect,"",scale,attributes),itemRef(weakItemRef),labelType(labelType){}
//NOTE: This version of the constructor will create a COPY of an item as opposed to the weakItemRef version which will reference an item say from an inventory...
inline MenuItemLabel(geom2d::rect<float>rect,const Item&strongItemRef,ItemDescriptionType labelType=ITEM_DESCRIPTION,float scale=1,ComponentAttr attributes=ComponentAttr::NONE)
:MenuLabel(rect,"",scale,attributes),itemRef(strongItemRef),labelType(labelType){}
inline virtual void Update(AiL*game)override{
MenuLabel::Update(game);
if(!itemRef.expired()){
std::string displayStr{};
if(std::holds_alternative<Item>(itemRef)){
Item&item{std::get<Item>(itemRef)};
switch(labelType){
case ITEM_DESCRIPTION:{
SetLabel(itemRef.lock()->Description(NON_COMPACT));
displayStr=item.Description(NON_COMPACT);
}break;
case ITEM_NAME:{
SetLabel(itemRef.lock()->DisplayName());
displayStr=item.DisplayName();
}break;
}
}else{
std::weak_ptr<Item>&ref{std::get<std::weak_ptr<Item>>(itemRef)};
if(!ref.expired()){
switch(labelType){
case ITEM_DESCRIPTION:{
displayStr=ref.lock()->Description(NON_COMPACT);
}break;
case ITEM_NAME:{
displayStr=ref.lock()->DisplayName();
}break;
}
}
inline virtual void SetItem(std::weak_ptr<Item>itemRef){
}
MenuLabel::SetLabel(displayStr);
}
inline virtual void SetItem(std::variant<Item,std::weak_ptr<Item>>itemRef){
this->itemRef=itemRef;
}
inline const std::variant<Item,std::weak_ptr<Item>>&GetItem()const{
return itemRef;
}
protected:
//DO NOT USE! Use MenuLabel::SetLabel() instead!
inline virtual void SetLabel(std::string text){
ERR("DO NOT SET THE LABEL DIRECTLY for a MenuItemLabel component! THIS IS NOT ALLOWED!")
};
};

@ -70,11 +70,11 @@ enum MenuType{
DEATH, //100% Controller Compatibility
CREDITS, //100% Controller Compatibility
ARTIFICER, //100% Controller Compatibility
ARTIFICER_REFINE,
ARTIFICER_REFINE_RESULT,
ARTIFICER_DISASSEMBLE,
ARTIFICER_ENCHANT,
ARTIFICER_ENCHANT_CONFIRM,
ARTIFICER_REFINE, //100% Controller Compatibility
ARTIFICER_REFINE_RESULT, //100% Controller Compatibility
ARTIFICER_DISASSEMBLE, //100% Controller Compatibility
ARTIFICER_ENCHANT, //100% Controller Compatibility
ARTIFICER_ENCHANT_CONFIRM, //100% Controller Compatibility
///////////////////////////////////////////////////////////
/*DO NOT REMOVE!!*/ENUM_END////////////////////////////////
///////////////////////////////////////////////////////////

@ -184,7 +184,7 @@ void Merchant::PurchaseItem(IT item,uint32_t amt){
}
Inventory::AddItem(item,amt);
game->GetPlayer()->SetMoney(game->GetPlayer()->GetMoney()-totalCost);
game->GetPlayer()->RemoveMoney(totalCost);
}
void Merchant::SellItem(std::weak_ptr<Item>item,uint32_t amt){
sellFunctionPrimed.Validate(item.lock()->ActualName(),amt);
@ -222,7 +222,7 @@ void Merchant::SellItem(std::weak_ptr<Item>item,uint32_t amt){
std::string itemName=item.lock()->ActualName(); //We need the item name since our reference to the item is about to be deleted.
Inventory::RemoveItem(item,amt);
game->GetPlayer()->SetMoney(game->GetPlayer()->GetMoney()+totalCost);
game->GetPlayer()->AddMoney(totalCost);
//If we still have some in our inventory, we'll add them back in.
if(foundLoadoutSlot!=-1&&Inventory::GetItemCount(itemName)>0){

@ -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()){
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()){
for(const std::string&anim:data.GetAnimations()){
animation.AddState(anim,ANIMATION_DATA[std::format("{}_{}",name,anim)]);
}
@ -81,6 +81,7 @@ Monster::Monster(vf2d pos,MonsterData data,bool upperLevel,bool bossMob):
randomFrameOffset=(util::random()%1000)/1000.f;
monsterWalkSoundTimer=util::random(1.f);
UpdateFacingDirection(game->GetPlayer()->GetPos());
animation.UpdateState(internal_animState,randomFrameOffset);
}
const vf2d&Monster::GetPos()const{
return pos;
@ -91,23 +92,23 @@ const int Monster::GetHealth()const{
const int Monster::GetMaxHealth()const{
return int(GetModdedStatBonuses("Health"));
}
int Monster::GetAttack(){
int Monster::GetAttack()const{
return int(GetModdedStatBonuses("Attack"));
}
float Monster::GetMoveSpdMult(){
float moveSpdPct=stats.A("Move Spd %")/100.f;
float Monster::GetMoveSpdMult()const{
float moveSpdPct=stats.A_Read("Move Spd %")/100.f;
float mod_moveSpd=moveSpdPct;
for(Buff&b:GetBuffs(SLOWDOWN)){
for(const Buff&b:GetBuffs(SLOWDOWN)){
mod_moveSpd-=moveSpdPct*b.intensity;
}
for(Buff&b:GetBuffs(SELF_INFLICTED_SLOWDOWN)){
for(const Buff&b:GetBuffs(SELF_INFLICTED_SLOWDOWN)){
mod_moveSpd-=moveSpdPct*b.intensity;
}
for(Buff&b:GetBuffs(LOCKON_SPEEDBOOST)){
for(const Buff&b:GetBuffs(LOCKON_SPEEDBOOST)){
mod_moveSpd+=moveSpdPct*b.intensity;
}
for(Buff&b:GetBuffs(SPEEDBOOST)){
for(const Buff&b:GetBuffs(SPEEDBOOST)){
mod_moveSpd+=moveSpdPct*b.intensity;
}
return mod_moveSpd;
@ -169,11 +170,16 @@ void Monster::PerformAnimation(const std::string_view animationName){
if(HasFourWaySprites())animation.ChangeState(internal_animState,std::format("{}_{}",animationName,int(facingDirection)));
else animation.ChangeState(internal_animState,std::string(animationName));
}
//Performs an animation, optionally changes the facing direction of this monster.
void Monster::PerformAnimation(const std::string_view animationName,const Direction facingDir){
facingDirection=facingDir;
PerformAnimation(animationName);
}
void Monster::PerformAnimation(const std::string_view animationName,const vf2d targetFacingDir){
facingDirection=GetFacingDirectionToTarget(targetFacingDir);
PerformAnimation(animationName);
}
bool Monster::_SetX(float x,const bool monsterInvoked){
vf2d newPos={x,pos.y};
vi2d tilePos=vi2d(newPos/float(game->GetCurrentMapData().tilewidth))*game->GetCurrentMapData().tilewidth;
@ -203,7 +209,8 @@ bool Monster::_SetX(float x,const bool monsterInvoked){
return true;
}else
if(monsterInvoked){ //If player invoked, we'll try the smart move system.
vf2d pushDir=geom2d::line<float>(collision.middle(),pos).vector().norm();
vf2d pushDir{vf2d{1.f,util::random(2*PI)}.cart()};
if(collision.middle()!=pos)pushDir=geom2d::line<float>(collision.middle(),pos).vector().norm();
newPos={newPos.x,pos.y+pushDir.y*12};
if(NoEnemyCollisionWithTile()){
return _SetY(pos.y+pushDir.y*game->GetElapsedTime()*12,false);
@ -242,7 +249,8 @@ bool Monster::_SetY(float y,const bool monsterInvoked){
return true;
}else
if(monsterInvoked){ //If player invoked, we'll try the smart move system.{
vf2d pushDir=geom2d::line<float>(collision.middle(),pos).vector().norm();
vf2d pushDir{vf2d{1.f,util::random(2*PI)}.cart()};
if(collision.middle()!=pos)pushDir=geom2d::line<float>(collision.middle(),pos).vector().norm();
newPos={pos.x+pushDir.x*12,newPos.y};
if(NoEnemyCollisionWithTile()){
return _SetX(pos.x+pushDir.x*game->GetElapsedTime()*12,false);
@ -260,7 +268,7 @@ bool Monster::SetY(float y){
return _SetY(y);
}
bool Monster::Update(float fElapsedTime){
void Monster::Update(const float fElapsedTime){
lastHitTimer=std::max(0.f,lastHitTimer-fElapsedTime);
lastDotTimer=std::max(0.f,lastDotTimer-fElapsedTime);
iframe_timer=std::max(0.f,iframe_timer-fElapsedTime);
@ -273,6 +281,14 @@ bool Monster::Update(float fElapsedTime){
lastFacingDirectionChange+=fElapsedTime;
timeSpentAlive+=fElapsedTime;
if(IsUnconscious()){
unconsciousTimer=std::max(0.f,unconsciousTimer-fElapsedTime);
if(unconsciousTimer==0.f){
Heal(GetMaxHealth());
PerformIdleAnimation();
}
}
if(IsSolid()&&FadeoutWhenStandingBehind()){
if(GetPos().y>=game->GetPlayer()->GetPos().y)solidFadeTimer=std::min(TileGroup::FADE_TIME,solidFadeTimer+game->GetElapsedTime());
else solidFadeTimer=std::max(0.f,solidFadeTimer-game->GetElapsedTime());
@ -387,17 +403,9 @@ bool Monster::Update(float fElapsedTime){
}
if(!game->TestingModeEnabled()&&CanMove())Monster::STRATEGY::RUN_STRATEGY(*this,fElapsedTime);
}
if(!IsAlive()){
deathTimer+=fElapsedTime;
if(deathTimer>3){
return false;
}
}
animation.UpdateState(internal_animState,randomFrameOffset+fElapsedTime);
if(HasMountedMonster())mounted_animation.value().UpdateState(internal_mounted_animState.value(),fElapsedTime);
randomFrameOffset=0;
animation.UpdateState(internal_animState,fElapsedTime);
if(HasMountedMonster())mounted_animation.value().UpdateState(internal_mounted_animState,fElapsedTime);
attackedByPlayer=false;
return true;
}
Direction Monster::GetFacingDirection()const{
return facingDirection;
@ -439,7 +447,7 @@ void Monster::UpdateFacingDirection(vf2d facingTargetPoint){
}
}
animation.ModifyDisplaySprite(internal_animState,std::format("{}_{}",animation.currentStateName.substr(0,animation.currentStateName.length()-2),int(facingDirection)));
if(HasMountedMonster())mounted_animation.value().ModifyDisplaySprite(internal_mounted_animState.value(),std::format("{}_{}",mounted_animation.value().currentStateName.substr(0,mounted_animation.value().currentStateName.length()-2),int(facingDirection)));
if(HasMountedMonster())mounted_animation.value().ModifyDisplaySprite(internal_mounted_animState,std::format("{}_{}",mounted_animation.value().currentStateName.substr(0,mounted_animation.value().currentStateName.length()-2),int(facingDirection)));
}else{
if(diff.x>0){
facingDirection=Direction::WEST;
@ -452,7 +460,7 @@ void Monster::Draw()const{
if(markedForDeletion)return;
Pixel blendCol{WHITE};
std::optional<std::reference_wrapper<Buff>>glowPurpleBuff;
std::optional<Buff>glowPurpleBuff;
if(GetBuffs(BuffType::GLOW_PURPLE).size()>0)glowPurpleBuff=GetBuffs(BuffType::GLOW_PURPLE)[0];
@ -465,7 +473,7 @@ void Monster::Draw()const{
else if(HasBuff(BuffType::CURSE_OF_DEATH))blendCol=GetBuffBlendCol(BuffType::CURSE_OF_DEATH,1.4f,{255,0,0});
else if(HasBuff(BuffType::COLOR_MOD))blendCol=GetBuffBlendCol(BuffType::COLOR_MOD,1.4f,PixelRaw(GetBuffs(BuffType::COLOR_MOD)[0].intensity),1.f);
else if(HasBuff(BuffType::SLOWDOWN))blendCol=GetBuffBlendCol(BuffType::SLOWDOWN,1.4f,{255,255,128},0.5f);
else if(glowPurpleBuff.has_value())blendCol=Pixel{uint8_t(255*abs(sin(1.4*glowPurpleBuff.value().get().duration))),uint8_t(255*abs(sin(1.4*glowPurpleBuff.value().get().duration))),uint8_t(128+127*abs(sin(1.4*glowPurpleBuff.value().get().duration)))};
else if(glowPurpleBuff.has_value())blendCol=Pixel{uint8_t(255*abs(sin(1.4*glowPurpleBuff.value().duration))),uint8_t(255*abs(sin(1.4*glowPurpleBuff.value().duration))),uint8_t(128+127*abs(sin(1.4*glowPurpleBuff.value().duration)))};
const vf2d hitTimerOffset=vf2d{sin(20*PI*lastHitTimer+randomFrameOffset),0.f}*2.f*GetSizeMult();
const vf2d zOffset=-vf2d{0,GetZ()};
@ -480,10 +488,10 @@ void Monster::Draw()const{
const bool NotOnTitleScreen=GameState::STATE!=GameState::states[States::MAIN_MENU];
uint8_t blendColAlpha=blendCol.a;
if(fadeTimer>0.f)blendColAlpha=uint8_t(util::lerp(0,blendCol.a,fadeTimer)); //Fade timer goes from 1 to 0 seconds.
if(fadeTimer>0.f)blendColAlpha=uint8_t(util::lerp(uint8_t(0),blendCol.a,fadeTimer)); //Fade timer goes from 1 to 0 seconds.
else if(NotOnTitleScreen
&&(game->GetPlayer()->HasIframes()||OnUpperLevel()!=game->GetPlayer()->OnUpperLevel()||abs(GetZ()-game->GetPlayer()->GetZ())>1))blendColAlpha=blendCol.a*0.62f;
else if(IsSolid()&&solidFadeTimer>0.f)blendColAlpha=uint8_t(util::lerp(blendCol.a,255-TileGroup::FADE_AMT,solidFadeTimer/TileGroup::FADE_TIME));
&&(OnUpperLevel()!=game->GetPlayer()->OnUpperLevel()||abs(GetZ()-game->GetPlayer()->GetZ())>1))blendColAlpha=blendCol.a*0.62f;
else if(IsSolid()&&solidFadeTimer>0.f)blendColAlpha=uint8_t(util::lerp(blendCol.a,uint8_t(255-TileGroup::FADE_AMT),solidFadeTimer/TileGroup::FADE_TIME));
blendCol.a=blendColAlpha;
@ -629,6 +637,8 @@ void Monster::DrawReflection(float drawRatioX,float multiplierX){
};
}
if(IsUnconscious()&&animation.HasState(MONSTER_DATA.at(name).GetDefaultDeathAnimation()))animation.ChangeState(internal_animState,GetDeathAnimationName());
game->view.DrawPartialWarpedDecal(GetFrame().GetSourceImage()->Decal(),points,GetFrame().GetSourceRect().pos,GetFrame().GetSourceRect().size);
game->SetDecalMode(DecalMode::NORMAL);
}
@ -951,13 +961,13 @@ std::vector<std::reference_wrapper<Buff>>Monster::EditBuffs(BuffType buff){
return filteredBuffs;
}
std::vector<Buff>Monster::GetBuffs(BuffType buff)const{
const std::vector<Buff>Monster::GetBuffs(BuffType buff)const{
std::vector<Buff>filteredBuffs;
std::copy_if(buffList.begin(),buffList.end(),std::back_inserter(filteredBuffs),[&buff](const Buff&b){return b.type==buff;});
return filteredBuffs;
}
std::vector<Buff>Monster::GetBuffs(std::vector<BuffType>buffs)const{
const std::vector<Buff>Monster::GetBuffs(std::vector<BuffType>buffs)const{
std::vector<Buff>filteredBuffs;
std::copy_if(buffList.begin(),buffList.end(),std::back_inserter(filteredBuffs),[&buffs](const Buff&b){return std::find(buffs.begin(),buffs.end(),b.type)!=buffs.end();});
return filteredBuffs;
@ -1058,6 +1068,15 @@ void Monster::OnDeath(){
exitRing.zone.pos.y=std::clamp(exitRing.zone.pos.y,clampedArena.pos.y-"boss_spawn_ring_radius"_I,clampedArena.pos.y-"boss_spawn_ring_radius"_I+clampedArena.size.y);
game->AddZone("EndZone",exitRing); //Create a 144x144 ring around the dead boss.
Audio::SetAudioEvent("BossFanfare");
game->GetPlayer()->AddTimer(PlayerTimerType::FANFARE_WAIT_TIMER,Timer{"Fanfare Wait Timer",1.f,[](){
SoundEffect::PlaySFX("Fanfare",SoundEffect::CENTERED);
game->GetPlayer()->AddTimer(PlayerTimerType::FANFARE_WAIT_TIMER,Timer{"Fanfare Wait Timer",5.f,[](){
Audio::SetAudioEvent("BossDefeated");
}});
}});
}
}
}
@ -1151,6 +1170,8 @@ void Monster::OnDeath(){
game->GetPlayer()->AddAccumulatedXP(MONSTER_DATA.at(name).GetXP());
RemoveBuff(BuffType::TRAPPER_MARK);
unconsciousTimer=MONSTER_DATA.at(name).UnconsciousTime();
}
const ItemAttributable&Monster::GetStats()const{
@ -1231,20 +1252,14 @@ const float Monster::GetDamageReductionFromBuffs()const{
const float Monster::GetCollisionDamage()const{
float collisionDmg=0.f;
for(Buff&b:GetBuffs(FIXED_COLLISION_DMG)){
for(const Buff&b:GetBuffs(FIXED_COLLISION_DMG)){
collisionDmg+=b.intensity;
}
if(collisionDmg>0)return collisionDmg;
else return MONSTER_DATA[name].GetCollisionDmg();
}
//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.
//Arguments are:
// GameEvent& - The death event itself.
// Monster& - The monster reference
// const std::string& - The strategy name.
void Monster::SetStrategyDeathFunction(std::function<bool(GameEvent&,Monster&,const std::string&)>func){
void Monster::SetStrategyDeathFunction(std::function<bool(GameEvent&event,Monster&monster,const std::string&strategyName)>func){
strategyDeathFunc=func;
}
@ -1300,13 +1315,12 @@ const bool Monster::HasFourWaySprites()const{
}
const bool Monster::HasMountedMonster()const{
if(internal_mounted_animState.has_value()^mounted_animation.has_value())ERR("WARNING! The internal mounted animation state and the mounted animation variables are not matching! They should both either be on or both be off! THIS SHOULD NOT BE HAPPENING!");
return internal_mounted_animState.has_value()&&mounted_animation.has_value();
return mounted_animation.has_value();
}
const std::optional<const Animate2D::Frame>Monster::GetMountedFrame()const{
if(!HasMountedMonster())return {};
else return mounted_animation.value().GetFrame(internal_mounted_animState.value());
else return mounted_animation.value().GetFrame(internal_mounted_animState);
}
DeathSpawnInfo::DeathSpawnInfo(const std::string_view monsterName,const uint8_t spawnAmt,const vf2d spawnOffset)
@ -1332,7 +1346,7 @@ void Monster::ProximityKnockback(const vf2d centerPoint,const float knockbackFac
const bool Monster::IgnoresTerrainCollision()const{
return MONSTER_DATA.at(GetName()).IgnoresTerrainCollision();
return MONSTER_DATA.at(GetName()).IgnoresTerrainCollision()||manualIgnoreTerrain;
}
const float Monster::TimeSpentAlive()const{
@ -1343,7 +1357,7 @@ const bool Monster::Immovable()const{
return MONSTER_DATA.at(GetName()).Immovable();
}
const bool Monster::Invulnerable()const{
return MONSTER_DATA.at(GetName()).Invulnerable();
return MONSTER_DATA.at(GetName()).Invulnerable()||IsUnconscious();
}
const std::optional<float>Monster::GetLifetime()const{
return lifetime;
@ -1352,7 +1366,7 @@ const std::optional<float>Monster::GetTotalLifetime()const{
return MONSTER_DATA.at(GetName()).GetLifetime();
}
const float Monster::GetCollisionRadius()const{
return MONSTER_DATA.at(GetName()).GetCollisionRadius()*GetSizeMult();
return collisionRadius;
}
void Monster::MarkForDeletion(){
@ -1395,8 +1409,9 @@ const bool Monster::_DealTrueDamage(const uint32_t damageAmt,HurtFlag::HurtFlag
return _Hurt(damageAmt,OnUpperLevel(),GetZ(),TrueDamageFlag::IGNORE_DAMAGE_RULES,hurtFlags);
}
void Monster::Heal(const int healAmt){
void Monster::Heal(const int healAmt,const bool displayDamageNumber){
hp=std::clamp(hp+healAmt,0,int(GetMaxHealth()));
if(displayDamageNumber)DAMAGENUMBER_LIST.emplace_back(std::make_shared<DamageNumber>(GetPos(),healAmt,false,DamageNumberType::HEALTH_GAIN));
}
const float Monster::GetModdedStatBonuses(std::string_view stat)const{
@ -1543,7 +1558,7 @@ std::optional<std::weak_ptr<Monster>>Monster::GetNearestMonster(const vf2d point
}
const bool Monster::InUndamageableState(const bool onUpperLevel,const float z)const{
return Invulnerable()||!IsAlive()||onUpperLevel!=OnUpperLevel()||AttackAvoided(z)||IsNPC();
return Invulnerable()||!IsAlive()||onUpperLevel!=OnUpperLevel()||AttackAvoided(z)||IsNPC()||GetCollisionRadius()<=0.f;
}
void Monster::AddBuff(BuffRestorationType type,BuffOverTimeType::BuffOverTimeType overTimeType,float duration,float intensity,float timeBetweenTicks){
@ -1628,3 +1643,29 @@ void Monster::ResetCurseOfDeathDamage(){
void Monster::AddAddedVelocity(vf2d vel){
this->addedVel+=vel;
}
void Monster::MoveForward(const vf2d&moveForwardVec,const float fElapsedTime){
if(moveForwardVec.mag()==0.f)ERR("WARNING! Passed a zero length vector into Move Forward! THIS IS NOT ALLOWED!");
SetPos(pos+moveForwardVec.norm()*100.f*fElapsedTime*GetMoveSpdMult());
}
const bool Monster::IsUnconscious()const{
return unconsciousTimer>0.f;
}
const float Monster::UnconsciousTime()const{
return MONSTER_DATA.at(name).UnconsciousTime();
}
void Monster::SetPhase(const std::string&strategyName,int phase){
this->phase[strategyName]=phase;
}
const int Monster::GetPhase(const std::string&strategyName){
if(!phase.contains(strategyName))phase[strategyName]=0;
return this->phase[strategyName];
}
const bool Monster::HasBuff(BuffType buff)const{
return std::find_if(buffList.begin(),buffList.end(),[type=buff](const Buff&buff){return buff.type==type;})!=buffList.end();
}
const float Monster::GetOriginalCollisionRadius()const{
return MONSTER_DATA.at(GetName()).GetCollisionRadius()*GetSizeMult();
}
void Monster::SetCollisionRadius(const float collisionRadius){
this->collisionRadius=collisionRadius;
}

@ -60,6 +60,8 @@ enum class Attribute;
class GameEvent;
using StrategyName=std::string;
namespace MonsterTests{
class MonsterTest;
};
@ -84,14 +86,14 @@ public:
const vf2d&GetPos()const;
const int GetHealth()const;
const int GetMaxHealth()const;
int GetAttack();
int GetAttack()const;
//This function returns the multiplier for the movespd from the base spd. So 100 movespd is 1.0.
float GetMoveSpdMult();
float GetMoveSpdMult()const;
//Obtains the size multiplier (from 0.f-1.f).
float GetSizeMult()const;
Animate2D::Frame GetFrame()const;
const std::optional<const Animate2D::Frame>GetMountedFrame()const;
bool Update(float fElapsedTime);
void Update(const float fElapsedTime);
//Returns true when damage is actually dealt. Provide whether or not the attack is on the upper level or not. Monsters must be on the same level to get hit by it. (there is a death check and level check here.)
//If you need to hurt multiple enemies try AiL::HurtEnemies()
bool Hurt(HurtDamageInfo damageData);
@ -128,7 +130,8 @@ public:
void PerformNPCLeftAnimation();
void PerformNPCRightAnimation();
void PerformAnimation(const std::string_view animationName);
void PerformAnimation(const std::string_view animationName,const Direction facingDir);
void PerformAnimation(const std::string_view animationName,const Direction facingDir); //Performs an animation, optionally changes the facing direction of this monster.
void PerformAnimation(const std::string_view animationName,const vf2d targetFacingDir); //Faces a target vector while performing an animation.
const Animate2D::FrameSequence&GetCurrentAnimation()const;
const Animate2D::FrameSequence&GetAnimation(const std::string_view animationName)const;
const bool OnUpperLevel()const;
@ -146,8 +149,9 @@ public:
void AddBuff(BuffRestorationType type,BuffOverTimeType::BuffOverTimeType overTimeType,float duration,float intensity,float timeBetweenTicks,Buff::MonsterBuffExpireCallbackFunction expireCallback);
void AddBuff(BuffType type,BuffRestorationType restorationType,BuffOverTimeType::BuffOverTimeType overTimeType,float duration,float intensity,float timeBetweenTicks);
void AddBuff(BuffType type,BuffRestorationType restorationType,BuffOverTimeType::BuffOverTimeType overTimeType,float duration,float intensity,float timeBetweenTicks,Buff::MonsterBuffExpireCallbackFunction expireCallback);
std::vector<Buff>GetBuffs(BuffType buff)const;
std::vector<Buff>GetBuffs(std::vector<BuffType>buffs)const;
const bool HasBuff(BuffType buff)const;
const std::vector<Buff>GetBuffs(BuffType buff)const;
const std::vector<Buff>GetBuffs(std::vector<BuffType>buffs)const;
//Removes all buffs of a given type.
void RemoveBuff(BuffType type);
State::State GetState();
@ -162,8 +166,8 @@ public:
const std::function<void(Monster&,float,std::string)>&GetStrategy()const;
void SetSize(float newSize,bool immediate=true);
geom2d::circle<float>BulletCollisionHitbox();
void SetStrategyDrawFunction(std::function<void(AiL*,Monster&,const std::string&)>func);
void SetStrategyDrawOverlayFunction(std::function<void(AiL*,Monster&,const std::string&)>func);
void SetStrategyDrawFunction(std::function<void(AiL*game,Monster&monster,const std::string&strategy)>func);
void SetStrategyDrawOverlayFunction(std::function<void(AiL*game,Monster&monster,const std::string&strategy)>func);
std::function<void(AiL*,Monster&,const std::string&)>strategyDraw=[](AiL*pge,Monster&m,const std::string&strategy){};
std::function<void(AiL*,Monster&,const std::string&)>strategyDrawOverlay=[](AiL*pge,Monster&m,const std::string&strategy){};
const ItemAttributable&GetStats()const;
@ -202,7 +206,7 @@ public:
const bool ReachedTargetPos(const float maxDistanceFromTarget=4.f)const;
const float GetHealthRatio()const;
const bool _DealTrueDamage(const uint32_t damageAmt,HurtFlag::HurtFlag hurtFlags=HurtFlag::NONE);
void Heal(const int healAmt);
void Heal(const int healAmt,const bool displayDamageNumber=false);
const float GetModdedStatBonuses(std::string_view stat)const;
//The collision rectangle is only used currently for determining the safe spots for the stone golem boss fight.
const std::optional<geom2d::rect<float>>&GetRectangleCollision()const;
@ -227,6 +231,11 @@ public:
const bool FadeoutWhenStandingBehind()const;
const bool FaceTarget()const;
void ResetCurseOfDeathDamage();
void MoveForward(const vf2d&moveForwardVec,const float fElapsedTime); //Moves the monster forward in given vector direction (will be auto-normalized) applying speeed boosts and other proper movement requirements as if you wanted to move on a frame-by-frame basis.
void SetPhase(const std::string&strategyName,int phase);
const int GetPhase(const std::string&strategyName);
const float GetOriginalCollisionRadius()const;
void SetCollisionRadius(const float collisionRadius);
private:
//NOTE: Marking a monster for deletion does not trigger any death events. It just simply removes the monster from the field!!
// The way this works is that monsters marked for deletion will cause the monster update loop to detect there's at least one or more monsters that must be deleted and will call erase_if on the list at the end of the iteration loop.
@ -253,7 +262,7 @@ private:
Animate2D::Animation<std::string>animation;
Animate2D::AnimationState internal_animState;
std::optional<Animate2D::Animation<std::string>>mounted_animation;
std::optional<Animate2D::AnimationState>internal_mounted_animState;
Animate2D::AnimationState internal_mounted_animState;
float randomFrameOffset=0.f;
float deathTimer=0.f;
float monsterHurtSoundCooldown=0.f;
@ -271,7 +280,7 @@ private:
float spriteRot=0;
std::shared_ptr<DamageNumber>damageNumberPtr;
std::shared_ptr<DamageNumber>dotNumberPtr;
int phase=0;
std::unordered_map<StrategyName,int>phase{}; //NOTE: THIS SHOULD NOT BE MODIFIED DIRECTLY!!! Use SetPhase(), GetPhase() / PHASE() SETPHASE() macros!
bool diesNormally=true; //If set to false, the monster death is handled in a special way. Set it to true when it's time to die.
float targetSize=0;
bool isBoss=false;
@ -280,7 +289,9 @@ private:
NPCData npcData;
float lastPathfindingCooldown=0.f;
std::function<bool(GameEvent&,Monster&,const std::string&)>strategyDeathFunc{};
void SetStrategyDeathFunction(std::function<bool(GameEvent&,Monster&,const std::string&)>func);
//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);
//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.
@ -318,6 +329,12 @@ private:
float stunTimer{};
int accumulatedCurseOfDeathDamage{};
vf2d addedVel{};
std::weak_ptr<Monster>attachedTarget; //A monster attached to another monster can then use this to alter behaviors based on the state of that other monster.
float unconsciousTimer{};
const bool IsUnconscious()const;
const float UnconsciousTime()const;
bool manualIgnoreTerrain{false}; //A manual flag that can be toggled on to dynamically make this monster ignore terrain collision.
float collisionRadius{}; //The collision radius can be modified, it's just set initially to the monster database entry.
struct STRATEGY{
static std::string ERR;
@ -352,6 +369,16 @@ private:
static void DONOTHING(Monster&m,float fElapsedTime,std::string strategy);
static void STONE_GOLEM(Monster&m,float fElapsedTime,std::string strategy);
static void BREAKING_PILLAR(Monster&m,float fElapsedTime,std::string strategy);
static void PIRATE_MARAUDER(Monster&m,float fElapsedTime,std::string strategy);
static void PIRATE_CAPTAIN(Monster&m,float fElapsedTime,std::string strategy);
static void SEAGULL(Monster&m,float fElapsedTime,std::string strategy);
static void SANDWORM(Monster&m,float fElapsedTime,std::string strategy);
static void PIRATE_BUCCANEER(Monster&m,float fElapsedTime,std::string strategy);
static void PARROT(Monster&m,float fElapsedTime,std::string strategy);
static void CRAB(Monster&m,float fElapsedTime,std::string strategy);
static void GIANT_CRAB(Monster&m,float fElapsedTime,std::string strategy);
static void GIANT_OCTOPUS(Monster&m,float fElapsedTime,std::string strategy);
static void OCTOPUS_ARM(Monster&m,float fElapsedTime,std::string strategy);
};
bool bumpedIntoTerrain=false; //Gets set to true before a strategy executes if the monster runs into some terrain on this frame.
bool attackedByPlayer=false; //Gets set to true before a strategy executes if the monster has been attacked by the player.

@ -44,6 +44,7 @@ All rights reserved.
#define B(attr) GetBool(attr)
#define V(attr) GetVf2d(attr)
#define VEC(attr) GetVec(attr)
#define ANY(attr) GetAny(attr)
enum class Attribute{
IFRAME_TIME_UPON_HIT, //When this is set, the monster gains iframes if they take damage based on the value this is set to.
@ -78,7 +79,6 @@ enum class Attribute{
ITEM_QUANTITY,
LAST_INVENTORY_TYPE_OPENED,
NEXT_MENU, //Set to 0 for New Game, Set to 1 for Load Game Menu. This is used for the username checks
PHASE,
LOCKON_WAITTIME,
LOCKON_POS,
TARGET_TIMER,
@ -145,4 +145,15 @@ enum class Attribute{
SHOCKWAVE_COLOR,
FIRE_VELOCITY,
EXTENDED_LINE,
ENCHANT,
PARROT_FLY_TIMER,
MID_PHASE,
RUM_DRINK_COUNT,
SPEED_RAMPUP_TIMER,
BULLET_HAS_BEEN_SHOT,
SUCTION_TIMER,
STORED_ARC,
SWING_OCCURRED,
ARM_LOCATIONS,
ARM_LIST,
};

@ -57,6 +57,7 @@ MonsterData::MonsterData(std::string name,std::string displayName,int hp,int atk
name(name),displayName(displayName),hp(hp),atk(atk),xp(xp),moveSpd(moveSpd),size(size),strategy(strategy),dropData(drops),collisionDmg(collisionDmg),collisionRadius(8.f*this->size){}
void MonsterData::InitializeMonsterData(){
MONSTER_DATA.clear();
for(auto&[key,size]:DATA["Monsters"].GetKeys()){
std::string MonsterName=key;
std::string MonsterImgName=MonsterName;
@ -200,6 +201,8 @@ void MonsterData::InitializeMonsterData(){
if(DATA["Monsters"][MonsterName].HasProperty("Collision Radius"))monster.collisionRadius=DATA["Monsters"][MonsterName]["Collision Radius"].GetReal();
if(DATA["Monsters"][MonsterName].HasProperty("ShowBossIndicator"))monster.hasArrowIndicator=DATA["Monsters"][MonsterName]["ShowBossIndicator"].GetBool();
if(DATA["Monsters"][MonsterName].HasProperty("Unconscious Time"))monster.totalUnconsciousTime=DATA["Monsters"][MonsterName]["Unconscious Time"].GetReal();
if(DATA["Monsters"][MonsterName].HasProperty("Rectangle Collision")){
const datafile&rectData{DATA["Monsters"][MonsterName]["Rectangle Collision"]};
monster.rectCollision={{rectData.GetReal(0),rectData.GetReal(1)},{rectData.GetReal(2),rectData.GetReal(3)}};
@ -476,3 +479,6 @@ const bool MonsterData::FadeoutWhenStandingBehind()const{
const bool MonsterData::FaceTarget()const{
return !noFacing;
}
const float MonsterData::UnconsciousTime()const{
return totalUnconsciousTime;
}

@ -108,6 +108,7 @@ public:
const std::optional<geom2d::rect<float>>&GetRectangleCollision()const;
const bool FadeoutWhenStandingBehind()const;
const bool FaceTarget()const;
const float UnconsciousTime()const;
private:
std::string name;
int hp;
@ -140,4 +141,5 @@ private:
float collisionRadius{};
bool hasArrowIndicator{false};
std::optional<geom2d::rect<float>>rectCollision;
float totalUnconsciousTime{};
};

@ -50,3 +50,6 @@ All rights reserved.
#define ConfigFloatArr(param,ind) _GetFloat(m,param,strategy,ind)
#define ConfigStringArr(param,ind) _GetString(m,param,strategy,ind)
#define ConfigVecArr(param,ind) _GetVec(m,param,strategy,ind)
#define PHASE() m.GetPhase(strategy)
#define SETPHASE(phase) m.SetPhase(strategy,phase)

@ -53,7 +53,7 @@ INCLUDE_game
INCLUDE_DATA
void Monster::STRATEGY::NPC(Monster&m,float fElapsedTime,std::string strategy){
if(m.phase==0){ //Initialization.
if(PHASE()==0){ //Initialization.
if(m.npcData.function.length()>0){
m.SetStrategyDrawOverlayFunction([](AiL*game,Monster&m,const std::string&strategy){
vf2d nameTextSize=game->GetTextSizeProp(m.GetName());
@ -62,7 +62,7 @@ void Monster::STRATEGY::NPC(Monster&m,float fElapsedTime,std::string strategy){
game->KEY_CONFIRM.DrawPrimaryInput(&game->view,m.GetPos()+vf2d{ConfigFloatArr("Interaction Display Offset",0),ConfigFloatArr("Interaction Display Offset",1)},"Interact",alpha);
});
}
m.phase=1;
SETPHASE(1);
}
float distFromPlayer=geom2d::line<float>(m.GetPos(),game->GetPlayer()->GetPos()).length();
if(distFromPlayer<ConfigFloat("Interaction Distance")/100.f*24.f){

@ -0,0 +1,179 @@
#pragma region License
/*
License (OLC-3)
~~~~~~~~~~~~~~~
Copyright 2024 Joshua Sigona <sigonasr2@gmail.com>
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions or derivations of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions or derivative works in binary form must reproduce the above
copyright notice. This list of conditions and the following disclaimer must be
reproduced in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may
be used to endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
Portions of this software are copyright © 2024 The FreeType
Project (www.freetype.org). Please see LICENSE_FT.txt for more information.
All rights reserved.
*/
#pragma endregion
#include "MonsterStrategyHelpers.h"
#include "util.h"
#include "AdventuresInLestoria.h"
#include "SoundEffect.h"
#include "BulletTypes.h"
#include "Arc.h"
using A=Attribute;
INCLUDE_game
INCLUDE_MONSTER_LIST
INCLUDE_MONSTER_DATA
void Monster::STRATEGY::OCTOPUS_ARM(Monster&m,float fElapsedTime,std::string strategy){
enum PhaseName{
INIT,
RISE_ANIMATION,
SEARCH,
PREPARE_ATTACK,
ATTACK_ANIMATION,
ATTACK_RECOVERY,
SUBMERGE,
};
const auto GetAttackArc=[attackRadius=ConfigFloat("Attack Radius"),attackArc=ConfigFloat("Attack Arc")](const Monster&m){
return Arc{m.GetPos(),attackRadius/100.f*24,util::dirToAngle(m.GetFacingDirection()),util::degToRad(attackArc)};
};
if(m.ANY(A::STORED_ARC).has_value()){
game->DrawShadowStringDecal({100,100},std::format("Stored Arc Active: {}",std::any_cast<Arc>(m.ANY(A::STORED_ARC)).pos.str()));
const float growthRate=((ConfigFloat("Attack Radius")/100.f*24)/ConfigFloat("Attack Effect Time"))*fElapsedTime;
std::any_cast<Arc>(m.ANY(A::STORED_ARC)).GrowRadius(growthRate);
m.F(A::ENVIRONMENT_TIMER)-=fElapsedTime;
if(m.F(A::ENVIRONMENT_TIMER)<=0.f)m.ANY(A::STORED_ARC).reset();
}
switch(PHASE()){
case INIT:{
if(ConfigFloat("Attack Swing Damage Wait Time")>m.GetAnimation("ATTACKING").GetTotalAnimationDuration())ERR(std::format("The Attack Swing Damage Wait Time ({}s) should not be greater than the total attack time animation duration! ({}s)",ConfigFloat("Attack Swing Damage Wait Time"),m.GetAnimation("ATTACKING").GetTotalAnimationDuration()));
m.PerformAnimation("RISE",game->GetPlayer()->GetPos());
m.F(A::CASTING_TIMER)=m.GetCurrentAnimation().GetTotalAnimationDuration();
SETPHASE(RISE_ANIMATION);
m.SetStrategyDeathFunction([bossDamageOnDeath=ConfigInt("Boss Damage On Death")](GameEvent&event,Monster&m,const StrategyName&strategyName){
m.SetStrategyDrawFunction([](AiL*game,Monster&monster,const std::string&strategy){});
const std::string GIANT_OCTOPUS_NAME{"Giant Octopus"};
const std::string OCTOPUS_ARM_NAME{"Octopus Arm"};
if(!MONSTER_DATA.count(GIANT_OCTOPUS_NAME))ERR(std::format("WARNING! {} does not exist on the map! THIS SHOULD NOT BE HAPPENING!",GIANT_OCTOPUS_NAME));
auto boss{std::find_if(MONSTER_LIST.begin(),MONSTER_LIST.end(),[&GIANT_OCTOPUS_NAME](const std::shared_ptr<Monster>&m){
return m->GetName()==GIANT_OCTOPUS_NAME;
})};
if(boss!=MONSTER_LIST.end()){
Monster&bossMonster{**boss};
bossMonster.Hurt(bossDamageOnDeath,m.OnUpperLevel(),m.GetZ());
}
std::for_each(MONSTER_LIST.begin(),MONSTER_LIST.end(),[&OCTOPUS_ARM_NAME,&strategyName](const std::shared_ptr<Monster>&m){
if(m->GetName()==OCTOPUS_ARM_NAME){
m->PerformAnimation("SUBMERGE");
m->SetPhase(strategyName,SUBMERGE);
m->GetFloat(A::RECOVERY_TIME)=m->GetCurrentAnimation().GetTotalAnimationDuration();
}
});
return false;
});
}break;
case RISE_ANIMATION:{
m.F(A::CASTING_TIMER)-=fElapsedTime;
if(m.F(A::CASTING_TIMER)<=0.f){
m.PerformAnimation("IDLE",game->GetPlayer()->GetPos());
SETPHASE(SEARCH);
}
}break;
case SEARCH:{
if(util::distance(m.GetPos(),game->GetPlayer()->GetPos())<=ConfigFloat("Attack Radius")/100.f*24){
SETPHASE(PREPARE_ATTACK);
m.F(A::ATTACK_COOLDOWN)=util::random_range(ConfigFloatArr("Attack Wiggle Time Range",0),ConfigFloatArr("Attack Wiggle Time Range",1));
m.PerformAnimation("ATTACK",game->GetPlayer()->GetPos());
Arc attackArc{GetAttackArc(m)};
m.SetStrategyDrawFunction([arc=attackArc,&storedArc=m.ANY(A::STORED_ARC),&alphaTimer=m.F(A::ENVIRONMENT_TIMER),attackEffectTime=ConfigFloat("Attack Effect Time")](AiL*game,Monster&monster,const std::string&strategy){
const float alphaTimer{float(std::fmod(game->GetRunTime(),2.f))};
uint8_t alpha{util::lerp(uint8_t(0),uint8_t(255),alphaTimer)};
if(alphaTimer>1.f)alpha=util::lerp(0,255,1-(alphaTimer-1));
const_cast<Arc&>(arc).Draw(game,{0,0,255,uint8_t(alpha)});
if(storedArc.has_value()){
const uint8_t effectAlpha{util::lerp(uint8_t(0),uint8_t(255),alphaTimer/attackEffectTime)};
std::any_cast<Arc>(storedArc).Draw(game,{255,255,255,effectAlpha});
}
});
}
}break;
case PREPARE_ATTACK:{
m.F(A::ATTACK_COOLDOWN)-=fElapsedTime;
if(m.F(A::ATTACK_COOLDOWN)<=0.f){
SETPHASE(ATTACK_ANIMATION);
m.PerformAnimation("ATTACKING");
m.F(A::ENVIRONMENT_TIMER)=m.F(A::RECOVERY_TIME)=ConfigFloat("Attack Effect Time");
m.F(A::SWING_OCCURRED)=ConfigFloat("Attack Swing Damage Wait Time");
}
}break;
case ATTACK_ANIMATION:{
m.F(A::SWING_OCCURRED)-=fElapsedTime;
if(m.F(A::SWING_OCCURRED)<=0.f){
Arc attackArc{GetAttackArc(m)};
if(attackArc.overlaps(game->GetPlayer()->GetPos())){
game->GetPlayer()->Knockback(util::pointTo(m.GetPos(),game->GetPlayer()->GetPos())*ConfigFloat("Attack Knockback"));
game->GetPlayer()->Hurt(m.GetAttack(),m.OnUpperLevel(),m.GetZ());
}
m.F(A::RECOVERY_TIME)=m.GetCurrentAnimation().GetTotalAnimationDuration();
m.ANY(A::STORED_ARC)=GetAttackArc(m);
m.SetStrategyDrawFunction([&storedArc=m.ANY(A::STORED_ARC),attackEffectTime=ConfigFloat("Attack Effect Time")](AiL*game,Monster&monster,const std::string&strategy){
const float alphaTimer{float(std::fmod(game->GetRunTime(),2.f))};
if(storedArc.has_value()){
const uint8_t effectAlpha{util::lerp(uint8_t(0),uint8_t(255),alphaTimer/attackEffectTime)};
std::any_cast<Arc>(storedArc).Draw(game,{255,255,255,effectAlpha});
}
});
SETPHASE(ATTACK_RECOVERY);
}
}break;
case ATTACK_RECOVERY:{
m.F(A::RECOVERY_TIME)-=fElapsedTime;
if(m.F(A::RECOVERY_TIME)<=0.f){
m.PerformIdleAnimation();
m.SetStrategyDrawFunction([](AiL*game,Monster&monster,const std::string&strategy){});
SETPHASE(SEARCH);
}
}break;
case SUBMERGE:{
m.F(A::RECOVERY_TIME)-=fElapsedTime;
if(m.F(A::RECOVERY_TIME)<=0.f){
m.PerformAnimation("RISE");
m.F(A::CASTING_TIMER)=m.GetCurrentAnimation().GetTotalAnimationDuration();
SETPHASE(RISE_ANIMATION);
}
}break;
}
}

@ -0,0 +1,49 @@
Giant Octopus
42000 HP
Boss boss has a permanent 80% DMG reduction.
Tentacle
6000 HP
When a Tentacle dies boss takes 6000 Dmg
There are 8 Tentacles
Base Damage
Bullets: 40
Big bullet: 90
Tentacle: 50
Attack moves
- Boss shots 1 bullet every second
- Every 6. Shot Boss Shots a big bullet that explodes when i gets close to the Player and form a new ring of bullets flying away in a whirl instead of a straight line. (Boss rests for 2 seconds after big bullet)
- Tentacels do a delayed attack
- Every Tentacle and the boss get a 10% Attack buff on each Tentacle death
Tentacles:
- The Tentacles wiggle a bit around between attacks
- Wiggle phase is 2-4 seconds.
- Tentacle stopps moving for 1 sec before attacking, already indicating where it will attack.
- The Tentacle rest on the floor for a short period after an attack ~1 sec (can vary a bit depending how animation looks smoother)
- The Tentacle attacks with its entire wheight slaming the position where the player was when the movement stopped.
Reference for Tentacle attack https://youtu.be/YtR521QdsNk?si=_TUxuH-3HLnwr_t9&t=475 (Should start at time stamp 07:55 with a bossfight)
Instead of wiggling around the tentacles supmerge here in between attacks.
Also instead of this piercing attack i would prefer if its closer to a slam.
But i really like how the tentacle indicates that its about to attack in a specific direction.
Phases
12k missing Health on boss:
- boss starts Shooting an Ink Bubble instead of a normal projectile every 15 seconds that explodes and leaves an ink area Lasting for 30 seconds. moving into the ink gives a -10 movespd debuff for 25 seconds.
- 6 Attacks after first ink attack, every second small bullet has a different Color and has a homing effect. Homing effect is greatly reduced if Player is covered in ink
30k missing Health
Tentacles get +10% Speed on every animation or attack
every Projectile get the homing effect
other stuff
- On Tentacle death, all Tentacles submerge and recover 1k hp each. after 3 seconds tentacles ascend again one after another 0.25 seconds delay between each Tentacle
Once the first Tentacle got defeated every 15-25 seconds a Tentacle far away from the Player submerges and appear 2 seconds later again in an area close to the Player, (Only Triggers if there is a free Tentacle spawn close to the Player)
also: technically the limps of an Octopus arent called Tentacles... but whatever, dont feel like changing this in the entire document now.

@ -41,6 +41,7 @@ All rights reserved.
template<class T>
class Oscillator{
friend struct ThrownProjectile;
public:
inline Oscillator()
:Oscillator({},{},0.f){}

@ -0,0 +1,74 @@
#pragma region License
/*
License (OLC-3)
~~~~~~~~~~~~~~~
Copyright 2024 Joshua Sigona <sigonasr2@gmail.com>
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions or derivations of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions or derivative works in binary form must reproduce the above
copyright notice. This list of conditions and the following disclaimer must be
reproduced in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may
be used to endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
Portions of this software are copyright © 2024 The FreeType
Project (www.freetype.org). Please see LICENSE_FT.txt for more information.
All rights reserved.
*/
#pragma endregion
#include "MonsterStrategyHelpers.h"
#include "util.h"
#include "AdventuresInLestoria.h"
#include "SoundEffect.h"
#include "BulletTypes.h"
using A=Attribute;
INCLUDE_game
void Monster::STRATEGY::PARROT(Monster&m,float fElapsedTime,std::string strategy){
enum PhaseName{
RUN,
FLY_AWAY,
};
switch(PHASE()){
case RUN:{
if(!m.attachedTarget.expired()&&m.attachedTarget.lock()->IsAlive()){
m.PerformAnimation("FLYING");
HAWK(m,fElapsedTime,"Hawk");
}else{
m.lifetime=10.f;
SETPHASE(FLY_AWAY);
m.F(A::PATH_DIR)=1.f;
if(util::random(1.f)<0.5f)m.F(A::PATH_DIR)=-1.f;
m.manualIgnoreTerrain=true;
}
}break;
case FLY_AWAY:{
if(m.F(A::PATH_DIR)>0.f)m.PerformAnimation("FLYING",Direction::EAST);
else m.PerformAnimation("FLYING",Direction::WEST);
m.z+=fElapsedTime*ConfigFloat("Fly Away Z Speed");
m.SetVelocity({m.F(A::PATH_DIR)*ConfigFloat("Fly Away Speed"),0.f});
}break;
}
}

@ -0,0 +1,118 @@
#pragma region License
/*
License (OLC-3)
~~~~~~~~~~~~~~~
Copyright 2024 Joshua Sigona <sigonasr2@gmail.com>
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions or derivations of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions or derivative works in binary form must reproduce the above
copyright notice. This list of conditions and the following disclaimer must be
reproduced in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may
be used to endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
Portions of this software are copyright © 2024 The FreeType
Project (www.freetype.org). Please see LICENSE_FT.txt for more information.
All rights reserved.
*/
#pragma endregion
#include "MonsterStrategyHelpers.h"
#include "util.h"
#include "AdventuresInLestoria.h"
#include "SoundEffect.h"
#include "BulletTypes.h"
using A=Attribute;
INCLUDE_game
void Monster::STRATEGY::PIRATE_BUCCANEER(Monster&m,float fElapsedTime,std::string strategy){
#pragma region Phase, Animation, and Helper function setup
enum PhaseName{
MOVE,
LOCKON,
WINDUP,
FIRE_ANIMATION,
};
#pragma endregion
switch(PHASE()){
case MOVE:{
m.F(A::ATTACK_COOLDOWN)+=fElapsedTime;
float distToPlayer=m.GetDistanceFrom(game->GetPlayer()->GetPos());
const bool outsideMaxShootingRange=distToPlayer>=ConfigPixelsArr("Stand Still and Shoot Range",1);
auto PrepareToShoot=[&](){
SETPHASE(WINDUP);
m.F(A::SHOOT_TIMER)=ConfigFloat("Attack Windup Time");
m.PerformAnimation("SHOOT",m.GetFacingDirectionToTarget(game->GetPlayer()->GetPos()));
};
if(outsideMaxShootingRange){
m.target=game->GetPlayer()->GetPos();
RUN_TOWARDS(m,fElapsedTime,"Run Towards");
}else
if(m.F(A::ATTACK_COOLDOWN)>=ConfigFloat("Attack Reload Time")){
PrepareToShoot();
}else
if(distToPlayer<ConfigPixels("Run Away Range")){
m.target=geom2d::line<float>(m.GetPos(),game->GetPlayer()->GetPos()).upoint(-1);
RUN_TOWARDS(m,fElapsedTime,"Run Towards");
}else
if(m.F(A::ATTACK_COOLDOWN)>=ConfigFloat("Attack Reload Time")/2.f){ //Only the stand still and shoot range remains, which is twice as fast...
PrepareToShoot();
}
}break;
case WINDUP:{
m.F(A::SHOOT_TIMER)-=fElapsedTime;
if(m.F(A::SHOOT_TIMER)<=0){
m.F(A::ATTACK_COOLDOWN)=0.f;
CreateBullet(ChargedArrow)("musket_bullet.png","musket_trail.png",m.GetPos(),util::pointTo(m.GetPos(),m.V(A::LOCKON_POS))*ConfigFloat("Arrow Spd"),ConfigFloat("Arrow Hitbox Radius"),m.GetAttack(),m.OnUpperLevel())EndBullet;
m.PerformAnimation("SHOOTING",m.GetFacingDirectionToTarget(game->GetPlayer()->GetPos()));
m.SetStrategyDrawFunction([](AiL*game,Monster&monster,const std::string&strategy){});
SETPHASE(FIRE_ANIMATION);
m.F(A::SHOOT_TIMER)=m.GetCurrentAnimation().GetTotalAnimationDuration();
}else
if(m.F(A::SHOOT_TIMER)>=ConfigFloat("Attack Lockon Time")){
m.V(A::LOCKON_POS)=game->GetPlayer()->GetPos();
m.PerformAnimation("SHOOT",m.GetFacingDirectionToTarget(game->GetPlayer()->GetPos()));
const float arrowHitboxRadius{ConfigFloat("Arrow Hitbox Radius")};
m.SetStrategyDrawFunction([arrowHitboxRadius](AiL*game,Monster&monster,const std::string&strategy){
vf2d midpoint{geom2d::line<float>(monster.GetPos(),monster.V(A::LOCKON_POS)).rpoint(800.f)};
float shootingAngle{util::angleTo(monster.GetPos(),monster.V(A::LOCKON_POS))+PI/2};
vf2d imgSize{arrowHitboxRadius*2.f,800.f};
game->view.DrawPartialRotatedDecal(midpoint,GFX["line_indicator.png"].Decal(),shootingAngle,GFX["line_indicator.png"].Sprite()->Size()/2,{},GFX["line_indicator.png"].Sprite()->Size(),imgSize/GFX["line_indicator.png"].Sprite()->Size());
});
}
}break;
case FIRE_ANIMATION:{
m.F(A::SHOOT_TIMER)-=fElapsedTime;
if(m.F(A::SHOOT_TIMER)<=0.f){
m.PerformAnimation("IDLE",m.GetFacingDirectionToTarget(game->GetPlayer()->GetPos()));
SETPHASE(MOVE);
}
}break;
}
}

@ -0,0 +1,183 @@
#pragma region License
/*
License (OLC-3)
~~~~~~~~~~~~~~~
Copyright 2024 Joshua Sigona <sigonasr2@gmail.com>
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions or derivations of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions or derivative works in binary form must reproduce the above
copyright notice. This list of conditions and the following disclaimer must be
reproduced in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may
be used to endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
Portions of this software are copyright © 2024 The FreeType
Project (www.freetype.org). Please see LICENSE_FT.txt for more information.
All rights reserved.
*/
#pragma endregion
#include "MonsterStrategyHelpers.h"
#include "util.h"
#include "AdventuresInLestoria.h"
#include "SoundEffect.h"
#include "BulletTypes.h"
using A=Attribute;
INCLUDE_game
INCLUDE_ANIMATION_DATA
void Monster::STRATEGY::PIRATE_CAPTAIN(Monster&m,float fElapsedTime,std::string strategy){
enum PhaseName{
INIT,
MOVE,
PREPARE_SHOOT,
SHOOT_RELOAD,
DRINK_RUM,
WINDUP,
RECOVERY,
};
enum AttackType{
STAB,
SLASH
};
switch(PHASE()){
case INIT:{
m.F(A::TARGET_TIMER)=ConfigFloat("Shooting Frequency");
m.F(A::PARROT_FLY_TIMER)=ConfigFloat("Parrot Fly Wait Time");
m.mounted_animation=Animate2D::Animation<std::string>{};
for(bool firstAnimation=true;const std::string&animation:Config("Imposed Monster Animations").GetValues()){
m.mounted_animation.value().AddState(animation,ANIMATION_DATA.at(animation));
if(firstAnimation)m.mounted_animation.value().ChangeState(m.internal_mounted_animState,animation);
firstAnimation=false;
}
m.mountedSprOffset=ConfigVec("Imposed Monster Offset");
m.deathData.emplace_back(ConfigString("Spawned Monster"),1U);
SETPHASE(MOVE);
}break;
case MOVE:{
if(m.F(A::PARROT_FLY_TIMER)>0.f){
m.F(A::PARROT_FLY_TIMER)-=fElapsedTime;
if(m.F(A::PARROT_FLY_TIMER)<=0.f){
m.mounted_animation.reset();
Monster&parrot{game->SpawnMonster(m.GetPos(),MONSTER_DATA.at("Parrot"),m.OnUpperLevel())};
parrot.attachedTarget=m.GetWeakPointer();
}
}
m.F(A::TARGET_TIMER)-=fElapsedTime;
if(m.F(A::TARGET_TIMER)<=0.f){
const float diceRoll{util::random(100)};
if(diceRoll<=ConfigFloat("Shooting Chance")){
const float distToPlayer{util::distance(game->GetPlayer()->GetPos(),m.GetPos())};
if(distToPlayer<=ConfigFloat("Shoot Max Range")/100.f*24){
m.F(A::SHOOT_TIMER)=ConfigFloat("Shooting Delay");
SETPHASE(PREPARE_SHOOT);
m.PerformAnimation("SHOOT",game->GetPlayer()->GetPos());
m.V(A::LOCKON_POS)=game->GetPlayer()->GetPos();
}
}
m.F(A::TARGET_TIMER)=ConfigFloat("Shooting Frequency");
}else
if(m.GetHealth()<ConfigInt("Rum Drink Threshold")&&m.I(A::RUM_DRINK_COUNT)<ConfigInt("Rum Drink Count")){
m.PerformAnimation("DRINK");
m.F(A::BREAK_TIME)=m.GetCurrentAnimation().GetTotalAnimationDuration();
SETPHASE(DRINK_RUM);
m.I(A::RUM_DRINK_COUNT)++;
}
else{
float distToPlayer=m.GetDistanceFrom(game->GetPlayer()->GetPos());
if(distToPlayer>ConfigFloat("Attack Spacing")/100.f*24){
RUN_TOWARDS(m,fElapsedTime,"Run Towards");
}else{
SETPHASE(WINDUP);
m.I(A::ATTACK_TYPE)=util::random()%2; //Choose randomly between stab or slash.
switch(m.I(A::ATTACK_TYPE)){
case STAB:{
m.F(A::CASTING_TIMER)=ConfigFloat("Stab Windup Time");
m.PerformAnimation("STAB",m.GetFacingDirectionToTarget(game->GetPlayer()->GetPos()));
}break;
case SLASH:{
m.F(A::CASTING_TIMER)=ConfigFloat("Slash Windup Time");
m.PerformAnimation("SLASH",m.GetFacingDirectionToTarget(game->GetPlayer()->GetPos()));
}break;
default:ERR(std::format("WARNING! Invalid Attack type {} provided. THIS SHOULD NOT BE HAPPENING!",m.I(A::ATTACK_TYPE)));
}
}
}
}break;
case PREPARE_SHOOT:{
m.F(A::SHOOT_TIMER)-=fElapsedTime;
if(m.F(A::SHOOT_TIMER)<=0.f){
CreateBullet(Bullet)(m.GetPos(),util::pointTo(m.GetPos(),m.V(A::LOCKON_POS))*ConfigFloat("Bullet Speed"),ConfigFloat("Bullet Radius"),m.GetAttack(),m.OnUpperLevel(),false,ConfigPixel("Bullet Color"),vf2d{1.f,1.f}*ConfigFloat("Bullet Radius")/3.f)EndBullet;
m.PerformAnimation("SHOOTING");
m.F(A::SHOOT_ANIMATION_TIME)=m.GetCurrentAnimation().GetTotalAnimationDuration();
SETPHASE(SHOOT_RELOAD);
}
}break;
case SHOOT_RELOAD:{
m.F(A::SHOOT_ANIMATION_TIME)-=fElapsedTime;
if(m.F(A::SHOOT_ANIMATION_TIME)<=0.f){
m.PerformAnimation("IDLE");
SETPHASE(MOVE);
}
}break;
case DRINK_RUM:{
m.F(A::BREAK_TIME)-=fElapsedTime;
if(m.F(A::BREAK_TIME)<=0.f){
m.Heal(ConfigInt("Rum Health Recovery"),true);
SETPHASE(MOVE);
}
}break;
case WINDUP:{
m.F(A::CASTING_TIMER)-=fElapsedTime;
if(m.F(A::CASTING_TIMER)<=0){
SETPHASE(RECOVERY);
switch(m.I(A::ATTACK_TYPE)){
case STAB:{
vf2d stabTarget=game->GetPlayer()->GetPos();
m.PerformAnimation("STABBING");
CreateBullet(DaggerStab)(m,ConfigString("Dagger Stab Image"),ConfigFloat("Dagger Hit Radius"),m.GetAttack(),ConfigFloat("Dagger Stab Knockback"),m.OnUpperLevel(),m.GetFacingDirection(),ConfigFloat("Dagger Frame Duration"),ConfigFloat("Dagger Stab Distance"),
DaggerStab::DirectionOffsets{ConfigVec("Dagger Up Offset"),ConfigVec("Dagger Down Offset"),ConfigVec("Dagger Right Offset"),ConfigVec("Dagger Left Offset")})EndBullet;
}break;
case SLASH:{
vf2d slashTarget=game->GetPlayer()->GetPos();
m.PerformAnimation("SLASHING");
CreateBullet(DaggerSlash)(m,ConfigString("Dagger Slash Image"),ConfigFloat("Dagger Hit Radius"),m.GetAttack(),ConfigFloat("Dagger Slash Knockback"),m.OnUpperLevel(),m.GetFacingDirection(),ConfigFloat("Dagger Frame Duration"),ConfigFloat("Dagger Slash Distance"))EndBullet;
}break;
default:ERR(std::format("WARNING! Invalid Attack type {} provided. THIS SHOULD NOT BE HAPPENING!",m.I(A::ATTACK_TYPE)));
}
m.F(A::CASTING_TIMER)=m.GetCurrentAnimation().GetTotalAnimationDuration();
m.F(A::RECOVERY_TIME)=ConfigFloat("Attack Recovery Time");
}
}break;
case RECOVERY:{
m.F(A::CASTING_TIMER)-=fElapsedTime;
m.F(A::RECOVERY_TIME)-=fElapsedTime;
if(m.F(A::CASTING_TIMER)<=0){m.PerformIdleAnimation(m.GetFacingDirectionToTarget(game->GetPlayer()->GetPos()));}
if(m.F(A::RECOVERY_TIME)<=0)SETPHASE(MOVE);
}break;
}
}

@ -0,0 +1,175 @@
#pragma region License
/*
License (OLC-3)
~~~~~~~~~~~~~~~
Copyright 2024 Joshua Sigona <sigonasr2@gmail.com>
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions or derivations of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions or derivative works in binary form must reproduce the above
copyright notice. This list of conditions and the following disclaimer must be
reproduced in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may
be used to endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
Portions of this software are copyright © 2024 The FreeType
Project (www.freetype.org). Please see LICENSE_FT.txt for more information.
All rights reserved.
*/
#pragma endregion
#include "MonsterStrategyHelpers.h"
#include "util.h"
#include "AdventuresInLestoria.h"
#include "SoundEffect.h"
#include "BulletTypes.h"
using A=Attribute;
INCLUDE_game
void Monster::STRATEGY::PIRATE_MARAUDER(Monster&m,float fElapsedTime,std::string strategy){
enum PhaseName{
INIT,
MOVE,
WINDUP,
RECOVERY,
LEAP,
PREPARE_WHIRLWIND,
WHIRLWIND,
};
switch(PHASE()){
case INIT:{
m.F(A::CHASE_TIMER)=ConfigFloat("Ability Choose Timer");
SETPHASE(MOVE);
}break;
case MOVE:{
float distToPlayer=m.GetDistanceFrom(game->GetPlayer()->GetPos());
m.F(A::CHASE_TIMER)-=fElapsedTime;
if(m.F(A::CHASE_TIMER)<=0.f){
m.I(A::ABILITY_COUNT)++;
m.F(A::CHASE_TIMER)=ConfigFloat("Ability Choose Timer");
}
if(m.I(A::ABILITY_COUNT)>0){
float roll{util::random_range(0.f,100.f)};
float distanceToPlayer{util::distance(m.GetPos(),game->GetPlayer()->GetPos())};
std::pair<float,float>jumpAttackRollRange{0.f,ConfigFloat("Jump Attack Chance")};
std::pair<float,float>whirlwindAttackRollRange{jumpAttackRollRange.second,jumpAttackRollRange.second+ConfigFloat("Whirlwind Attack Chance")};
if(roll<jumpAttackRollRange.second&&distanceToPlayer>=ConfigFloatArr("Jump Attack Ranges",0)/100.f*24.f&&distanceToPlayer<=ConfigFloatArr("Jump Attack Ranges",1)/100.f*24.f){
SETPHASE(LEAP);
m.V(A::JUMP_TARGET_POS)=game->GetPlayer()->GetPos();
m.I(A::ABILITY_COUNT)--;
const float impactArea{ConfigFloat("Jump Attack Impact Area")};
m.SetStrategyDrawFunction([impactArea](AiL*game,Monster&monster,const std::string&strategy){
game->view.DrawRotatedDecal(monster.V(A::JUMP_TARGET_POS),GFX["range_indicator.png"].Decal(),0.f,GFX["range_indicator.png"].Sprite()->Size()/2.f,vf2d{1,1}*(impactArea/100.f),{255,0,0,96});
});
m.F(A::JUMP_MOVE_TO_TARGET_TIMER)=0.f;
m.V(A::PREV_POS)=m.GetPos();
m.PerformAnimation("LEAPING",m.V(A::JUMP_TARGET_POS));
break;
}else
if(roll>=whirlwindAttackRollRange.first&&roll<whirlwindAttackRollRange.second
&&distanceToPlayer>=ConfigFloatArr("Whirlwind Attack Ranges",0)/100.f*24.f&&distanceToPlayer<=ConfigFloatArr("Whirlwind Attack Ranges",1)/100.f*24.f){
SETPHASE(PREPARE_WHIRLWIND);
m.I(A::ABILITY_COUNT)--;
vf2d aimingTarget{game->GetPlayer()->GetPos()};
if(aimingTarget==m.GetPos()){ //Handle edge case.
aimingTarget=m.GetPos()+vf2d{1.f,util::random(2*PI)}.cart();
}
m.V(A::PATH_DIR)=util::pointTo(m.GetPos(),aimingTarget);
m.PerformAnimation("SPIN",m.V(A::LOCKON_POS));
m.F(A::CASTING_TIMER)=ConfigFloat("Whirlwind Chargeup Time");
m.AddBuff(BuffType::SPEEDBOOST,ConfigFloat("Whirlwind Spin Time"),ConfigFloat("Whirlwind Bonus Movespd")/100.f);
break;
}
}
NormalBehavior:
if(distToPlayer>ConfigFloat("Attack Spacing")/100.f*24){
RUN_TOWARDS(m,fElapsedTime,"Run Towards");
}else{
SETPHASE(WINDUP);
m.F(A::CASTING_TIMER)=ConfigFloat("Slash Windup Time");
m.PerformAnimation("SLASH",m.GetFacingDirectionToTarget(game->GetPlayer()->GetPos()));
}
}break;
case WINDUP:{
m.F(A::CASTING_TIMER)-=fElapsedTime;
if(m.F(A::CASTING_TIMER)<=0){
SETPHASE(RECOVERY);
vf2d slashTarget=game->GetPlayer()->GetPos();
m.PerformAnimation("SLASHING",m.GetFacingDirectionToTarget(game->GetPlayer()->GetPos()));
CreateBullet(DaggerSlash)(m,ConfigString("Dagger Slash Image"),ConfigFloat("Dagger Hit Radius"),m.GetAttack(),ConfigFloat("Dagger Slash Knockback"),m.OnUpperLevel(),m.GetFacingDirectionToTarget(slashTarget),ConfigFloat("Dagger Frame Duration"),ConfigFloat("Dagger Slash Distance"))EndBullet;
m.F(A::CASTING_TIMER)=m.GetCurrentAnimation().GetTotalAnimationDuration();
m.F(A::RECOVERY_TIME)=ConfigFloat("Attack Recovery Time");
}
}break;
case RECOVERY:{
m.F(A::CASTING_TIMER)-=fElapsedTime;
m.F(A::RECOVERY_TIME)-=fElapsedTime;
if(m.F(A::CASTING_TIMER)<=0){m.PerformIdleAnimation(m.GetFacingDirectionToTarget(game->GetPlayer()->GetPos()));}
if(m.F(A::RECOVERY_TIME)<=0)SETPHASE(MOVE);
}break;
case LEAP:{
m.F(A::JUMP_MOVE_TO_TARGET_TIMER)+=fElapsedTime;
const float halfJumpTime{ConfigFloat("Jump Attack Duration")/2.f};
if(m.F(A::JUMP_MOVE_TO_TARGET_TIMER)<halfJumpTime)m.SetZ(util::smoothstep(0,ConfigFloat("Jump Attack Height"),m.F(A::JUMP_MOVE_TO_TARGET_TIMER)/halfJumpTime));
else m.SetZ(util::smoothstep(ConfigFloat("Jump Attack Height"),0,(m.F(A::JUMP_MOVE_TO_TARGET_TIMER)-halfJumpTime)/halfJumpTime));
m.SetPos(m.V(A::PREV_POS).lerp(m.V(A::JUMP_TARGET_POS),m.F(A::JUMP_MOVE_TO_TARGET_TIMER)/ConfigFloat("Jump Attack Duration")));
if(m.F(A::JUMP_MOVE_TO_TARGET_TIMER)>=ConfigFloat("Jump Attack Duration")){
SoundEffect::PlaySFX("Leap Land",m.GetPos());
game->Hurt(m.GetPos(),ConfigFloat("Jump Attack Impact Area")/100.f*24.f,m.GetAttack(),m.OnUpperLevel(),m.GetZ(),HurtType::PLAYER);
game->ProximityKnockback(m.GetPos(),ConfigFloat("Jump Attack Impact Area")/100.f*24.f,ConfigFloat("Jump Attack Knockback Amount"),HurtType::MONSTER|HurtType::PLAYER);
m.SetZ(0.f);
m.SetPos(m.V(A::JUMP_TARGET_POS));
SETPHASE(MOVE);
m.PerformIdleAnimation();
m.SetStrategyDrawFunction([](AiL*game,Monster&monster,const std::string&strategy){});
}
}break;
case PREPARE_WHIRLWIND:{
m.F(A::CASTING_TIMER)-=fElapsedTime;
if(m.F(A::CASTING_TIMER)<=0.f){
SETPHASE(WHIRLWIND);
m.F(A::CHASE_TIMER)=0.f;
m.PerformAnimation("SPINNING");
}
}break;
case WHIRLWIND:{
m.F(A::CHASE_TIMER)+=fElapsedTime;
if(m.F(A::CHASE_TIMER)>=ConfigFloat("Whirlwind Spin Time")){
SETPHASE(MOVE);
m.PerformIdleAnimation();
}
m.MoveForward(m.V(A::PATH_DIR),fElapsedTime);
const HurtList hurtTargets{game->Hurt(m.GetPos(),ConfigFloat("Whirlwind Radius")/100.f*24.f,m.GetAttack(),m.OnUpperLevel(),m.GetZ(),HurtType::PLAYER)};
for(auto&[target,isHurt]:hurtTargets){
if(std::holds_alternative<Player*>(target)){
(std::get<Player*>(target))->ApplyIframes(0.5f);
}else ERR("WARNING! Somehow ended up with a non-player entity for whirlwind damage check! THIS SHOULD NOT BE HAPPENING!")
}
game->ProximityKnockback(m.GetPos(),ConfigFloat("Whirlwind Radius")/100.f*24.f,ConfigFloat("Whirlwind Knockback Amount"),HurtType::MONSTER|HurtType::PLAYER);
}break;
}
}

@ -199,7 +199,8 @@ bool Player::_SetX(float x,MoveFlag::MoveFlag flags,const bool playerInvoked){
return true;
}else
if(playerInvoked){ //If player invoked, we'll try the smart move system.
vf2d pushDir=geom2d::line<float>(collision.middle(),pos).vector().norm();
vf2d pushDir=vf2d{1.f,util::random(2*PI)}.cart();
if(collision.middle()!=pos)pushDir=geom2d::line<float>(collision.middle(),pos).vector().norm();
newPos={newPos.x,pos.y+pushDir.y*12};
if(NoPlayerCollisionWithTile()){
return _SetY(pos.y+pushDir.y*game->GetElapsedTime()*12,flags,false);
@ -232,7 +233,8 @@ bool Player::_SetY(float y,MoveFlag::MoveFlag flags,const bool playerInvoked){
return true;
}else
if(playerInvoked){ //If player invoked, we'll try the smart move system.{
vf2d pushDir=geom2d::line<float>(collision.middle(),pos).vector().norm();
vf2d pushDir=vf2d{1.f,util::random(2*PI)}.cart();
if(collision.middle()!=pos)geom2d::line<float>(collision.middle(),pos).vector().norm();
newPos={pos.x+pushDir.x*12,newPos.y};
if(NoPlayerCollisionWithTile()){
return _SetX(pos.x+pushDir.x*game->GetElapsedTime()*12,flags,false);
@ -633,7 +635,7 @@ void Player::Update(float fElapsedTime){
for(std::shared_ptr<Monster>&m:MONSTER_LIST){
const float playerRadius{12*GetSizeMult()/2};
const float monsterRadius{m->GetCollisionRadius()};
if(!HasIframes()&&abs(m->GetZ()-GetZ())<=1&&OnUpperLevel()==m->OnUpperLevel()&&geom2d::overlaps(geom2d::circle(pos,playerRadius),geom2d::circle(m->GetPos(),monsterRadius))){
if(!HasIframes()&&abs(m->GetZ()-GetZ())<=1&&OnUpperLevel()==m->OnUpperLevel()&&monsterRadius>0.f&&geom2d::overlaps(geom2d::circle(pos,playerRadius),geom2d::circle(m->GetPos(),monsterRadius))){
if(m->IsAlive()){
m->Collision(this);
}
@ -672,9 +674,11 @@ void Player::Update(float fElapsedTime){
game->GetPlayer()->SetPos(game->GetPlayer()->GetPos()+finalWindSpd*game->GetElapsedTime(),MoveFlag::PREVENT_CAST_CANCELLING);
if(vel!=vf2d{0,0}){
float newX=pos.x+vel.x*fElapsedTime;
float newY=pos.y+vel.y*fElapsedTime;
const vf2d totalVel{vel+addedVel};
if(totalVel!=vf2d{}){
float newX=pos.x+totalVel.x*fElapsedTime;
float newY=pos.y+totalVel.y*fElapsedTime;
SetX(newX);
SetY(newY);
if(vel.x>0){
@ -689,6 +693,8 @@ void Player::Update(float fElapsedTime){
}
}
addedVel={};
if(Menu::stack.empty()){
if(CanAct()&&attack_cooldown_timer==0&&AiL::KEY_ATTACK.Held()){
@ -1288,7 +1294,6 @@ void Player::SetAnimationBasedOnTargetingDirection(const std::string_view animat
std::string newAnimState{std::format("{}_{}_{}",Capitalize(GetClassName()),animation_name,facingChar)};
if(animation.HasState(newAnimState))UpdateAnimation(newAnimState);
else ERR(std::format("WARNING! Animation {} does not exist on the player! THIS SHOULD NOT BE HAPPENING!",newAnimState));
}
void Player::ApplyIframes(float duration){
@ -1539,12 +1544,12 @@ void Player::RecalculateEquipStats(){
if(GetClass()&WIZARD){
if(HasEnchant("Blink Portal"))GetRightClickAbility().COOLDOWN_TIME="Blink Portal"_ENC["COOLDOWN"];
if(HasEnchant("Summon Comet")){
float castTimeDiff{GetOriginalAbility3().precastInfo.castTime-"Summon Comet"_ENC["METEOR CAST TIME"]};
float castTimeDiff{_GetOriginalAbility3().precastInfo.castTime-"Summon Comet"_ENC["METEOR CAST TIME"]};
GetAbility3().precastInfo.castTime-=castTimeDiff;
GetAbility3().COOLDOWN_TIME+="Summon Comet"_ENC["COOLDOWN REDUCTION"]; //This is not a typo, we add because the cooldown reduction in the config is NEGATIVE!
}
if(HasEnchant("Solar Flare")){
float castTimeDiff{GetOriginalAbility3().precastInfo.castTime-"Solar Flare"_ENC["METEOR CAST TIME"]};
float castTimeDiff{_GetOriginalAbility3().precastInfo.castTime-"Solar Flare"_ENC["METEOR CAST TIME"]};
GetAbility3().precastInfo.castTime-=castTimeDiff;
GetAbility3().COOLDOWN_TIME+="Solar Flare"_ENC["COOLDOWN INCREASE"];
}
@ -1914,7 +1919,7 @@ void Player::ProximityKnockback(const vf2d centerPoint,const float knockbackFact
}
void Player::AddVelocity(vf2d vel){
this->vel+=vel*game->GetElapsedTime();
this->addedVel+=vel;
}
const float Player::GetHealthRatio()const{
@ -2289,5 +2294,25 @@ void Player::SetTestScreenAimingLocation(vf2d forcedAimingLoc){
testAimingLoc=forcedAimingLoc;
}
void Player::SetAbility4(const Ability&ability){
switch(GetClass()){
case WARRIOR:{Warrior::ability4=ability;}break;
case RANGER:{Ranger::ability4=ability;}break;
case WIZARD:{Wizard::ability4=ability;}break;
case THIEF:{Thief::ability4=ability;}break;
case TRAPPER:{Trapper::ability4=ability;}break;
case WITCH:{Witch::ability4=ability;}break;
default:{ERR(std::format("WARNING! Class {} is invalid! THIS SHOULD NOT BE HAPPENING!",int(GetClass())));}
}
}
void Player::RemoveMoney(const uint32_t moneyCost){
if(moneyCost>GetMoney())ERR(std::format("WARNING! Trying to spend more money than we actually have! THIS SHOULD NOT BE HAPPENING! Current Money: {} Trying to spend: {}",GetMoney(),moneyCost));
SetMoney(GetMoney()-moneyCost);
}
void Player::AddMoney(const uint32_t moneyCost){
SetMoney(GetMoney()+moneyCost);
}
const bool Player::HasBuff(BuffType buff)const{
return std::find_if(buffList.begin(),buffList.end(),[type=buff](const Buff&buff){return buff.type==type;})!=buffList.end();
}

@ -184,7 +184,7 @@ public:
void AddBuff(BuffRestorationType type,BuffOverTimeType::BuffOverTimeType overTimeType,float duration,float intensity,float timeBetweenTicks,Buff::PlayerBuffExpireCallbackFunction expireCallbackFunc);
void AddBuff(BuffType type,BuffRestorationType restorationType,BuffOverTimeType::BuffOverTimeType overTimeType,float duration,float intensity,float timeBetweenTicks);
void AddBuff(BuffType type,BuffRestorationType restorationType,BuffOverTimeType::BuffOverTimeType overTimeType,float duration,float intensity,float timeBetweenTicks,Buff::PlayerBuffExpireCallbackFunction expireCallbackFunc);
const bool HasBuff(BuffType buff)const;
const std::vector<Buff>GetBuffs(BuffType buff)const;
const std::vector<Buff>GetStatBuffs(const std::vector<std::string>&attr)const;
@ -231,10 +231,6 @@ public:
virtual Ability&GetAbility3()=0;
virtual Ability&GetAbility4()=0;
virtual void SetAbility4(const Ability&originalAbility)=0; //NOTE: Make sure to provide the original ability and not a current ability!
virtual Ability&GetOriginalAbility1()=0;
virtual Ability&GetOriginalAbility2()=0;
virtual Ability&GetOriginalAbility3()=0;
virtual Ability&GetOriginalRightClickAbility()=0;
virtual std::string&GetWalkNAnimation()=0;
virtual std::string&GetWalkEAnimation()=0;
virtual std::string&GetWalkSAnimation()=0;
@ -268,6 +264,8 @@ public:
static void AddMoneyListener(std::weak_ptr<MenuComponent>component);
uint32_t GetMoney()const;
void SetMoney(uint32_t newMoney);
void AddMoney(const uint32_t moneyCost);
void RemoveMoney(const uint32_t moneyCost);
void AddXP(const uint64_t xpGain);
void OnLevelUp();
const uint8_t LevelCap()const;
@ -296,7 +294,7 @@ public:
const float GetAtkGrowthRate()const;
const float GetIframeTime()const;
const Renderable&GetMinimapImage()const;
void AddVelocity(vf2d vel);
void AddVelocity(vf2d vel); //Use to add more applied velocity to the player on top of what is already happening. Resets every frame. Good for pulling in/pushing out forces.
const float GetHealthRatio()const;
const bool IsAlive()const;
//An all-in-one function that applies a stat plus any % modifiers the stat may have as well.
@ -433,6 +431,7 @@ private:
std::vector<std::pair<PlayerTimerType,ShieldAmount>>shield;
bool catForm{false};
std::optional<vf2d>testAimingLoc{};
vf2d addedVel{};
protected:
const float ATTACK_COOLDOWN="Warrior.Auto Attack.Cooldown"_F;
const float MAGIC_ATTACK_COOLDOWN="Wizard.Auto Attack.Cooldown"_F;
@ -485,6 +484,10 @@ protected:
vf2d leapStartingPos{};
float poisonArrowReadyTimer{};
void ActivateCatForm();
virtual Ability&_GetOriginalAbility1()=0;
virtual Ability&_GetOriginalAbility2()=0;
virtual Ability&_GetOriginalAbility3()=0;
virtual Ability&_GetOriginalRightClickAbility()=0;
};
#pragma region Warrior
@ -507,10 +510,14 @@ struct Warrior:Player{
Ability&GetAbility2()override;
Ability&GetAbility3()override;
Ability&GetAbility4()override;
Ability&GetOriginalAbility1()override;
Ability&GetOriginalAbility2()override;
Ability&GetOriginalAbility3()override;
Ability&GetOriginalRightClickAbility()override;
virtual Ability&_GetOriginalAbility1()override;
virtual Ability&_GetOriginalAbility2()override;
virtual Ability&_GetOriginalAbility3()override;
virtual Ability&_GetOriginalRightClickAbility()override;
static Ability&GetOriginalAbility1();
static Ability&GetOriginalAbility2();
static Ability&GetOriginalAbility3();
static Ability&GetOriginalRightClickAbility();
void SetAbility4(const Ability&originalAbility)override; //NOTE: Make sure to provide the original ability and not a current ability!
std::string&GetWalkNAnimation()override;
std::string&GetWalkEAnimation()override;
@ -547,10 +554,14 @@ struct Thief:Player{
Ability&GetAbility2()override;
Ability&GetAbility3()override;
Ability&GetAbility4()override;
Ability&GetOriginalAbility1()override;
Ability&GetOriginalAbility2()override;
Ability&GetOriginalAbility3()override;
Ability&GetOriginalRightClickAbility()override;
virtual Ability&_GetOriginalAbility1()override;
virtual Ability&_GetOriginalAbility2()override;
virtual Ability&_GetOriginalAbility3()override;
virtual Ability&_GetOriginalRightClickAbility()override;
static Ability&GetOriginalAbility1();
static Ability&GetOriginalAbility2();
static Ability&GetOriginalAbility3();
static Ability&GetOriginalRightClickAbility();
void SetAbility4(const Ability&originalAbility)override; //NOTE: Make sure to provide the original ability and not a current ability!
std::string&GetWalkNAnimation()override;
std::string&GetWalkEAnimation()override;
@ -587,10 +598,14 @@ struct Ranger:Player{
Ability&GetAbility2()override;
Ability&GetAbility3()override;
Ability&GetAbility4()override;
Ability&GetOriginalAbility1()override;
Ability&GetOriginalAbility2()override;
Ability&GetOriginalAbility3()override;
Ability&GetOriginalRightClickAbility()override;
virtual Ability&_GetOriginalAbility1()override;
virtual Ability&_GetOriginalAbility2()override;
virtual Ability&_GetOriginalAbility3()override;
virtual Ability&_GetOriginalRightClickAbility()override;
static Ability&GetOriginalAbility1();
static Ability&GetOriginalAbility2();
static Ability&GetOriginalAbility3();
static Ability&GetOriginalRightClickAbility();
void SetAbility4(const Ability&originalAbility)override; //NOTE: Make sure to provide the original ability and not a current ability!
std::string&GetWalkNAnimation()override;
std::string&GetWalkEAnimation()override;
@ -627,10 +642,14 @@ struct Trapper:Player{
Ability&GetAbility2()override;
Ability&GetAbility3()override;
Ability&GetAbility4()override;
Ability&GetOriginalAbility1()override;
Ability&GetOriginalAbility2()override;
Ability&GetOriginalAbility3()override;
Ability&GetOriginalRightClickAbility()override;
virtual Ability&_GetOriginalAbility1()override;
virtual Ability&_GetOriginalAbility2()override;
virtual Ability&_GetOriginalAbility3()override;
virtual Ability&_GetOriginalRightClickAbility()override;
static Ability&GetOriginalAbility1();
static Ability&GetOriginalAbility2();
static Ability&GetOriginalAbility3();
static Ability&GetOriginalRightClickAbility();
void SetAbility4(const Ability&originalAbility)override; //NOTE: Make sure to provide the original ability and not a current ability!
std::string&GetWalkNAnimation()override;
std::string&GetWalkEAnimation()override;
@ -663,14 +682,18 @@ struct Wizard:Player{
static void InitializeClassAbilities();
const std::string&GetClassName()override;
Ability&GetRightClickAbility()override;
Ability&GetAbility1()override;
Ability&GetAbility2()override;
Ability&GetAbility3()override;
Ability&GetAbility4()override;
Ability&GetOriginalAbility1()override;
Ability&GetOriginalAbility2()override;
Ability&GetOriginalAbility3()override;
Ability&GetOriginalRightClickAbility()override;
Ability&GetAbility1();
Ability&GetAbility2();
Ability&GetAbility3();
Ability&GetAbility4();
virtual Ability&_GetOriginalAbility1()override;
virtual Ability&_GetOriginalAbility2()override;
virtual Ability&_GetOriginalAbility3()override;
virtual Ability&_GetOriginalRightClickAbility()override;
static Ability&GetOriginalAbility1();
static Ability&GetOriginalAbility2();
static Ability&GetOriginalAbility3();
static Ability&GetOriginalRightClickAbility();
void SetAbility4(const Ability&originalAbility)override; //NOTE: Make sure to provide the original ability and not a current ability!
std::string&GetWalkNAnimation()override;
std::string&GetWalkEAnimation()override;
@ -707,10 +730,14 @@ struct Witch:Player{
Ability&GetAbility2()override;
Ability&GetAbility3()override;
Ability&GetAbility4()override;
Ability&GetOriginalAbility1()override;
Ability&GetOriginalAbility2()override;
Ability&GetOriginalAbility3()override;
Ability&GetOriginalRightClickAbility()override;
virtual Ability&_GetOriginalAbility1()override;
virtual Ability&_GetOriginalAbility2()override;
virtual Ability&_GetOriginalAbility3()override;
virtual Ability&_GetOriginalRightClickAbility()override;
static Ability&GetOriginalAbility1();
static Ability&GetOriginalAbility2();
static Ability&GetOriginalAbility3();
static Ability&GetOriginalRightClickAbility();
void SetAbility4(const Ability&originalAbility)override; //NOTE: Make sure to provide the original ability and not a current ability!
std::string&GetWalkNAnimation()override;
std::string&GetWalkEAnimation()override;

@ -45,4 +45,5 @@ enum class PlayerTimerType{
ADVANCE_SHIELD_TIMER,
CAT_FORM_ALREADY_ACTIVE,
BLINK_PORTAL_SECOND_CAST,
FANFARE_WAIT_TIMER,
};

@ -37,25 +37,16 @@ All rights reserved.
#pragma endregion
#include "BulletTypes.h"
#include "AdventuresInLestoria.h"
#include <ranges>
#include "util.h"
#include "SoundEffect.h"
INCLUDE_game
PoisonBottle::PoisonBottle(vf2d pos,vf2d targetPos,float explodeRadius,float bounceExplodeRadius,float z,float totalFallTime,float totalRiseZAmt,int damage,int additionalBounceCount,bool upperLevel,bool hitsMultiple,float lifetime,bool friendly,Pixel col,vf2d scale,float image_angle)
:Bullet(pos,util::pointTo(pos,targetPos)*util::distance(pos,targetPos)/totalFallTime,0.f,damage,"poison_bottle.png",upperLevel,hitsMultiple,lifetime,false,friendly,col,scale,image_angle),initialZ(z),explodeRadius(explodeRadius),bounceExplodeRadius(bounceExplodeRadius),totalRiseZAmt(totalRiseZAmt),totalFallTime(totalFallTime),startingPos(pos),targetPos(targetPos),originalRisingTime(totalFallTime*0.25f/(additionalBounceCount+1)),risingTime(originalRisingTime),originalFallingTime(totalFallTime*0.75f/(additionalBounceCount+1)),fallingTime(originalFallingTime),additionalBounceCount(additionalBounceCount){
PoisonBottle::PoisonBottle(vf2d pos,vf2d targetPos,const std::string&img,float explodeRadius,float bounceExplodeRadius,float z,float totalFallTime,float totalRiseZAmt,int damage,int additionalBounceCount,bool upperLevel,bool hitsMultiple,float lifetime,bool friendly,Pixel col,vf2d scale,float image_angle)
:ThrownProjectile(pos,targetPos,img,explodeRadius,z,totalFallTime,totalRiseZAmt,damage,upperLevel,hitsMultiple,lifetime,friendly,col,scale,image_angle),bounceExplodeRadius(bounceExplodeRadius),additionalBounceCount(additionalBounceCount){
this->z=z;
this->risingTime=this->originalRisingTime=totalFallTime*0.25f/(additionalBounceCount+1);
this->fallingTime=this->originalFallingTime=totalFallTime*0.75f/(additionalBounceCount+1);
}
void PoisonBottle::Update(float fElapsedTime){
if(IsDeactivated())return;
image_angle+=0.5*PI*fElapsedTime;
const bool Landed{fallingTime<=0.f};
if(Landed){
z=0.f;
void PoisonBottle::OnGroundLand(){
SoundEffect::PlaySFX("Glass Break",pos);
SoundEffect::PlaySFX("Poison Pool",pos);
float poisonCircleScale{"Witch.Ability 2.Casting Size"_F/100.f};
@ -72,7 +63,7 @@ void PoisonBottle::Update(float fElapsedTime){
game->AddEffect(std::make_unique<Effect>(pos+vf2d{util::random(16)*poisonCircleScale,util::random(2*PI)}.cart(),util::random_range(1.f,4.f),"circle.png",game->GetPlayer()->OnUpperLevel(),vf2d{size,size},util::random_range(0.2f,0.5f),vf2d{util::random_range(-6.f,6.f),util::random_range(-12.f,-4.f)},col));
}
if(additionalBounceCount==0&&game->GetPlayer()->HasEnchant("Pooling Poison")){
game->AddEffect(std::make_unique<PoisonPool>(pos,"poison_pool.png",bounceExplodeRadius,game->GetPlayer()->GetAttack()*"Pooling Poison"_ENC["POISON POOL DAMAGE PCT"]/100.f,"Pooling Poison"_ENC["POISON POOL DAMAGE FREQUENCY"],friendly?HurtType::MONSTER:HurtType::PLAYER,"Pooling Poison"_ENC["SPLASH LINGER TIME"],0.5f,OnUpperLevel(),poisonCircleScale,vf2d{},WHITE,0.f,0.f,false,0.05f,[pos=pos,col=col,poisonCircleScale](const Effect&self){
game->AddEffect(std::make_unique<LingeringEffect>(pos,"poison_pool.png","Poison Pool",bounceExplodeRadius,game->GetPlayer()->GetAttack()*"Pooling Poison"_ENC["POISON POOL DAMAGE PCT"]/100.f,"Pooling Poison"_ENC["POISON POOL DAMAGE FREQUENCY"],friendly?HurtType::MONSTER:HurtType::PLAYER,"Pooling Poison"_ENC["SPLASH LINGER TIME"],0.5f,OnUpperLevel(),poisonCircleScale,vf2d{},WHITE,0.f,0.f,false,0.05f,[pos=pos,col=col,poisonCircleScale](const Effect&self){
float size{util::random_range(0.4f,0.8f)};
return Effect{pos+vf2d{util::random(16)*poisonCircleScale,util::random(2*PI)}.cart(),util::random_range(1.f,4.f),"circle.png",game->GetPlayer()->OnUpperLevel(),vf2d{size,size},util::random_range(0.2f,0.5f),vf2d{util::random_range(-6.f,6.f),util::random_range(-12.f,-4.f)},col};
}),true);
@ -103,15 +94,6 @@ void PoisonBottle::Update(float fElapsedTime){
fadeOutTime=0.25f;
Deactivate();
}
}else{
if(risingTime>0.f){
risingTime-=fElapsedTime;
z=util::lerp(initialZ,initialZ+totalRiseZAmt,1-(risingTime/originalRisingTime));
}else{
fallingTime-=fElapsedTime;
z=util::lerp(initialZ+totalRiseZAmt,0.f,1-(fallingTime/originalFallingTime));
}
}
}
void PoisonBottle::ModifyOutgoingDamageData(HurtDamageInfo&data){

@ -42,10 +42,10 @@ All rights reserved.
INCLUDE_game
PoisonPool::PoisonPool(vf2d pos,const std::string&img,float radius,int damage,float damageFreq,const HurtType friendly,float lifetime,float cycleSpd,bool onUpperLevel,float size,vf2d spd,Pixel col,float rotation,float rotationSpd,bool additiveBlending,float particleSpawnFreq,const std::function<Effect(const Effect&self)>&particleGenerator)
:damage(damage),damageTimer(damageFreq),originalDamageTimer(damageTimer),radius(radius),friendly(friendly),poisonPoolSFXID(SoundEffect::PlayLoopingSFX("Poison Pool",pos)),FadeInOutEffect(pos,img,lifetime,cycleSpd,onUpperLevel,size,spd,col,rotation,rotationSpd,additiveBlending,particleSpawnFreq,particleGenerator){}
LingeringEffect::LingeringEffect(vf2d pos,const std::string&img,const std::string&soundEffect,float radius,int damage,float damageFreq,const HurtType friendly,float lifetime,float cycleSpd,bool onUpperLevel,float size,vf2d spd,Pixel col,float rotation,float rotationSpd,bool additiveBlending,float particleSpawnFreq,const std::function<Effect(const Effect&self)>&particleGenerator)
:damage(damage),damageTimer(damageFreq),originalDamageTimer(damageTimer),radius(radius),friendly(friendly),sfxID(SoundEffect::PlayLoopingSFX(soundEffect,pos)),FadeInOutEffect(pos,img,lifetime,cycleSpd,onUpperLevel,size,spd,col,rotation,rotationSpd,additiveBlending,particleSpawnFreq,particleGenerator){}
bool PoisonPool::Update(float fElapsedTime){
bool LingeringEffect::Update(float fElapsedTime){
damageTimer-=fElapsedTime;
if(damageTimer<=0.f){
game->Hurt(pos,radius,damage,OnUpperLevel(),GetZ(),friendly,HurtFlag::DOT);
@ -53,7 +53,7 @@ bool PoisonPool::Update(float fElapsedTime){
}
if(FadeInOutEffect::Update(fElapsedTime))return true;
else{
SoundEffect::StopLoopingSFX(poisonPoolSFXID);
SoundEffect::StopLoopingSFX(sfxID);
return false;
}
}

@ -67,6 +67,16 @@ void Monster::InitializeStrategies(){
STRATEGY_DATA.insert("Do Nothing",Monster::STRATEGY::DONOTHING);
STRATEGY_DATA.insert("Stone Golem",Monster::STRATEGY::STONE_GOLEM);
STRATEGY_DATA.insert("Breaking Pillar",Monster::STRATEGY::BREAKING_PILLAR);
STRATEGY_DATA.insert("Pirate Marauder",Monster::STRATEGY::PIRATE_MARAUDER);
STRATEGY_DATA.insert("Pirate Captain",Monster::STRATEGY::PIRATE_CAPTAIN);
STRATEGY_DATA.insert("Pirate Buccaneer",Monster::STRATEGY::PIRATE_BUCCANEER);
STRATEGY_DATA.insert("Crab",Monster::STRATEGY::CRAB);
STRATEGY_DATA.insert("Seagull",Monster::STRATEGY::SEAGULL);
STRATEGY_DATA.insert("Sandworm",Monster::STRATEGY::SANDWORM);
STRATEGY_DATA.insert("Parrot",Monster::STRATEGY::PARROT);
STRATEGY_DATA.insert("Giant Crab",Monster::STRATEGY::GIANT_CRAB);
STRATEGY_DATA.insert("Giant Octopus",Monster::STRATEGY::GIANT_OCTOPUS);
STRATEGY_DATA.insert("Octopus Arm",Monster::STRATEGY::OCTOPUS_ARM);
STRATEGY_DATA.SetInitialized();
}

@ -95,7 +95,7 @@ public:
tint=BLACK;
}
window.DrawDecal(rect.pos+vf2d{2,2},const_cast<Decal*>(itemRef.lock()->Decal()),{scaleFactor,scaleFactor},tint);
window.DrawDecal(rect.pos+vf2d{2,2},const_cast<Decal*>(itemRef.lock()->Icon().Decal()),{scaleFactor,scaleFactor},tint);
window.DrawRectDecal(rect.pos+vf2d{2,2},iconSize,borderCol);
if(itemIsSelected){

@ -147,9 +147,9 @@ void Monster::STRATEGY::RUN_TOWARDS(Monster&m,float fElapsedTime,std::string str
}
}
if(m.F(A::JUMP_LANDING_TIMER)>=m.F(A::JUMP_ORIGINAL_LANDING_TIMER)/2){
m.SetZ(util::lerp(0,float(ConfigInt("JumpHeight")),1-jumpLandingTimerRatio));
m.SetZ(util::lerp(0.f,float(ConfigInt("JumpHeight")),1-jumpLandingTimerRatio));
}else{
m.SetZ(util::lerp(0,float(ConfigInt("JumpHeight")),jumpLandingTimerRatio*2));
m.SetZ(util::lerp(0.f,float(ConfigInt("JumpHeight")),jumpLandingTimerRatio*2));
}
if(m.F(A::JUMP_LANDING_TIMER)==0){
m.state=State::RECOVERY;

@ -0,0 +1,139 @@
#pragma region License
/*
License (OLC-3)
~~~~~~~~~~~~~~~
Copyright 2024 Joshua Sigona <sigonasr2@gmail.com>
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions or derivations of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions or derivative works in binary form must reproduce the above
copyright notice. This list of conditions and the following disclaimer must be
reproduced in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may
be used to endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
Portions of this software are copyright © 2024 The FreeType
Project (www.freetype.org). Please see LICENSE_FT.txt for more information.
All rights reserved.
*/
#pragma endregion
#include "MonsterStrategyHelpers.h"
#include "util.h"
#include "AdventuresInLestoria.h"
#include "SoundEffect.h"
#include "BulletTypes.h"
using A=Attribute;
INCLUDE_game
void Monster::STRATEGY::SANDWORM(Monster&m,float fElapsedTime,std::string strategy){
enum PhaseName{
INITIALIZE,
UNDERGROUND,
SURFACING,
ATTACK,
SHOOTING,
BURROWING,
};
m.F(A::ATTACK_COOLDOWN)-=fElapsedTime;
m.F(A::SUCTION_TIMER)-=fElapsedTime;
const float distToPlayer{util::distance(game->GetPlayer()->GetPos(),m.GetPos())};
if(m.F(A::SUCTION_TIMER)>0.f&&distToPlayer<=ConfigPixels("Suction Radius")){
game->GetPlayer()->AddVelocity(util::pointTo(game->GetPlayer()->GetPos(),m.GetPos())*100.f*ConfigFloat("Suction Pull Amount")/100.f);
}
const auto AcquireNewUndergroundTarget=[&](){
const float randomRange=util::random_range(0,ConfigPixels("Burrow Target Range"));
m.target=game->GetPlayer()->GetPos()+vf2d{randomRange,util::random(2*PI)}.cart();
};
switch(PHASE()){
case INITIALIZE:{
SETPHASE(UNDERGROUND);
AcquireNewUndergroundTarget();
}break;
case UNDERGROUND:{
m.SetCollisionRadius(0.f);
RUN_TOWARDS(m,fElapsedTime,"Run Towards");
m.PerformAnimation("SWIM",m.GetFacingDirection());
if(m.ReachedTargetPos()){
m.PerformAnimation("EMERGE",game->GetPlayer()->GetPos());
m.F(A::RECOVERY_TIME)=m.GetCurrentAnimation().GetTotalAnimationDuration();
game->AddEffect(std::make_unique<Effect>(m.GetPos(),ConfigInt("Suction Duration"),"sand_suction.png",m.OnUpperLevel(),0.25f,0.25f,ConfigFloat("Suction Animation Size")/300.f*vf2d{1.f,1.f},vf2d{},WHITE,util::random(),-PI/8),true);
SETPHASE(SURFACING);
m.F(A::SUCTION_TIMER)=ConfigFloat("Suction Duration")+0.25f;
}
}break;
case SURFACING:{
m.F(A::RECOVERY_TIME)-=fElapsedTime;
if(m.F(A::RECOVERY_TIME)<=0.f){
SETPHASE(ATTACK);
m.PerformAnimation("IDLE",game->GetPlayer()->GetPos());
m.SetCollisionRadius(m.GetOriginalCollisionRadius());
m.I(A::ATTACK_COUNT)=ConfigInt("Max Attack Count");
}
}break;
case ATTACK:{
if(m.I(A::ATTACK_COUNT)<=0||distToPlayer>=ConfigPixels("Max Attack Range")){
m.SetCollisionRadius(0.f);
m.PerformAnimation("BURROW");
m.F(A::CASTING_TIMER)=m.GetCurrentAnimation().GetTotalAnimationDuration();
SETPHASE(BURROWING);
break;
}
if(m.F(A::ATTACK_COOLDOWN)<=0.f){
m.PerformAnimation("SAND ATTACK",game->GetPlayer()->GetPos());
m.F(A::CASTING_TIMER)=ConfigFloat("Shoot Delay");
m.F(A::SHOOT_ANIMATION_TIME)=m.GetCurrentAnimation().GetTotalAnimationDuration();
m.B(A::BULLET_HAS_BEEN_SHOT)=false;
SETPHASE(SHOOTING);
}
}break;
case SHOOTING:{
const auto ShootBullet=[&](){
m.B(A::BULLET_HAS_BEEN_SHOT)=true;
m.I(A::ATTACK_COUNT)--;
CreateBullet(Bullet)(m.GetPos(),vf2d{ConfigPixels("Bullet Speed"),util::angleTo(m.GetPos(),game->GetPlayer()->GetPos())}.cart(),ConfigFloat("Bullet Radius"),m.GetAttack(),m.OnUpperLevel(),false,ConfigPixel("Bullet Color"),ConfigFloat("Bullet Radius")*vf2d{1.f,1.f}/2.f)EndBullet;
};
m.F(A::CASTING_TIMER)-=fElapsedTime;
m.F(A::SHOOT_ANIMATION_TIME)-=fElapsedTime;
if(m.F(A::CASTING_TIMER)<=0.f&&!m.B(A::BULLET_HAS_BEEN_SHOT)){
ShootBullet();
}
if(m.F(A::SHOOT_ANIMATION_TIME)<=0.f){
if(!m.B(A::BULLET_HAS_BEEN_SHOT))ShootBullet();
m.PerformAnimation("IDLE",game->GetPlayer()->GetPos());
SETPHASE(ATTACK);
}
}break;
case BURROWING:{
m.F(A::CASTING_TIMER)-=fElapsedTime;
if(m.F(A::CASTING_TIMER)<=0.f){
AcquireNewUndergroundTarget();
SETPHASE(UNDERGROUND);
}
}break;
}
}

@ -68,6 +68,11 @@ protected:
return geom2d::overlaps(geom2d::rect<float>{{},rect.size},geom2d::rect<float>{component.lock()->rect.pos+vf2d{2,2},component.lock()->rect.size-vf2d{2,2}});
}
public:
enum ScrollResult{
NOT_SCROLLED,
SCROLLED,
};
inline ScrollableWindowComponent(geom2d::rect<float>rect,ComponentAttr attributes=ComponentAttr::BACKGROUND|ComponentAttr::OUTLINE)
:MenuComponent(rect,"",[](MenuFuncData data){return true;},ButtonAttr::UNSELECTABLE|ButtonAttr::UNSELECTABLE_VIA_KEYBOARD){
background=attributes&ComponentAttr::BACKGROUND;
@ -139,10 +144,32 @@ public:
if(size_t(prevIndex)!=size_t(selectionIndex)){Menu::menus[parentMenu]->SetSelection(components[size_t(selectionIndex)],false);}
}
inline void IncreaseSelectionIndex(const float val){
inline const size_t GetSelectionIndex()const{
return selectionIndex;
}
inline std::weak_ptr<MenuComponent>GetSelectedComponent(){
return GetComponents().at(selectionIndex);
}
inline ScrollResult IncreaseSelectionIndex(const float val){
float prevIndex=selectionIndex;
selectionIndex=std::clamp(selectionIndex+val,0.f,float(components.size()-1));
if(size_t(prevIndex)!=size_t(selectionIndex)){Menu::menus[parentMenu]->SetSelection(components[size_t(selectionIndex)],false);}
if(size_t(prevIndex)!=size_t(selectionIndex)){
Menu::menus[parentMenu]->SetSelection(components[size_t(selectionIndex)],false);
return SCROLLED;
}
return NOT_SCROLLED;
}
inline ScrollResult DecreaseSelectionIndex(const float val){
float prevIndex=selectionIndex;
selectionIndex=std::clamp(selectionIndex-val,0.f,float(components.size()-1));
if(size_t(prevIndex)!=size_t(selectionIndex)){
Menu::menus[parentMenu]->SetSelection(components[size_t(selectionIndex)],false);
return SCROLLED;
}
return NOT_SCROLLED;
}
protected:
virtual inline vf2d GetScrollAmount()const{
@ -402,6 +429,7 @@ public:
button->renderInMain=false; //Now we are in control!
button->parentComponent=DYNAMIC_POINTER_CAST<ScrollableWindowComponent>(Menu::menus[parentMenu]->components[this->GetName()]);
button->disable=disable;
button->name=key;
CalculateBounds();

@ -0,0 +1,86 @@
#pragma region License
/*
License (OLC-3)
~~~~~~~~~~~~~~~
Copyright 2024 Joshua Sigona <sigonasr2@gmail.com>
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions or derivations of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions or derivative works in binary form must reproduce the above
copyright notice. This list of conditions and the following disclaimer must be
reproduced in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may
be used to endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
Portions of this software are copyright © 2024 The FreeType
Project (www.freetype.org). Please see LICENSE_FT.txt for more information.
All rights reserved.
*/
#pragma endregion
#include "MonsterStrategyHelpers.h"
#include "util.h"
#include "AdventuresInLestoria.h"
#include "SoundEffect.h"
#include "BulletTypes.h"
using A=Attribute;
INCLUDE_game
void Monster::STRATEGY::SEAGULL(Monster&m,float fElapsedTime,std::string strategy){
enum PhaseName{
CHILLING,
STARTLED,
FLY_AWAY,
DISABLED,
};
switch(PHASE()){
case CHILLING:{
m.PerformAnimation("IDLE",game->GetPlayer()->GetPos());
float distToPlayer{util::distance(game->GetPlayer()->GetPos(),m.GetPos())};
if(distToPlayer<=ConfigFloat("Startled Range")/100.f*24){
SETPHASE(STARTLED);
m.F(A::CASTING_TIMER)=ConfigFloat("Takeoff Time");
m.V(A::PREV_POS)=m.GetPos();
}
}break;
case STARTLED:{
m.PerformAnimation("FLY",m.GetPos()+util::pointTo(game->GetPlayer()->GetPos(),m.GetPos()));
m.F(A::CASTING_TIMER)-=fElapsedTime;
if(m.F(A::CASTING_TIMER)<=0.f)SETPHASE(FLY_AWAY);
}break;
case FLY_AWAY:{
vf2d moveVec{m.GetPos()-game->GetPlayer()->GetPos()};
m.PerformAnimation("FLY",m.GetPos()+util::pointTo(game->GetPlayer()->GetPos(),m.GetPos()));
m.MoveForward(moveVec,fElapsedTime);
float distTraveled{util::distance(m.V(A::PREV_POS),m.GetPos())};
if(distTraveled>=ConfigFloat("Despawn Range")/100.f*24){
m.lifetime=1.f;
SETPHASE(DISABLED);
}
}break;
case DISABLED:{
//NO-OP
}break;
}
}

@ -136,7 +136,7 @@ void Monster::STRATEGY::SLIMEKING(Monster&m,float fElapsedTime,std::string strat
};
const auto Recovered=[&](){
switch(m.phase){
switch(PHASE()){
case 2:{
switch(m.I(A::JUMP_COUNT)){
case 1:
@ -191,9 +191,9 @@ void Monster::STRATEGY::SLIMEKING(Monster&m,float fElapsedTime,std::string strat
}
}
if(m.F(A::JUMP_LANDING_TIMER)>=m.F(A::JUMP_ORIGINAL_LANDING_TIMER)/2){
m.SetZ(util::lerp(0,float(ConfigInt("JumpHeight")),1-jumpLandingTimerRatio));
m.SetZ(util::lerp(0.f,float(ConfigInt("JumpHeight")),1-jumpLandingTimerRatio));
}else{
m.SetZ(util::lerp(0,float(ConfigInt("JumpHeight")),jumpLandingTimerRatio*2));
m.SetZ(util::lerp(0.f,float(ConfigInt("JumpHeight")),jumpLandingTimerRatio*2));
}
if(m.F(A::JUMP_LANDING_TIMER)==0){
m.state=State::RECOVERY;
@ -211,7 +211,7 @@ void Monster::STRATEGY::SLIMEKING(Monster&m,float fElapsedTime,std::string strat
lineToPlayer={m.GetPos(),m.GetPos()+vf2d{cos(randomDir),sin(randomDir)}*1};
}
game->GetPlayer()->Knockback(lineToPlayer.vector().norm()*float(ConfigInt("JumpKnockbackFactor")));
if(m.phase!=2){
if(PHASE()!=2){
game->GetPlayer()->ApplyIframes(1.f);
}else{ //In phase 2 you can get hit by multiple knockbacks, so the iframe time is a lot shorter.
game->GetPlayer()->ApplyIframes(0.2f);
@ -219,7 +219,7 @@ void Monster::STRATEGY::SLIMEKING(Monster&m,float fElapsedTime,std::string strat
}
}
m.SetZ(0);
Landed(m.phase);
Landed(PHASE());
m.SetStrategyDrawFunction([](AiL*game,Monster&m,const std::string&strategy){});
} else
if(m.F(A::JUMP_LANDING_TIMER)<=ConfigFloat("JumpWarningIndicatorTime")){
@ -244,19 +244,19 @@ void Monster::STRATEGY::SLIMEKING(Monster&m,float fElapsedTime,std::string strat
return;
}
switch(m.phase){
switch(PHASE()){
case 0:{
m.size=ConfigInt("Phase1.Size")/100.f;
m.diesNormally=false;
m.F(A::IFRAME_TIME_UPON_HIT)=0;
m.ApplyIframes(ConfigFloat("Phase5.IframeTimePerHit"));
m.phase=ConfigInt("StartPhase");
SETPHASE(ConfigInt("StartPhase"));
}break;
case 1:{
if(m.GetHealthRatio()<=ConfigFloat("Phase2.Change")/100.f){
m.phase=2;
SETPHASE(2);
m.SetSize(ConfigFloat("Phase2.Size")/100,false);
TransitionPhase(m.phase);
TransitionPhase(PHASE());
return;
}
if(m.F(A::SHOOT_RING_TIMER)==0){
@ -287,12 +287,12 @@ void Monster::STRATEGY::SLIMEKING(Monster&m,float fElapsedTime,std::string strat
}break;
case 2:{
if(m.GetHealthRatio()<=ConfigFloat("Phase3.Change")/100.f){
m.phase=3;
SETPHASE(3);
m.SetSize(ConfigFloat("Phase3.Size")/100,false);
if(m.I(A::PATTERN_REPEAT_COUNT)==0){
m.I(A::PATTERN_REPEAT_COUNT)=1;
}
TransitionPhase(m.phase);
TransitionPhase(PHASE());
return;
}
if(m.F(A::SHOOT_TIMER)==0){
@ -315,10 +315,10 @@ void Monster::STRATEGY::SLIMEKING(Monster&m,float fElapsedTime,std::string strat
}break;
case 3:{
if(m.GetHealthRatio()<=ConfigFloat("Phase4.Change")/100.f){
m.phase=4;
SETPHASE(4);
m.SetSize(ConfigFloat("Phase4.Size")/100,false);
m.AddBuff(BuffType::SLOWDOWN,99999,ConfigFloat("Phase4.MoveSpdModifier")/100);
TransitionPhase(m.phase);
TransitionPhase(PHASE());
return;
}
if(m.I(A::PATTERN_REPEAT_COUNT)==0){
@ -344,10 +344,10 @@ void Monster::STRATEGY::SLIMEKING(Monster&m,float fElapsedTime,std::string strat
}break;
case 4:{
if(m.hp<=1){ //HP can't reach 0 when the dies normally flag is on.
m.phase=5;
SETPHASE(5);
m.F(A::IFRAME_TIME_UPON_HIT)=1;
m.I(A::HITS_UNTIL_DEATH)=int(m.GetSizeMult()*100/ConfigFloat("Phase5.SizeLossPerHit"))-1;
TransitionPhase(m.phase);
TransitionPhase(PHASE());
return;
}
if(m.I(A::PHASE_REPEAT_COUNT)>=5){

@ -58,8 +58,8 @@ void RepeatingSoundEffect::StopAllSounds(){
playingSoundEffects.clear();
}
SoundEffect::SoundEffect(const std::string_view filename,const float&vol,const float&minPitch,const float&maxPitch,const bool combatSound)
:filename(filename),vol(vol),minPitch(minPitch),maxPitch(maxPitch),combatSound(combatSound){
SoundEffect::SoundEffect(const std::string_view filename,const float&vol,const float&minPitch,const float&maxPitch,const bool combatSound,const bool treatAsBGM)
:filename(filename),vol(vol),minPitch(minPitch),maxPitch(maxPitch),combatSound(combatSound),treatAsBGM(treatAsBGM){
if(vol<0.f||vol>1.f)ERR(std::format("WARNING! Volume must be between 0.0f ~ 1.0f! Provided value {}",vol));
}
@ -67,16 +67,20 @@ void SoundEffect::Initialize(){
for(auto&[key,size]:DATA["Events"]["SFX"]){
int counter=0;
bool combatSound=false;
bool treatAsBGM{false};
if(DATA["Events"]["SFX"][key].HasProperty("CombatSound")){
combatSound=DATA["Events"]["SFX"][key]["CombatSound"].GetBool();
}
if(DATA["Events"]["SFX"][key].HasProperty("Treat as BGM")){
treatAsBGM=DATA["Events"]["SFX"][key]["Treat as BGM"].GetBool();
}
while(DATA["Events"]["SFX"][key].HasProperty(std::format("File[{}]",counter))){
utils::datafile&data=DATA["Events"]["SFX"][key][std::format("File[{}]",counter)];
float minPitch=0.9f;
float maxPitch=1.1f;
if(data.GetValueCount()>=3){minPitch=data.GetInt(2)/100.f;}
if(data.GetValueCount()>=4){maxPitch=data.GetInt(3)/100.f;}
SOUND_EFFECTS.insert({key,SoundEffect{data.GetString(0),data.GetInt(1)/100.f,minPitch,maxPitch,combatSound}});
SOUND_EFFECTS.insert({key,SoundEffect{data.GetString(0),data.GetInt(1)/100.f,minPitch,maxPitch,combatSound,treatAsBGM}});
counter++;
}
auto itr=SOUND_EFFECTS.equal_range(key);
@ -96,7 +100,8 @@ void SoundEffect::PlaySFX(const std::string&eventName,const vf2d&pos){
float pitch=util::random(pitchDiff)+sfx.minPitch;
if(pos==CENTERED){
Audio::Engine().Play(operator""_SFX(sfx.filename.c_str(),sfx.filename.length()),Audio::GetCalculatedSFXVolume(sfx.vol*Audio::GetSFXVolume()),0.0f,pitch);
float vol{Audio::GetCalculatedSFXVolume(sfx)};
Audio::Engine().Play(operator""_SFX(sfx.filename.c_str(),sfx.filename.length()),vol,0.0f,pitch);
}else{
const float soundActivationRange="Audio.Environmental Audio Activation Range"_F;
@ -105,7 +110,7 @@ void SoundEffect::PlaySFX(const std::string&eventName,const vf2d&pos){
float distRatio=1-distanceFromPlayer/soundActivationRange; //0-1 where 1 is full volume.
float xDistRatio=(pos.x-game->GetPlayer()->GetX())/soundActivationRange; //0-1 where 1 is full volume.
float vol=distRatio*sfx.vol*Audio::GetSFXVolume()*Audio::GetMuteMult();
float vol=distRatio*Audio::GetCalculatedSFXVolume(sfx);
float pan=xDistRatio;
Audio::Engine().Play(operator""_SFX(sfx.filename.c_str(),sfx.filename.length()),vol,pan,pitch);
}
@ -143,3 +148,10 @@ SoundEffect&SoundEffect::GetRandomSFXFromFile(const std::string&eventName){
return (*it).second;
}
const float&SoundEffect::GetVolume()const{
return vol;
}
const bool&SoundEffect::TreatAsBPM()const{
return treatAsBGM;
}

@ -53,18 +53,21 @@ private:
class SoundEffect{
public:
SoundEffect(const std::string_view filename,const float&vol,const float&minPitch=0.9f,const float&maxPitch=1.1f,const bool combatSound=false);
SoundEffect(const std::string_view filename,const float&vol,const float&minPitch=0.9f,const float&maxPitch=1.1f,const bool combatSound=false,const bool treatAsBGM=false);
static void PlaySFX(const std::string&eventName,const vf2d&pos);
//Sets up a sound to be looped continuously.
static size_t PlayLoopingSFX(const std::string&eventName,const vf2d&pos);
static void StopLoopingSFX(const int id);
static void Initialize();
static const vf2d CENTERED;
const float&GetVolume()const;
const bool&TreatAsBPM()const;
private:
static SoundEffect&GetRandomSFXFromFile(const std::string&eventName);
static std::multimap<EventName,SoundEffect>SOUND_EFFECTS;
std::string filename;
float vol;
bool treatAsBGM{false};
bool combatSound=false;
float minPitch=0.9f;
float maxPitch=1.1f;

@ -75,7 +75,7 @@ void State_Death::OnUserUpdate(AiL*game){
}
game->view.SetZoom(util::lerp(1.f,2.f,(gameSlowdownPct-1)/10.f),game->view.WorldToScreen(game->GetPlayer()->GetPos()));
game->SetWorldColor({uint8_t(util::lerp(255,0,(gameSlowdownPct-1)/10.f)),uint8_t(util::lerp(255,0,(gameSlowdownPct-1)/10.f)),uint8_t(util::lerp(255,0,(gameSlowdownPct-1)/10.f)),255});
Audio::SetBGMPitch(util::lerp(1.f,0,(gameSlowdownPct-1)/10.f));
Audio::SetBGMPitch(util::lerp(1.f,0.f,(gameSlowdownPct-1)/10.f));
if(gameSlowdownPct<5.f){
game->SetMosaicEffect(uint8_t(util::lerp(1.f,9.f,(gameSlowdownPct-1)/7.f)));
}else{

@ -111,7 +111,7 @@ void Monster::STRATEGY::STONE_GOLEM(Monster&m,float fElapsedTime,std::string str
std::for_each(m.VEC(A::STAGE_POLYGONS).begin(),m.VEC(A::STAGE_POLYGONS).end(),[&](std::any&data){
StageMaskPolygon&polygon{std::any_cast<StageMaskPolygon&>(data)};
Pixel newCol{PixelLerp(Pixel{uint32_t(m.I(A::SHOCKWAVE_COLOR))},BLACK,sin(geom2d::pi*game->GetRunTime()*2)/2.f+0.5f)};
newCol.a=util::lerp(Pixel{uint32_t(m.I(A::SHOCKWAVE_COLOR))}.a,0.f,sin(geom2d::pi*game->GetRunTime()*2)/2.f+0.5f);
newCol.a=util::lerp(Pixel{uint32_t(m.I(A::SHOCKWAVE_COLOR))}.a,uint8_t(0),sin(geom2d::pi*game->GetRunTime()*2)/2.f+0.5f);
polygon.SetBlendColor(newCol);
polygon.Draw();
});
@ -157,14 +157,14 @@ void Monster::STRATEGY::STONE_GOLEM(Monster&m,float fElapsedTime,std::string str
}
#pragma endregion
switch(m.phase){
switch(PHASE()){
case INITIALIZE:{
m.F(A::RECOVERY_TIME)=ConfigFloat("Beginning Phase.Pillar Cast Delay Time");
m.I(A::PATTERN_REPEAT_COUNT)=ConfigInt("Beginning Phase.Repeat Count");
m.F(A::HEALTH_PCT_PHASE)=1.f;
m.F(A::NEXT_HEALTH_PCT_PILLAR_PHASE)=ConfigFloat("Pillar Respawns.Start HP Threshold")/100.f;
m.I(A::SHOCKWAVE_COLOR)=ConfigPixel("Shockwave.Danger Area Color").n;
m.phase=SPAWN_PILLAR_PREPARE;
SETPHASE(SPAWN_PILLAR_PREPARE);
if(ConfigIntArr("Pillar Respawns.Respawn Count",0)<ConfigIntArr("Pillar Respawns.Respawn Count",1))ERR(std::format("WARNING! {} Stone golem pillars were declared damaged when only {} will spawn. Please make sure the number is the same or equal to the total spawned pillars! (\"Pillar Respawns.Respawn Count\" strategy property)",ConfigIntArr("Pillar Respawns.Respawn Count",1),ConfigIntArr("Pillar Respawns.Respawn Count",0)));
}break;
@ -176,7 +176,7 @@ void Monster::STRATEGY::STONE_GOLEM(Monster&m,float fElapsedTime,std::string str
m.PerformAnimation("CAST",m.GetFacingDirectionToTarget(m.V(A::LOCKON_POS)));
game->AddEffect(std::make_unique<SpellCircle>(m.V(A::LOCKON_POS),ConfigFloat("Beginning Phase.Pillar Cast Time"),"range_indicator.png","spell_insignia.png",m.OnUpperLevel(),vf2d{1.f,1.f}*(MONSTER_DATA.at("Stone Golem Pillar").GetCollisionRadius()*MONSTER_DATA.at("Stone Golem Pillar").GetSizeMult()/12.f)*1.25f,0.3f,vf2d{},ConfigPixel("Beginning Phase.Pillar Spell Circle Color"),util::random(2*PI),util::degToRad(ConfigFloat("Beginning Phase.Pillar Spell Circle Rotation Spd")),false,vf2d{1.f,1.f}*(MONSTER_DATA.at("Stone Golem Pillar").GetCollisionRadius()*MONSTER_DATA.at("Stone Golem Pillar").GetSizeMult()/12.f)*0.9f,0.3f,vf2d{},ConfigPixel("Beginning Phase.Pillar Spell Insignia Color"),util::random(2*PI),util::degToRad(ConfigFloat("Beginning Phase.Pillar Spell Insignia Rotation Spd"))),true);
m.F(A::CASTING_TIMER)=ConfigFloat("Beginning Phase.Pillar Cast Time");
m.phase=SPAWN_PILLAR_CAST;
SETPHASE(SPAWN_PILLAR_CAST);
}
}break;
case SPAWN_PILLAR_CAST:{
@ -188,11 +188,11 @@ void Monster::STRATEGY::STONE_GOLEM(Monster&m,float fElapsedTime,std::string str
game->SpawnMonster(m.V(A::LOCKON_POS),MONSTER_DATA.at("Stone Golem Pillar"),m.OnUpperLevel());
game->Hurt(m.V(A::LOCKON_POS),MONSTER_DATA.at("Stone Golem Pillar").GetCollisionRadius()*MONSTER_DATA.at("Stone Golem Pillar").GetSizeMult(),MONSTER_DATA.at("Stone Golem Pillar").GetAttack(),m.OnUpperLevel(),0.f,HurtType::PLAYER);
if(m.I(A::PATTERN_REPEAT_COUNT)<=0){
m.phase=STANDARD;
SETPHASE(STANDARD);
}else{
m.PerformIdleAnimation(m.GetFacingDirectionToTarget(game->GetPlayer()->GetPos()));
m.F(A::RECOVERY_TIME)=ConfigFloat("Beginning Phase.Pillar Cast Delay Time");
m.phase=SPAWN_PILLAR_PREPARE;
SETPHASE(SPAWN_PILLAR_PREPARE);
}
}
}break;
@ -205,7 +205,7 @@ void Monster::STRATEGY::STONE_GOLEM(Monster&m,float fElapsedTime,std::string str
m.SIZET(A::PREVIOUS_MONSTER_COUNT)=MONSTER_LIST.size();
m.PerformAnimation("CAST",m.GetFacingDirectionToTarget(game->GetPlayer()->GetPos()));
PrepareSafeAreas();
m.phase=SHOCKWAVE;
SETPHASE(SHOCKWAVE);
break;
}
if(m.F(A::NEXT_HEALTH_PCT_PILLAR_PHASE)>=m.GetHealthRatio()){
@ -218,7 +218,7 @@ void Monster::STRATEGY::STONE_GOLEM(Monster&m,float fElapsedTime,std::string str
}
if(StoneThrowRollSucceeds){ //The intent is one or the other attack is supposed to happen. We can't do the slam and a throw, rerolling repeatedly each tick is unncessary.
m.phase=STONE_THROW_CAST;
SETPHASE(STONE_THROW_CAST);
m.V(A::LOCKON_POS)=game->GetPlayer()->GetPos();
m.PerformAnimation("TOSS ROCK CAST");
m.F(A::CASTING_TIMER)=ConfigFloat("Standard Attack.Stone Throw Cast Time");
@ -233,7 +233,7 @@ void Monster::STRATEGY::STONE_GOLEM(Monster&m,float fElapsedTime,std::string str
CreateBullet(LargeStone)(m.GetPos()+vf2d{0,ConfigFloat("Standard Attack.Stone Throw Height Offset")/2.f},ConfigFloat("Standard Attack.Stone Throw Time"),m.V(A::LOCKON_POS),m.F(A::CASTING_TIMER),ConfigPixels("Standard Attack.Stone Radius"),ConfigFloat("Standard Attack.Stone Throw Height Offset"),acc,ConfigInt("Standard Attack.Stone Damage"),ConfigFloat("Standard Attack.Stone Throw Knockback Factor"),m.OnUpperLevel(),false,INFINITY,false,WHITE,vf2d{1,1}*m.GetSizeMult(),util::random(2*PI))EndBullet;
}else{
m.phase=BEAR_ATTACK;
SETPHASE(BEAR_ATTACK);
m.F(A::CHASE_TIMER)=0.f;
}
}break;
@ -243,13 +243,13 @@ void Monster::STRATEGY::STONE_GOLEM(Monster&m,float fElapsedTime,std::string str
SoundEffect::StopLoopingSFX(m.SIZET(A::LOOPING_SOUND_ID));
m.PerformAnimation("TOSS ROCK");
m.F(A::RECOVERY_TIME)=m.GetCurrentAnimation().GetTotalAnimationDuration();
m.phase=STONE_THROW_FINISH_ANIMATION;
SETPHASE(STONE_THROW_FINISH_ANIMATION);
}
}break;
case STONE_THROW_FINISH_ANIMATION:{
m.F(A::RECOVERY_TIME)-=fElapsedTime;
if(m.F(A::RECOVERY_TIME)<=0.f){
m.phase=STANDARD;
SETPHASE(STANDARD);
}
}break;
case SHOCKWAVE:{
@ -259,7 +259,7 @@ void Monster::STRATEGY::STONE_GOLEM(Monster&m,float fElapsedTime,std::string str
game->SetWorldColor(newCol);
if(m.SIZET(A::PREVIOUS_MONSTER_COUNT)!=MONSTER_LIST.size()){ //The monster list has changed...Whether it's a pillar getting added or removed, it's important we recalculate safe areas proper.
m.phase=FIX_SAFE_AREAS; //HACK ALERT! Since spawning/removing monsters doesn't immediately occur in the MONSTER_LIST structure, we must defer the safe areas until the next tick and then recalculate them. To do this, we put the monster into another state and pause the shockwave attack for a frame to fix the new spawn areas on the next tick.
SETPHASE(FIX_SAFE_AREAS); //HACK ALERT! Since spawning/removing monsters doesn't immediately occur in the MONSTER_LIST structure, we must defer the safe areas until the next tick and then recalculate them. To do this, we put the monster into another state and pause the shockwave attack for a frame to fix the new spawn areas on the next tick.
m.F(A::SAFE_AREA_WAIT_TIMER)=0.01f;
m.SIZET(A::PREVIOUS_MONSTER_COUNT)=MONSTER_LIST.size();
}
@ -288,14 +288,14 @@ void Monster::STRATEGY::STONE_GOLEM(Monster&m,float fElapsedTime,std::string str
});
SoundEffect::PlaySFX("Shockwave",m.GetPos());
game->AddEffect(std::make_unique<ExpandingRing>(m.GetPos(),ConfigFloat("Shockwave.Shockwave Ring Lifetime"),"finishring.png",m.OnUpperLevel(),vf2d{ConfigFloat("Shockwave.Ring Expand Speed"),ConfigFloat("Shockwave.Ring Expand Speed")},vf2d{1.f,1.f},ConfigFloat("Shockwave.Shockwave Fadeout Time"),vf2d{},ConfigPixel("Shockwave.Shockwave Color")),true);
m.phase=STANDARD;
SETPHASE(STANDARD);
game->SetWorldColor(WHITE);
}
}break;
case FIX_SAFE_AREAS:{
if(m.F(A::SAFE_AREA_WAIT_TIMER)<=0.f){
PrepareSafeAreas(); //Recalculate safe areas if the shockwave attack is going off.
m.phase=SHOCKWAVE;
SETPHASE(SHOCKWAVE);
}else m.F(A::SAFE_AREA_WAIT_TIMER)-=fElapsedTime;
}break;
case BEAR_ATTACK:{
@ -304,11 +304,11 @@ void Monster::STRATEGY::STONE_GOLEM(Monster&m,float fElapsedTime,std::string str
//Extending the bear script's variables to read the state of it...
const bool SlamHasFinished=m.I(A::ATTACK_COUNT)!=m.I(A::BEAR_STOMP_COUNT); //The bear script uses the internal phase variable to determine the state of things.
if(SlamHasFinished){
m.phase=STANDARD;
SETPHASE(STANDARD);
m.I(A::ATTACK_COUNT)=m.I(A::BEAR_STOMP_COUNT);
}else
if(m.I(A::PHASE)==0&&m.F(A::CHASE_TIMER)>=ConfigFloat("Max Chase Time")){
m.phase=DOUBLE_ROCK_TOSS;
if(m.GetPhase("Bear")==0&&m.F(A::CHASE_TIMER)>=ConfigFloat("Max Chase Time")){
SETPHASE(DOUBLE_ROCK_TOSS);
m.PerformAnimation("RAISE ROCK");
m.I(A::STONE_TOSS_COUNT)=ConfigInt("Stone Rain.Initial Stone Toss Count");
m.F(A::STONE_TOSS_TIMER)=m.GetCurrentAnimation().GetTotalAnimationDuration();
@ -325,7 +325,7 @@ void Monster::STRATEGY::STONE_GOLEM(Monster&m,float fElapsedTime,std::string str
game->AddEffect(std::make_unique<RockLaunch>(m.GetPos()+vf2d{util::random_range(throwPos.GetReal(0),throwPos.GetReal(2)),util::random_range(throwPos.GetReal(1),throwPos.GetReal(3))},10.f,"rock.png",ConfigFloat("Stone Rain.Stone Toss Delay"),ConfigFloat("Stone Rain.Stone Toss Rock Size Mult"),0.1f,vf2d{0.f,-ConfigFloat("Stone Rain.Stone Toss Throw Speed")},WHITE,util::random(2*PI),0.f));
if(m.I(A::STONE_TOSS_COUNT)<=0){
m.phase=STONE_RAIN;
SETPHASE(STONE_RAIN);
m.F(A::BREAK_TIME)=ConfigFloat("Stone Rain.Stone Golem Wait Time");
m.PerformAnimation("CAST");
@ -338,7 +338,7 @@ void Monster::STRATEGY::STONE_GOLEM(Monster&m,float fElapsedTime,std::string str
case STONE_RAIN:{
m.F(A::BREAK_TIME)-=fElapsedTime;
if(m.F(A::BREAK_TIME)<=0.f){
m.phase=STANDARD;
SETPHASE(STANDARD);
}
}break;
}

@ -72,15 +72,15 @@ void Monster::STRATEGY::STONE_ELEMENTAL(Monster&m,float fElapsedTime,std::string
};
auto ReturnToWaitingPhase=[&](){
m.phase=WAITING;
SETPHASE(WAITING);
m.PerformIdleAnimation();
m.F(A::ATTACK_COOLDOWN)=0.f;
};
switch(m.phase){
switch(PHASE()){
case INITIALIZE:{
m.F(A::ATTACK_COOLDOWN)=util::random(ConfigFloat("Attack Wait Time")/1.5f);
m.phase=WAITING;
SETPHASE(WAITING);
}break;
case WAITING:{
m.F(A::ATTACK_COOLDOWN)+=fElapsedTime;
@ -95,7 +95,7 @@ void Monster::STRATEGY::STONE_ELEMENTAL(Monster&m,float fElapsedTime,std::string
case 0:{
m.PerformAnimation("STONE PILLAR CAST");
m.SIZET(A::LOOPING_SOUND_ID)=SoundEffect::PlayLoopingSFX("Rock Toss Cast",m.GetPos());
m.phase=STONE_PILLAR_CAST;
SETPHASE(STONE_PILLAR_CAST);
m.F(A::CASTING_TIMER)=ConfigFloat("Stone Pillar Cast Time");
m.V(A::LOCKON_POS)=game->GetPlayer()->GetPos();
game->AddEffect(std::make_unique<SpellCircle>(m.V(A::LOCKON_POS),ConfigFloat("Stone Pillar Cast Time"),"range_indicator.png","spell_insignia.png",m.OnUpperLevel(),vf2d{1.f,1.f}*(MONSTER_DATA.at("Stone Pillar").GetCollisionRadius()*MONSTER_DATA.at("Stone Pillar").GetSizeMult()/12.f)*1.25f,0.3f,vf2d{},ConfigPixel("Stone Pillar Spell Circle Color"),util::random(2*PI),util::degToRad(ConfigFloat("Stone Pillar Spell Circle Rotation Spd")),false,vf2d{1.f,1.f}*(MONSTER_DATA.at("Stone Pillar").GetCollisionRadius()*MONSTER_DATA.at("Stone Pillar").GetSizeMult()/12.f)*0.9f,0.3f,vf2d{},ConfigPixel("Stone Pillar Spell Insignia Color"),util::random(2*PI),util::degToRad(ConfigFloat("Stone Pillar Spell Insignia Rotation Spd"))),false);
@ -103,7 +103,7 @@ void Monster::STRATEGY::STONE_ELEMENTAL(Monster&m,float fElapsedTime,std::string
case 1:{
m.PerformAnimation("ROCK TOSS CAST");
m.SIZET(A::LOOPING_SOUND_ID)=SoundEffect::PlayLoopingSFX("Rock Toss Cast",m.GetPos());
m.phase=SHOOT_STONE_CAST;
SETPHASE(SHOOT_STONE_CAST);
m.B(A::PLAYED_FLAG)=false;
m.F(A::CASTING_TIMER)=ConfigFloat("Rock Toss Track Time")+ConfigFloat("Rock Toss Wait Time");
CreateBullet(LevitatingRock)(m,game->GetPlayer()->GetPos(),1.f,0.f,ConfigPixels("Rock Toss Max Spawn Distance"),ConfigFloat("Rock Toss Track Time"),ConfigFloat("Rock Toss Wait Time"),ConfigFloat("Rock Toss Bullet Speed"),ConfigFloat("Rock Radius"),std::max(1,ConfigInt("Rock Toss Damage")/5),m.OnUpperLevel(),false,WHITE,vf2d{1,1})EndBullet;
@ -121,7 +121,7 @@ void Monster::STRATEGY::STONE_ELEMENTAL(Monster&m,float fElapsedTime,std::string
case 2:{
SoundEffect::PlaySFX("Dig",m.GetPos());
m.PerformAnimation("BURROW UNDERGROUND");
m.phase=DIVE_UNDERGROUND_DIG;
SETPHASE(DIVE_UNDERGROUND_DIG);
m.F(A::CASTING_TIMER)=m.GetCurrentAnimation().GetTotalAnimationDuration();
}break;
}
@ -167,7 +167,7 @@ void Monster::STRATEGY::STONE_ELEMENTAL(Monster&m,float fElapsedTime,std::string
case DIVE_UNDERGROUND_DIG:{
m.F(A::CASTING_TIMER)-=fElapsedTime;
if(m.F(A::CASTING_TIMER)<=0.f){
m.phase=DIVE_UNDERGROUND_MOVE;
SETPHASE(DIVE_UNDERGROUND_MOVE);
float randomAngle=util::random(2*PI);
const float minDist=ConfigPixelsArr("Burrow Teleport Distance",0);
@ -198,7 +198,7 @@ void Monster::STRATEGY::STONE_ELEMENTAL(Monster&m,float fElapsedTime,std::string
m.B(A::IGNORE_DEFAULT_ANIMATIONS)=false;
SoundEffect::PlaySFX("Rise",m.GetPos());
m.PerformAnimation("RISE FROM UNDERGROUND");
m.phase=DIVE_UNDERGROUND_SURFACE;
SETPHASE(DIVE_UNDERGROUND_SURFACE);
m.targetAcquireTimer=0;
m.F(A::CASTING_TIMER)=m.GetCurrentAnimation().GetTotalAnimationDuration();

@ -144,6 +144,7 @@ private:
std::vector<NPCData>npcs;
MapType mapType{};
std::string bgmSongName="";
std::string audioEvent{"Default Volume"};
std::unordered_map<Class,float>devCompletionTrialTime;
BackdropName backdrop="";
std::set<std::string>spawns;
@ -166,6 +167,7 @@ public:
const int Spawn_pop(); //Grabs the next spawn controller ID and removes it from the stack.
const Renderable*const GetOptimizedMap()const;
const std::map<std::string,std::vector<::ZoneData>>&GetZones()const;
const std::string&GetDefaultAudioEvent()const;
std::string FormatLayerData(std::ostream& os, std::vector<LayerTag>tiles);
std::string FormatSpawnerData(std::ostream& os, std::map<int,SpawnerTag>tiles);
friend std::ostream& operator << (std::ostream& os, Map& rhs);
@ -352,6 +354,9 @@ class TMXParser{
const Renderable*const Map::GetOptimizedMap()const{
return optimizedTile;
}
const std::string&Map::GetDefaultAudioEvent()const{
return audioEvent;
}
NPCData::NPCData(){}
NPCData::NPCData(XMLTag npcTag){
const std::array<std::string,7>tags={"Function","NPC Name","Roaming Range","Unlock Condition","Spritesheet","x","y"};
@ -539,6 +544,9 @@ class TMXParser{
parsedMapInfo.backdrop=newTag.data["value"];
}
}else
if(newTag.tag=="property"&&newTag.data["name"]=="Audio Event"){
parsedMapInfo.audioEvent=newTag.data["value"];
}else
if (newTag.tag=="object"&&newTag.data["type"]=="AudioEnvironmentalSound") {
parsedMapInfo.environmentalAudioData.emplace_back();
prevAudioData=&parsedMapInfo.environmentalAudioData.back();

@ -1,39 +1,15 @@
Time Trial Idea
Upon completing a stage for the first time, the player is shown the completion time and a record time.
On first clears, the player will always obtain a new record. Make it apparent to the player they obtained a new record.
The overworld map will show a new section that says "Completion Time" for any previous completed stages.
Upon the second time entering a stage, the game will spawn a timer that the player can run into to begin a time trial-like mode.
During the Time Trial mode the player can defeat monsters to freeze the timer by 1 second per mob killed.
Upon completion of a stage in time trial mode if the player beat their previous time (which they likely will) the record will update.
For each class and stage combination there will be a "dev time"
Traveling merchants of different colors/looks.
Look into removing OVERRIDE from rumble settings. It looks like it was used to purposefully disable rumble, but looks very unnecessary.
New Monster Sound Effects
Add a Helper Function: Change proximity knockback checks regardless of player/monster friendliness to be a function call instead.
Add rectangular hitbox possibility to the game for monsters. (specifically for use with pillars)
Fanfare -> Post boss song
Add unconscious monster state.
DEMO
====
Add Unit Test for Attack Speed Reduction stat.
Move censoredTextEntry calculation in MenuLabel draw somewhere else perhaps?
PGETinker notes
===============
Changing zoom size does not affect the indentation breadcumb immediately (requires scrolling to change the view)
Enabling javid mode does not immediately re-evaluate code.
Add Sherman and Blacksmith story images
Have all song ideas by Nov 1st
- Chapter 3 boss outro
- Chapter 4 Stage + boss + outro
- Chapter 5 Stage + boss + outro
Adding new class animations
===========================

@ -0,0 +1,95 @@
#pragma region License
/*
License (OLC-3)
~~~~~~~~~~~~~~~
Copyright 2024 Joshua Sigona <sigonasr2@gmail.com>
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions or derivations of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions or derivative works in binary form must reproduce the above
copyright notice. This list of conditions and the following disclaimer must be
reproduced in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may
be used to endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
Portions of this software are copyright © 2024 The FreeType
Project (www.freetype.org). Please see LICENSE_FT.txt for more information.
All rights reserved.
*/
#pragma endregion
#include "BulletTypes.h"
#include "AdventuresInLestoria.h"
#include <ranges>
#include "util.h"
#include "SoundEffect.h"
INCLUDE_game
ThrownProjectile::ThrownProjectile(vf2d pos,vf2d targetPos,const std::string&img,float explodeRadius,float z,float totalFallTime,float totalRiseZAmt,int damage,bool upperLevel,bool hitsMultiple,float lifetime,bool friendly,Pixel col,vf2d scale,float image_angle,const std::optional<Effect>explodeEffect,const std::optional<std::string>explodeSoundEffect,const std::optional<LingeringEffect>lingeringEffect)
:Bullet(pos,util::pointTo(pos,targetPos)*util::distance(pos,targetPos)/totalFallTime,0.f,damage,img,upperLevel,hitsMultiple,lifetime,false,friendly,col,scale,image_angle),initialZ(z),explodeRadius(explodeRadius),totalRiseZAmt(totalRiseZAmt),totalFallTime(totalFallTime),startingPos(pos),targetPos(targetPos),originalRisingTime(totalFallTime*0.25f),risingTime(originalRisingTime),originalFallingTime(totalFallTime*0.75f),fallingTime(originalFallingTime),explodeEffect(explodeEffect),img(img),explodeSoundEffect(explodeSoundEffect),lingeringEffect(lingeringEffect){
this->z=z;
}
void ThrownProjectile::OnGroundLand(){
const HurtList hurtList{game->Hurt(pos,explodeRadius,damage,OnUpperLevel(),z,HurtType::MONSTER,HurtFlag::PLAYER_ABILITY)};
if(explodeEffect){
Effect&explosionEffect{game->AddEffect(std::make_unique<Effect>(explodeEffect.value()))};
explosionEffect.pos+=pos;
}
if(explodeSoundEffect)SoundEffect::PlaySFX(explodeSoundEffect.value(),pos);
if(lingeringEffect){
LingeringEffect&lingerEffect{dynamic_cast<LingeringEffect&>(game->AddEffect(std::make_unique<LingeringEffect>(lingeringEffect.value())))};
lingerEffect.posOscillator.val1+=pos;
lingerEffect.posOscillator.val2+=pos;
lingerEffect.pos+=pos;
}
vel={};
fadeOutTime=0.25f;
Deactivate();
}
void ThrownProjectile::_OnGroundLand(){
z=0.f;
OnGroundLand();
}
void ThrownProjectile::Update(float fElapsedTime){
if(IsDeactivated())return;
image_angle+=0.5*PI*fElapsedTime;
const bool Landed{fallingTime<=0.f};
if(Landed){
_OnGroundLand();
}else{
if(risingTime>0.f){
risingTime-=fElapsedTime;
z=util::lerp(initialZ,initialZ+totalRiseZAmt,1-(risingTime/originalRisingTime));
}else{
fallingTime-=fElapsedTime;
z=util::lerp(initialZ+totalRiseZAmt,0.f,1-(fallingTime/originalFallingTime));
}
}
}
void ThrownProjectile::ModifyOutgoingDamageData(HurtDamageInfo&data){
if(friendly)data.hurtFlags|=HurtFlag::PLAYER_ABILITY;
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save