Separate ThrowingProjectiles from PoisonBottle class to create generic throwable item bullets. Added throwable projectile item script. Made Bomb item functional. Release Build 11748.

master
sigonasr2 1 month ago
parent 8c3dd0f071
commit 868a089666
  1. 3
      Adventures in Lestoria Tests/MonsterTests.cpp
  2. 8
      Adventures in Lestoria/Adventures in Lestoria.vcxproj
  3. 8
      Adventures in Lestoria/Adventures in Lestoria.vcxproj.filters
  4. 18
      Adventures in Lestoria/AdventuresInLestoria.cpp
  5. 2
      Adventures in Lestoria/AdventuresInLestoria.h
  6. 1
      Adventures in Lestoria/Animation.cpp
  7. 21
      Adventures in Lestoria/BulletTypes.h
  8. 1
      Adventures in Lestoria/DEFINES.h
  9. 79
      Adventures in Lestoria/Item.cpp
  10. 12
      Adventures in Lestoria/Item.h
  11. 94
      Adventures in Lestoria/ItemScript.cpp
  12. 110
      Adventures in Lestoria/PoisonBottle.cpp
  13. 3
      Adventures in Lestoria/TODO.txt
  14. 88
      Adventures in Lestoria/ThrownProjectile.cpp
  15. 2
      Adventures in Lestoria/Version.h
  16. 2
      Adventures in Lestoria/VisualNovel.cpp
  17. 2
      Adventures in Lestoria/Witch.cpp
  18. BIN
      Adventures in Lestoria/assets/bomb.png
  19. 5
      Adventures in Lestoria/assets/config/audio/events.txt
  20. 1
      Adventures in Lestoria/assets/config/gfx/gfx.txt
  21. 12
      Adventures in Lestoria/assets/config/items/ItemDatabase.txt
  22. 18
      Adventures in Lestoria/assets/config/items/ItemScript.txt
  23. BIN
      Adventures in Lestoria/assets/gamepack.pak
  24. BIN
      Adventures in Lestoria/assets/sounds/Throw Projectile.ogg
  25. BIN
      Adventures in Lestoria/assets/sounds/bomb_explosion.ogg
  26. BIN
      x64/Release/Adventures in Lestoria.exe

@ -552,5 +552,8 @@ namespace MonsterTests
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"))};
}
};
}

@ -993,6 +993,10 @@
</SubType>
</ClCompile>
<ClCompile Include="ItemLoadoutWindow.cpp" />
<ClCompile Include="ItemScript.cpp">
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="Key.cpp" />
<ClCompile Include="LargeStone.cpp" />
<ClCompile Include="LargeTornado.cpp">
@ -1071,6 +1075,10 @@
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="ThrownProjectile.cpp">
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="PoisonPool.cpp">
<SubType>
</SubType>

@ -1190,7 +1190,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">
@ -1253,6 +1253,12 @@
<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>
</ItemGroup>
<ItemGroup>
<None Include="cpp.hint" />

