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. 28
      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); Game::Update(1.f);
Assert::AreEqual(int(game->GetPlayer()->GetMaxHealth()-parrot.GetCollisionDamage()),game->GetPlayer()->GetHealth(),L"Player should take collision damage from the parrot."); 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> </SubType>
</ClCompile> </ClCompile>
<ClCompile Include="ItemLoadoutWindow.cpp" /> <ClCompile Include="ItemLoadoutWindow.cpp" />
<ClCompile Include="ItemScript.cpp">
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="Key.cpp" /> <ClCompile Include="Key.cpp" />
<ClCompile Include="LargeStone.cpp" /> <ClCompile Include="LargeStone.cpp" />
<ClCompile Include="LargeTornado.cpp"> <ClCompile Include="LargeTornado.cpp">
@ -1071,6 +1075,10 @@
<SubType> <SubType>
</SubType> </SubType>
</ClCompile> </ClCompile>
<ClCompile Include="ThrownProjectile.cpp">
<SubType>
</SubType>
</ClCompile>
<ClCompile Include="PoisonPool.cpp"> <ClCompile Include="PoisonPool.cpp">
<SubType> <SubType>
</SubType> </SubType>

@ -1190,7 +1190,7 @@
<ClCompile Include="PurpleEnergyBall.cpp"> <ClCompile Include="PurpleEnergyBall.cpp">
<Filter>Source Files\Bullet Types</Filter> <Filter>Source Files\Bullet Types</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="PoisonBottle.cpp"> <ClCompile Include="ThrownProjectile.cpp">
<Filter>Source Files\Bullet Types</Filter> <Filter>Source Files\Bullet Types</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="State_Dialog.cpp"> <ClCompile Include="State_Dialog.cpp">
@ -1253,6 +1253,12 @@
<ClCompile Include="GiantCrab.cpp"> <ClCompile Include="GiantCrab.cpp">
<Filter>Source Files\Monster Strategies</Filter> <Filter>Source Files\Monster Strategies</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="ItemScript.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="PoisonBottle.cpp">
<Filter>Source Files\Bullet Types</Filter>
</ClCompile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="cpp.hint" /> <None Include="cpp.hint" />

