Compare commits

..

139 Commits
demo ... master

Author SHA1 Message Date
Quapsel ead0648a62 Chapter 5 Presets progress. 2 days ago
sigonasr2 ded7e53fd7 Slash animation plays when ghost saber hits player. Release Build 12036. 4 days ago
sigonasr2 d24c016d3e Pirate Captain Ghost moves towards player. Release Build 12032. 4 days ago
sigonasr2 071e98aecd Added ghost saber expanding behavior. Fix Prediction bombardment pattern to properly use player's previous position. Release Build 12029. 5 days ago
sigonasr2 fdef405ed8 Merge branch 'master' of http://sig.projectdivar.com/sigonasr2/AdventuresInLestoria 6 days ago
sigonasr2 51a3feb5b6 Add Ghost Saber attack. Release Build 12016. 6 days ago
Quapsel 95e51f2324 Chapter 5 Tilesets added. First Steps on Chapter 5 Presets. 6 days ago
sigonasr2 24d36da0ab Add afterimage rendering for monsters. Release Build 12003. 3 weeks ago
sigonasr2 4e099b71f9 Add placeholder item graphics for chapter 4 items. Setup map classes for chapter 4 maps. Add in force set position function for monsters. Release Build 12001. 3 weeks ago
sigonasr2 a7308f4ac5 Merge branch 'master' of http://sig.projectdivar.com/sigonasr2/AdventuresInLestoria 3 weeks ago
sigonasr2 6c5648bac9 Add previous player position variable and getter for the player. Add in other cannon pattern types to rotation. 3 weeks ago
Quapsel 15b5b541e0 „Adventures in Lestoria/assets/config/levels.txt“ ändern 3 weeks ago
Quapsel f2681aebaf „Adventures in Lestoria/assets/config/items/ItemDatabase.txt“ ändern 3 weeks ago
sigonasr2 27bfd8e1d4 Add shrapnel shot attack. Fix random dispersion of cannon shots (incorrect math that overcorrected for random angle + random distance vector). Release Build 11980. 4 weeks ago
sigonasr2 c3261bb301 Remove extraneous log lines. Restore original cannon cycle. Release Build 11972. 4 weeks ago
sigonasr2 0d15ad2fa7 Fix Issue #74. Falling Bullet properly activates side effect instead of randomly choosing to due to order of IBullet vs child class call updates. Update credits with Special Thanks section. Release Build 11970. 4 weeks ago
sigonasr2 dc9717c07f Tweak and fix Falling Bullet behavior to use proper coloring. Release Build 11958. 1 month ago
sigonasr2 e2b6e9318a Add in initial cannonball attack. Setup Chapter 3 boss spawn. Release Build 11946. 1 month ago
sigonasr2 1a9d5b12d0 Merge branch 'master' of http://sig.projectdivar.com/sigonasr2/AdventuresInLestoria 1 month ago
sigonasr2 9732c57983 Generalized FallingStone into FallingBullet to supply a custom image. Prepare graphics and add extra map items and basic AI structure for Chapter 3 boss. Release Build 11936. 1 month ago
Quapsel d85e062ed5 4_8 added. 1 month ago
sigonasr2 e893d3a54e Add in camp versions of the pause menu/item loadout menu. Allow the player to change loadout items from the camp. Make mana gain numbers not stack on top of health gain numbers (for items like Berries). Release Build 11935. 2 months ago
sigonasr2 48a35faf49 Use game state for camp hub checks instead of the direct map name. Prevent added velocity external factors from cancelling a player's spell cast. Release Build 11928. 2 months ago
sigonasr2 7d0b15f652 Allow loadout items to be used in the camp without consuming the item. Release Build 11922. 2 months ago
sigonasr2 d89b82bc41 Remove chapter settings from State_OverworldMap class' control and add a Chapter change and set player world location command to the Visual Novel parser. Add base camp location to Chapter 3 area. Release Build 11917. 2 months ago
sigonasr2 594dd352ec Add random submerge time between arm attacks. Release Build 11916. 2 months ago
sigonasr2 f6c76b3881 Added homing bullet behaviors. Added turn_towards_target util helper function; a higher level funcution derived from turn_towards_direction. Modify Ink slowdown to reset duration instead of stacking (per spec). Release Build 11914. 2 months ago
sigonasr2 6b0ea4422f Merge branch 'master' of http://sig.projectdivar.com/sigonasr2/AdventuresInLestoria 2 months ago
sigonasr2 8eb1878074 Add additive blending property to bullets. Ink bubble explosion. Ink Bubble bullet behavior added. Add Ink Slowdown debuff property. Release Build 11900. 2 months ago
Quapsel b160e9b421 4_7 added. 2 months ago
Quapsel 0cb1d0e6f3 4_5 & 4_6 added. 2 months ago
sigonasr2 b38b42f1d0 Setup structure, classes, graphics, and variables for Ink Bullet mechanics. 2 months ago
sigonasr2 8cf9d0b9dd Fix missing burst bullet in bullet ring. Reduce collision radius of ring bullets to better match the graphic. Release Build 11896. 2 months ago
sigonasr2 a006924fe2 Fix crash on collision with Octopus big bullet. Fix animation sheet directions and facing direction commands for GiantOctopus AI. Release Build 11895. 2 months ago
sigonasr2 7b779aa7e3 Add missing item and accessory graphics for Chapter 3. Include Sherman and Greg story character sprites in story. Release Build 11893. 2 months ago
sigonasr2 9c8906308c Fix unit testing paths to refer to correct directories. Added error handling and control return path for Entity functions. Release Build 11891. 2 months ago
sigonasr2 cb28ab82aa Update 'Adventures in Lestoria/TODO.txt' 2 months ago
Quapsel 4b0535d0db Stages 4_4 and 4_B1 added. 2 months ago
sigonasr2 dbeb9fbee7 Add burst bullet functionality, add generalized Entity connector helper class. Burst bullet explode sound effect. Burst Bullet extra bullets on explosion. Damage through Damage Data modification will cancel if set to zero if damage resolution is modified. RotatingBullet class and functionality added. Release Build 11878. 2 months ago
sigonasr2 8da0813ac3 Burst Bullet class structure. 2 months ago
sigonasr2 60b31a602c Fix Move Spd% typo on Octopus and Captain's Diamond Ring. Add missing item icons / hud icons. Fix typos in status effects and shards for accessories. Add in Octopus bullet shots. 2 months ago
Quapsel 2752f6630d „Adventures in Lestoria/assets/config/items/Weapons.txt“ ändern 2 months ago
Quapsel 4031b03974 „Adventures in Lestoria/assets/config/items/Equipment.txt“ ändern 2 months ago
Quapsel 0a9524c5e2 „Adventures in Lestoria/assets/config/levels.txt“ ändern 2 months ago
Quapsel 94f29020c6 „Adventures in Lestoria/assets/config/Monsters.txt“ ändern 2 months ago
Quapsel debe498570 „Adventures in Lestoria/assets/config/items/Accessories.txt“ ändern 2 months ago
Quapsel e1ad07ded2 „Adventures in Lestoria/assets/config/Monsters.txt“ ändern 2 months ago
Quapsel aa26348727 „Adventures in Lestoria/assets/config/items/ItemDatabase.txt“ ändern 2 months ago
sigonasr2 ef0b00e771 Attack arc ranges increased. Reset attack arc display when submerging occurs. Release Build 11865. 2 months ago
sigonasr2 c735f74a83 Takoyaki. 2 months ago
sigonasr2 6fb8556c79 Merge branch 'master' of http://sig.projectdivar.com/sigonasr2/AdventuresInLestoria 2 months ago
sigonasr2 bec8c67ee9 Add staggered rise animations to Octopus arms. Add attack up buffs and animation speed up buffs to the boss and Octopus arms per tentacle death. Release Build 11852. 2 months ago
Quapsel eef449f2a3 4_3 added. 3 months ago
sigonasr2 58be1c8999 Octopus Arm now has proper detection for empty pool spawns and responsive to player moving around (spawn in nearby empty water pools). Release Build 11848. 3 months ago
sigonasr2 6ea6b9b88d Merge branch 'master' of http://sig.projectdivar.com/sigonasr2/AdventuresInLestoria 3 months ago
sigonasr2 442b22f640 Fix octopus arm transition damage to be true damage instead of player damage (no critical hits). Fix octopus arm transition jump target positions (return (0,0) now) 3 months ago
Quapsel bc6e19d5ed 4_2 added. 3 months ago
sigonasr2 54c57a692f Fix missing tile in 1-2. Fix Chapter 3 bonus stage stage plate. 3 months ago
sigonasr2 f94d22ff48 Fix Pirate Buccaneer's aiming sight remaining on the field after death. Make aiming sight target line pulse. Fix bug with Frog Tongue one-shotting the player. Release Build 11815. 3 months ago
sigonasr2 c9a29b2705 Fix arc not showing up. 3-5 had a missing spawn point set. More Octopus Arm behavior fixes. Release Build 11808. 3 months ago
sigonasr2 9b94a90326 Octopus Boss implementation 3 months ago
Quapsel b7e486690d Monstespawns for 3_7 & 3_8 finished. 3 months ago
Quapsel f1bf83eb17 Monster spawns done for 3_5, 3_6 & 3_B1 3 months 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. 3 months ago
sigonasr2 10b8e68b22 Merge branch 'master' of http://sig.projectdivar.com/sigonasr2/AdventuresInLestoria 3 months ago
sigonasr2 f5038d4a24 Add in more AI. 3 months ago
Quapsel 6e435a5c54 Monster Spawns for 3_3 and 3_4 added. Spawn Group locations for all chapter 3 maps finished. 3 months ago
Quapsel 5e02dd6590 3_2 Monste spawns finished. 3_3 - 3_6 Spawn Groups placed. 3 months ago
Quapsel 19dcf0fad0 Monster Spawns for 3_1. 3 months ago
sigonasr2 debc23bfa3 Additional AI setup. 3 months ago
sigonasr2 81bfbfb860 Add map setup and monster templates for Octopus fight. 3 months 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 months ago
sigonasr2 139d4f4eac Merge branch 'master' of http://sig.projectdivar.com/sigonasr2/AdventuresInLestoria 3 months ago
sigonasr2 16708108e6 Move Attack Arc function into its own function for portability. 3 months ago
Quapsel 61b8efbb9f Chapter 4 presets completed. 4_1 Added. 4 months ago
Quapsel f181fca2a1 hub border, border extension in 1 random map, chapter 4 presets almost finished. 4 months ago
sigonasr2 46ba7a780d Octopus arm AI 4 months ago
sigonasr2 7b165a64d5 Arc implementation completed. Release Build 11776. 4 months ago
sigonasr2 d85cef1b86 Arcs, Cones, Polygon overlap code. Octopus Arm AI initialized. 4 months ago
sigonasr2 1bb0ceb096 Add monster entries and animations for Giant Octopus boss and Octopus Arm. Release Build 11756. 4 months ago
sigonasr2 3a3d405272 Implemented lingering effect item script capabilities. Added Molotov item and item graphic. Added burning sound effect. Release Build 11755. 4 months 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. 4 months ago
sigonasr2 8c3dd0f071 [demo] Fix stage alpha transitions in story view. Release Build 11576. 4 months ago
sigonasr2 2b0c1c070a [demo] Add Warrior story image to list of files. Missing asset. Release Build 11575. 4 months ago
sigonasr2 598b3b66c7 Add new chapter 3 consumables to item database and placeholder icons. 4 months ago
sigonasr2 b11d209813 Implemented Sandworm AI. Change added velocity function to be a standalone velocity variable added to original player velocity. Release Build 11725. 4 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. 4 months ago
sigonasr2 e0644fe809 Sandworm implementation + Monster collision radius changing implemented. 4 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. 4 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. 4 months ago
sigonasr2 3684e0f211 Implemented Crab AI. Release Build 11683. 4 months ago
sigonasr2 94de6bb10e Remove hidden incorrect collision on various stages. Release Build 11674. 4 months ago
sigonasr2 9ec850fdd0 Implemented Seagull AI. Release Build 11673. 4 months ago
sigonasr2 6b0c17b5ff Add ambient wave and gull sounds to Chapter 3 stages. Add slight pitch variance to environmental audio. Release Build 11667. 4 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. 4 months ago
sigonasr2 a09ea9a9a7 Merge branch 'master' of http://sig.projectdivar.com/sigonasr2/AdventuresInLestoria 4 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. 4 months ago
Quapsel 967af0fe5f Added details to the world map. 4 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. 5 months ago
sigonasr2 970b15ac5d Parrot Monster scripts initialized. 5 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. 5 months ago
sigonasr2 5f63f23635 Add missing item icons (placeholders). Add mounted parrot to Pirate Captain. Release Build 11649. 5 months ago
sigonasr2 d432966e3e Worked on Pirate Captain base AI. (Mounted Parrot AI still needs implemented). 5 months ago
sigonasr2 4e1fa8d4e3 Merge branch 'master' of http://sig.projectdivar.com/sigonasr2/AdventuresInLestoria 5 months ago
sigonasr2 c16dccc623 Begin Pirate Captain AI implementation. 5 months ago
Quapsel 888cea1581 „Adventures in Lestoria/assets/config/items/ItemDatabase.txt“ ändern 5 months ago
sigonasr2 da2640fbc1 Health and mana indicators don't get squished strangely anymore due to floating point values. Release Build 11638. 5 months ago
sigonasr2 85ba978793 Fix new crashes pertaining to unchecked 0 distance in line normalizing functions. 5 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. 5 months ago
sigonasr2 d6c2b6c87f Implement Pirate Marauder AI. Release Build 11609. 5 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. 5 months ago
sigonasr2 90d9bc86b1 Add in templates, strategies, and setup for Chapter 3 monsters. Fix tile collisions on Chapter 3 tiles. Release Build 11595. 5 months ago
sigonasr2 8f5973d836 Crab and Giant Crab monster entries added. 5 months ago
sigonasr2 21c5af80c2 Pirate Marauder entry added. 5 months ago
sigonasr2 612cda41c3 Pirate Marauder/Pirate Captain data added. 5 months ago
sigonasr2 07f431a5aa Prep Pirate monster entry. 5 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. 5 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. 5 months ago
sigonasr2 2fe0991920 Push demo fixes into master branch. 5 months ago
sigonasr2 4d74d803b2 Linux fixes. Make sure audio doesn't require loading from filesystem (Use resource packs instead) 5 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. 5 months ago
sigonasr2 ca46f0b92e Update distribution scripts with cleaning out the monsters folder. 5 months ago
sigonasr2 004f896953 MonsterData should be loading graphics from Resource Packs instead. This should solve the issues with Issue #70. Release Build 11566. 5 months ago
sigonasr2 669a267a67 [demo] Remove hash verification functions from the game. See Issue #68. Release Build 11565. 5 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. 5 months ago
sigonasr2 a16ca296c7 Linux changes and fixes 5 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. 5 months ago
sigonasr2 36b9a071e3 Added controller compatibility to Artificer Enchant Window. Removed menu navigation using right analog stick. Release Build 11662. 5 months ago
sigonasr2 da5d596a37 Implemented controller compatbility for Artificer Disassembly Menu. Release Build 11653. 5 months ago
sigonasr2 86e2976549 Added some menu scrolling helper functions. Implemented Artificer Enchant window. Added controller compatibility to Artificer Refining Window. Release Build 11652. 5 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. 5 months ago
sigonasr2 d007e2bbf5 Fix key display for Ability 4 to match the keybind of the 4th ability. Release Build 11599. 5 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. 5 months ago
sigonasr2 0d7759b230 Fix a unit test calling BGM pitch change function (which doesn't need a test). Release Build 11564. 5 months ago
sigonasr2 796c0da6d0 Add back in correct World Map with stage plates for master branch. 5 months ago
sigonasr2 55f4347452 Update master build demo flag (set to false) and admin mode (set to true). Release Build 11562. 5 months ago
sigonasr2 8283147efe [demo] Add congratulatory message for completing the demo. Version updated to 1.3. Release Build 11561. 5 months ago
sigonasr2 fb66fcb04d [demo] Make the 3 class variations unselectable in demo build. Release Build 11558. 5 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. 5 months ago
  1. 11
      Adventures in Lestoria Tests/Adventures in Lestoria Tests.vcxproj
  2. 36
      Adventures in Lestoria Tests/BuffTests.cpp
  3. 40
      Adventures in Lestoria Tests/EnchantTests.cpp
  4. 2
      Adventures in Lestoria Tests/GameHelper.h
  5. 62
      Adventures in Lestoria Tests/ItemTests.cpp
  6. 49
      Adventures in Lestoria Tests/MonsterTests.cpp
  7. 20
      Adventures in Lestoria Tests/PlayerTests.cpp
  8. 34
      Adventures in Lestoria/Adventures in Lestoria.tiled-project
  9. 83
      Adventures in Lestoria/Adventures in Lestoria.vcxproj
  10. 91
      Adventures in Lestoria/Adventures in Lestoria.vcxproj.filters
  11. 275
      Adventures in Lestoria/AdventuresInLestoria.cpp
  12. 9
      Adventures in Lestoria/AdventuresInLestoria.h
  13. 15
      Adventures in Lestoria/Animation.cpp
  14. 79
      Adventures in Lestoria/Arc.cpp
  15. 59
      Adventures in Lestoria/Arc.h
  16. 60
      Adventures in Lestoria/ArtificerDisassembleWindow.cpp
  17. 73
      Adventures in Lestoria/ArtificerEnchantConfirmWindow.cpp
  18. 117
      Adventures in Lestoria/ArtificerEnchantWindow.cpp
  19. 7
      Adventures in Lestoria/ArtificerRefineResultWindow.cpp
  20. 56
      Adventures in Lestoria/ArtificerRefineWindow.cpp
  21. 15
      Adventures in Lestoria/ArtificerWindow.cpp
  22. 26
      Adventures in Lestoria/Attributable.h
  23. 3
      Adventures in Lestoria/AttributableStat.h
  24. 9
      Adventures in Lestoria/Audio.cpp
  25. 5
      Adventures in Lestoria/Audio.h
  26. 8
      Adventures in Lestoria/Bear.cpp
  27. 10
      Adventures in Lestoria/Boar.cpp
  28. 2
      Adventures in Lestoria/Bomb.cpp
  29. 4
      Adventures in Lestoria/BreakingPillar.cpp
  30. 1
      Adventures in Lestoria/Buff.h
  31. 2
      Adventures in Lestoria/Bullet.cpp
  32. 2
      Adventures in Lestoria/Bullet.h
  33. 115
      Adventures in Lestoria/BulletTypes.h
  34. 104
      Adventures in Lestoria/BurstBullet.cpp
  35. 49
      Adventures in Lestoria/Chapter_3_FinalBoss.txt
  36. 22
      Adventures in Lestoria/CharacterMenuWindow.cpp
  37. 11
      Adventures in Lestoria/ChargedArrow.cpp
  38. 118
      Adventures in Lestoria/Crab.cpp
  39. 2
      Adventures in Lestoria/CraftItemWindow.cpp
  40. 7
      Adventures in Lestoria/DEFINES.h
  41. 4
      Adventures in Lestoria/DaggerSlash.cpp
  42. 4
      Adventures in Lestoria/DaggerStab.cpp
  43. 14
      Adventures in Lestoria/DamageNumber.cpp
  44. 4
      Adventures in Lestoria/Effect.cpp
  45. 19
      Adventures in Lestoria/Effect.h
  46. 60
      Adventures in Lestoria/Entity.cpp
  47. 56
      Adventures in Lestoria/Entity.h
  48. 1
      Adventures in Lestoria/EnvironmentalAudio.cpp
  49. 2
      Adventures in Lestoria/EquipSlotButton.h
  50. 3
      Adventures in Lestoria/Error.h
  51. 21
      Adventures in Lestoria/FallingBullet.cpp
  52. 12
      Adventures in Lestoria/Frog.cpp
  53. 5
      Adventures in Lestoria/FrogTongue.cpp
  54. 199
      Adventures in Lestoria/GhostOfPirateCaptain.cpp
  55. 84
      Adventures in Lestoria/GhostSaber.cpp
  56. 84
      Adventures in Lestoria/GiantCrab.cpp
  57. 179
      Adventures in Lestoria/GiantOctopus.cpp
  58. 9
      Adventures in Lestoria/Goblin_Boar_Rider.cpp
  59. 4
      Adventures in Lestoria/Goblin_Bomb.cpp
  60. 10
      Adventures in Lestoria/Goblin_Bow.cpp
  61. 12
      Adventures in Lestoria/Goblin_Dagger.cpp
  62. 10
      Adventures in Lestoria/Hawk.cpp
  63. 54
      Adventures in Lestoria/HomingBullet.cpp
  64. 130
      Adventures in Lestoria/HubPauseMenu.cpp
  65. 19
      Adventures in Lestoria/IBullet.cpp
  66. 4
      Adventures in Lestoria/IBullet.h
  67. 52
      Adventures in Lestoria/Ink.cpp
  68. 66
      Adventures in Lestoria/InkBullet.cpp
  69. 149
      Adventures in Lestoria/Item.cpp
  70. 35
      Adventures in Lestoria/Item.h
  71. 11
      Adventures in Lestoria/ItemDrop.cpp
  72. 2
      Adventures in Lestoria/ItemDrop.h
  73. 26
      Adventures in Lestoria/ItemEnchant.cpp
  74. 6
      Adventures in Lestoria/ItemEnchant.h
  75. 148
      Adventures in Lestoria/ItemHubLoadout.cpp
  76. 102
      Adventures in Lestoria/ItemScript.cpp
  77. 40
      Adventures in Lestoria/Menu.cpp
  78. 6
      Adventures in Lestoria/Menu.h
  79. 6
      Adventures in Lestoria/MenuItemButton.h
  80. 8
      Adventures in Lestoria/MenuItemItemButton.h
  81. 40
      Adventures in Lestoria/MenuItemLabel.h
  82. 12
      Adventures in Lestoria/MenuType.h
  83. 4
      Adventures in Lestoria/Merchant.cpp
  84. 189
      Adventures in Lestoria/Monster.cpp
  85. 66
      Adventures in Lestoria/Monster.h
  86. 32
      Adventures in Lestoria/MonsterAttribute.h
  87. 11
      Adventures in Lestoria/MonsterData.cpp
  88. 4
      Adventures in Lestoria/MonsterData.h
  89. 3
      Adventures in Lestoria/MonsterStrategyHelpers.h
  90. 4
      Adventures in Lestoria/NPC.cpp
  91. 221
      Adventures in Lestoria/OctopusArm.cpp
  92. 49
      Adventures in Lestoria/Oktopus boss.txt
  93. 1
      Adventures in Lestoria/Oscillator.h
  94. 74
      Adventures in Lestoria/Parrot.cpp
  95. 129
      Adventures in Lestoria/Pirate_Buccaneer.cpp
  96. 183
      Adventures in Lestoria/Pirate_Captain.cpp
  97. 175
      Adventures in Lestoria/Pirate_Marauder.cpp
  98. 67
      Adventures in Lestoria/Player.cpp
  99. 98
      Adventures in Lestoria/Player.h
  100. 1
      Adventures in Lestoria/PlayerTimerType.h
  101. Some files were not shown because too many files have changed in this diff Show More

@ -47,7 +47,7 @@
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Unit Testing|x64'">
<LinkIncremental>true</LinkIncremental>
<IncludePath>C:\Users\niconiconii\source\repos\AdventuresInLestoria\Adventures in Lestoria;C:\Users\sigon\source\repos\AdventuresInLestoria\Adventures in Lestoria;$(IncludePath)</IncludePath>
<IncludePath>J:\AdventuresInLestoria\Adventures in Lestoria;C:\Users\niconiconii\source\repos\AdventuresInLestoria\Adventures in Lestoria;C:\Users\sigon\source\repos\AdventuresInLestoria\Adventures in Lestoria;$(IncludePath)</IncludePath>
<OutDir>..\x64\Unit Testing</OutDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Unit Testing|Win32'">
@ -58,12 +58,12 @@
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<AdditionalIncludeDirectories>C:\Users\niconiconii\source\repos\AdventuresInLestoria\Adventures in Lestoria\steam;C:\Users\sigon\source\repos\AdventuresInLestoria\Adventures in Lestoria\steam;C:\Users\niconiconii\source\repos\AdventuresInLestoria\Adventures in Lestoria\discord-files;C:\Users\sigon\source\repos\AdventuresInLestoria\Adventures in Lestoria\discord-files;C:\Users\niconiconii\OneDrive\Documents\include;C:\Users\sigon\OneDrive\Documents\include;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalIncludeDirectories>J:\AdventuresInLestoria\Adventures in Lestoria\steam;J:\AdventuresInLestoria\Adventures in Lestoria\discord-files;C:\Users\niconiconii\source\repos\AdventuresInLestoria\Adventures in Lestoria\steam;C:\Users\sigon\source\repos\AdventuresInLestoria\Adventures in Lestoria\steam;C:\Users\niconiconii\source\repos\AdventuresInLestoria\Adventures in Lestoria\discord-files;C:\Users\sigon\source\repos\AdventuresInLestoria\Adventures in Lestoria\discord-files;C:\Users\niconiconii\OneDrive\Documents\include;C:\Users\sigon\OneDrive\Documents\include;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<UseFullPaths>true</UseFullPaths>
<PrecompiledHeaderFile>
</PrecompiledHeaderFile>
<LanguageStandard>stdcpp20</LanguageStandard>
<LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
@ -103,7 +103,9 @@
<ClCompile Include="..\Adventures in Lestoria\discord-files\store_manager.cpp" />
<ClCompile Include="..\Adventures in Lestoria\discord-files\user_manager.cpp" />
<ClCompile Include="..\Adventures in Lestoria\discord-files\voice_manager.cpp" />
<ClCompile Include="BuffTests.cpp" />
<ClCompile Include="BuffTests.cpp">
<AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Unit Testing|x64'">$(ProjectDir)..\Adventures in Lestoria\discord-files;$(ProjectDir)..\Adventures in Lestoria\steam;C:\Users\niconiconii\source\repos\AdventuresInLestoria\Adventures in Lestoria\steam;C:\Users\sigon\source\repos\AdventuresInLestoria\Adventures in Lestoria\steam;C:\Users\niconiconii\source\repos\AdventuresInLestoria\Adventures in Lestoria\discord-files;C:\Users\sigon\source\repos\AdventuresInLestoria\Adventures in Lestoria\discord-files;C:\Users\niconiconii\OneDrive\Documents\include;C:\Users\sigon\OneDrive\Documents\include;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<ClCompile Include="EnchantTests.cpp">
<SubType>
</SubType>
@ -111,6 +113,7 @@
<ClCompile Include="EngineTests.cpp">
<SubType>
</SubType>
<AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Unit Testing|x64'">$(ProjectDir)..\Adventures in Lestoria\discord-files;$(ProjectDir)..\Adventures in Lestoria\steam;C:\Users\niconiconii\source\repos\AdventuresInLestoria\Adventures in Lestoria\steam;C:\Users\sigon\source\repos\AdventuresInLestoria\Adventures in Lestoria\steam;C:\Users\niconiconii\source\repos\AdventuresInLestoria\Adventures in Lestoria\discord-files;C:\Users\sigon\source\repos\AdventuresInLestoria\Adventures in Lestoria\discord-files;C:\Users\niconiconii\OneDrive\Documents\include;C:\Users\sigon\OneDrive\Documents\include;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<ClCompile Include="FileTests.cpp">
<SubType>

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

@ -227,7 +227,7 @@
<PreprocessorDefinitions>_DEBUG;_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>
<TreatSpecificWarningsAsErrors>4099;5030;4715;4172;4834</TreatSpecificWarningsAsErrors>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
</ClCompile>
@ -251,8 +251,8 @@
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</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>
<LanguageStandard>stdcpplatest</LanguageStandard>
<AdditionalIncludeDirectories>$(ProjectDir)steam;$(ProjectDir)discord-files;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>
<TreatSpecificWarningsAsErrors>4099;5030;4715;4172;4834</TreatSpecificWarningsAsErrors>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
</ClCompile>
@ -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>
@ -404,6 +408,10 @@
<SubType>
</SubType>
</ClInclude>
<ClInclude Include="Entity.h">
<SubType>
</SubType>
</ClInclude>
<ClInclude Include="HurtDamageInfo.h">
<SubType>
</SubType>
@ -764,6 +772,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 +838,12 @@
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="BurstBullet.cpp" />
<ClCompile Include="Crab.cpp" />
<ClCompile Include="Entity.cpp">
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="ExplosiveTrap.cpp">
<SubType>
</SubType>
@ -834,6 +852,27 @@
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="GhostSaber.cpp">
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="GhostOfPirateCaptain.cpp">
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="GiantCrab.cpp">
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="GiantOctopus.cpp" />
<ClCompile Include="HomingBullet.cpp">
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="HubPauseMenu.cpp">
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="IBullet.cpp" />
<ClCompile Include="BuyItemWindow.cpp">
<SubType>
@ -956,6 +995,14 @@
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="Ink.cpp">
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="InkBullet.cpp">
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="InputHelper.cpp">
<SubType>
</SubType>
@ -987,7 +1034,15 @@
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="ItemHubLoadout.cpp">
<SubType>
</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 +1098,7 @@
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="OctopusArm.cpp" />
<ClCompile Include="Overlay.cpp">
<SubType>
</SubType>
@ -1053,15 +1109,27 @@
</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="RotateBullet.cpp">
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="ThrownProjectile.cpp">
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="PoisonPool.cpp">
<SubType>
</SubType>
@ -1084,6 +1152,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 +1161,7 @@
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="Seagull.cpp" />
<ClCompile Include="SellItemWindow.cpp">
<SubType>
</SubType>
@ -1115,7 +1185,7 @@
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="FallingStone.cpp">
<ClCompile Include="FallingBullet.cpp">
<SubType>
</SubType>
</ClCompile>
@ -1225,6 +1295,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" />
@ -1275,6 +1346,7 @@
<Text Include="Chapter_1_Creatures_Part_2.txt" />
<Text Include="Chapter_2_Boss.txt" />
<Text Include="Chapter_2_Monsters.txt" />
<Text Include="Chapter_3_FinalBoss.txt" />
<Text Include="Chapter_3_Monsters.txt" />
<Text Include="characters.txt" />
<Text Include="ConsoleCommands.txt" />
@ -1286,6 +1358,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,12 @@
<ClInclude Include="DynamicMenuLabel.h">
<Filter>Header Files\Interface</Filter>
</ClInclude>
<ClInclude Include="Arc.h">
<Filter>Header Files\Utils</Filter>
</ClInclude>
<ClInclude Include="Entity.h">
<Filter>Header Files\Utils</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="Player.cpp">
@ -1163,7 +1172,7 @@
<ClCompile Include="RockLaunch.cpp">
<Filter>Source Files\Effects</Filter>
</ClCompile>
<ClCompile Include="FallingStone.cpp">
<ClCompile Include="FallingBullet.cpp">
<Filter>Source Files\Bullet Types</Filter>
</ClCompile>
<ClCompile Include="ShineEffect.cpp">
@ -1187,7 +1196,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 +1235,75 @@
<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>
<ClCompile Include="BurstBullet.cpp">
<Filter>Source Files\Bullet Types</Filter>
</ClCompile>
<ClCompile Include="Entity.cpp">
<Filter>Source Files\Utils</Filter>
</ClCompile>
<ClCompile Include="RotateBullet.cpp">
<Filter>Source Files\Bullet Types</Filter>
</ClCompile>
<ClCompile Include="InkBullet.cpp">
<Filter>Source Files\Bullet Types</Filter>
</ClCompile>
<ClCompile Include="Ink.cpp">
<Filter>Source Files\Effects</Filter>
</ClCompile>
<ClCompile Include="HomingBullet.cpp">
<Filter>Source Files\Bullet Types</Filter>
</ClCompile>
<ClCompile Include="HubPauseMenu.cpp">
<Filter>Source Files\Interface</Filter>
</ClCompile>
<ClCompile Include="ItemHubLoadout.cpp">
<Filter>Source Files\Interface</Filter>
</ClCompile>
<ClCompile Include="GhostOfPirateCaptain.cpp">
<Filter>Source Files\Monster Strategies</Filter>
</ClCompile>
<ClCompile Include="GhostSaber.cpp">
<Filter>Source Files\Bullet Types</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="cpp.hint" />
@ -1478,6 +1556,15 @@
<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>
<Text Include="Chapter_3_FinalBoss.txt">
<Filter>Documentation\Mechanics</Filter>
</Text>
</ItemGroup>
<ItemGroup>
<Image Include="assets\heart.ico">

@ -82,6 +82,8 @@ All rights reserved.
#include "SteamKeyboardCallbackHandler.h"
#include "SteamStatsReceivedHandler.h"
#include "StageMaskPolygon.h"
#include <stacktrace>
#include <ranges>
INCLUDE_EMITTER_LIST
INCLUDE_ITEM_CATEGORIES
@ -90,8 +92,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};
@ -99,7 +101,7 @@ safemap<std::string,Animate2D::FrameSequence>ANIMATION_DATA;
std::vector<std::shared_ptr<Monster>>MONSTER_LIST;
std::unordered_map<MonsterSpawnerID,MonsterSpawner>SPAWNER_LIST;
std::vector<std::shared_ptr<DamageNumber>>DAMAGENUMBER_LIST;
std::vector<std::unique_ptr<IBullet>>BULLET_LIST;
std::vector<std::unique_ptr<IBullet>>BULLET_LIST{MAX_BULLETS};
std::optional<std::queue<MonsterSpawnerID>>SPAWNER_CONTROLLER;
safemap<std::string,Renderable>GFX;
utils::datafile DATA;
@ -281,7 +283,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 +391,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 +439,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;
@ -525,16 +528,20 @@ bool AiL::DownPressed(){
}
void AiL::HandleUserInput(float fElapsedTime){
if(!Menu::stack.empty()||GameState::STATE==GameState::states[States::DIALOG])return; //A window being opened means there's no user input allowed.
if(!Menu::stack.empty()||GameState::GetCurrentState()==States::DIALOG)return; //A window being opened means there's no user input allowed.
if(GetKey(SCROLL).bPressed)displayHud=!displayHud;
bool setIdleAnimation=true;
bool heldDownMovementKey=false; //Is true when a movement key has been held down.
if(KEY_MENU.Released()){
Menu::OpenMenu(MenuType::PAUSE);
if(GameState::GetCurrentState()==States::GAME_HUB)Menu::OpenMenu(MenuType::HUB_PAUSE);
else Menu::OpenMenu(MenuType::PAUSE);
}
float animationSpd=0.f;
if(player->GetPreviousPos()!=player->GetPos()){
player->previousPos=player->GetPos();
}
player->movementVelocity={};
if((player->GetVelocity().mag()<"Player.Move Allowed Velocity Lower Limit"_F&&player->CanMove())||(player->GetState()==State::ROLL&&"Thief.Right Click Ability.Roll Time"_F-player->rolling_timer>=0.2f)){
auto GetPlayerStaircaseDirection=[&](){
@ -814,6 +821,7 @@ void AiL::UpdateBullets(float fElapsedTime){
IBullet*b=(*it).get();
b->_Update(fElapsedTime);
}
if(BULLET_LIST.size()>MAX_BULLETS)ERR(std::format("WARNING! Bullet list reached greater than maximum expected size: {}. Consider expanding!!!",MAX_BULLETS));
std::erase_if(BULLET_LIST,[](std::unique_ptr<IBullet>&b){return b->IsDead();});
}
@ -946,99 +954,17 @@ void AiL::ProximityKnockback(const vf2d pos,const float radius,const float knock
}
void AiL::PopulateRenderLists(){
monstersBeforeLower.clear();
monstersAfterLower.clear();
monstersBeforeUpper.clear();
monstersAfterUpper.clear();
bulletsLower.clear();
bulletsUpper.clear();
backgroundEffectsLower.clear();
backgroundEffectsUpper.clear();
foregroundEffectsLower.clear();
foregroundEffectsUpper.clear();
endZones.clear();
upperEndZones.clear();
dropsBeforeLower.clear();
dropsAfterLower.clear();
dropsBeforeUpper.clear();
dropsAfterUpper.clear();
tilesWithCollision.clear();
tilesWithoutCollision.clear();
Player*pl=GetPlayer();
pl->rendered=false;
std::sort(MONSTER_LIST.begin(),MONSTER_LIST.end(),[](std::shared_ptr<Monster>&m1,std::shared_ptr<Monster>&m2){return m1->GetPos().y<m2->GetPos().y;});
std::sort(ItemDrop::drops.begin(),ItemDrop::drops.end(),[](ItemDrop&id1,ItemDrop&id2){return id1.GetPos().y<id2.GetPos().y;});
std::sort(BULLET_LIST.begin(),BULLET_LIST.end(),[](std::unique_ptr<IBullet>&b1,std::unique_ptr<IBullet>&b2){return b1->pos.y<b2->pos.y;});
std::sort(foregroundEffects.begin(),foregroundEffects.end(),[](std::unique_ptr<Effect>&e1,std::unique_ptr<Effect>&e2){return e1->pos.y<e2->pos.y;});
std::sort(backgroundEffects.begin(),backgroundEffects.end(),[](std::unique_ptr<Effect>&e1,std::unique_ptr<Effect>&e2){return e1->pos.y<e2->pos.y;});
for(auto it=MONSTER_LIST.begin();it!=MONSTER_LIST.end();++it){
Monster&m=**it;
if(m.GetPos().y<pl->GetPos().y){//This monster renders before the player does (behind the player)
if(m.OnUpperLevel()){
monstersBeforeUpper.push_back(&m);
}else{
monstersBeforeLower.push_back(&m);
}
} else {//This monster renders after the player does (in front of the player)
if(m.OnUpperLevel()){
monstersAfterUpper.push_back(&m);
}else{
monstersAfterLower.push_back(&m);
}
}
}
for(int i=0;i<ItemDrop::drops.size();i++){
ItemDrop&drop=ItemDrop::drops[i];
if(drop.GetPos().y<pl->GetPos().y){//This item drop renders before the player does (behind the player)
if(drop.OnUpperLevel()){
dropsBeforeUpper.push_back(i);
}else{
dropsBeforeLower.push_back(i);
}
} else {//This item drop renders after the player does (in front of the player)
if(drop.OnUpperLevel()){
dropsAfterUpper.push_back(i);
}else{
dropsAfterLower.push_back(i);
}
}
}
for(auto it=BULLET_LIST.begin();it!=BULLET_LIST.end();++it){
IBullet*b=(*it).get();
if(b->OnUpperLevel()){
bulletsUpper.push_back(b);
}else{
bulletsLower.push_back(b);
}
}
for(auto it=foregroundEffects.begin();it!=foregroundEffects.end();++it){
Effect*e=(*it).get();
if(e->OnUpperLevel()){
foregroundEffectsUpper.push_back(e);
}else{
foregroundEffectsLower.push_back(e);
}
}
for(auto it=backgroundEffects.begin();it!=backgroundEffects.end();++it){
Effect*e=(*it).get();
if(e->OnUpperLevel()){
backgroundEffectsUpper.push_back(e);
}else{
backgroundEffectsLower.push_back(e);
}
}
for(const ZoneData&zone:GetZones().at("EndZone")){
if(zone.isUpper){
upperEndZones.push_back(zone);
}else{
endZones.push_back(zone);
}
}
std::ranges::sort(MONSTER_LIST,[](std::shared_ptr<Monster>&m1,std::shared_ptr<Monster>&m2){return m1->GetPos().y<m2->GetPos().y;});
std::ranges::sort(ItemDrop::drops,[](ItemDrop&id1,ItemDrop&id2){return id1.GetPos().y<id2.GetPos().y;});
std::ranges::sort(BULLET_LIST,[](std::unique_ptr<IBullet>&b1,std::unique_ptr<IBullet>&b2){return b1->pos.y<b2->pos.y;});
std::ranges::sort(foregroundEffects,[](std::unique_ptr<Effect>&e1,std::unique_ptr<Effect>&e2){return e1->pos.y<e2->pos.y;});
std::ranges::sort(backgroundEffects,[](std::unique_ptr<Effect>&e1,std::unique_ptr<Effect>&e2){return e1->pos.y<e2->pos.y;});
}
void AiL::RenderTile(vi2d pos,TilesheetData tileSheet,int tileSheetIndex,vi2d tileSheetPos){
@ -1103,6 +1029,25 @@ void AiL::RenderWorld(float fElapsedTime){
PopulateRenderLists();
#pragma region Populate Render Lists
auto monstersBeforeUpper{MONSTER_LIST|std::views::filter([&pl](const std::shared_ptr<Monster>&m){return m->GetPos().y<pl->GetPos().y&&m->OnUpperLevel();})};
auto monstersBeforeLower{MONSTER_LIST|std::views::filter([&pl](const std::shared_ptr<Monster>&m){return m->GetPos().y<pl->GetPos().y&&!m->OnUpperLevel();})};
auto monstersAfterUpper{MONSTER_LIST|std::views::filter([&pl](const std::shared_ptr<Monster>&m){return m->GetPos().y>=pl->GetPos().y&&m->OnUpperLevel();})};
auto monstersAfterLower{MONSTER_LIST|std::views::filter([&pl](const std::shared_ptr<Monster>&m){return m->GetPos().y>=pl->GetPos().y&&!m->OnUpperLevel();})};
auto dropsBeforeUpper{ItemDrop::drops|std::views::filter([&pl](const ItemDrop&drop){return drop.GetPos().y<pl->GetPos().y&&drop.OnUpperLevel();})};
auto dropsBeforeLower{ItemDrop::drops|std::views::filter([&pl](const ItemDrop&drop){return drop.GetPos().y<pl->GetPos().y&&!drop.OnUpperLevel();})};
auto dropsAfterUpper{ItemDrop::drops|std::views::filter([&pl](const ItemDrop&drop){return drop.GetPos().y>=pl->GetPos().y&&drop.OnUpperLevel();})};
auto dropsAfterLower{ItemDrop::drops|std::views::filter([&pl](const ItemDrop&drop){return drop.GetPos().y>=pl->GetPos().y&&!drop.OnUpperLevel();})};
auto bulletsUpper{BULLET_LIST|std::views::filter([](const std::unique_ptr<IBullet>&bullet){return bullet->OnUpperLevel();})};
auto bulletsLower{BULLET_LIST|std::views::filter([](const std::unique_ptr<IBullet>&bullet){return !bullet->OnUpperLevel();})};
auto foregroundEffectsUpper{foregroundEffects|std::views::filter([](const std::unique_ptr<Effect>&effect){return effect->OnUpperLevel();})};
auto foregroundEffectsLower{foregroundEffects|std::views::filter([](const std::unique_ptr<Effect>&effect){return !effect->OnUpperLevel();})};
auto backgroundEffectsUpper{backgroundEffects|std::views::filter([](const std::unique_ptr<Effect>&effect){return effect->OnUpperLevel();})};
auto backgroundEffectsLower{backgroundEffects|std::views::filter([](const std::unique_ptr<Effect>&effect){return !effect->OnUpperLevel();})};
auto upperEndZones{ZONE_LIST["EndZone"]|std::views::filter([](const ZoneData&zone){return zone.isUpper;})};
auto endZones{ZONE_LIST["EndZone"]|std::views::filter([](const ZoneData&zone){return !zone.isUpper;})};
#pragma endregion
auto RenderPlayer=[&](vf2d pos,vf2d scale){
if(player->IsInvisible())return;
vf2d playerScale=vf2d(player->GetSizeMult(),player->GetSizeMult());
@ -1122,11 +1067,15 @@ void AiL::RenderWorld(float fElapsedTime){
const std::vector<Buff>movespeedBuffs{player->GetBuffs(BuffType::SPEEDBOOST)};
const std::vector<Buff>adrenalineRushBuffs{player->GetBuffs(BuffType::ADRENALINE_RUSH)};
const std::vector<Buff>damageReductionBuffs{player->GetBuffs(BuffType::DAMAGE_REDUCTION)};
const std::vector<Buff>inkSlowdownDebuff{player->GetBuffs(BuffType::INK_SLOWDOWN)};
Pixel playerCol{WHITE};
if(attackBuffs.size()>0)playerCol={255,uint8_t(255*abs(sin(1.4f*attackBuffs[0].duration))),uint8_t(255*abs(sin(1.4f*attackBuffs[0].duration)))};
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)))};
else if(inkSlowdownDebuff.size()>0)playerCol={uint8_t(255*abs(sin(2.f*inkSlowdownDebuff[0].duration))),uint8_t(255*abs(sin(2.f*inkSlowdownDebuff[0].duration))),uint8_t(255*abs(sin(2.f*inkSlowdownDebuff[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))))});
@ -1450,7 +1399,7 @@ void AiL::RenderWorld(float fElapsedTime){
break;
}
while(dropsBeforeLowerIt!=dropsBeforeLower.end()){
const ItemDrop&drop=ItemDrop::drops[*dropsBeforeLowerIt];
const ItemDrop&drop{*dropsBeforeLowerIt};
if(drop.pos.y<topTileY){
drop.Draw();
++dropsBeforeLowerIt;
@ -1466,7 +1415,7 @@ void AiL::RenderWorld(float fElapsedTime){
++monstersBeforeLowerIt;
}
while(dropsBeforeLowerIt!=dropsBeforeLower.end()){
const ItemDrop&drop=ItemDrop::drops[*dropsBeforeLowerIt];
const ItemDrop&drop{*dropsBeforeLowerIt};
drop.Draw();
++dropsBeforeLowerIt;
}
@ -1489,7 +1438,7 @@ void AiL::RenderWorld(float fElapsedTime){
break;
}
while(dropsAfterLowerIt!=dropsAfterLower.end()){
const ItemDrop&drop=ItemDrop::drops[*dropsAfterLowerIt];
const ItemDrop&drop{*dropsAfterLowerIt};
if(drop.pos.y<topTileY){
drop.Draw();
++dropsAfterLowerIt;
@ -1511,7 +1460,7 @@ void AiL::RenderWorld(float fElapsedTime){
++monstersBeforeLowerIt;
}
while(dropsBeforeLowerIt!=dropsBeforeLower.end()){
const ItemDrop&drop=ItemDrop::drops[*dropsBeforeLowerIt];
const ItemDrop&drop{*dropsBeforeLowerIt};
drop.Draw();
++dropsBeforeLowerIt;
}
@ -1532,7 +1481,7 @@ void AiL::RenderWorld(float fElapsedTime){
break;
}
while(dropsAfterLowerIt!=dropsAfterLower.end()){
const ItemDrop&drop=ItemDrop::drops[*dropsAfterLowerIt];
const ItemDrop&drop{*dropsAfterLowerIt};
if(drop.pos.y<tile->group->GetCollisionRange().middle().y){
drop.Draw();
++dropsAfterLowerIt;
@ -1562,16 +1511,16 @@ void AiL::RenderWorld(float fElapsedTime){
#pragma endregion
#pragma region Remaining Rendering
while(monstersBeforeLowerIt!=monstersBeforeLower.end()){
Monster*const m=*monstersBeforeLowerIt;
m->Draw();
const Monster&m=**monstersBeforeLowerIt;
m.Draw();
++monstersBeforeLowerIt;
}
for(const Effect*const e:backgroundEffectsLower){
for(std::unique_ptr<Effect>&e:backgroundEffectsLower){
e->_Draw();
}
while(dropsBeforeLowerIt!=dropsBeforeLower.end()){
const int dropInd=*dropsBeforeLowerIt;
ItemDrop::drops[dropInd].Draw();
const ItemDrop&drop{*dropsBeforeLowerIt};
drop.Draw();
++dropsBeforeLowerIt;
}
if(!player->rendered&&!player->upperLevel){
@ -1580,22 +1529,24 @@ void AiL::RenderWorld(float fElapsedTime){
vf2d shadowScale=vf2d{8*player->GetSizeMult()/3.f,1}/std::max(1.f,player->GetZ()/24);
view.DrawDecal(player->GetPos()-vf2d{3,3}*shadowScale/2+vf2d{0,6*player->GetSizeMult()},GFX["circle.png"].Decal(),shadowScale,BLACK);
}
if(player->IsUsingAdditiveBlending())SetDecalMode(DecalMode::ADDITIVE);
else SetDecalMode(DecalMode::NORMAL);
RenderPlayer(player->GetPos(),{1,1});
}
while(monstersAfterLowerIt!=monstersAfterLower.end()){
Monster*const m=*monstersAfterLowerIt;
m->Draw();
const Monster&m=**monstersAfterLowerIt;
m.Draw();
++monstersAfterLowerIt;
}
while(dropsAfterLowerIt!=dropsAfterLower.end()){
const int dropInd=*dropsAfterLowerIt;
ItemDrop::drops[dropInd].Draw();
const ItemDrop&drop{*dropsAfterLowerIt};
drop.Draw();
++dropsAfterLowerIt;
}
for(const IBullet*const b:bulletsLower){
for(std::unique_ptr<IBullet>&b:bulletsLower){
b->_Draw();
}
for(const Effect*const e:foregroundEffectsLower){
for(std::unique_ptr<Effect>&e:foregroundEffectsLower){
e->_Draw();
}
#pragma endregion
@ -1743,7 +1694,7 @@ void AiL::RenderWorld(float fElapsedTime){
break;
}
while(dropsBeforeUpperIt!=dropsBeforeUpper.end()){
const ItemDrop&drop=ItemDrop::drops[*dropsBeforeUpperIt];
const ItemDrop&drop{*dropsBeforeUpperIt};
if(drop.pos.y<topTileY){
drop.Draw();
++dropsBeforeUpperIt;
@ -1759,7 +1710,7 @@ void AiL::RenderWorld(float fElapsedTime){
++monstersBeforeUpperIt;
}
while(dropsBeforeUpperIt!=dropsBeforeUpper.end()){
const ItemDrop&drop=ItemDrop::drops[*dropsBeforeUpperIt];
const ItemDrop&drop{*dropsBeforeUpperIt};
drop.Draw();
++dropsBeforeUpperIt;
}
@ -1768,6 +1719,8 @@ void AiL::RenderWorld(float fElapsedTime){
vf2d shadowScale=vf2d{8*player->GetSizeMult()/3.f,1}/std::max(1.f,player->GetZ()/24);
view.DrawDecal(player->GetPos()-vf2d{3,3}*shadowScale/2+vf2d{0,6*player->GetSizeMult()},GFX["circle.png"].Decal(),shadowScale,BLACK);
}
if(player->IsUsingAdditiveBlending())SetDecalMode(DecalMode::ADDITIVE);
else SetDecalMode(DecalMode::NORMAL);
RenderPlayer(player->GetPos(),{1,1});
}
while(monstersAfterUpperIt!=monstersAfterUpper.end()){
@ -1780,7 +1733,7 @@ void AiL::RenderWorld(float fElapsedTime){
break;
}
while(dropsAfterUpperIt!=dropsAfterUpper.end()){
const ItemDrop&drop=ItemDrop::drops[*dropsAfterUpperIt];
const ItemDrop&drop{*dropsAfterUpperIt};
if(drop.pos.y<topTileY){
drop.Draw();
++dropsAfterUpperIt;
@ -1804,7 +1757,7 @@ void AiL::RenderWorld(float fElapsedTime){
break;
}
while(dropsBeforeUpperIt!=dropsBeforeUpper.end()){
const ItemDrop&drop=ItemDrop::drops[*dropsBeforeUpperIt];
const ItemDrop&drop{*dropsBeforeUpperIt};
if(drop.pos.y<tile->group->GetCollisionRange().middle().y){
drop.Draw();
++dropsBeforeUpperIt;
@ -1820,7 +1773,7 @@ void AiL::RenderWorld(float fElapsedTime){
++monstersBeforeUpperIt;
}
while(dropsBeforeUpperIt!=dropsBeforeUpper.end()){
const ItemDrop&drop=ItemDrop::drops[*dropsBeforeUpperIt];
const ItemDrop&drop{*dropsBeforeUpperIt};
drop.Draw();
++dropsBeforeUpperIt;
}
@ -1829,6 +1782,8 @@ void AiL::RenderWorld(float fElapsedTime){
vf2d shadowScale=vf2d{8*player->GetSizeMult()/3.f,1}/std::max(1.f,player->GetZ()/24);
view.DrawDecal(player->GetPos()-vf2d{3,3}*shadowScale/2+vf2d{0,6*player->GetSizeMult()},GFX["circle.png"].Decal(),shadowScale,BLACK);
}
if(player->IsUsingAdditiveBlending())SetDecalMode(DecalMode::ADDITIVE);
else SetDecalMode(DecalMode::NORMAL);
RenderPlayer(player->GetPos(),{1,1});
}
while(monstersAfterUpperIt!=monstersAfterUpper.end()){
@ -1841,7 +1796,7 @@ void AiL::RenderWorld(float fElapsedTime){
break;
}
while(dropsAfterUpperIt!=dropsAfterUpper.end()){
const ItemDrop&drop=ItemDrop::drops[*dropsAfterUpperIt];
const ItemDrop&drop{*dropsAfterUpperIt};
if(drop.pos.y<tile->group->GetCollisionRange().middle().y){
drop.Draw();
++dropsAfterUpperIt;
@ -1863,16 +1818,16 @@ void AiL::RenderWorld(float fElapsedTime){
#pragma endregion
#pragma region Remaining Upper Rendering
while(monstersBeforeUpperIt!=monstersBeforeUpper.end()){
Monster*const m=*monstersBeforeUpperIt;
m->Draw();
const Monster&m=**monstersBeforeUpperIt;
m.Draw();
++monstersBeforeUpperIt;
}
for(const Effect*const e:backgroundEffectsUpper){
for(std::unique_ptr<Effect>&e:backgroundEffectsUpper){
e->_Draw();
}
while(dropsBeforeUpperIt!=dropsBeforeUpper.end()){
const int dropInd=*dropsBeforeUpperIt;
ItemDrop::drops[dropInd].Draw();
const ItemDrop&drop{*dropsBeforeUpperIt};
drop.Draw();
++dropsBeforeUpperIt;
}
if(!player->rendered&&player->upperLevel){
@ -1884,19 +1839,19 @@ void AiL::RenderWorld(float fElapsedTime){
RenderPlayer(player->GetPos(),{1,1});
}
while(monstersAfterUpperIt!=monstersAfterUpper.end()){
Monster*const m=*monstersAfterUpperIt;
m->Draw();
const Monster&m=**monstersAfterUpperIt;
m.Draw();
++monstersAfterUpperIt;
}
while(dropsAfterUpperIt!=dropsAfterUpper.end()){
const int dropInd=*dropsAfterUpperIt;
ItemDrop::drops[dropInd].Draw();
const ItemDrop&drop{*dropsAfterUpperIt};
drop.Draw();
++dropsAfterUpperIt;
}
for(const IBullet*const b:bulletsUpper){
for(std::unique_ptr<IBullet>&b:bulletsUpper){
b->_Draw();
}
for(const Effect*const e:foregroundEffectsUpper){
for(std::unique_ptr<Effect>&e:foregroundEffectsUpper){
e->_Draw();
}
#pragma endregion
@ -2035,9 +1990,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 +2072,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 +2404,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 +2644,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 +2671,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 +2684,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);
@ -3068,6 +3026,17 @@ int main(const int argn,char**args)
LOG(std::format("Found {} args",argn));
bool usingSteam=true;
std::set_terminate([](){
try{
std::exception_ptr eptr{std::current_exception()};
if(eptr)std::rethrow_exception(eptr);
else std::cerr<<"Exiting without exception\n";
}catch(const std::exception& ex){std::cerr<<"Exception: "<<ex.what()<< '\n';}
catch(...){std::cerr << "Unknown exception caught\n";}
LOG(std::stacktrace::current());
std::exit(EXIT_FAILURE);
});
for(int i=0;i<argn;i++){
if(std::string(args[i])=="nosteam"){
LOG("nosteam flag detected. Disabling steam API...");
@ -3925,31 +3894,34 @@ 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]);
Component<MenuItemItemButton>(MenuType::ITEM_HUB_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]);
Component<MenuItemItemButton>(MenuType::ITEM_HUB_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]);
Component<MenuItemItemButton>(MenuType::ITEM_HUB_LOADOUT,"Loadout Item 3")->SetItem(loadout[slot]);
}break;
}
@ -3970,13 +3942,15 @@ 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);
if(GameState::GetCurrentState()!=States::GAME_HUB){
Inventory::AddLoadoutItemUsed(GetLoadoutItem(slot).lock()->ActualName(),slot);
GetLoadoutItem(slot).lock()->amt--;
}
if(GetLoadoutItem(slot).lock()->UseSound().length()>0){
SoundEffect::PlaySFX(GetLoadoutItem(slot).lock()->UseSound(),SoundEffect::CENTERED);
}
@ -4004,27 +3978,38 @@ 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);
Component<MenuItemItemButton>(MenuType::ITEM_LOADOUT,"Loadout Item 1")->UpdateIcon();
Component<MenuItemItemButton>(MenuType::ITEM_HUB_LOADOUT,"Loadout Item 1")->SetItem(Item::BLANK);
Component<MenuItemItemButton>(MenuType::ITEM_HUB_LOADOUT,"Loadout Item 1")->UpdateIcon();
}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);
Component<MenuItemItemButton>(MenuType::ITEM_LOADOUT,"Loadout Item 2")->UpdateIcon();
Component<MenuItemItemButton>(MenuType::ITEM_HUB_LOADOUT,"Loadout Item 2")->SetItem(Item::BLANK);
Component<MenuItemItemButton>(MenuType::ITEM_HUB_LOADOUT,"Loadout Item 2")->UpdateIcon();
}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);
Component<MenuItemItemButton>(MenuType::ITEM_LOADOUT,"Loadout Item 3")->UpdateIcon();
Component<MenuItemItemButton>(MenuType::ITEM_HUB_LOADOUT,"Loadout Item 3")->SetItem(Item::BLANK);
Component<MenuItemItemButton>(MenuType::ITEM_HUB_LOADOUT,"Loadout Item 3")->UpdateIcon();
}break;
}
}
@ -4040,7 +4025,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
@ -4071,7 +4056,7 @@ bool AiL::GamePaused(){
return
fadeOutDuration>0
||disableFadeIn
||Menu::IsMenuOpen()&&Menu::stack.front()==Menu::menus[MenuType::PAUSE]/*The pause menu would be the only thing open and would be the menu in front.*/
||Menu::IsMenuOpen()&&(Menu::stack.front()==Menu::menus[MenuType::PAUSE]||Menu::stack.front()==Menu::menus[MenuType::HUB_PAUSE])/*The pause menu would be the only thing open and would be the menu in front.*/
||LoadingScreen::loading;
}

@ -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,12 +168,11 @@ 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;
std::vector<TileRenderData*>tilesWithCollision,tilesWithoutCollision;
std::vector<int>dropsBeforeLower,dropsAfterLower,dropsBeforeUpper,dropsAfterUpper;
std::vector<ZoneData>endZones,upperEndZones;
std::vector<vf2d>circleCooldownPoints;
std::vector<vf2d>squareCircleCooldownPoints;
std::map<std::string,TilesetData>MAP_TILESETS;
@ -188,9 +188,6 @@ private:
int bridgeLayerIndex=-1;
float bridgeFadeFactor=0.f;
int DEBUG_PATHFINDING=0;
std::vector<Monster*>monstersBeforeLower,monstersAfterLower,monstersBeforeUpper,monstersAfterUpper;
std::vector<IBullet*>bulletsLower,bulletsUpper;
std::vector<Effect*>backgroundEffectsLower,backgroundEffectsUpper,foregroundEffectsLower,foregroundEffectsUpper;
float reflectionUpdateTimer=0;
float reflectionStepTime=0;
std::set<vi2d>visibleTiles;
@ -362,7 +359,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,9 @@ 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}});
CreateHorizontalAnimationSequence("ghost_dagger.png",3,{24,24},{0.1f,Animate2D::Style::PingPong});
CreateStillAnimation("meteor.png",{192,192});
@ -433,6 +436,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,9 +462,13 @@ 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}});
CreateHorizontalAnimationSequence("burstbullet.png",4,{24,24},AnimationData{.frameDuration{0.1f},.style{Animate2D::Style::OneShot}});
CreateHorizontalAnimationSequence("inkbubble_explode.png",4,{24,24},AnimationData{.frameDuration{0.1f},.style{Animate2D::Style::OneShot}});
//!!!!!WARNING!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//DO NOT CREATE MORE ANIMATION SEQUENCES UNDERNEATH HERE AS THE NEXT BLOCK WILL CREATE DEFAULT ANIMATIONS
//FOR ALL NON-SPECIFIED ANIMATION SEQUENCES!

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

@ -65,6 +65,7 @@ enum BuffType{
CURSE_OF_PAIN,
CURSE_OF_DEATH,
AFFECTED_BY_LIGHTNING_BOLT, //Intensity indicates number of repeats remaining.
INK_SLOWDOWN, //Intensity indicates % movespd slowdown.
};
enum class BuffRestorationType{
ONE_OFF, //This is used as a hack fix for the RestoreDuringCast Item script since they require us to restore 1 tick immediately. Over time buffs do not apply a tick immediately.

@ -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);
};

@ -40,6 +40,9 @@ All rights reserved.
#include "Direction.h"
#include "Effect.h"
#include "TrailEffect.h"
#include "Entity.h"
#include <variant>
#include <memory>
struct EnergyBolt:public Bullet{
float lastParticleSpawn=0;
@ -98,10 +101,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 +154,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 +167,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()!!
@ -276,9 +282,9 @@ private:
const float knockbackAmt;
};
struct FallingStone:public Bullet{
//The position for this bullet represents where the falling stone should land.
FallingStone(vf2d targetPos,vf2d vel,float zVel,float indicatorDisplayTime,float radius,int damage,bool upperLevel,bool hitsMultiple=false,float knockbackAmt=0.f,float lifetime=INFINITE,bool friendly=false,Pixel col=WHITE,vf2d scale={1,1},float image_angle=0.f,float spellCircleRotation=0.f,float spellCircleRotationSpd=0.f,Pixel insigniaCol=WHITE,float insigniaRotation=0.f,float insigniaRotationSpd=0.f);
struct FallingBullet:public Bullet{
//The position for this bullet represents where the falling bullet should land.
FallingBullet(const std::string&imgName,vf2d targetPos,vf2d vel,float zVel,float indicatorDisplayTime,float radius,int damage,bool upperLevel,bool hitsMultiple=false,float knockbackAmt=0.f,float lifetime=INFINITE,bool friendly=false,Pixel spellCircleCol=WHITE,vf2d scale={1,1},float image_angle=0.f,float spellCircleRotation=0.f,float spellCircleRotationSpd=0.f,Pixel insigniaCol=WHITE,float insigniaRotation=0.f,float insigniaRotationSpd=0.f);
protected:
void Update(float fElapsedTime)override;
void Draw(const Pixel blendCol)const override;
@ -290,6 +296,7 @@ private:
SpellCircle indicator;
const float knockbackAmt;
float lastTrailEffect{};
const float collisionRadius{};
};
//While not a bullet directly, the DeadlyDash class generates a bunch of afterimages and collision checks.
@ -348,19 +355,109 @@ 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;
};
struct BurstBullet:public Bullet{
BurstBullet(vf2d pos,vf2d vel,const Entity target,const float explodeDist,const int extraBulletCount,const float extraBulletHeadingAngleChange,const float extraBulletRadius,const vf2d extraBulletScale,const float extraBulletStartSpeed,const float extraBulletAcc,float radius,int damage,bool upperLevel,bool friendly,Pixel col,vf2d scale);
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:
void Explode();
const Entity target;
const float explodeDist;
Oscillator<vf2d>scaleOscillator;
bool activated{false};
float explodeTimer{0.f};
const int extraBulletCount;
const float extraBulletHeadingAngleChange; //In radians.
const float extraBulletRadius;
const vf2d extraBulletScale;
const float extraBulletAcc;
const float extraBulletStartSpeed;
};
struct RotateBullet:public Bullet{
//headingAngleChange determines how the startingAngle adjusts over time (in radians).
RotateBullet(vf2d pos,float startingAng,float startSpeed,float acc,float headingAngleChange,float radius,int damage,bool upperLevel,bool friendly,Pixel col);
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:
const float headingAngleChange;
};
struct InkBullet:public Bullet{
InkBullet(const vf2d pos,const vf2d targetPos,const vf2d vel,const float inkExplosionRadius,const float inkPuddleLifetime,const float inkSlowdownDuration,const float inkSlowdownPct,const float inkPuddleRadius,const bool upperLevel,const bool friendly);
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:
void Splat();
const vf2d targetPos;
const float inkExplosionRadius;
const float inkSlowdownPct;
const float inkSlowdownDuration;
const float inkPuddleLifetime;
const float inkPuddleRadius;
};
struct HomingBullet:public Bullet{
HomingBullet(const vf2d pos,const Entity target,const float rotateTowardsSpeed,const float rotateTowardsSpeedCoveredInInk,const float lifetime,const float speed,const float radius,const int damage,const bool upperLevel,const bool friendly=false,const Pixel col=WHITE,const vf2d scale={1,1},const float image_angle=0.f);
void Update(float fElapsedTime)override;
void ModifyOutgoingDamageData(HurtDamageInfo&data);
private:
const Entity target;
const float rotateTowardsSpeed;
const float rotateTowardsSpeedCoveredInInk;
};
struct GhostSaber:public Bullet{
GhostSaber(const vf2d pos,const std::weak_ptr<Monster>target,const float lifetime,const float distFromTarget,const float knockbackAmt,const float initialRot,const float radius,const float expandSpd,const int damage,const bool upperLevel,const float rotSpd,const bool friendly=false,const Pixel col=WHITE,const vf2d scale={1,1},const float image_angle=0.f);
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:
const std::weak_ptr<Monster>attachedMonster;
const float rotSpd;
float distFromTarget;
float rot;
const float knockbackAmt;
float particleTimer{};
float aliveTime{};
float expandSpd{};
Oscillator<uint8_t>alphaOscillator{128U,255U,0.6f};
};

@ -0,0 +1,104 @@
#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 "Effect.h"
#include "AdventuresInLestoria.h"
#include "DEFINES.h"
#include "util.h"
#include "SoundEffect.h"
#include "TrailEffect.h"
#include <ranges>
INCLUDE_game
INCLUDE_MONSTER_LIST
INCLUDE_ANIMATION_DATA
BurstBullet::BurstBullet(vf2d pos,vf2d vel,const Entity target,const float explodeDist,const int extraBulletCount,const float extraBulletHeadingAngleChange,const float extraBulletRadius,const vf2d extraBulletScale,const float extraBulletStartSpeed,const float extraBulletAcc,float radius,int damage,bool upperLevel,bool friendly,Pixel col,vf2d scale)
:Bullet(pos,vel,radius,damage,"burstrotatebullet.png",upperLevel,false,INFINITE,true,friendly,col,scale),extraBulletStartSpeed(extraBulletStartSpeed),extraBulletAcc(extraBulletAcc),extraBulletRadius(extraBulletRadius),extraBulletScale(extraBulletScale),extraBulletHeadingAngleChange(extraBulletHeadingAngleChange),extraBulletCount(extraBulletCount),target(target),explodeDist(explodeDist),scaleOscillator({0.85f,0.85f},{1.f,1.f},0.3f){
animation.mult=0.f; //Prevent animating.
}
void BurstBullet::Explode(){
fadeOutTime=0.1f;
HurtType targets{friendly?HurtType::MONSTER:HurtType::PLAYER};
for(const auto&[target,isHurt]:game->Hurt(pos,radius,damage,OnUpperLevel(),GetZ(),targets)){
if(isHurt){
Entity ent{target};
ent.SetIframeTime(0.3f);
}
}
SoundEffect::PlaySFX("Burst Bullet Explode",pos);
for(int i:std::ranges::iota_view(0,extraBulletCount+1)){
const float angle{(360.f/extraBulletCount)*i};
const vf2d newPos{pos+vf2d{radius/2.f,angle}.cart()};
CreateBullet(RotateBullet)(newPos,angle,extraBulletStartSpeed,extraBulletAcc,extraBulletHeadingAngleChange,extraBulletRadius,damage,OnUpperLevel(),friendly,WHITE)EndBullet;
}
Deactivate();
}
void BurstBullet::Update(float fElapsedTime){
if(IsActivated()){
if(!activated){
scaleOscillator.Update(fElapsedTime);
if(util::distance(target.GetPos(),pos)<=explodeDist){
activated=true;
animation.mult=1.f;
explodeTimer=ANIMATION_DATA[animation.currentStateName].GetTotalAnimationDuration();
}
}else if(explodeTimer>0.f){
explodeTimer-=fElapsedTime;
if(explodeTimer<=0.f)Explode();
}
}
}
BulletDestroyState BurstBullet::PlayerHit(Player*player){
Explode();
player->ApplyIframes(0.2f);
return BulletDestroyState::KEEP_ALIVE;
}
BulletDestroyState BurstBullet::MonsterHit(Monster&monster,const uint8_t markStacksBeforeHit){
Explode();
monster.ApplyIframes(0.2f);
return BulletDestroyState::KEEP_ALIVE;
}
void BurstBullet::ModifyOutgoingDamageData(HurtDamageInfo&data){
data.damage=0; //Nullify the damage because proximity damage will occur instead.
}

@ -0,0 +1,49 @@
Ghost of Pirate Captain
60 000 HP
Size 600%
Movespeed 50%
Damage:
Canonshot 80 dmg
Shrapnel 35 dmg
Sabres 75 dmg
Collision 50 dmg
Curse nothing for first 15 seconds, after that 5 dmg per second.
----------------------
Skills:
- Command Canons (2 Kinds of Attack, Big Canon Balls and spreaded shrapnel shot)
- Spreading the curse (The Ghost Throws a Coin towards the Player, and the Player gets marked. starts getting dot damage after time if the coin doesnt get returned to the Pirate Treasure, which has a fixed position on the map)
- Hide and Seek (The Ghost hides Behind a Rock, and Canons Bombard the area until player hits the Captain once)
- Ghost Sabers (Spinning Ghost Swords)
- 70%, 40% & 10% Curse + Hide - permanent Bombardment + Shrapnel Shot every 5 seconds.
Once found on the 10% Hide and Seek Spawns a Cage around the Pirate Treasure, and Spreads the curse on the player again. (If player didnt cleanse the curse before finding the captain it just stays up.)
Both kind of Canon attacks happen at all time now, 10 Ghost Sabers spawn that circle around the boss in different directions
Boss stands still in the middle (maybe raising an arm animation wise or something to show that he is controlling the Sabers)
----------------------
Normal Fight
Canon Shots happen with a 0,4 second delay. between shooting an impact is a 2.5 second delay.
Canon have a 350 impact size
Shrapnel shoots similar to small bolders from chapter 2 boss. 25 hits, 50 radius. Delay between shooting and impact also 2.5 seconds.
its 8 canon shots, 2 second silence, 1 shrapnel shot, 2 second silence, repeat.
the Canon shots can happen in one of the following patterns:
- Bombardment: every shot is at a random location within 900 Range of the player. (This is also the move during hide and seek.)
- precise Bombardment: same as before but within 700 range of the player.
- Shooting in a line: the 4th or 5th hit would hit the player if the player doesnt move at all.
- Sharpshooter: aiming directly at the player, skipping every 2nd shot. (only 4 instead of 8 shots)
- prediction: shoots in the direction of the current or last players movement and predicts where the player would be in 2.8 seconds. (slightly further then where the player would be at impact. thats why 2.8 instead of 2.5 seconds)
Boss spawns after the first Canon rotation ends.
Boss constantly Slowly floats towards player, within 400 range it uses Ghost Sabers once every 4 seconds. (Throws them, Similar movement like Hammers of a Hammerdin in Diablo 2 - https://www.youtube.com/watch?v=jZfH4SY4Lsc - both the spinning animation and the flightpath are a good example. Sabre always spawn of oposite side towards player to give meeles time to dodge.)
On Collision damage trigger a slash animation with its saber.

@ -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;
@ -80,6 +81,8 @@ using MonsterSpawnerID=int;
#undef INFINITE
#define INFINITE 999999
#define MAX_BULLETS size_t(512)
#define SETUP_CLASS(class) \
class::class() \
:Player::Player(){} \
@ -121,6 +124,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,8 +143,18 @@ void DamageNumber::Draw(){
float DamageNumber::GetOriginalRiseSpd(){
float riseSpd{20.f};
if(type==INTERRUPT||type==MANA_GAIN)riseSpd=40.f;
if(type==DOT)riseSpd=-10.f;
switch(type){
case INTERRUPT:
case HEALTH_GAIN:{
riseSpd=40.f;
}break;
case MANA_GAIN:{
riseSpd=-20.f;
}break;
case DOT:{
riseSpd=-10.f;
}break;
}
return riseSpd;
}

@ -44,15 +44,19 @@ INCLUDE_ANIMATION_DATA
INCLUDE_game
INCLUDE_GFX
//Note: The fadeout time activates when the original lifetime of the bullet expires! It doesn't begin immediately.
Effect::Effect(vf2d pos,float lifetime,const std::string&imgFile,bool upperLevel,float size,float fadeout,vf2d spd,Pixel col,float rotation,float rotationSpd,bool additiveBlending)
:Effect(pos,lifetime,imgFile,upperLevel,0.f,fadeout,vf2d{size,size},spd,col,rotation,rotationSpd,additiveBlending){}
//Note: The fadeout time activates when the original lifetime of the bullet expires! It doesn't begin immediately.
Effect::Effect(vf2d pos,float lifetime,const std::string&imgFile,bool upperLevel,vf2d size,float fadeout,vf2d spd,Pixel col,float rotation,float rotationSpd,bool additiveBlending)
:Effect(pos,lifetime,imgFile,upperLevel,0.f,fadeout,size,spd,col,rotation,rotationSpd,additiveBlending){}
//Note: The fadeout time activates when the original lifetime of the bullet expires! It doesn't begin immediately.
Effect::Effect(vf2d pos,float lifetime,const std::string&imgFile,bool upperLevel,float fadein,float fadeout,vf2d size,vf2d spd,Pixel col,float rotation,float rotationSpd,bool additiveBlending)
:Effect(pos,lifetime,imgFile,upperLevel,fadein,fadeout,size,spd,EffectType::NONE,col,rotation,rotationSpd,additiveBlending){}
//Note: The fadeout time activates when the original lifetime of the bullet expires! It doesn't begin immediately.
Effect::Effect(vf2d pos,float lifetime,const std::string&imgFile,bool upperLevel,float fadein,float fadeout,vf2d size,vf2d spd,EffectType type,Pixel col,float rotation,float rotationSpd,bool additiveBlending)
:pos(pos),lifetime(lifetime),upperLevel(upperLevel),size(size),fadein(fadein),original_fadeInTime(fadein),fadeout(fadeout),original_fadeOutTime(fadeout),spd(spd),col(col),rotation(rotation),rotationSpd(rotationSpd),additiveBlending(additiveBlending),type(type){
this->animation.AddState(imgFile,ANIMATION_DATA.at(imgFile));

@ -55,7 +55,7 @@ enum class EffectType{
struct Effect{
friend class AiL;
friend struct FallingStone;
friend struct FallingBullet;
vf2d pos={0,0};
float lifetime=0;
float fadeout=0;
@ -204,13 +204,24 @@ 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{};
};
struct Ink:Effect{
//NOTE: The way ink works is that every update frame, each ink stack will check for distance to player, and if the player collides with the radius of the ink it will add one to the ink debuff for the player.
// At the end of AdventuresInLestoria::HandleUserInput(), the ink stack counter is reset after handling user input for moving the player.
Ink(vf2d pos,float radius,float lifetime,float inkSlowdownDuration,float inkSlowdownPct,float fadeinTime,float fadeoutTime,vf2d size,Pixel col,bool onUpperLevel,float rotation);
virtual bool Update(float fElapsedTime)override final;
private:
const float radius;
const float inkSlowdownDuration;
const float inkSlowdownAmount;
};

@ -0,0 +1,60 @@
#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 "Entity.h"
#include "Player.h"
#define is(type) std::holds_alternative<type>(entity)
#define get(type) std::get<type>(entity)
Entity::Entity(Player*player):entity(player){}
Entity::Entity(Monster*monster):entity(monster){}
Entity::Entity(const std::variant<Monster*,Player*>ent):entity(ent){}
const vf2d Entity::GetPos()const{
if(is(Player*))return get(Player*)->GetPos();
if(is(Monster*))return get(Monster*)->GetPos();
ERR("Entity is not a valid type! THIS SHOULD NOT BE HAPPENING!");
return {}; //NOTE: Unreachable
}
void Entity::SetIframeTime(const float iframeTime){
if(is(Player*))get(Player*)->ApplyIframes(iframeTime);
else if(is(Monster*))get(Monster*)->ApplyIframes(iframeTime);
else ERR("Entity is not a valid type! THIS SHOULD NOT BE HAPPENING!");
}

@ -0,0 +1,56 @@
#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 <variant>
class Player;
class Monster;
//A helper class to connect multiple entity types based on their commonalities without using inheritance shenanigans.
class Entity{
public:
Entity(Player*player);
Entity(Monster*monster);
Entity(const std::variant<Monster*,Player*>ent);
const vf2d GetPos()const;
void SetIframeTime(const float iframeTime);
private:
const std::variant<Monster*,Player*>entity;
};

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

@ -43,6 +43,7 @@ All rights reserved.
#include <memory>
#include <source_location>
#include <fstream>
#include <stacktrace>
inline std::ofstream debugLogger;
@ -80,7 +81,9 @@ inline std::ofstream debugLogger;
}
inline static void log(std::stringstream&str,std::source_location loc){
debugLogger<<loc.file_name()<<"("<<loc.line()<<":"<<loc.column()<<") "<<loc.function_name()<<": "<<str.str()<<std::endl;
debugLogger<<std::stacktrace::current()<<std::endl;
std::cout<<loc.file_name()<<"("<<loc.line()<<":"<<loc.column()<<") "<<loc.function_name()<<": "<<str.str()<<std::endl;
std::cout<<std::stacktrace::current()<<std::endl;
throw std::runtime_error{std::format("{}({}:{}) {}: {}",loc.file_name(),loc.line(),loc.column(),loc.function_name(),str.str()).c_str()};
}
};

@ -45,14 +45,14 @@ All rights reserved.
INCLUDE_game
FallingStone::FallingStone(vf2d targetPos,vf2d vel,float zVel,float indicatorDisplayTime,float radius,int damage,bool upperLevel,bool hitsMultiple,float knockbackAmt,float lifetime,bool friendly,Pixel col,vf2d scale,float image_angle,float spellCircleRotation,float spellCircleRotationSpd,Pixel insigniaCol,float insigniaRotation,float insigniaRotationSpd)
:Bullet(targetPos,vel,radius,damage,"rock.png",upperLevel,false,lifetime+0.1f,false,friendly,col,scale,image_angle),targetPos(targetPos),zVel(zVel),indicatorDisplayTime(indicatorDisplayTime),knockbackAmt(knockbackAmt),
indicator(targetPos,lifetime+0.1f,"range_indicator.png","spell_insignia.png",upperLevel,radius/12.f,0.5f,{},col,spellCircleRotation,spellCircleRotationSpd,false,radius/12.f,0.f,{},insigniaCol,insigniaRotation,insigniaRotationSpd,false){
FallingBullet::FallingBullet(const std::string&imgName,vf2d targetPos,vf2d vel,float zVel,float indicatorDisplayTime,float radius,int damage,bool upperLevel,bool hitsMultiple,float knockbackAmt,float lifetime,bool friendly,Pixel spellCircleCol,vf2d scale,float image_angle,float spellCircleRotation,float spellCircleRotationSpd,Pixel insigniaCol,float insigniaRotation,float insigniaRotationSpd)
:Bullet(targetPos,vel,0.f,damage,imgName,upperLevel,false,lifetime+0.1f,false,friendly,WHITE,scale,image_angle),targetPos(targetPos),zVel(zVel),indicatorDisplayTime(indicatorDisplayTime),knockbackAmt(knockbackAmt),collisionRadius(radius),
indicator(targetPos,lifetime+0.1f,"range_indicator.png","spell_insignia.png",upperLevel,radius/12.f,0.5f,{},spellCircleCol,spellCircleRotation,spellCircleRotationSpd,false,radius/12.f,0.f,{},insigniaCol,insigniaRotation,insigniaRotationSpd,false){
pos+=-vel*lifetime;
z=-zVel*lifetime;
}
void FallingStone::Update(float fElapsedTime){
void FallingBullet::Update(float fElapsedTime){
z+=zVel*fElapsedTime;
lastTrailEffect-=fElapsedTime;
if(z<=0.f){
@ -62,16 +62,15 @@ void FallingStone::Update(float fElapsedTime){
fadeOutTime=0.5f;
SoundEffect::PlaySFX("Stone Land",pos);
if(friendly){
for(auto&[monsterPtr,hurt]:game->Hurt(targetPos,radius,damage,OnUpperLevel(),z,HurtType::MONSTER)){
for(auto&[monsterPtr,hurt]:game->Hurt(pos,collisionRadius,damage,OnUpperLevel(),z,HurtType::MONSTER)){
if(hurt)std::get<Monster*>(monsterPtr)->ApplyIframes(0.1f);
}
}
else{
for(auto&[playerPtr,hurt]:game->Hurt(targetPos,radius,damage,OnUpperLevel(),z,HurtType::PLAYER)){
}else{
for(auto&[playerPtr,hurt]:game->Hurt(pos,collisionRadius,damage,OnUpperLevel(),z,HurtType::PLAYER)){
if(hurt)std::get<Player*>(playerPtr)->ApplyIframes(0.1f);
}
}
game->ProximityKnockback(targetPos,radius,knockbackAmt,HurtType::PLAYER|HurtType::MONSTER);
game->ProximityKnockback(pos,collisionRadius,knockbackAmt,HurtType::PLAYER|HurtType::MONSTER);
for(int i:std::ranges::iota_view(0,30))game->AddEffect(std::make_unique<Effect>(pos-vf2d{0.f,GetZ()},util::random_range(0.05f,0.2f),"circle_outline.png",OnUpperLevel(),util::random_range(0.5f,1.f),0.2f,vf2d{util::random_range(-10.f,10.f),util::random_range(-3.f,0.f)},PixelLerp(BLACK,col,util::random(1.f)),0.f,0.f,true));
Deactivate();
}
@ -82,11 +81,11 @@ void FallingStone::Update(float fElapsedTime){
}
indicator.Update(fElapsedTime);
}
void FallingStone::Draw(const Pixel blendCol)const{
void FallingBullet::Draw(const Pixel blendCol)const{
if(lifetime<=indicatorDisplayTime){
indicator._Draw();
}
Bullet::Draw(blendCol);
}
void FallingStone::ModifyOutgoingDamageData(HurtDamageInfo&data){}
void FallingBullet::ModifyOutgoingDamageData(HurtDamageInfo&data){}

@ -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,13 +56,14 @@ 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);
if(!friendly&&geom2d::overlaps(game->GetPlayer()->Hitbox(),tongueLine)){
if(!friendly&&hitList.find(game->GetPlayer())==hitList.end()&&geom2d::overlaps(game->GetPlayer()->Hitbox(),tongueLine)){
_PlayerHit(game->GetPlayer());
hitList.insert(game->GetPlayer());
}
if(friendly){
for(std::shared_ptr<Monster>&m:MONSTER_LIST){

@ -0,0 +1,199 @@
#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 <ranges>
using A=Attribute;
INCLUDE_game
INCLUDE_MONSTER_LIST
void Monster::STRATEGY::GHOST_OF_PIRATE_CAPTAIN(Monster&m,float fElapsedTime,std::string strategy){
enum PhaseName{
INIT,
NORMAL,
AFTERIMAGE_FADEIN,
GHOSTSABER_SLASH=999,
};
enum CannonShotType{
BOMBARDMENT,//every shot is at a random location within 900 Range of the player. (This is also the move during hide and seek.)
PRECISE_BOMBARDMENT,//same as before but within 700 range of the player.
LINE,//the 4th or 5th hit would hit the player if the player doesnt move at all.
SHARPSHOOTER,//aiming directly at the player, skipping every 2nd shot. (only 4 instead of 8 shots)
PREDICTION,//shoots in the direction of the current or last players movement and predicts where the player would be in 2.8 seconds. (slightly further then where the player would be at impact. thats why 2.8 instead of 2.5 seconds)
};
static const uint8_t PHASE_COUNT{uint8_t(DATA.GetProperty("MonsterStrategy.Ghost of Pirate Captain.Cannon Cycle").GetValueCount())};
static uint8_t TOTAL_CANNON_SHOTS{0};
const auto AdvanceCannonPhase{[&m,&strategy](){
m.GetFloat(A::CANNON_TIMER)=0.f;
if(m.GetInt(A::CANNON_PHASE)+1>=PHASE_COUNT)m.GetInt(A::CANNON_SHOT_TYPE)=util::random()%5;
const int prevCannonPhase{m.I(A::CANNON_PHASE)};
m.I(A::CANNON_PHASE)=(m.I(A::CANNON_PHASE)+1)%PHASE_COUNT;
if(prevCannonPhase>m.I(A::CANNON_PHASE)){//Phase has wrapped around, reset cannon shot count.
m.I(A::CANNON_SHOT_COUNT)=0;
if(!m.B(A::FIRST_WAVE_COMPLETE)){
m.B(A::FIRST_WAVE_COMPLETE)=true;
m.ForceSetPos(m.spawnPos);
m.SetupAfterImage();
m.afterImagePos=m.GetPos();
m.SetCollisionRadius(0.f);
m.F(A::CASTING_TIMER)=1.f;
SETPHASE(AFTERIMAGE_FADEIN);
}
}
}};
if(m.F(A::SHRAPNEL_SHOT_FALL_TIMER)>0.f){
m.F(A::SHRAPNEL_SHOT_FALL_TIMER)-=fElapsedTime;
while(m.I(A::SHRAPNEL_SHOT_COUNT)&&m.F(A::SHRAPNEL_SHOT_FALL_TIMER)<=0.f){
const float randomAng{util::random_range(0,2*PI)};
const float range{util::random_range(0,ConfigPixels("Bombardment Max Distance"))};
const vf2d targetPos{game->GetPlayer()->GetPos()+vf2d{range,randomAng}.cart()};
m.F(A::SHRAPNEL_SHOT_FALL_TIMER)+=ConfigFloat("Shrapnel Shot Bullet Separation");
CreateBullet(FallingBullet)("cannonball.png",targetPos,ConfigVec("Cannon Vel"),ConfigFloatArr("Cannon Vel",2),ConfigFloat("Indicator Time"),ConfigPixels("Shrapnel Shot Bullet Radius"),ConfigInt("Shrapnel Shot Damage"),m.OnUpperLevel(),false,ConfigFloat("Shrapnel Knockback Amt"),ConfigFloat("Shrapnel Shot Impact Time"),false,ConfigPixel("Cannon Spell Circle Color"),vf2d{ConfigFloat("Shrapnel Shot Bullet Radius")/100.f*1.75f,ConfigFloat("Shrapnel Shot Bullet Radius")/100.f*1.75f},util::random(2*PI),util::random(2*PI),util::degToRad(ConfigFloat("Cannon Spell Circle Rotation Spd")),ConfigPixel("Cannon Spell Insignia Color"),util::random(2*PI),util::degToRad(ConfigFloat("Cannon Spell Insignia Rotation Spd")))EndBullet;
m.I(A::SHRAPNEL_SHOT_COUNT)--;
}
}
m.F(A::GHOST_SABER_TIMER)-=fElapsedTime;
if(m.B(A::FIRST_WAVE_COMPLETE)){
m.UpdateFacingDirection(m.GetFacingDirectionToTarget(game->GetPlayer()->GetPos()));
m.SetVelocity(util::pointTo(m.GetPos(),game->GetPlayer()->GetPos())*100.f*m.GetMoveSpdMult());
const float distToPlayer{util::distance(m.GetPos(),game->GetPlayer()->GetPos())};
if(m.F(A::GHOST_SABER_TIMER)<=0.f&&distToPlayer<=ConfigPixels("Ghost Saber Activation Range")){
m.F(A::GHOST_SABER_TIMER)=ConfigFloat("Ghost Saber Cooldown");
const float playerToMonsterAngle{util::pointTo(game->GetPlayer()->GetPos(),m.GetPos()).polar().y};
CreateBullet(GhostSaber)(m.GetPos(),m.GetWeakPointer(),ConfigFloat("Ghost Saber Lifetime"),ConfigFloat("Ghost Saber Distance"),ConfigFloat("Ghost Saber Knockback Amt"),playerToMonsterAngle,ConfigFloat("Ghost Saber Radius"),ConfigFloat("Ghost Saber Expand Spd"),ConfigInt("Ghost Saber Damage"),m.OnUpperLevel(),util::degToRad(ConfigFloat("Ghost Saber Rotation Spd")))EndBullet;
}
}
switch(PHASE()){
enum CannonPhaseType{
CANNON_SHOT,
SILENCE,
SHRAPNEL_SHOT,
};
case INIT:{
for(int i:std::ranges::iota_view(0U,PHASE_COUNT)){
const int cannonCycle{DATA.GetProperty("MonsterStrategy.Ghost of Pirate Captain.Cannon Cycle").GetInt(i)};
m.VEC(A::CANNON_PHASES).emplace_back(cannonCycle);
if(cannonCycle==CANNON_SHOT)TOTAL_CANNON_SHOTS++;
}
m.B(A::FIRST_WAVE_COMPLETE)=false;
m.I(A::CANNON_SHOT_TYPE)=BOMBARDMENT;
m.ForceSetPos({-400.f,-400.f});
SETPHASE(NORMAL);
}break;
case NORMAL:{
m.F(A::CANNON_TIMER)+=fElapsedTime;
const int phase{std::any_cast<int>(m.VEC(A::CANNON_PHASES)[m.I(A::CANNON_PHASE)])};
switch(phase){
case CANNON_SHOT:{//Normal Cannon Shot. Takes on one of five varieties.
if(m.F(A::CANNON_TIMER)>=ConfigFloat("Cannon Shot Delay")){
switch(m.I(A::CANNON_SHOT_TYPE)){
case BOMBARDMENT:{
const float randomAng{util::random_range(0,2*PI)};
const float range{util::random_range(0,ConfigPixels("Bombardment Max Distance"))};
const vf2d targetPos{game->GetPlayer()->GetPos()+vf2d{range,randomAng}.cart()};
CreateBullet(FallingBullet)("cannonball.png",targetPos,ConfigVec("Cannon Vel"),ConfigFloatArr("Cannon Vel",2),ConfigFloat("Indicator Time"),ConfigPixels("Cannon Radius"),ConfigInt("Cannon Damage"),m.OnUpperLevel(),false,ConfigFloat("Cannon Knockback Amt"),ConfigFloat("Cannon Shot Impact Time"),false,ConfigPixel("Cannon Spell Circle Color"),vf2d{ConfigFloat("Cannon Radius")/100.f*1.75f,ConfigFloat("Cannon Radius")/100.f*1.75f},util::random(2*PI),util::random(2*PI),util::degToRad(ConfigFloat("Cannon Spell Circle Rotation Spd")),ConfigPixel("Cannon Spell Insignia Color"),util::random(2*PI),util::degToRad(ConfigFloat("Cannon Spell Insignia Rotation Spd")))EndBullet;
}break;
case PRECISE_BOMBARDMENT:{
const float randomAng{util::random_range(0,2*PI)};
const float range{util::random_range(0,ConfigPixels("Precise Bombardment Max Distance"))};
const vf2d targetPos{game->GetPlayer()->GetPos()+vf2d{range,randomAng}.cart()};
CreateBullet(FallingBullet)("cannonball.png",targetPos,ConfigVec("Cannon Vel"),ConfigFloatArr("Cannon Vel",2),ConfigFloat("Indicator Time"),ConfigPixels("Cannon Radius"),ConfigInt("Cannon Damage"),m.OnUpperLevel(),false,ConfigFloat("Cannon Knockback Amt"),ConfigFloat("Cannon Shot Impact Time"),false,ConfigPixel("Cannon Spell Circle Color"),vf2d{ConfigFloat("Cannon Radius")/100.f*1.75f,ConfigFloat("Cannon Radius")/100.f*1.75f},util::random(2*PI),util::random(2*PI),util::degToRad(ConfigFloat("Cannon Spell Circle Rotation Spd")),ConfigPixel("Cannon Spell Insignia Color"),util::random(2*PI),util::degToRad(ConfigFloat("Cannon Spell Insignia Rotation Spd")))EndBullet;
}break;
case LINE:{
//Draw a line from one side of the screen to the other, drawing through the middle.
if(m.I(A::CANNON_SHOT_COUNT)==0)m.F(A::LINE_SHOT_ANG)=util::random_range(0,2*PI);
const vf2d targetPos{geom2d::line{game->GetPlayer()->GetPos()+vf2d{float(game->ScreenHeight()),m.F(A::LINE_SHOT_ANG)}.cart(),game->GetPlayer()->GetPos()+vf2d{float(game->ScreenHeight()),m.F(A::LINE_SHOT_ANG)+PI}.cart()}.upoint(float(m.I(A::CANNON_SHOT_COUNT))/TOTAL_CANNON_SHOTS)};
CreateBullet(FallingBullet)("cannonball.png",targetPos,ConfigVec("Cannon Vel"),ConfigFloatArr("Cannon Vel",2),ConfigFloat("Indicator Time"),ConfigPixels("Cannon Radius"),ConfigInt("Cannon Damage"),m.OnUpperLevel(),false,ConfigFloat("Cannon Knockback Amt"),ConfigFloat("Cannon Shot Impact Time"),false,ConfigPixel("Cannon Spell Circle Color"),vf2d{ConfigFloat("Cannon Radius")/100.f*1.75f,ConfigFloat("Cannon Radius")/100.f*1.75f},util::random(2*PI),util::random(2*PI),util::degToRad(ConfigFloat("Cannon Spell Circle Rotation Spd")),ConfigPixel("Cannon Spell Insignia Color"),util::random(2*PI),util::degToRad(ConfigFloat("Cannon Spell Insignia Rotation Spd")))EndBullet;
}break;
case SHARPSHOOTER:{
if(m.I(A::CANNON_SHOT_COUNT)%2==0)CreateBullet(FallingBullet)("cannonball.png",game->GetPlayer()->GetPos(),ConfigVec("Cannon Vel"),ConfigFloatArr("Cannon Vel",2),ConfigFloat("Indicator Time"),ConfigPixels("Cannon Radius"),ConfigInt("Cannon Damage"),m.OnUpperLevel(),false,ConfigFloat("Cannon Knockback Amt"),ConfigFloat("Cannon Shot Impact Time"),false,ConfigPixel("Cannon Spell Circle Color"),vf2d{ConfigFloat("Cannon Radius")/100.f*1.75f,ConfigFloat("Cannon Radius")/100.f*1.75f},util::random(2*PI),util::random(2*PI),util::degToRad(ConfigFloat("Cannon Spell Circle Rotation Spd")),ConfigPixel("Cannon Spell Insignia Color"),util::random(2*PI),util::degToRad(ConfigFloat("Cannon Spell Insignia Rotation Spd")))EndBullet;
}break;
case PREDICTION:{
LOG(std::format("Previous Pos: {} Current: {}",game->GetPlayer()->GetPreviousPos().str(),game->GetPlayer()->GetPos().str()));
const float angle{util::angleTo(game->GetPlayer()->GetPreviousPos(),game->GetPlayer()->GetPos())};
const float range{util::random_range(0,100.f*game->GetPlayer()->GetMoveSpdMult())*ConfigFloat("Cannon Shot Impact Time")};
LOG(std::format("Range/Angle: {}",vf2d{range,angle}.str()));
const vf2d targetPos{game->GetPlayer()->GetPos()+vf2d{range,angle}.cart()};
CreateBullet(FallingBullet)("cannonball.png",targetPos,ConfigVec("Cannon Vel"),ConfigFloatArr("Cannon Vel",2),ConfigFloat("Indicator Time"),ConfigPixels("Cannon Radius"),ConfigInt("Cannon Damage"),m.OnUpperLevel(),false,ConfigFloat("Cannon Knockback Amt"),ConfigFloat("Cannon Shot Impact Time"),false,ConfigPixel("Cannon Spell Circle Color"),vf2d{ConfigFloat("Cannon Radius")/100.f*1.75f,ConfigFloat("Cannon Radius")/100.f*1.75f},util::random(2*PI),util::random(2*PI),util::degToRad(ConfigFloat("Cannon Spell Circle Rotation Spd")),ConfigPixel("Cannon Spell Insignia Color"),util::random(2*PI),util::degToRad(ConfigFloat("Cannon Spell Insignia Rotation Spd")))EndBullet;
}break;
}
AdvanceCannonPhase();
m.I(A::CANNON_SHOT_COUNT)++;
}
}break;
case SILENCE:{
if(m.F(A::CANNON_TIMER)>=ConfigFloat("Silence Time"))AdvanceCannonPhase();
}break;
case SHRAPNEL_SHOT:{
if(m.F(A::CANNON_TIMER)>=ConfigFloat("Shrapnel Shot Delay")){
m.I(A::SHRAPNEL_SHOT_COUNT)=ConfigInt("Shrapnel Shot Bullet Count");
m.F(A::SHRAPNEL_SHOT_FALL_TIMER)=ConfigFloat("Shrapnel Shot Bullet Separation");
AdvanceCannonPhase();
}
}break;
}
}break;
case AFTERIMAGE_FADEIN:{
m.F(A::CASTING_TIMER)-=fElapsedTime;
if(m.F(A::CASTING_TIMER)<=0.f){
SETPHASE(NORMAL);
m.SetCollisionRadius(m.GetOriginalCollisionRadius());
}
}break;
case GHOSTSABER_SLASH:{ //This was triggered by GhostSaber when
m.F(A::GHOST_SABER_SLASH_ANIMATION_TIMER)-=fElapsedTime;
if(m.F(A::GHOST_SABER_SLASH_ANIMATION_TIMER)<=0.f){
m.PerformIdleAnimation();
SETPHASE(m.I(A::PREVIOUS_PHASE));
}
}break;
}
}

@ -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 "BulletTypes.h"
#include "Attributable.h"
GhostSaber::GhostSaber(const vf2d pos,const std::weak_ptr<Monster>target,const float lifetime,const float distFromTarget,const float knockbackAmt,const float initialRot,const float radius,const float expandSpd,const int damage,const bool upperLevel,const float rotSpd,const bool friendly,const Pixel col,const vf2d scale,const float image_angle)
:Bullet(target.lock()->GetPos()+vf2d{distFromTarget,initialRot}.cart(),{},radius,damage,"ghost_dagger.png",upperLevel,false,INFINITE,false,friendly,col,scale,image_angle),attachedMonster(target),rotSpd(rotSpd),distFromTarget(distFromTarget),rot(initialRot),aliveTime(lifetime),knockbackAmt(knockbackAmt),expandSpd(expandSpd){}
void GhostSaber::Update(float fElapsedTime){
alphaOscillator.Update(fElapsedTime);
particleTimer-=fElapsedTime;
aliveTime-=fElapsedTime;
distFromTarget+=expandSpd*fElapsedTime;
if(particleTimer<=0.f){
particleTimer+=0.05f;
game->AddEffect(std::make_unique<ShineEffect>(pos,0.1f,0.1f,"pixel.png",2.f,vf2d{},Pixel{239,215,98,192},util::random(2*PI),0.f,true));
}
rot+=rotSpd*fElapsedTime;
if(!attachedMonster.expired()){
pos=attachedMonster.lock()->GetPos()+vf2d{distFromTarget,rot}.cart();
}
if(aliveTime<=0.f&&!IsDeactivated()){
Deactivate();
fadeOutTime=0.5f;
}
col.a=alphaOscillator.get();
image_angle=-rot*2;
};
BulletDestroyState GhostSaber::PlayerHit(Player*player){
player->ApplyIframes(0.2f);
player->Knockback(vf2d{knockbackAmt,rot}.cart());
if(!attachedMonster.expired()){
const int GHOSTSABER_SLASH_PHASEID{999};
attachedMonster.lock()->GetInt(Attribute::PREVIOUS_PHASE)=attachedMonster.lock()->GetPhase(attachedMonster.lock()->GetStrategyName());
attachedMonster.lock()->SetPhase(attachedMonster.lock()->GetStrategyName(),GHOSTSABER_SLASH_PHASEID);
attachedMonster.lock()->PerformAnimation("SLASHING");
attachedMonster.lock()->GetFloat(Attribute::GHOST_SABER_SLASH_ANIMATION_TIMER)=attachedMonster.lock()->GetCurrentAnimation().GetTotalAnimationDuration();
}
return BulletDestroyState::KEEP_ALIVE;
}
BulletDestroyState GhostSaber::MonsterHit(Monster&monster,const uint8_t markStacksBeforeHit){
monster.ApplyIframes(0.2f);
monster.Knockback(vf2d{knockbackAmt,rot}.cart());
return BulletDestroyState::KEEP_ALIVE;
}
void GhostSaber::ModifyOutgoingDamageData(HurtDamageInfo&data){}

@ -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,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"
using A=Attribute;
INCLUDE_game
INCLUDE_MONSTER_LIST
void Monster::STRATEGY::GIANT_OCTOPUS(Monster&m,float fElapsedTime,std::string strategy){
enum PhaseName{
INIT,
IDENTIFY_ARMS,
NORMAL,
HURT_ANIMATION,
DEAD,
};
if(!m.B(A::ARM_SPEEDS_INCREASED)&&m.GetHealth()<=ConfigInt("Arm Speedup Health Threshold")){
m.B(A::ARM_SPEEDS_INCREASED)=true;
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};
if(!armPtr.expired())armPtr.lock()->GetFloat(A::ATTACK_ANIMATION_SPEED)=1.f+ConfigFloat("Arm Animation Speed Increase")/100.f;
}
}
}
switch(PHASE()){
case INIT:{
m.F(A::BREAK_TIME)=0.5f;
m.AddBuff(BuffType::DAMAGE_REDUCTION,INFINITE,ConfigFloat("Permanent Resistance Buff")/100.f);
m.SetStrategyDeathFunction([](GameEvent&event,Monster&m,const std::string&strategy){
std::string takoyakiImgDir{"item_img_directory"_S+"Takoyaki.png"};
if(!GFX.count(takoyakiImgDir))ERR(std::format("WARNING! Could not find item image {}",takoyakiImgDir));
game->AddEffect(std::make_unique<Effect>(m.GetPos(),INFINITE,"item_img_directory"_S+"Takoyaki.png",m.OnUpperLevel(),1.f,0.f,vf2d{4.f,4.f},vf2d{},WHITE));
m.SetLifetime(4.f);
SETPHASE(DEAD);
return false;
});
SETPHASE(IDENTIFY_ARMS);
}break;
case IDENTIFY_ARMS:{
m.F(A::BREAK_TIME)-=fElapsedTime;
if(m.F(A::BREAK_TIME)<=0.f){
m.F(A::CASTING_TIMER)=util::random_range(ConfigFloatArr("Arm Move Timer",0),ConfigFloatArr("Arm 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());
}
}
SETPHASE(NORMAL);
}
}break;
case NORMAL:{
const bool InSecondPhase{m.GetHealth()<=ConfigInt("Phase 2 Health Threshold")};
m.F(A::SHOOT_TIMER)-=fElapsedTime;
m.F(A::CASTING_TIMER)-=fElapsedTime;
m.F(A::LAST_SHOOT_TIMER)-=fElapsedTime;
m.F(A::LAST_INK_SHOOT_TIMER)-=fElapsedTime;
if(m.F(A::SHOOT_ANIMATION_TIME)>0.f){
m.F(A::SHOOT_ANIMATION_TIME)-=fElapsedTime;
if(m.F(A::SHOOT_ANIMATION_TIME)<=0.f)m.PerformIdleAnimation();
}
if(m.F(A::CASTING_TIMER)<=0.f){
int deadMonsterCount{0};
std::vector<vf2d>unoccupiedArmLocs;
AddAllUnoccupedArmLocations:
std::for_each(m.VEC(A::ARM_LOCATIONS).begin(),m.VEC(A::ARM_LOCATIONS).end(),[&unoccupiedArmLocs](const std::any&armLoc){unoccupiedArmLocs.emplace_back(std::any_cast<vf2d>(armLoc));});
std::vector<std::any>liveArms;
RemoveOccupiedArmLocationsAndDetectAliveArms:
std::copy_if(m.VEC(A::ARM_LIST).begin(),m.VEC(A::ARM_LIST).end(),std::back_inserter(liveArms),[&unoccupiedArmLocs,&deadMonsterCount](const std::any&arm){
const std::weak_ptr<Monster>&m{std::any_cast<std::weak_ptr<Monster>>(arm)};
const bool isLive{!m.expired()&&m.lock()->IsAlive()};
if(isLive)std::erase_if(unoccupiedArmLocs,[&m](const vf2d&armLoc){return m.lock()->GetPos()==armLoc;});
else deadMonsterCount++;
return isLive;
});
RemoveArmLocationsTooFarFromPlayer:
std::erase_if(unoccupiedArmLocs,[maxDist=ConfigPixels("Arm Max Move Distance From Player")](const vf2d&armLoc){return util::distance(game->GetPlayer()->GetPos(),armLoc)>maxDist;});
const bool AtLeastOneArmAlive{deadMonsterCount!=m.VEC(A::ARM_LIST).size()};
if(deadMonsterCount>0&&AtLeastOneArmAlive&&unoccupiedArmLocs.size()>0){
const std::weak_ptr<Monster>&randomArm{std::any_cast<std::weak_ptr<Monster>>(liveArms[util::random()%liveArms.size()])};
const vf2d&randomLoc{std::any_cast<vf2d>(unoccupiedArmLocs[util::random()%unoccupiedArmLocs.size()])};
randomArm.lock()->PerformAnimation("SUBMERGE");
randomArm.lock()->SetPhase("Octopus Arm",randomArm.lock()->I(A::SUBMERGE_STRAT_ID));
randomArm.lock()->GetFloat(A::RECOVERY_TIME)=randomArm.lock()->GetCurrentAnimation().GetTotalAnimationDuration();
randomArm.lock()->SetCollisionRadius(0.f);
randomArm.lock()->V(A::JUMP_TARGET_POS)=randomLoc;
randomArm.lock()->SetStrategyDrawFunction([](AiL*game,Monster&monster,const std::string&strategy){});
}
m.F(A::CASTING_TIMER)=util::random_range(ConfigFloatArr("Arm Move Timer",0),ConfigFloatArr("Arm Move Timer",1));
}
if(m.F(A::SHOOT_TIMER)<=0.f){
const auto CreateBurstBullet=[&](){
CreateBullet(BurstBullet)(m.GetPos(),util::pointTo(m.GetPos(),game->GetPlayer()->GetPos())*ConfigFloat("Big Bullet Speed"),game->GetPlayer(),ConfigPixels("Big Bullet Detection Radius"),ConfigInt("Big Bullet Extra Bullet Count"),util::degToRad(ConfigFloat("Big Bullet Extra Bullet Rotate Speed")),ConfigFloat("Big Bullet Extra Bullet Radius"),vf2d{ConfigFloatArr("Big Bullet Extra Bullet Image Scale",0),ConfigFloatArr("Big Bullet Extra Bullet Image Scale",1)},ConfigFloat("Big Bullet Extra Bullet Speed"),ConfigFloat("Big Bullet Extra Bullet Acceleration"),ConfigFloat("Big Bullet Radius"),ConfigInt("Big Bullet Damage"),m.OnUpperLevel(),false,ConfigPixel("Big Bullet Color"),vf2d{ConfigFloat("Big Bullet Image Scale"),ConfigFloat("Big Bullet Image Scale")})EndBullet;
m.F(A::SHOOT_TIMER)=ConfigFloat("Big Bullet Boss Rest Time");
m.I(A::ATTACK_COUNT)=-1;
};
if(InSecondPhase){
if(m.F(A::LAST_INK_SHOOT_TIMER)<=0.f){
CreateBullet(InkBullet)(m.GetPos(),game->GetPlayer()->GetPos(),vf2d{ConfigFloat("Phase 2.Ink Bullet Speed"),0.f},ConfigFloat("Phase 2.Ink Explosion Radius"),ConfigFloat("Phase 2.Ink Puddle Lifetime"),ConfigFloat("Phase 2.Ink Slowdown Time"),ConfigFloat("Phase 2.Ink Slowdown Amount")/100.f,ConfigFloat("Phase 2.Ink Puddle Collision Radius"),m.OnUpperLevel(),false)EndBullet;
m.F(A::LAST_INK_SHOOT_TIMER)=ConfigFloat("Phase 2.Ink Bullet Frequency");
goto BulletShot;
}else
if(m.I(A::BULLET_COUNT_AFTER_INK_ATTACK)>ConfigInt("Phase 2.Homing Bullet Starts After")){
if(m.I(A::ATTACK_COUNT)%ConfigInt("Phase 2.Homing Bullet Frequency")==0)CreateBullet(HomingBullet)(m.GetPos(),Entity{game->GetPlayer()},util::degToRad(ConfigFloat("Phase 2.Homing Bullet Rotation Speed")),util::degToRad(ConfigFloat("Phase 2.Homing Bullet Rotation Speed Player Covered In Ink")),ConfigFloat("Phase 2.Homing Bullet Lifetime"),ConfigFloat("Bullet Speed"),ConfigFloat("Bullet Radius"),ConfigFloat("Bullet Damage"),m.OnUpperLevel(),false,ConfigPixel("Phase 2.Homing Bullet Color"),{ConfigFloat("Bullet Radius"),ConfigFloat("Bullet Radius")})EndBullet;
else
if(m.I(A::ATTACK_COUNT)>=ConfigInt("Big Bullet Frequency")-1)CreateBurstBullet();
else CreateBullet(Bullet)(m.GetPos(),util::pointTo(m.GetPos(),game->GetPlayer()->GetPos())*ConfigFloat("Bullet Speed"),ConfigFloat("Bullet Radius"),ConfigFloat("Bullet Damage"),m.OnUpperLevel(),false,ConfigPixel("Bullet Color"),{ConfigFloat("Bullet Radius"),ConfigFloat("Bullet Radius")})EndBullet;
m.F(A::SHOOT_TIMER)=ConfigFloat("Shoot Frequency");
goto BulletShot;
}
m.I(A::BULLET_COUNT_AFTER_INK_ATTACK)++;
}
if(m.I(A::ATTACK_COUNT)>=ConfigInt("Big Bullet Frequency")-1)CreateBurstBullet();
else{
m.F(A::SHOOT_TIMER)=ConfigFloat("Shoot Frequency");
CreateBullet(Bullet)(m.GetPos(),util::pointTo(m.GetPos(),game->GetPlayer()->GetPos())*ConfigFloat("Bullet Speed"),ConfigFloat("Bullet Radius"),ConfigFloat("Bullet Damage"),m.OnUpperLevel(),false,ConfigPixel("Bullet Color"),{ConfigFloat("Bullet Radius"),ConfigFloat("Bullet Radius")})EndBullet;
}
BulletShot:
m.F(A::LAST_SHOOT_TIMER)=1.f;
m.PerformShootAnimation(m.GetFacingDirectionToTarget(game->GetPlayer()->GetPos()));
m.I(A::ATTACK_COUNT)++;
}
if(m.F(A::LAST_SHOOT_TIMER)<=0.f){
m.PerformIdleAnimation(m.GetFacingDirectionToTarget(game->GetPlayer()->GetPos()));
}
}break;
case HURT_ANIMATION:{
}break;
case DEAD:{}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();
}

@ -0,0 +1,54 @@
#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"
HomingBullet::HomingBullet(const vf2d pos,const Entity target,const float rotateTowardsSpeed,const float rotateTowardsSpeedCoveredInInk,const float lifetime,const float speed,const float radius,const int damage,const bool upperLevel,const bool friendly,const Pixel col,const vf2d scale,const float image_angle)
:Bullet(pos,util::pointTo(pos,target.GetPos())*speed,radius,damage,upperLevel,friendly,col,scale,image_angle),target(target),rotateTowardsSpeed(rotateTowardsSpeed),rotateTowardsSpeedCoveredInInk(rotateTowardsSpeedCoveredInInk){
this->lifetime=lifetime;
}
void HomingBullet::Update(float fElapsedTime){
const float magnitude{vel.mag()};
float bulletAngle{vel.polar().y};
float bulletRotationSpeed{rotateTowardsSpeed};
if(game->GetPlayer()->CoveredInInk())bulletRotationSpeed=rotateTowardsSpeedCoveredInInk;
if(util::distance(pos,game->GetPlayer()->GetPos())>=7*24.f)bulletRotationSpeed=0.f; //Don't rotate if basically offscreen.
util::turn_towards_target(bulletAngle,pos,target.GetPos(),bulletRotationSpeed,fElapsedTime);
vel=vf2d{magnitude,bulletAngle}.cart();
}
void HomingBullet::ModifyOutgoingDamageData(HurtDamageInfo&data){}

@ -0,0 +1,130 @@
#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 "Menu.h"
#include "MenuComponent.h"
#include "GameState.h"
#include "MenuLabel.h"
#include "AdventuresInLestoria.h"
#include "CharacterRotatingDisplay.h"
#include "ClassInfo.h"
#include "Unlock.h"
INCLUDE_game
INCLUDE_GFX
void Menu::InitializeHubPauseWindow(){
Menu*hubPauseWindow=CreateMenu(MenuType::HUB_PAUSE,CENTERED,vi2d{96,168});
hubPauseWindow->ADD("Resume Button",MenuComponent)(geom2d::rect<float>{{6.f,0.f},{84.f,24.f}},"Resume",[](MenuFuncData data){void ClearGarbage();
game->ClearGarbage();
Menu::CloseMenu();
return true;
},ButtonAttr::FIT_TO_LABEL)END;
hubPauseWindow->ADD("Character Button",MenuComponent)(geom2d::rect<float>{{6.f,28.f},{84.f,24.f}},"Character",[](MenuFuncData data){
Component<CharacterRotatingDisplay>(CHARACTER_MENU,"Character Rotating Display")->SetIcon(GFX[classutils::GetClassInfo(game->GetPlayer()->GetClassName()).classFullImgName].Decal());
Component<MenuComponent>(CHARACTER_MENU,"Equip Selection Select Button")->Click();
Menu::OpenMenu(CHARACTER_MENU);
return true;
},ButtonAttr::FIT_TO_LABEL)END;
hubPauseWindow->ADD("Change Loadout Button",MenuComponent)(geom2d::rect<float>{{6.f,56.f},{84.f,24.f}},"Change\nLoadout",[](MenuFuncData data){
Menu::OpenMenu(ITEM_HUB_LOADOUT);
return true;
},ButtonAttr::FIT_TO_LABEL)END;
hubPauseWindow->ADD("Inventory Button",MenuComponent)(geom2d::rect<float>{{6.f,84.f},{84.f,24.f}},"Inventory",[](MenuFuncData data){
Menu::OpenMenu(INVENTORY);
return true;
},ButtonAttr::FIT_TO_LABEL)END;
hubPauseWindow->ADD("Settings Button",MenuComponent)(geom2d::rect<float>{{6.f,112.f},{84.f,24.f}},"Settings",[](MenuFuncData data){
Menu::OpenMenu(SETTINGS);
return true;
},ButtonAttr::FIT_TO_LABEL)END;
hubPauseWindow->ADD("Return to Camp Button",MenuComponent)(geom2d::rect<float>{{6.f,140.f},{84.f,24.f}},"Leave Area",[](MenuFuncData data){
Component<MenuLabel>(LEVEL_COMPLETE,"Stage Complete Label")->SetLabel("Stage Summary");
Component<MenuComponent>(LEVEL_COMPLETE,"Level Details Outline")->SetLabel("");
if(game->GetCurrentMapName()=="HUB"&&game->PreviousStageCompleted())Unlock::UnlockCurrentMap(); //Special unlock for the hub area when leaving.
if(Unlock::IsUnlocked("STORY_1_1")){
Component<MenuComponent>(LEVEL_COMPLETE,"Next Button")->Enable();
}else{
Component<MenuComponent>(LEVEL_COMPLETE,"Next Button")->Disable();
}
if(GameState::STATE==GameState::states[States::GAME_HUB]){
GameState::ChangeState(States::OVERWORLD_MAP,0.4f);
}else{
GameState::ChangeState(States::LEVEL_COMPLETE,0.4f);
}
return true;
},ButtonAttr::FIT_TO_LABEL)END;
hubPauseWindow->SetupKeyboardNavigation(
[](MenuType type,Data&returnData){ //On Open
returnData="Resume Button";
},
{ //Button Key
{game->KEY_SCROLL,{"Navigate",[](MenuType type){}}},
{game->KEY_BACK,{"Resume",[](MenuType type){
Component<MenuComponent>(type,"Resume Button")->Click();
}}},
{game->KEY_MENU,{"Resume",[](MenuType type){
Component<MenuComponent>(type,"Resume Button")->Click();
}}},
{game->KEY_CONFIRM,{"Select",[](MenuType type){}}},
}
,{ //Button Navigation Rules
{"Resume Button",{
.up="Return to Camp Button",
.down="Character Button",}},
{"Character Button",{
.up="Resume Button",
.down="Change Loadout Button",}},
{"Change Loadout Button",{
.up="Character Button",
.down="Inventory Button",}},
{"Inventory Button",{
.up="Change Loadout Button",
.down="Settings Button",}},
{"Settings Button",{
.up="Inventory Button",
.down="Return to Camp Button",}},
{"Return to Camp Button",{
.up="Settings Button",
.down="Resume Button",}},
});
}

@ -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);
@ -108,9 +108,7 @@ void IBullet::_Update(const float fElapsedTime){
if(m->Hurt(damageData)){
hitList.insert(&*m);
if(!hitsMultiple){
if(_MonsterHit(*m,markStacksBeforeHit)==BulletDestroyState::DESTROY){
dead=true;
}
if(_MonsterHit(*m,markStacksBeforeHit)==BulletDestroyState::DESTROY)dead=true;
return false;
}else _MonsterHit(*m,markStacksBeforeHit);
if(!CollisionCheckRequired())return false;
@ -127,9 +125,7 @@ void IBullet::_Update(const float fElapsedTime){
if(game->GetPlayer()->Hurt(damageData)){
hitList.insert(game->GetPlayer());
if(!hitsMultiple){
if(_PlayerHit(&*game->GetPlayer())==BulletDestroyState::DESTROY){
dead=true;
}
if(_PlayerHit(&*game->GetPlayer())==BulletDestroyState::DESTROY)dead=true;
return false;
}else _PlayerHit(&*game->GetPlayer());
if(!CollisionCheckRequired())return false;
@ -143,6 +139,7 @@ void IBullet::_Update(const float fElapsedTime){
while(iterations>0){
iterations--;
if(IsPlayerAutoAttackProjectile()){pos+=(game->GetWindSpeed()*game->GetElapsedTime())/float(totalIterations);}
vel=vf2d{vel.polar().x+(acc*fElapsedTime/float(totalIterations)),vel.polar().y}.cart();
pos+=(vel*fElapsedTime)/float(totalIterations);
if(!CollisionCheck()){
goto DeadBulletCheck;
@ -154,6 +151,7 @@ void IBullet::_Update(const float fElapsedTime){
}
}else{
if(IsPlayerAutoAttackProjectile()){pos+=game->GetWindSpeed()*game->GetElapsedTime();}
vel=vf2d{vel.polar().x+acc*fElapsedTime,vel.polar().y}.cart();
pos+=vel*fElapsedTime;
}
@ -170,11 +168,13 @@ void IBullet::_Update(const float fElapsedTime){
}
void IBullet::_Draw()const{
game->SetDecalMode(DecalMode::NORMAL);
if(additiveBlending)game->SetDecalMode(DecalMode::ADDITIVE);
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);
@ -188,6 +188,7 @@ void IBullet::_Draw()const{
blendCol.a/=1.59f; //Comes from 255 divided by 160 which is roughly what we want the alpha to be when the bullet has full transparency.
}
Draw(blendCol);
game->SetDecalMode(DecalMode::NORMAL);
}
void IBullet::Draw(const Pixel blendCol)const{

@ -60,6 +60,7 @@ enum class BulletDestroyState{
struct IBullet{
friend class AiL;
float acc{};
vf2d vel;
vf2d pos;
float radius;
@ -82,6 +83,7 @@ protected:
virtual void Update(float fElapsedTime);
void Deactivate();
void Activate();
bool additiveBlending{false};
private:
float fadeOutTimer=0;
float fadeInTimer=0;
@ -110,7 +112,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);

@ -0,0 +1,52 @@
#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_game
Ink::Ink(vf2d pos,float radius,float lifetime,float inkSlowdownDuration,float inkSlowdownPct,float fadeinTime,float fadeoutTime,vf2d size,Pixel col,bool onUpperLevel,float rotation)
:Effect(pos,lifetime,"ink.png",onUpperLevel,fadeinTime,fadeoutTime,size,{},EffectType::NONE,col,rotation,0.f,false),radius(radius),inkSlowdownDuration(inkSlowdownDuration),inkSlowdownAmount(inkSlowdownPct){}
bool Ink::Update(float fElapsedTime){
const float distToPlayer{util::distance(game->GetPlayer()->GetPos(),pos)};
if(distToPlayer<=radius){
Buff&currentInkSlowdown{game->GetPlayer()->GetOrAddBuff(BuffType::INK_SLOWDOWN,{inkSlowdownDuration,inkSlowdownAmount})};
currentInkSlowdown.duration=inkSlowdownDuration; //Reset the duration if needed.
}
return Effect::Update(fElapsedTime);
}

@ -0,0 +1,66 @@
#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_ANIMATION_DATA
InkBullet::InkBullet(const vf2d pos,const vf2d targetPos,const vf2d vel,const float inkExplosionRadius,const float inkPuddleLifetime,const float inkSlowdownDuration,const float inkSlowdownPct,const float inkPuddleRadius,const bool upperLevel,const bool friendly)
:Bullet(pos,vel,inkExplosionRadius,0,"inkbullet.png",upperLevel,friendly),targetPos(targetPos),inkExplosionRadius(inkExplosionRadius),inkSlowdownPct(inkSlowdownPct),inkSlowdownDuration(inkSlowdownDuration),inkPuddleLifetime(inkPuddleLifetime),inkPuddleRadius(inkPuddleRadius){
additiveBlending=true;
}
void InkBullet::Update(float fElapsedTime){
const float magnitude{vel.mag()};
vel=util::pointTo(pos,targetPos)*magnitude;
if(util::distance(pos,targetPos)<=8.f){Splat();}
else if(GetAliveTime()>=30.f)ERR(std::format("WARNING! An Ink Bullet did not resolve to target position {} in time! Last reported pos: {}! THE DISTANCE THRESHOLD FOR INK EXPLOSIONS IS LIKELY TOO HIGH! THIS SHOULD NOT BE HAPPENING!",targetPos.str(),pos.str())); //NOTE: THIS SHOULD NOT BE HAPPENING! If this does, then we probably just set the distance threshold WAY TOO LOW! Let's crash for now.
}
void InkBullet::Splat(){
game->AddEffect(std::make_unique<Effect>(pos,ANIMATION_DATA["inkbubble_explode.png"].GetTotalAnimationDuration(),"inkbubble_explode.png",OnUpperLevel(),vf2d{1.f,1.f},0.f,vf2d{},WHITE,0.f,0.f,additiveBlending));
game->AddEffect(std::make_unique<Ink>(pos,inkPuddleRadius,inkPuddleLifetime,inkSlowdownDuration,inkSlowdownPct,0.4f,0.4f,vf2d{1.f,1.f},WHITE,OnUpperLevel(),int(util::random_range(0,4.f))*PI/2),true);
lifetime=0.f;
}
BulletDestroyState InkBullet::PlayerHit(Player*player){
Splat();
return BulletDestroyState::DESTROY;
}
BulletDestroyState InkBullet::MonsterHit(Monster&monster,const uint8_t markStacksBeforeHit){
Splat();
return BulletDestroyState::DESTROY;
}
void InkBullet::ModifyOutgoingDamageData(HurtDamageInfo&data){}

@ -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,12 +526,14 @@ 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)){
if(GameState::GetCurrentState()!=States::GAME_HUB){
return RemoveItem(GetItem(it)[0]);
}else return false;
}
}
return false;
@ -680,9 +648,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 +758,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 +774,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 +822,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 +1002,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 +1015,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 +1409,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 +1433,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 +1495,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;

@ -69,7 +69,7 @@ vf2d ItemDrop::GetPos()const{
return pos;
}
bool ItemDrop::OnUpperLevel(){
bool ItemDrop::OnUpperLevel()const{
return upperLevel;
}
@ -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);
}
@ -99,8 +99,7 @@ void ItemDrop::UpdateDrops(float fElapsedTime){
if(drop.z<=0){
drop.zSpeed=0;
drop.z=0;
}
else{
}else{
drop.zSpeed+=gravity*fElapsedTime;
drop.pos+=drop.speed*fElapsedTime;
}
@ -110,7 +109,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){

@ -59,7 +59,7 @@ class ItemDrop{
public:
static void Initialize();
vf2d GetPos()const;
bool OnUpperLevel();
bool OnUpperLevel()const;
void Draw()const;
static void UpdateDrops(float fElapsedTime);
static const std::vector<ItemDrop>&GetDrops();

@ -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,148 @@
#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 "DEFINES.h"
#include "Menu.h"
#include "MenuLabel.h"
#include "MenuItemLoadoutButton.h"
#include "State_OverworldMap.h"
INCLUDE_game
using A=Attribute;
void Menu::InitializeItemHubLoadoutWindow(){
Menu*itemHubLoadoutWindow=CreateMenu(ITEM_HUB_LOADOUT,CENTERED,game->GetScreenSize()-vi2d{4,4});
float itemLoadoutWindowWidth=(game->GetScreenSize().x-5.f);
itemHubLoadoutWindow->ADD("Loadout Label",MenuLabel)(geom2d::rect<float>{{0,24},{itemLoadoutWindowWidth,24}},"Setup Item Loadout",2,ComponentAttr::SHADOW|ComponentAttr::BACKGROUND|ComponentAttr::OUTLINE)END;
itemHubLoadoutWindow->ADD("Loadout Description Label",MenuLabel)(geom2d::rect<float>{{0,58},{itemLoadoutWindowWidth,24}},"Bring up to 3 items with you.",1,ComponentAttr::SHADOW)END;
float buttonBorderPadding=64;
itemHubLoadoutWindow->ADD("Loadout Item 1",MenuItemLoadoutButton)(geom2d::rect<float>{{64,90},{48,48}},0,[](MenuFuncData data){
Menu::menus.at(INVENTORY_CONSUMABLES)->I(A::LOADOUT_SLOT)=0;
Menu::OpenMenu(INVENTORY_CONSUMABLES);
return true;
},[](MenuFuncData data){
std::weak_ptr<MenuItemLoadoutButton>loadoutButton=DYNAMIC_POINTER_CAST<MenuItemLoadoutButton>(data.component);
if(!ISBLANK(loadoutButton.lock()->GetItem())){
Component<MenuLabel>(ITEM_HUB_LOADOUT,"Item Name Label")->SetLabel(loadoutButton.lock()->GetItem().lock()->DisplayName());
Component<MenuLabel>(ITEM_HUB_LOADOUT,"Item Description")->SetLabel(loadoutButton.lock()->GetItem().lock()->Description());
}
return true;
},[](MenuFuncData data){
Component<MenuLabel>(ITEM_HUB_LOADOUT,"Item Name Label")->SetLabel("");
Component<MenuLabel>(ITEM_HUB_LOADOUT,"Item Description")->SetLabel("");
return true;
})END
->SetIconScale({2.f,2.f});
itemHubLoadoutWindow->ADD("Loadout Item 2",MenuItemLoadoutButton)(geom2d::rect<float>{{itemLoadoutWindowWidth/2-24,90},{48,48}},1,[](MenuFuncData data){
Menu::menus.at(INVENTORY_CONSUMABLES)->I(A::LOADOUT_SLOT)=1;
Menu::OpenMenu(INVENTORY_CONSUMABLES);
return true;
},[](MenuFuncData data){
std::weak_ptr<MenuItemLoadoutButton>loadoutButton=DYNAMIC_POINTER_CAST<MenuItemLoadoutButton>(data.component);
if(!ISBLANK(loadoutButton.lock()->GetItem())){
Component<MenuLabel>(ITEM_HUB_LOADOUT,"Item Name Label")->SetLabel(loadoutButton.lock()->GetItem().lock()->DisplayName());
Component<MenuLabel>(ITEM_HUB_LOADOUT,"Item Description")->SetLabel(loadoutButton.lock()->GetItem().lock()->Description());
}
return true;
},[](MenuFuncData data){
Component<MenuLabel>(ITEM_HUB_LOADOUT,"Item Name Label")->SetLabel("");
Component<MenuLabel>(ITEM_HUB_LOADOUT,"Item Description")->SetLabel("");
return true;
})END
->SetIconScale({2.f,2.f});
itemHubLoadoutWindow->ADD("Loadout Item 3",MenuItemLoadoutButton)(geom2d::rect<float>{{itemLoadoutWindowWidth-48-64,90},{48,48}},2,[](MenuFuncData data){
Menu::menus.at(INVENTORY_CONSUMABLES)->I(A::LOADOUT_SLOT)=2;
Menu::OpenMenu(INVENTORY_CONSUMABLES);
return true;
},[](MenuFuncData data){
std::weak_ptr<MenuItemLoadoutButton>loadoutButton=DYNAMIC_POINTER_CAST<MenuItemLoadoutButton>(data.component);
if(!ISBLANK(loadoutButton.lock()->GetItem())){
Component<MenuLabel>(ITEM_HUB_LOADOUT,"Item Name Label")->SetLabel(loadoutButton.lock()->GetItem().lock()->DisplayName());
Component<MenuLabel>(ITEM_HUB_LOADOUT,"Item Description")->SetLabel(loadoutButton.lock()->GetItem().lock()->Description());
}
return true;
},[](MenuFuncData data){
Component<MenuLabel>(ITEM_HUB_LOADOUT,"Item Name Label")->SetLabel("");
Component<MenuLabel>(ITEM_HUB_LOADOUT,"Item Description")->SetLabel("");
return true;
})END
->SetIconScale({2.f,2.f});
itemHubLoadoutWindow->ADD("Item Name Label",MenuLabel)(geom2d::rect<float>{{0,146},{itemLoadoutWindowWidth,12}},"",1,ComponentAttr::SHADOW)END;
itemHubLoadoutWindow->ADD("Item Description",MenuLabel)(geom2d::rect<float>{{0,158},{itemLoadoutWindowWidth,24}},"",1,ComponentAttr::SHADOW)END;
itemHubLoadoutWindow->ADD("Back Button",MenuComponent)(geom2d::rect<float>{{itemLoadoutWindowWidth/2-32,202},{64,16}},"Back",[](MenuFuncData data){Menu::CloseMenu();return true;})END;
itemHubLoadoutWindow->SetupKeyboardNavigation(
[](MenuType type,Data&returnData){ //On Open
returnData="Loadout Item 1";
},
{ //Button Key
{game->KEY_BACK,{"Back",[](MenuType type){
Component<MenuComponent>(type,"Back Button")->Click();
}}},
{game->KEY_CONFIRM,{"Select",[](MenuType type){}}},
}
,{ //Button Navigation Rules
{"Loadout Item 1",{
.up="Back Button",
.down="Back Button",
.left="Loadout Item 3",
.right="Loadout Item 2",}},
{"Loadout Item 2",{
.up="Back Button",
.down="Back Button",
.left="Loadout Item 1",
.right="Loadout Item 3",}},
{"Loadout Item 3",{
.up="Back Button",
.down="Back Button",
.left="Loadout Item 2",
.right="Loadout Item 1",}},
{"Back Button",{
.up="Loadout Item 1",
.down="Loadout Item 1",
.left="Back Button",
.right="Back Button",}},
});
}

@ -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.");
}

@ -124,6 +124,8 @@ void Menu::InitializeMenus(){
InitializeArtificerDisassembleWindow();
InitializeArtificerEnchantWindow();
InitializeArtificerEnchantConfirmWindow();
InitializeHubPauseWindow();
InitializeItemHubLoadoutWindow();
for(MenuType type=MenuType(int(MenuType::ENUM_START)+1);type<MenuType::ENUM_END;type=MenuType(int(type+1))){
if(menus.count(type)==0){
@ -409,8 +411,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 +420,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 +432,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 +444,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 +456,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 +821,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];
}
}
}

@ -117,6 +117,8 @@ class Menu:public IAttributable{
static void InitializeArtificerDisassembleWindow();
static void InitializeArtificerEnchantWindow();
static void InitializeArtificerEnchantConfirmWindow();
static void InitializeHubPauseWindow();
static void InitializeItemHubLoadoutWindow();
friend class AiL;
friend class ItemInfo;
@ -208,6 +210,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,13 @@ 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
HUB_PAUSE, //100% Controller Compatibility
ITEM_HUB_LOADOUT, //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,9 @@ 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&currentRect{GetFrame().GetSourceRect().size};
afterImage.Create(currentRect.x,currentRect.y);
}
const vf2d&Monster::GetPos()const{
return pos;
@ -91,23 +94,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 +172,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 +211,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 +251,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 +270,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);
@ -272,6 +282,15 @@ bool Monster::Update(float fElapsedTime){
specialMarkApplicationTimer=std::max(0.f,specialMarkApplicationTimer-fElapsedTime);
lastFacingDirectionChange+=fElapsedTime;
timeSpentAlive+=fElapsedTime;
floatOscillator.Update(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());
@ -387,17 +406,32 @@ 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,fElapsedTime);
if(HasMountedMonster())mounted_animation.value().UpdateState(internal_mounted_animState,fElapsedTime);
attackedByPlayer=false;
#pragma region Afterimage Handling
const auto RemoveScanLine=[&](uint8_t scanLine){
for(int x:std::ranges::iota_view(0,afterImage.Sprite()->width)){
afterImage.Sprite()->SetPixel({x,scanLine},BLANK);
}
afterImage.Decal()->Update();
};
//Scan Line goes through 1-(height-1) (odd numbers) first, then 0-22.
const bool ScanLineFinished{scanLine==afterImage.Sprite()->height};
if(!ScanLineFinished){
removeLineTimer-=fElapsedTime;
if(removeLineTimer<=0.f){
removeLineTimer=TIME_BETWEEN_LINE_REMOVALS;
RemoveScanLine(scanLine);
scanLine+=2;
if(scanLine>afterImage.Sprite()->height-1&&scanLine%2==1){
scanLine=0;
}
}
animation.UpdateState(internal_animState,randomFrameOffset+fElapsedTime);
if(HasMountedMonster())mounted_animation.value().UpdateState(internal_mounted_animState.value(),fElapsedTime);
randomFrameOffset=0;
attackedByPlayer=false;
return true;
}
#pragma endregion
}
Direction Monster::GetFacingDirection()const{
return facingDirection;
@ -439,7 +473,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 +486,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,10 +499,10 @@ 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()};
const vf2d zOffset=-vf2d{0,GetZ()}+vf2d{0,(MONSTER_DATA[GetName()].IsFloating()?floatOscillator.get():0.f)};
const vf2d drawPos=GetPos()+zOffset+hitTimerOffset;
@ -480,10 +514,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;
@ -504,6 +538,7 @@ void Monster::Draw()const{
};
if(glowPurpleBuff.has_value())DrawBaseMonster(glowPurpleImageScale,{43,0,66,blendCol.a});
DrawAfterImage:game->view.DrawRotatedDecal(afterImagePos,afterImage.Decal(),0.f,afterImage.Sprite()->Size()/2,{GetSizeMult(),GetSizeMult()},Pixel{0xFFDCDA});
DrawBaseMonster(imageScale,blendCol);
if(overlaySprite.length()!=0){
if(glowPurpleBuff.has_value())DrawOverlayMonster(imageScale,{43,0,66,overlaySpriteTransparency});
@ -566,8 +601,8 @@ void Monster::Draw()const{
#pragma endregion
if(GameSettings::TerrainCollisionBoxesEnabled()&&IsSolid()&&solidFadeTimer>0.f){
float distToPlayer=geom2d::line<float>(game->GetPlayer()->GetPos(),GetPos()).length();
const float collisionRadiusFactor=GetCollisionRadius()/12.f;
const float distToPlayer{geom2d::line<float>(game->GetPlayer()->GetPos(),GetPos()).length()};
const float collisionRadiusFactor{GetCollisionRadius()/12.f};
if(distToPlayer<24*3*collisionRadiusFactor){
game->DrawPie(game->view.WorldToScreen(GetPos()),GetCollisionRadius(),0.f,{255,0,0,uint8_t(128*(blendColAlpha/255.f)/sqrt(distToPlayer*collisionRadiusFactor))});
game->SetDecalMode(DecalMode::WIREFRAME);
@ -629,6 +664,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);
}
@ -682,11 +719,11 @@ void Monster::Moved(){
}
if(!std::isfinite(pos.x)){
ERR(std::format("WARNING! Player X position is {}...Trying to recover. THIS SHOULD NOT BE HAPPENING!",pos.x));
ERR(std::format("WARNING! Monster X position is {}...Trying to recover. THIS SHOULD NOT BE HAPPENING!",pos.x));
pos.x=spawnPos.x;
}
if(!std::isfinite(pos.y)){
ERR(std::format("WARNING! Player Y position is {}...Trying to recover. THIS SHOULD NOT BE HAPPENING!",pos.y));
ERR(std::format("WARNING! Monster Y position is {}...Trying to recover. THIS SHOULD NOT BE HAPPENING!",pos.y));
pos.y=spawnPos.y;
}
}
@ -698,6 +735,7 @@ const bool Monster::AttackAvoided(const float attackZ)const{
}
bool Monster::Hurt(HurtDamageInfo damageData){
if(damageData.damage==0)return false; //Cancel the hit.
return _Hurt(damageData.damage,damageData.onUpperLevel,damageData.z,damageData.damageRule,damageData.hurtFlags);
}
@ -951,13 +989,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 +1096,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 +1198,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 +1280,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 +1343,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 +1374,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 +1385,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 +1394,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 +1437,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 +1586,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 +1671,49 @@ 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;
}
void Monster::SetLifetime(const float lifetime){
this->lifetime=lifetime;
}
void Monster::ForceSetPos(vf2d pos){
this->pos=pos;
Moved();
}
void Monster::SetupAfterImage(){
game->SetDrawTarget(afterImage.Sprite());
game->Clear(BLANK);
game->DrawPartialSprite({},animation.GetFrame(internal_animState).GetSourceImage()->Sprite(),animation.GetFrame(internal_animState).GetSourceRect().pos,animation.GetFrame(internal_animState).GetSourceRect().size,1U,0U,{255,255,254}); //Off-white so that the sprite is rendered completely in white.
game->SetDrawTarget(nullptr);
afterImage.Decal()->Update();
removeLineTimer=TIME_BETWEEN_LINE_REMOVALS;
scanLine=1U;
}
const std::string&Monster::GetStrategyName()const{
return strategy;
}

@ -49,6 +49,7 @@ All rights reserved.
#include "MonsterData.h"
#include "Direction.h"
#include "HurtDamageInfo.h"
#include "Oscillator.h"
INCLUDE_ITEM_DATA
INCLUDE_MONSTER_DATA
@ -60,6 +61,8 @@ enum class Attribute;
class GameEvent;
using StrategyName=std::string;
namespace MonsterTests{
class MonsterTest;
};
@ -72,7 +75,7 @@ public:
DeathSpawnInfo(const std::string_view monsterName,const uint8_t spawnAmt,const vf2d spawnOffset={});
void Spawn(const vf2d monsterDeathPos,const bool onUpperLevel);
};
class Monster:IAttributable{
class Monster:public IAttributable{
friend struct STRATEGY;
friend class AiL;
friend class InventoryCreator;
@ -84,14 +87,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 +131,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 +150,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 +167,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;
@ -194,6 +199,7 @@ public:
//If an object has a lifetime set, returns it.
const std::optional<float>GetLifetime()const;
const std::optional<float>GetTotalLifetime()const;
void SetLifetime(const float lifetime);
//If an object has a collision radius, returns it.
const float GetCollisionRadius()const;
const bool IsDead()const;
@ -202,7 +208,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 +233,12 @@ 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);
const std::string&GetStrategyName()const;
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 +265,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 +283,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 +292,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 +332,21 @@ 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.
//Ignores collision checking and sets the monster at the given position.
void ForceSetPos(vf2d pos);
Renderable afterImage;
void SetupAfterImage();
float removeLineTimer{};
const float TIME_BETWEEN_LINE_REMOVALS{0.025f};
uint8_t scanLine{24};
vf2d afterImagePos{};
Oscillator<float>floatOscillator{0.f,8.f,0.5f};
struct STRATEGY{
static std::string ERR;
@ -352,6 +381,17 @@ 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);
static void GHOST_OF_PIRATE_CAPTAIN(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,34 @@ 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,
SUBMERGE_LOCATION,
SUBMERGE_STRAT_ID,
ATTACK_ANIMATION_SPEED,
ARM_SPEEDS_INCREASED,
LAST_SHOOT_TIMER,
BULLET_COUNT_AFTER_INK_ATTACK,
LAST_INK_SHOOT_TIMER,
CANNON_TIMER,
CANNON_PHASE,
CANNON_SHOT_TYPE,
CANNON_PHASES,
SHRAPNEL_SHOT_COUNT,
SHRAPNEL_SHOT_FALL_TIMER,
CANNON_SHOT_COUNT,
LINE_SHOT_ANG,
LAST_PLAYER_POS,
FIRST_WAVE_COMPLETE,
GHOST_SABER_TIMER,
GHOST_SABER_SLASH_ANIMATION_TIMER,
};

@ -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,9 @@ 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("Floating"))monster.floating=DATA["Monsters"][MonsterName]["Floating"].GetBool();
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 +480,10 @@ const bool MonsterData::FadeoutWhenStandingBehind()const{
const bool MonsterData::FaceTarget()const{
return !noFacing;
}
const float MonsterData::UnconsciousTime()const{
return totalUnconsciousTime;
}
const bool MonsterData::IsFloating()const{
return floating;
}

@ -108,6 +108,8 @@ public:
const std::optional<geom2d::rect<float>>&GetRectangleCollision()const;
const bool FadeoutWhenStandingBehind()const;
const bool FaceTarget()const;
const float UnconsciousTime()const;
const bool IsFloating()const; //A floating monster oscillates back and forth.
private:
std::string name;
int hp;
@ -140,4 +142,6 @@ private:
float collisionRadius{};
bool hasArrowIndicator{false};
std::optional<geom2d::rect<float>>rectCollision;
float totalUnconsciousTime{};
bool floating{false};
};

@ -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,221 @@
#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();
}
fElapsedTime*=m.F(A::ATTACK_ANIMATION_SPEED);
switch(PHASE()){
case INIT:{
m.I(A::SUBMERGE_STRAT_ID)=SUBMERGE;
m.F(A::ATTACK_ANIMATION_SPEED)=1.f;
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([deathFadeTime=ConfigFloat("Death Fade Time"),bossDamageOnDeath=ConfigInt("Boss Damage On Death"),armHealAmtOnDeath=ConfigInt("Arm Heal Amount On Death"),reemergeWaitTime=ConfigFloat("Re-Emerge Wait Time"),delayTimeIncreasePerArm=ConfigFloat("Delay Time Increase Per Arm"),armAttackBuffOnDeath=ConfigFloat("Arm Attack Buff On Death"),bossAttackBuffOnDeath=ConfigFloat("Boss Attack Buff 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._DealTrueDamage(bossDamageOnDeath);
}
float delayTimePerArm{0.f};
std::for_each(MONSTER_LIST.begin(),MONSTER_LIST.end(),[&OCTOPUS_ARM_NAME,&GIANT_OCTOPUS_NAME,&strategyName,&armHealAmtOnDeath,&reemergeWaitTime,&delayTimeIncreasePerArm,&delayTimePerArm,&armAttackBuffOnDeath,&bossAttackBuffOnDeath](const std::shared_ptr<Monster>&m){
if(m->GetName()==OCTOPUS_ARM_NAME){
m->PerformAnimation("SUBMERGE");
m->SetPhase(strategyName,SUBMERGE);
m->V(A::JUMP_TARGET_POS)=m->GetPos();
m->SetCollisionRadius(0.f);
if(!m->IsDead())m->Heal(armHealAmtOnDeath,true);
m->GetFloat(A::RECOVERY_TIME)=reemergeWaitTime+delayTimePerArm;
m->SetStrategyDrawFunction([](AiL*game,Monster&monster,const std::string&strategy){});
if(m->HasBuff(BuffType::STAT_UP)){
bool found{false};
for(Buff&b:m->EditBuffs(BuffType::STAT_UP)){
for(const ItemAttribute&attr:b.attr){
if(attr.ActualName()=="Attack %"){
found=true;
b.intensity+=armAttackBuffOnDeath/100.f;
}
}
}
if(!found)m->AddBuff(BuffType::STAT_UP,INFINITE,armAttackBuffOnDeath/100.f,{"Attack %"});
}else m->AddBuff(BuffType::STAT_UP,INFINITE,armAttackBuffOnDeath/100.f,{"Attack %"});
delayTimePerArm+=delayTimeIncreasePerArm;
}else if(m->GetName()==GIANT_OCTOPUS_NAME){
if(m->HasBuff(BuffType::STAT_UP)){
bool found{false};
for(Buff&b:m->EditBuffs(BuffType::STAT_UP)){
for(const ItemAttribute&attr:b.attr){
if(attr.ActualName()=="Attack %"){
found=true;
b.intensity+=bossAttackBuffOnDeath/100.f;
}
}
}
if(!found)m->AddBuff(BuffType::STAT_UP,INFINITE,bossAttackBuffOnDeath/100.f,{"Attack %"});
}else m->AddBuff(BuffType::STAT_UP,INFINITE,bossAttackBuffOnDeath/100.f,{"Attack %"});
}
});
m.SetLifetime(deathFadeTime);
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());
m.SetCollisionRadius(m.GetOriginalCollisionRadius());
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(128),alphaTimer)};
if(alphaTimer>1.f)alpha=util::lerp(0,128,1-(alphaTimer-1));
const_cast<Arc&>(arc).Draw(game,{0,0,128,uint8_t(alpha)});
if(storedArc.has_value()){
const uint8_t effectAlpha{util::lerp(uint8_t(0),uint8_t(128),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.PerformAnimation("SUBMERGE");
SETPHASE(SUBMERGE);
m.GetFloat(A::RECOVERY_TIME)=m.GetCurrentAnimation().GetTotalAnimationDuration()*util::random_range(1.f,2.f);
m.SetCollisionRadius(0.f);
m.V(A::JUMP_TARGET_POS)=m.GetPos();
m.SetStrategyDrawFunction([](AiL*game,Monster&monster,const std::string&strategy){});
}
}break;
case SUBMERGE:{
m.F(A::RECOVERY_TIME)-=fElapsedTime;
if(m.F(A::RECOVERY_TIME)<=0.f){
m.PerformAnimation("RISE");
m.SetPos(m.V(A::JUMP_TARGET_POS));
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,129 @@
#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{
INIT,
MOVE,
LOCKON,
WINDUP,
FIRE_ANIMATION,
};
#pragma endregion
switch(PHASE()){
case INIT:{
m.SetStrategyDeathFunction([](GameEvent&ev,Monster&m,const std::string&strategy){
m.SetStrategyDrawFunction([](AiL*game,Monster&monster,const std::string&strategy){});
return false;
});
SETPHASE(MOVE);
}break;
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){
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));
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(),{255,255,255,uint8_t(alpha)});
});
}
}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);
@ -316,6 +318,9 @@ float Player::GetMoveSpdMult(){
for(const Buff&b:GetBuffs(BuffType::BLOCK_SLOWDOWN)){
mod_moveSpd-=moveSpdPct*b.intensity;
}
for(const Buff&b:GetBuffs(BuffType::INK_SLOWDOWN)){
mod_moveSpd-=moveSpdPct*b.intensity;
}
for(const Buff&b:GetBuffs(LOCKON_SPEEDBOOST)){
mod_moveSpd+=moveSpdPct*b.intensity;
}
@ -394,6 +399,7 @@ void Player::Update(float fElapsedTime){
hpRecoveryTimer=std::max(0.f,hpRecoveryTimer-fElapsedTime);
hp6RecoveryTimer=std::max(0.f,hp6RecoveryTimer-fElapsedTime);
hp4RecoveryTimer=std::max(0.f,hp4RecoveryTimer-fElapsedTime);
RunTimers();
PerformHPRecovery();
@ -633,7 +639,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,11 +678,15 @@ 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;
SetX(newX);
SetY(newY);
const vf2d totalVel{vel+addedVel};
if(totalVel!=vf2d{}){
float newX=pos.x+totalVel.x*fElapsedTime;
float newY=pos.y+totalVel.y*fElapsedTime;
MoveFlag::MoveFlag flag{MoveFlag::NONE};
if(vel==vf2d{})flag=MoveFlag::PREVENT_CAST_CANCELLING; //This means added velocity is the only thing affecting movement, we don't stop casting for additional velocity actors.
SetX(newX,flag);
SetY(newY,flag);
if(vel.x>0){
vel.x=std::max(0.f,vel.x-friction*fElapsedTime);
} else {
@ -689,6 +699,8 @@ void Player::Update(float fElapsedTime){
}
}
addedVel={};
if(Menu::stack.empty()){
if(CanAct()&&attack_cooldown_timer==0&&AiL::KEY_ATTACK.Held()){
@ -877,8 +889,7 @@ bool Player::CanAct(){
const bool Player::CanAct(const Ability&ability){
return knockUpTimer==0&&!ability.waitForRelease&&(ability.canCancelCast||state!=State::CASTING)&&state!=State::ANIMATION_LOCK&&state!=State::DEADLYDASH&&state!=State::RETREAT&&
(GameState::STATE==GameState::states[States::GAME_RUN]
||GameState::STATE==GameState::states[States::GAME_HUB]&&!ability.itemAbility);
(GameState::STATE==GameState::states[States::GAME_RUN]||GameState::STATE==GameState::states[States::GAME_HUB]);
}
bool Player::HasIframes(){
@ -886,6 +897,7 @@ bool Player::HasIframes(){
}
bool Player::Hurt(HurtDamageInfo damageData){
if(damageData.damage==0)return false; //Cancel the hit.
return Hurt(damageData.damage,damageData.onUpperLevel,damageData.z,damageData.damageRule,damageData.hurtFlags);
}
@ -1288,7 +1300,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 +1550,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"];
}
@ -1908,13 +1919,13 @@ void Player::ProximityKnockback(const vf2d centerPoint,const float knockbackFact
float dist=lineToPlayer.length();
if(dist<0.001f){
float randomDir=util::random(2*PI);
lineToPlayer={centerPoint,centerPoint+vf2d{cos(randomDir),sin(randomDir)}*1};
lineToPlayer={centerPoint,centerPoint+vf2d{cos(randomDir),sin(randomDir)}*knockbackFactor};
}
game->GetPlayer()->Knockback(lineToPlayer.vector().norm()*knockbackFactor);
}
void Player::AddVelocity(vf2d vel){
this->vel+=vel*game->GetElapsedTime();
this->addedVel+=vel;
}
const float Player::GetHealthRatio()const{
@ -2289,5 +2300,31 @@ 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();
}
const bool Player::CoveredInInk()const{
return HasBuff(BuffType::INK_SLOWDOWN);
}
const vf2d&Player::GetPreviousPos()const{
return previousPos;
}

@ -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.
@ -340,6 +338,8 @@ public:
void CastSpell(Ability&ability); //NOTE: This usually is not called unless we are running tests! CastSpell is meant to be used when we have chosen a pre-casting target position and have released the associated key to cast the spell.
void PrepareCast(Ability&ability); //NOTE: This usually is not called unless we are running tests! PrepareCast is meant to be used before we use CastSpell in unit tests.
void SetTestScreenAimingLocation(vf2d forcedAimingLoc);
const bool CoveredInInk()const;
const vf2d&GetPreviousPos()const; //The position the player was at on the last frame, can be used for comparison purposes to predict where the player may be next.
private:
static std::vector<std::reference_wrapper<Ability>>ABILITY_LIST;
const int SHIELD_CAPACITY{32};
@ -433,6 +433,8 @@ private:
std::vector<std::pair<PlayerTimerType,ShieldAmount>>shield;
bool catForm{false};
std::optional<vf2d>testAimingLoc{};
vf2d addedVel{};
vf2d previousPos{pos};
protected:
const float ATTACK_COOLDOWN="Warrior.Auto Attack.Cooldown"_F;
const float MAGIC_ATTACK_COOLDOWN="Wizard.Auto Attack.Cooldown"_F;
@ -485,6 +487,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 +513,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 +557,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 +601,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 +645,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 +685,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 +733,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,
};

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

Loading…
Cancel
Save