@ -3929,28 +3929,28 @@ void AiL::SetLoadoutItem(int slot,std::string itemName){
inputGroup=&Player::KEY_ITEM3;
}break;
}
Ability itemAbility{itemName,"","","Item.Item Cooldown Time"_F,0,inputGroup,"items/"+itemName+".png",VERY_DARK_RED,DARK_RED,PrecastData{GetLoadoutItem(slot).lock()->CastTime(),0,0},true};
Ability itemAbility{itemName,"","","Item.Item Cooldown Time"_F,0,inputGroup,"items/"+itemName+".png",VERY_DARK_RED,DARK_RED,PrecastData{GetLoadoutItem(slot).lock()->CastTime(),GetLoadoutItem(slot).lock()->CastRange(),GetLoadoutItem(slot).lock()->CastSize()},true};
itemAbility.actionPerformedDuringCast=GetLoadoutItem(slot).lock()->UseDuringCast();
itemAbility.itemAbility=true;
switch(slot){
case 0:{
itemAbility.action=[&](Player*p,vf2d pos={}){
return game->UseLoadoutItem(0);
return game->UseLoadoutItem(0,pos);
};
game->GetPlayer()->SetItem1UseFunc(itemAbility);
Component<MenuItemItemButton>(MenuType::ITEM_LOADOUT,"Loadout Item 1")->SetItem(loadout[slot]);
}break;
case 1:{
itemAbility.action=[&](Player*p,vf2d pos={}){
return game->UseLoadoutItem(1);
return game->UseLoadoutItem(1,pos);
};
game->GetPlayer()->SetItem2UseFunc(itemAbility);
Component<MenuItemItemButton>(MenuType::ITEM_LOADOUT,"Loadout Item 2")->SetItem(loadout[slot]);
}break;
case 2:{
itemAbility.action=[&](Player*p,vf2d pos={}){
return game->UseLoadoutItem(2);
return game->UseLoadoutItem(2,pos);
};
game->GetPlayer()->SetItem3UseFunc(itemAbility);
Component<MenuItemItemButton>(MenuType::ITEM_LOADOUT,"Loadout Item 3")->SetItem(loadout[slot]);
@ -3974,11 +3974,11 @@ void AiL::SetLoadoutItem(int slot,std::string itemName){
}
}
bool AiL::UseLoadoutItem(int slot){
bool AiL::UseLoadoutItem(int slot,const std::optional<vf2d>targetingPos){
if(slot<0||slot>GetLoadoutSize()-1)ERR("Invalid inventory slot "+std::to_string(slot)+", please choose a slot in range (0-"+std::to_string(GetLoadoutSize()-1)+").");
if(!ISBLANK(GetLoadoutItem(slot).lock())&&GetLoadoutItem(slot).lock()->Amt()>0){
Tutorial::GetTask(TutorialTaskName::USE_RECOVERY_ITEMS).I(A::ITEM_USE_COUNT)++;
Inventory::UseItem(GetLoadoutItem(slot).lock()->ActualName());
Inventory::UseItem(GetLoadoutItem(slot).lock()->ActualName(),1U,targetingPos);
Inventory::AddLoadoutItemUsed(GetLoadoutItem(slot).lock()->ActualName(),slot);
GetLoadoutItem(slot).lock()->amt--;
if(GetLoadoutItem(slot).lock()->UseSound().length()>0){
@ -4008,7 +4008,7 @@ void AiL::ClearLoadoutItem(int slot){
switch(slot){
case 0:{
itemAbility.action=[&](Player*p,vf2d pos={}){
return game->UseLoadoutItem(0);
return game->UseLoadoutItem(0,pos);
};
game->GetPlayer()->SetItem1UseFunc(itemAbility);
Component<MenuItemItemButton>(MenuType::ITEM_LOADOUT,"Loadout Item 1")->SetItem(Item::BLANK);
@ -4016,7 +4016,7 @@ void AiL::ClearLoadoutItem(int slot){
}break;
case 1:{
itemAbility.action=[&](Player*p,vf2d pos={}){
return game->UseLoadoutItem(1);
return game->UseLoadoutItem(1,pos);
};
game->GetPlayer()->SetItem2UseFunc(itemAbility);
Component<MenuItemItemButton>(MenuType::ITEM_LOADOUT,"Loadout Item 2")->SetItem(Item::BLANK);
@ -4024,7 +4024,7 @@ void AiL::ClearLoadoutItem(int slot){
}break;
case 2:{
itemAbility.action=[&](Player*p,vf2d pos={}){
return game->UseLoadoutItem(2);
return game->UseLoadoutItem(2,pos);
};
game->GetPlayer()->SetItem3UseFunc(itemAbility);
Component<MenuItemItemButton>(MenuType::ITEM_LOADOUT,"Loadout Item 3")->SetItem(Item::BLANK);

@ -363,7 +363,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();

@ -402,6 +402,7 @@ void sig::Animation::InitializeAnimations(){
CreateHorizontalAnimationSequence("tornado2.png",4,{24,48},AnimationData{.frameDuration{0.1f},.style{Animate2D::Style::Repeat}});
CreateHorizontalAnimationSequence("large_tornado.png",4,{72,144},AnimationData{.frameDuration{0.1f},.style{Animate2D::Style::Repeat}});
CreateHorizontalAnimationSequence("sand_suction.png",4,{72,72},AnimationData{.frameDuration{0.2f},.style{Animate2D::Style::Repeat}});
CreateHorizontalAnimationSequence("bomb.png",4,{24,24},AnimationData{.frameDuration{0.2f},.style{Animate2D::Style::OneShot}});
CreateStillAnimation("meteor.png",{192,192});

@ -351,19 +351,32 @@ 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={});
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::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;
};

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

@ -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;
@ -452,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){}
@ -561,11 +526,11 @@ uint32_t Inventory::GetItemCount(IT it){
}
//Returns true if the item has been consumed completely and there are 0 remaining of that type in our inventory.
bool Inventory::UseItem(IT it,uint32_t amt){
bool Inventory::UseItem(IT it,uint32_t amt,const std::optional<vf2d>targetingPos){
if(!_inventory.count(it))return false;
//There are two places to manipulate items in (Both the sorted inventory and the actual inventory)
for(uint32_t i=0;i<amt;i++){
if(ExecuteAction(it)){
if(ExecuteAction(it,targetingPos)){
return RemoveItem(GetItem(it)[0]);
}
}
@ -681,9 +646,9 @@ void Inventory::InsertIntoStageInventoryCategory(std::shared_ptr<Item>itemRef,co
Menu::InventorySlotsUpdated(stageInventoryCategory);
}
bool Inventory::ExecuteAction(IT item){
bool Inventory::ExecuteAction(IT item,const std::optional<vf2d>targetingPos){
if(ITEM_SCRIPTS.count(ITEM_DATA.at(item).useFunc)){
return ITEM_SCRIPTS.at(ITEM_DATA.at(item).useFunc)(game,ITEM_DATA[item].customProps);
return ITEM_SCRIPTS.at(ITEM_DATA.at(item).useFunc)(game,targetingPos,ITEM_DATA[item].customProps);
}else{
return false;
}
@ -1540,3 +1505,19 @@ const ItemEnchant&Item::ApplyRandomEnchant(){
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,
@ -217,6 +217,8 @@ public:
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!!
@ -295,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.
@ -328,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;
@ -356,6 +358,8 @@ 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;
@ -398,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;

@ -0,0 +1,94 @@
#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.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,std::optional<vf2d>targetingPos,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,std::optional<vf2d>targetingPos,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["Projectile"]=[](AiL*game,std::optional<vf2d>targetingPos,ItemProps props){
const int projectileDamage{props.GetIntProp("Base Damage")+int(game->GetPlayer()->GetAttack()*props.GetFloatProp("Player Damage Mult"))};
CreateBullet(ThrownProjectile)(game->GetPlayer()->GetPos(),targetingPos.value(),props.GetStringProp("Image"),props.GetFloatProp("Cast Size")/100.f*24,game->GetPlayer()->GetZ(),0.3f,8.f,projectileDamage,game->GetPlayer()->OnUpperLevel(),false,INFINITE,true,WHITE,props.GetFloatProp("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.GetFloatProp("Cast Size")/100.f*vf2d{1.f,1.f}},props.GetStringProp("Explode Sound Effect"))EndBullet;
return true;
};
ITEM_SCRIPTS.SetInitialized();
LOG(ITEM_SCRIPTS.size()<<" item scripts have been loaded.");
}

@ -37,80 +37,62 @@ All rights reserved.
#pragma endregion
#include "BulletTypes.h"
#include "AdventuresInLestoria.h"
#include <ranges>
#include "util.h"
#include "SoundEffect.h"
INCLUDE_game
PoisonBottle::PoisonBottle(vf2d pos,vf2d targetPos,float explodeRadius,float bounceExplodeRadius,float z,float totalFallTime,float totalRiseZAmt,int damage,int additionalBounceCount,bool upperLevel,bool hitsMultiple,float lifetime,bool friendly,Pixel col,vf2d scale,float image_angle)
:Bullet(pos,util::pointTo(pos,targetPos)*util::distance(pos,targetPos)/totalFallTime,0.f,damage,"poison_bottle.png",upperLevel,hitsMultiple,lifetime,false,friendly,col,scale,image_angle),initialZ(z),explodeRadius(explodeRadius),bounceExplodeRadius(bounceExplodeRadius),totalRiseZAmt(totalRiseZAmt),totalFallTime(totalFallTime),startingPos(pos),targetPos(targetPos),originalRisingTime(totalFallTime*0.25f/(additionalBounceCount+1)),risingTime(originalRisingTime),originalFallingTime(totalFallTime*0.75f/(additionalBounceCount+1)),fallingTime(originalFallingTime),additionalBounceCount(additionalBounceCount){
PoisonBottle::PoisonBottle(vf2d pos,vf2d targetPos,const std::string&img,float explodeRadius,float bounceExplodeRadius,float z,float totalFallTime,float totalRiseZAmt,int damage,int additionalBounceCount,bool upperLevel,bool hitsMultiple,float lifetime,bool friendly,Pixel col,vf2d scale,float image_angle)
:ThrownProjectile(pos,targetPos,img,explodeRadius,z,totalFallTime,totalRiseZAmt,damage,upperLevel,hitsMultiple,lifetime,friendly,col,scale,image_angle),bounceExplodeRadius(bounceExplodeRadius),additionalBounceCount(additionalBounceCount){
this->z=z;
this->risingTime=this->originalRisingTime=totalFallTime*0.25f/(additionalBounceCount+1);
this->fallingTime=this->originalFallingTime=totalFallTime*0.75f/(additionalBounceCount+1);
}
void PoisonBottle::Update(float fElapsedTime){
if(IsDeactivated())return;
image_angle+=0.5*PI*fElapsedTime;
const bool Landed{fallingTime<=0.f};
if(Landed){
z=0.f;
SoundEffect::PlaySFX("Glass Break",pos);
SoundEffect::PlaySFX("Poison Pool",pos);
float poisonCircleScale{"Witch.Ability 2.Casting Size"_F/100.f};
if(additionalBounceCount>0)poisonCircleScale="Poison Bounce"_ENC["BOUNCE EXPLODE RADIUS"]/100.f;
float currentExplodeRadius{explodeRadius};
if(additionalBounceCount>0)currentExplodeRadius=bounceExplodeRadius;
game->AddEffect(std::make_unique<Effect>(pos,0.f,"poison_pool.png",game->GetPlayer()->OnUpperLevel(),0.5f,1.2f,vf2d{poisonCircleScale,poisonCircleScale},vf2d{},WHITE,0.f,0.f,false),true);
for(int i:std::ranges::iota_view(0,200)){
void PoisonBottle::OnGroundLand(){
SoundEffect::PlaySFX("Glass Break",pos);
SoundEffect::PlaySFX("Poison Pool",pos);
float poisonCircleScale{"Witch.Ability 2.Casting Size"_F/100.f};
if(additionalBounceCount>0)poisonCircleScale="Poison Bounce"_ENC["BOUNCE EXPLODE RADIUS"]/100.f;
float currentExplodeRadius{explodeRadius};
if(additionalBounceCount>0)currentExplodeRadius=bounceExplodeRadius;
game->AddEffect(std::make_unique<Effect>(pos,0.f,"poison_pool.png",game->GetPlayer()->OnUpperLevel(),0.5f,1.2f,vf2d{poisonCircleScale,poisonCircleScale},vf2d{},WHITE,0.f,0.f,false),true);
for(int i:std::ranges::iota_view(0,200)){
float size{util::random_range(0.4f,0.8f)};
Pixel col{PixelLerp(VERY_DARK_GREEN,DARK_GREEN,util::random(1))};
col.a=util::random_range(60,200);
game->AddEffect(std::make_unique<Effect>(pos+vf2d{util::random(16)*poisonCircleScale,util::random(2*PI)}.cart(),util::random_range(1.f,4.f),"circle.png",game->GetPlayer()->OnUpperLevel(),vf2d{size,size},util::random_range(0.2f,0.5f),vf2d{util::random_range(-6.f,6.f),util::random_range(-12.f,-4.f)},col));
}
if(additionalBounceCount==0&&game->GetPlayer()->HasEnchant("Pooling Poison")){
game->AddEffect(std::make_unique<PoisonPool>(pos,"poison_pool.png",bounceExplodeRadius,game->GetPlayer()->GetAttack()*"Pooling Poison"_ENC["POISON POOL DAMAGE PCT"]/100.f,"Pooling Poison"_ENC["POISON POOL DAMAGE FREQUENCY"],friendly?HurtType::MONSTER:HurtType::PLAYER,"Pooling Poison"_ENC["SPLASH LINGER TIME"],0.5f,OnUpperLevel(),poisonCircleScale,vf2d{},WHITE,0.f,0.f,false,0.05f,[pos=pos,col=col,poisonCircleScale](const Effect&self){
float size{util::random_range(0.4f,0.8f)};
Pixel col{PixelLerp(VERY_DARK_GREEN,DARK_GREEN,util::random(1))};
col.a=util::random_range(60,200);
game->AddEffect(std::make_unique<Effect>(pos+vf2d{util::random(16)*poisonCircleScale,util::random(2*PI)}.cart(),util::random_range(1.f,4.f),"circle.png",game->GetPlayer()->OnUpperLevel(),vf2d{size,size},util::random_range(0.2f,0.5f),vf2d{util::random_range(-6.f,6.f),util::random_range(-12.f,-4.f)},col));
}
if(additionalBounceCount==0&&game->GetPlayer()->HasEnchant("Pooling Poison")){
game->AddEffect(std::make_unique<PoisonPool>(pos,"poison_pool.png",bounceExplodeRadius,game->GetPlayer()->GetAttack()*"Pooling Poison"_ENC["POISON POOL DAMAGE PCT"]/100.f,"Pooling Poison"_ENC["POISON POOL DAMAGE FREQUENCY"],friendly?HurtType::MONSTER:HurtType::PLAYER,"Pooling Poison"_ENC["SPLASH LINGER TIME"],0.5f,OnUpperLevel(),poisonCircleScale,vf2d{},WHITE,0.f,0.f,false,0.05f,[pos=pos,col=col,poisonCircleScale](const Effect&self){
float size{util::random_range(0.4f,0.8f)};
return Effect{pos+vf2d{util::random(16)*poisonCircleScale,util::random(2*PI)}.cart(),util::random_range(1.f,4.f),"circle.png",game->GetPlayer()->OnUpperLevel(),vf2d{size,size},util::random_range(0.2f,0.5f),vf2d{util::random_range(-6.f,6.f),util::random_range(-12.f,-4.f)},col};
}),true);
}
int poolDamage{damage};
if(additionalBounceCount>0)poolDamage=game->GetPlayer()->GetAttack()*"Poison Bounce"_ENC["BOUNCE DAMAGE"]/100.f;
return Effect{pos+vf2d{util::random(16)*poisonCircleScale,util::random(2*PI)}.cart(),util::random_range(1.f,4.f),"circle.png",game->GetPlayer()->OnUpperLevel(),vf2d{size,size},util::random_range(0.2f,0.5f),vf2d{util::random_range(-6.f,6.f),util::random_range(-12.f,-4.f)},col};
}),true);
}
const HurtList hurtList{game->Hurt(pos,bounceExplodeRadius,poolDamage,OnUpperLevel(),z,HurtType::MONSTER,HurtFlag::PLAYER_ABILITY)};
if(additionalBounceCount==0){
for(const auto&[targetPtr,wasHit]:hurtList){
if(wasHit){
Monster*monsterPtr{std::get<Monster*>(targetPtr)};
const int buffDamage{int(game->GetPlayer()->GetAttack()*"Witch.Ability 2.Poison Debuff"_f[0])};
const float buffDuration{"Witch.Ability 2.Poison Debuff"_f[2]};
const float buffTimeBetweenTicks{"Witch.Ability 2.Poison Debuff"_f[1]};
monsterPtr->AddBuff(BuffType::COLOR_MOD,buffDuration,Pixel{GREEN}.n);
monsterPtr->ApplyDot(buffDuration,buffDamage,buffTimeBetweenTicks);
}
int poolDamage{damage};
if(additionalBounceCount>0)poolDamage=game->GetPlayer()->GetAttack()*"Poison Bounce"_ENC["BOUNCE DAMAGE"]/100.f;
const HurtList hurtList{game->Hurt(pos,bounceExplodeRadius,poolDamage,OnUpperLevel(),z,HurtType::MONSTER,HurtFlag::PLAYER_ABILITY)};
if(additionalBounceCount==0){
for(const auto&[targetPtr,wasHit]:hurtList){
if(wasHit){
Monster*monsterPtr{std::get<Monster*>(targetPtr)};
const int buffDamage{int(game->GetPlayer()->GetAttack()*"Witch.Ability 2.Poison Debuff"_f[0])};
const float buffDuration{"Witch.Ability 2.Poison Debuff"_f[2]};
const float buffTimeBetweenTicks{"Witch.Ability 2.Poison Debuff"_f[1]};
monsterPtr->AddBuff(BuffType::COLOR_MOD,buffDuration,Pixel{GREEN}.n);
monsterPtr->ApplyDot(buffDuration,buffDamage,buffTimeBetweenTicks);
}
}
if(additionalBounceCount>0){
additionalBounceCount--;
risingTime=originalRisingTime;
fallingTime=originalFallingTime;
}else{
vel={};
fadeOutTime=0.25f;
Deactivate();
}
}
if(additionalBounceCount>0){
additionalBounceCount--;
risingTime=originalRisingTime;
fallingTime=originalFallingTime;
}else{
if(risingTime>0.f){
risingTime-=fElapsedTime;
z=util::lerp(initialZ,initialZ+totalRiseZAmt,1-(risingTime/originalRisingTime));
}else{
fallingTime-=fElapsedTime;
z=util::lerp(initialZ+totalRiseZAmt,0.f,1-(fallingTime/originalFallingTime));
}
vel={};
fadeOutTime=0.25f;
Deactivate();
}
}

@ -3,9 +3,8 @@ Add unconscious monster state.
DEMO
====
Save file backup and rolling back on failure
Add Sherman and Blacksmith story images
Adding new class animations
===========================

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

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

@ -65,7 +65,6 @@ void VisualNovel::Initialize(){
std::string line;
std::string currentStory;
while(file.good()){
auto trim = [](std::string& s)
{
s.erase(0, s.find_first_not_of(" \t\n\r\f\v"));
@ -179,7 +178,6 @@ void VisualNovel::Initialize(){
data.push_back(std::make_unique<DialogCommand>(line));
}break;
}
}
}

@ -169,7 +169,7 @@ void Witch::InitializeClassAbilities(){
int additionalBounceCount{0};
if(p->HasEnchant("Poison Bounce"))additionalBounceCount+="Poison Bounce"_ENC["BOUNCE COUNT"];
const float totalFallTime{util::lerp(1/30.f*(additionalBounceCount+1),0.3f,util::distance(p->GetPos(),pos)/("Witch.Ability 2.Casting Range"_F/100.f*24))};
CreateBullet(PoisonBottle)(p->GetPos(),pos,"Witch.Ability 2.Casting Size"_F/100.f*24,"Poison Bounce"_ENC["BOUNCE EXPLODE RADIUS"]/100.f*24,12.f,totalFallTime,"Witch.Ability 2.Toss Max Z"_F,p->GetAttack()*"Witch.Ability 2.Damage Mult"_F,additionalBounceCount,p->OnUpperLevel(),false,INFINITE,true,WHITE,vf2d{1.f,1.f},util::random(2*PI))EndBullet;
CreateBullet(PoisonBottle)(p->GetPos(),pos,"poison_bottle.png","Witch.Ability 2.Casting Size"_F/100.f*24,"Poison Bounce"_ENC["BOUNCE EXPLODE RADIUS"]/100.f*24,12.f,totalFallTime,"Witch.Ability 2.Toss Max Z"_F,p->GetAttack()*"Witch.Ability 2.Damage Mult"_F,additionalBounceCount,p->OnUpperLevel(),false,INFINITE,true,WHITE,vf2d{1.f,1.f},util::random(2*PI))EndBullet;
return true;
};
#pragma endregion

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

@ -315,6 +315,11 @@ Events
# Specify file names, followed by volume %. Optional min and max pitch adjustment (Defaults are 90%,110%)
File[0] = poison_pool.ogg, 80%
}
Projectile Toss
{
# Specify file names, followed by volume %. Optional min and max pitch adjustment (Defaults are 90%,110%)
File[0] = Throw Projectile.ogg, 50%
}
Ranger Auto Attack
{
CombatSound = True

@ -138,6 +138,7 @@ Images
GFX_LineIndicator = line_indicator.png
GFX_MusketBullet = musket_bullet.png
GFX_SandSuction = sand_suction.png
GFX_Bomb = bomb.png
GFX_Thief_Sheet = nico-thief.png
GFX_Trapper_Sheet = nico-trapper.png

@ -186,11 +186,12 @@ ItemDatabase
{
Description = Throw a bomb, dealing damage around an area.
ItemCategory = Consumables
ItemScript = Bomb
ItemScript = Projectile
Image = bomb.png
Base Damage = 50
Player Damage Mult = 1.6x
Explode Radius = 350
Explode Sound Effect = Bomb Explode
Cast Size = 350
Cast Range = 700
Cooldown Time = 5.0
Cast Time = 0.0
@ -212,15 +213,18 @@ ItemDatabase
{
Description = Throw a molotov dealing damage and leave behind a burning area.
ItemCategory = Consumables
ItemScript = Bomb
ItemScript = Projectile
Image = molotov.png
Base Damage = 50
Player Damage Mult = 1.6x
Explode Radius = 250
Cast Size = 250
Cast Range = 700
Tick Rate = 1s
Tick Base Damage = 10
Tick Player Damage Mult = 0.4x
Explode Sound Effect = Bomb Explode
Lingering Effect = pixel.png
Linger Radius = 250
Linger Time = 4s
Cooldown Time = 5.0
Cast Time = 0.0

@ -48,4 +48,22 @@ ItemScript
MP Restore = 0,0.0,0.0
MP % Restore = 0,0.0,0.0
}
Projectile
{
Image = pixel.png
# Extra flat damage
Base Damage = 0
# Additional damage based on player ATK
Player Damage Mult = 1.0x
Cast Size = 250
Explode Sound Effect = Bomb Explode
# Set a lingering time to have a damage over time effect.
Linger Time = 0s
Lingering Effect = pixel.png
Linger Radius = 250
Tick Rate = 1s
Tick Base Damage = 10
Tick Player Damage Mult = 0.4x
}
}
Loading…
Cancel
Save