@ -3929,28 +3929,28 @@ void AiL::SetLoadoutItem(int slot,std::string itemName){
inputGroup=&Player::KEY_ITEM3; inputGroup=&Player::KEY_ITEM3;
}break; }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.actionPerformedDuringCast=GetLoadoutItem(slot).lock()->UseDuringCast();
itemAbility.itemAbility=true; itemAbility.itemAbility=true;
switch(slot){ switch(slot){
case 0:{ case 0:{
itemAbility.action=[&](Player*p,vf2d pos={}){ itemAbility.action=[&](Player*p,vf2d pos={}){
return game->UseLoadoutItem(0); return game->UseLoadoutItem(0,pos);
}; };
game->GetPlayer()->SetItem1UseFunc(itemAbility); game->GetPlayer()->SetItem1UseFunc(itemAbility);
Component<MenuItemItemButton>(MenuType::ITEM_LOADOUT,"Loadout Item 1")->SetItem(loadout[slot]); Component<MenuItemItemButton>(MenuType::ITEM_LOADOUT,"Loadout Item 1")->SetItem(loadout[slot]);
}break; }break;
case 1:{ case 1:{
itemAbility.action=[&](Player*p,vf2d pos={}){ itemAbility.action=[&](Player*p,vf2d pos={}){
return game->UseLoadoutItem(1); return game->UseLoadoutItem(1,pos);
}; };
game->GetPlayer()->SetItem2UseFunc(itemAbility); game->GetPlayer()->SetItem2UseFunc(itemAbility);
Component<MenuItemItemButton>(MenuType::ITEM_LOADOUT,"Loadout Item 2")->SetItem(loadout[slot]); Component<MenuItemItemButton>(MenuType::ITEM_LOADOUT,"Loadout Item 2")->SetItem(loadout[slot]);
}break; }break;
case 2:{ case 2:{
itemAbility.action=[&](Player*p,vf2d pos={}){ itemAbility.action=[&](Player*p,vf2d pos={}){
return game->UseLoadoutItem(2); return game->UseLoadoutItem(2,pos);
}; };
game->GetPlayer()->SetItem3UseFunc(itemAbility); game->GetPlayer()->SetItem3UseFunc(itemAbility);
Component<MenuItemItemButton>(MenuType::ITEM_LOADOUT,"Loadout Item 3")->SetItem(loadout[slot]); 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(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){ if(!ISBLANK(GetLoadoutItem(slot).lock())&&GetLoadoutItem(slot).lock()->Amt()>0){
Tutorial::GetTask(TutorialTaskName::USE_RECOVERY_ITEMS).I(A::ITEM_USE_COUNT)++; 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); Inventory::AddLoadoutItemUsed(GetLoadoutItem(slot).lock()->ActualName(),slot);
GetLoadoutItem(slot).lock()->amt--; GetLoadoutItem(slot).lock()->amt--;
if(GetLoadoutItem(slot).lock()->UseSound().length()>0){ if(GetLoadoutItem(slot).lock()->UseSound().length()>0){
@ -4008,7 +4008,7 @@ void AiL::ClearLoadoutItem(int slot){
switch(slot){ switch(slot){
case 0:{ case 0:{
itemAbility.action=[&](Player*p,vf2d pos={}){ itemAbility.action=[&](Player*p,vf2d pos={}){
return game->UseLoadoutItem(0); return game->UseLoadoutItem(0,pos);
}; };
game->GetPlayer()->SetItem1UseFunc(itemAbility); game->GetPlayer()->SetItem1UseFunc(itemAbility);
Component<MenuItemItemButton>(MenuType::ITEM_LOADOUT,"Loadout Item 1")->SetItem(Item::BLANK); Component<MenuItemItemButton>(MenuType::ITEM_LOADOUT,"Loadout Item 1")->SetItem(Item::BLANK);
@ -4016,7 +4016,7 @@ void AiL::ClearLoadoutItem(int slot){
}break; }break;
case 1:{ case 1:{
itemAbility.action=[&](Player*p,vf2d pos={}){ itemAbility.action=[&](Player*p,vf2d pos={}){
return game->UseLoadoutItem(1); return game->UseLoadoutItem(1,pos);
}; };
game->GetPlayer()->SetItem2UseFunc(itemAbility); game->GetPlayer()->SetItem2UseFunc(itemAbility);
Component<MenuItemItemButton>(MenuType::ITEM_LOADOUT,"Loadout Item 2")->SetItem(Item::BLANK); Component<MenuItemItemButton>(MenuType::ITEM_LOADOUT,"Loadout Item 2")->SetItem(Item::BLANK);
@ -4024,7 +4024,7 @@ void AiL::ClearLoadoutItem(int slot){
}break; }break;
case 2:{ case 2:{
itemAbility.action=[&](Player*p,vf2d pos={}){ itemAbility.action=[&](Player*p,vf2d pos={}){
return game->UseLoadoutItem(2); return game->UseLoadoutItem(2,pos);
}; };
game->GetPlayer()->SetItem3UseFunc(itemAbility); game->GetPlayer()->SetItem3UseFunc(itemAbility);
Component<MenuItemItemButton>(MenuType::ITEM_LOADOUT,"Loadout Item 3")->SetItem(Item::BLANK); Component<MenuItemItemButton>(MenuType::ITEM_LOADOUT,"Loadout Item 3")->SetItem(Item::BLANK);

@ -363,7 +363,7 @@ public:
int GetLoadoutSize()const; int GetLoadoutSize()const;
void RestockLoadoutItems(); void RestockLoadoutItems();
//Returns true if the item can be used (we have >0 of it) //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. //Blanks out this loadout item.
void ClearLoadoutItem(int slot); void ClearLoadoutItem(int slot);
void RenderFadeout(); 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("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("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("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}); CreateStillAnimation("meteor.png",{192,192});

@ -351,19 +351,32 @@ private:
std::optional<std::weak_ptr<Monster>>homingTarget; std::optional<std::weak_ptr<Monster>>homingTarget;
}; };
struct PoisonBottle:public Bullet{ struct ThrownProjectile: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}); 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 Update(float fElapsedTime)override;
void ModifyOutgoingDamageData(HurtDamageInfo&data); 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 targetPos;
const vf2d startingPos; const vf2d startingPos;
const float totalFallTime; const float totalFallTime;
const float originalRisingTime,originalFallingTime; float originalRisingTime;
float originalFallingTime;
float risingTime,fallingTime; float risingTime,fallingTime;
const float initialZ; const float initialZ;
const float totalRiseZAmt; const float totalRiseZAmt;
const float explodeRadius; 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; const float bounceExplodeRadius;
int additionalBounceCount; int additionalBounceCount;
}; };

@ -63,6 +63,7 @@ using MonsterSpawnerID=int;
#define INCLUDE_ITEM_CONVERSIONS extern safemap<std::string,IT>ITEM_CONVERSIONS; #define INCLUDE_ITEM_CONVERSIONS extern safemap<std::string,IT>ITEM_CONVERSIONS;
#define INCLUDE_ADMIN_MODE extern bool ADMIN_MODE; #define INCLUDE_ADMIN_MODE extern bool ADMIN_MODE;
#define INCLUDE_DEMO_BUILD extern bool DEMO_BUILD; #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; #define INCLUDE_PACK_KEY extern std::string PACK_KEY;

@ -142,6 +142,8 @@ void ItemInfo::InitializeItems(){
std::string scriptName="",description="",category=""; std::string scriptName="",description="",category="";
std::string setName=""; std::string setName="";
float castTime=0; float castTime=0;
float castRange=0;
float castSize=0;
std::vector<std::string> slot; std::vector<std::string> slot;
float cooldownTime="Item.Item Cooldown Time"_F; float cooldownTime="Item.Item Cooldown Time"_F;
std::vector<ItemAttribute>statValueList; std::vector<ItemAttribute>statValueList;
@ -167,6 +169,12 @@ void ItemInfo::InitializeItems(){
}else }else
if(keyName=="Cast Time"){ if(keyName=="Cast Time"){
castTime=float(data[key][keyName].GetReal()); 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 }else
if(keyName=="Cooldown Time"){ if(keyName=="Cooldown Time"){
cooldownTime=float(data[key][keyName].GetReal()); cooldownTime=float(data[key][keyName].GetReal());
@ -302,6 +310,8 @@ void ItemInfo::InitializeItems(){
it.description=description; it.description=description;
it.category=category; it.category=category;
it.castTime=castTime; it.castTime=castTime;
it.castRange=castRange;
it.castSize=castSize;
it.cooldownTime=cooldownTime; it.cooldownTime=cooldownTime;
it.slot=EquipSlot::NONE; it.slot=EquipSlot::NONE;
it.set=setName; it.set=setName;
@ -452,51 +462,6 @@ const std::unordered_map<std::string,BuffOverTimeType::BuffOverTimeType>ItemInfo
{"MP % Restore",BuffOverTimeType::MP_PCT_RESTORATION}, {"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() Item::Item()
:amt(0),it(nullptr),enhancementLevel(0){} :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. //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; if(!_inventory.count(it))return false;
//There are two places to manipulate items in (Both the sorted inventory and the actual inventory) //There are two places to manipulate items in (Both the sorted inventory and the actual inventory)
for(uint32_t i=0;i<amt;i++){ for(uint32_t i=0;i<amt;i++){
if(ExecuteAction(it)){ if(ExecuteAction(it,targetingPos)){
return RemoveItem(GetItem(it)[0]); return RemoveItem(GetItem(it)[0]);
} }
} }
@ -681,9 +646,9 @@ void Inventory::InsertIntoStageInventoryCategory(std::shared_ptr<Item>itemRef,co
Menu::InventorySlotsUpdated(stageInventoryCategory); 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)){ 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{ }else{
return false; return false;
} }
@ -1540,3 +1505,19 @@ const ItemEnchant&Item::ApplyRandomEnchant(){
void Item::RemoveEnchant(){ void Item::RemoveEnchant(){
enchant.reset(); 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 EventName=std::string;
using EnchantName=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{ enum class EquipSlot{
HELMET= 0b0000'0001, HELMET= 0b0000'0001,
@ -217,6 +217,8 @@ public:
const Stats GetStats()const; const Stats GetStats()const;
const ItemScript&OnUseAction()const; const ItemScript&OnUseAction()const;
const float CastTime()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 bool UseDuringCast()const; //When true, the item is activated during the cast instead of after the cast.
const float CooldownTime()const; const float CooldownTime()const;
//Use ISBLANK macro instead!! This should not be called directly!! //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::shared_ptr<Item>>CopyItem(IT it);
static std::vector<std::weak_ptr<Item>>GetItem(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. //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. //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); 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. //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: private:
static void InsertIntoSortedInv(std::shared_ptr<Item>itemRef); static void InsertIntoSortedInv(std::shared_ptr<Item>itemRef);
static void InsertIntoStageInventoryCategory(std::shared_ptr<Item>itemRef,const bool monsterDrop); 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::multimap<IT,std::shared_ptr<Item>>_inventory;
static std::vector<std::shared_ptr<Item>>blacksmithInventory; static std::vector<std::shared_ptr<Item>>blacksmithInventory;
static std::map<EquipSlot,std::weak_ptr<Item>>equipment; static std::map<EquipSlot,std::weak_ptr<Item>>equipment;
@ -356,6 +358,8 @@ class ItemInfo{
std::string description; std::string description;
std::string category; std::string category;
float castTime=0; float castTime=0;
float castRange=0;
float castSize=0;
float cooldownTime=0; float cooldownTime=0;
EquipSlot slot=EquipSlot::NONE; EquipSlot slot=EquipSlot::NONE;
EnhancementInfo enhancement; EnhancementInfo enhancement;
@ -398,6 +402,8 @@ public:
const ItemScript&OnUseAction()const; const ItemScript&OnUseAction()const;
const Stats GetStats(int enhancementLevel)const; const Stats GetStats(int enhancementLevel)const;
const float CastTime()const; const float CastTime()const;
const float CastRange()const;
const float CastSize()const;
const float CooldownTime()const; const float CooldownTime()const;
const EquipSlot Slot()const; const EquipSlot Slot()const;
const std::optional<const::ItemSet*const>ItemSet()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,25 +37,16 @@ All rights reserved.
#pragma endregion #pragma endregion
#include "BulletTypes.h" #include "BulletTypes.h"
#include "AdventuresInLestoria.h"
#include <ranges> #include <ranges>
#include "util.h"
#include "SoundEffect.h"
INCLUDE_game 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){
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){
this->z=z; 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){ void PoisonBottle::OnGroundLand(){
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("Glass Break",pos);
SoundEffect::PlaySFX("Poison Pool",pos); SoundEffect::PlaySFX("Poison Pool",pos);
float poisonCircleScale{"Witch.Ability 2.Casting Size"_F/100.f}; float poisonCircleScale{"Witch.Ability 2.Casting Size"_F/100.f};
@ -103,15 +94,6 @@ void PoisonBottle::Update(float fElapsedTime){
fadeOutTime=0.25f; fadeOutTime=0.25f;
Deactivate(); Deactivate();
} }
}else{
if(risingTime>0.f){
risingTime-=fElapsedTime;
z=util::lerp(initialZ,initialZ+totalRiseZAmt,1-(risingTime/originalRisingTime));
}else{
fallingTime-=fElapsedTime;
z=util::lerp(initialZ+totalRiseZAmt,0.f,1-(fallingTime/originalFallingTime));
}
}
} }
void PoisonBottle::ModifyOutgoingDamageData(HurtDamageInfo&data){ void PoisonBottle::ModifyOutgoingDamageData(HurtDamageInfo&data){

@ -3,9 +3,8 @@ Add unconscious monster state.
DEMO DEMO
==== ====
Save file backup and rolling back on failure
Add Sherman and Blacksmith story images
Adding new class animations 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_MAJOR 1
#define VERSION_MINOR 3 #define VERSION_MINOR 3
#define VERSION_PATCH 0 #define VERSION_PATCH 0
#define VERSION_BUILD 11725 #define VERSION_BUILD 11748
#define stringify(a) stringify_(a) #define stringify(a) stringify_(a)
#define stringify_(a) #a #define stringify_(a) #a

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

@ -169,7 +169,7 @@ void Witch::InitializeClassAbilities(){
int additionalBounceCount{0}; int additionalBounceCount{0};
if(p->HasEnchant("Poison Bounce"))additionalBounceCount+="Poison Bounce"_ENC["BOUNCE COUNT"]; 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))}; 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; return true;
}; };
#pragma endregion #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%) # Specify file names, followed by volume %. Optional min and max pitch adjustment (Defaults are 90%,110%)
File[0] = poison_pool.ogg, 80% 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 Ranger Auto Attack
{ {
CombatSound = True CombatSound = True

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

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

@ -48,4 +48,22 @@ ItemScript
MP Restore = 0,0.0,0.0 MP Restore = 0,0.0,0.0
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