The open source repository for the action RPG game in development by Sig Productions titled 'Adventures in Lestoria'! https://forums.lestoria.net
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
AdventuresInLestoria/Adventures in Lestoria/Item.cpp

1538 lines
56 KiB

#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 "Item.h"
#include "safemap.h"
#include "DEFINES.h"
#include "AdventuresInLestoria.h"
#include "Menu.h"
#include "Ability.h"
#include "AttributableStat.h"
#include <numeric>
#include "util.h"
#include "SoundEffect.h"
#include "ClassInfo.h"
#include "RowInventoryScrollableWindowComponent.h"
#ifndef __EMSCRIPTEN__
#include "steam/isteamuserstats.h"
#endif
#include <ranges>
#include "ItemEnchant.h"
INCLUDE_game
INCLUDE_DATA
INCLUDE_GFX
const std::string Item::BLANK_ITEM_NAME="";
safemap<std::string,ItemInfo>ITEM_DATA;
safemap<std::string,IT>ITEM_CONVERSIONS;
safemap<std::string,ItemScript>ITEM_SCRIPTS;
safemap<std::string,std::set<std::string>>ITEM_CATEGORIES;
std::shared_ptr<Item>Item::BLANK=std::make_shared<Item>();
std::multimap<IT,std::shared_ptr<Item>>Inventory::_inventory;
std::vector<std::shared_ptr<Item>>Inventory::blacksmithInventory;
std::map<ITCategory,std::vector<std::shared_ptr<Item>>>Inventory::sortedInv;
std::array<std::pair<IT,int>,3U>Inventory::loadoutItemsUsed;
std::vector<ItemOverlay>ItemOverlay::items;
std::map<std::string,ItemSet>ItemSet::sets;
std::map<EquipSlot,std::weak_ptr<Item>>Inventory::equipment;
std::map<std::string,EquipSlot>ItemInfo::nameToEquipSlot;
int Item::IsBlankStaticCallCounter=0;
safemap<int,float>Stats::maxDamageReductionTable;
ItemEnhancementFunctionPrimingData Item::enhanceFunctionPrimed("CanEnhanceItem()");
std::vector<std::shared_ptr<Item>>ItemInfo::craftableConsumables;
std::vector<std::string>ItemSortRules::primarySort;
std::vector<std::string>ItemSortRules::secondarySort;
Stats Stats::NO_MAX_HIGHLIGHT;
ItemInfo::ItemInfo()
:customProps({nullptr,nullptr}){}
void ItemInfo::InitializeItems(){
Stats::NO_MAX_HIGHLIGHT={};
ItemSortRules::secondarySort.clear();
ItemSortRules::primarySort.clear();
ItemInfo::craftableConsumables.clear();
Stats::maxDamageReductionTable.Reset();
ItemInfo::nameToEquipSlot.clear();
Inventory::equipment.clear();
ItemSet::sets.clear();
ItemOverlay::items.clear();
Inventory::loadoutItemsUsed={};
Inventory::sortedInv.clear();
Inventory::blacksmithInventory.clear();
Inventory::_inventory.clear();
ITEM_CONVERSIONS.Reset();
ITEM_DATA.Reset();
ITEM_CATEGORIES.Reset();
ITEM_CONVERSIONS.Reset();
for(const std::string&data:DATA.GetProperty("Item.Equipment Sort Order Primary").GetValues()){
ItemSortRules::primarySort.push_back(data);
}
for(const std::string&data:DATA.GetProperty("Item.Equipment Sort Order Secondary").GetValues()){
ItemSortRules::secondarySort.push_back(data);
}
for(int i=int(EquipSlot::HELMET);i<=int(EquipSlot::RING2);i<<=1){
Inventory::equipment[EquipSlot(i)]=Item::BLANK;
}
nameToEquipSlot["Helmet"]=EquipSlot::HELMET;
nameToEquipSlot["Weapon"]=EquipSlot::WEAPON;
nameToEquipSlot["Armor"]=EquipSlot::ARMOR;
nameToEquipSlot["Gloves"]=EquipSlot::GLOVES;
nameToEquipSlot["Pants"]=EquipSlot::PANTS;
nameToEquipSlot["Shoes"]=EquipSlot::SHOES;
nameToEquipSlot["Ring1"]=EquipSlot::RING1;
nameToEquipSlot["Ring2"]=EquipSlot::RING2;
InitializeScripts();
InitializeSets();
for(auto&[key,value]:DATA["ItemCategory"].GetKeys()){
ITEM_CATEGORIES[key];
Inventory::sortedInv[key];
Menu::InitializeMenuListenerCategory(key);
}
auto ReadItems=[&](datafile&data){
for(auto&[key,value]:data.GetKeys()){
if(key=="")ERR("Failed to read an item block ,no name specified!");
std::string imgPath="assets/"+"item_img_directory"_S+key+".png";
Renderable&img=GFX["item_img_directory"_S+key+".png"];
game->LoadResource(img,imgPath);
std::string scriptName="",description="",category="";
std::string setName="";
float castTime=0;
std::vector<std::string> slot;
float cooldownTime="Item.Item Cooldown Time"_F;
std::vector<ItemAttribute>statValueList;
uint32_t sellValue=0;
uint32_t buyValue=0;
Stats minStats;
Stats maxStats;
bool useDuringCast=false;
std::unordered_set<std::string>equippableClass;
EventName useSound;
std::optional<std::string>fragmentName;
std::optional<std::string>fragmentIcon;
for(auto&[itemKey,itemValue]:data[key].GetKeys()){
std::string keyName=itemKey;
if(keyName=="Description"){
description=data[key][keyName].GetFullString();
}else
if(keyName=="ItemCategory"){
category=data[key][keyName].GetString();
}else
if(keyName=="ItemScript"){
scriptName=data[key][keyName].GetString();
}else
if(keyName=="Cast Time"){
castTime=float(data[key][keyName].GetReal());
}else
if(keyName=="Cooldown Time"){
cooldownTime=float(data[key][keyName].GetReal());
}else
if(keyName=="Slot"){
for(auto&val:data[key][keyName].GetValues()){
slot.push_back(val);
}
}else
if(keyName=="StatValues"){
for(int i=0;i<data[key][keyName].GetValueCount();i++){
statValueList.push_back(ItemAttribute::Get(data[key]["StatValues"].GetString(i)));
}
}else
if(keyName=="PartofSet"){
setName=data[key][keyName].GetString();
}
if(keyName=="BuyValue"){
buyValue=data[key][keyName].GetInt();
}else
if(keyName=="SellValue"){
sellValue=data[key][keyName].GetInt();
}else
if(keyName=="UseSound"||keyName=="Equip Sound"){
useSound=data[key][keyName].GetString();
}else
if(keyName=="Class"){
for(int i:std::ranges::iota_view(0U,data[key][keyName].GetValueCount())){
equippableClass.insert(data[key][keyName].GetString(i));
classutils::StringToClass(data[key][keyName].GetString(i)); //If this errors out then we specified an invalid class!
}
}else
if(keyName.starts_with("Alternative Name")){
if(ITEM_CONVERSIONS.count(data[key][keyName].GetString()))ERR(std::format("Item {} already exists in Item Conversion database! Cannot add a duplicate entry!",data[key][keyName].GetString()));
ITEM_CONVERSIONS[data[key][keyName].GetString()]=key;
}else
if(keyName.starts_with("Fragment Name")){
fragmentName=data[key][keyName].GetString();
}else
if(keyName.starts_with("Fragment Icon")){
fragmentIcon=data[key][keyName].GetString();
}else{ //THis is a custom override modifier for a script. NO-OP
}
}
ItemInfo&it=ITEM_DATA[key];
if(data[key].HasProperty("StatValues[0]")){ //This means this has enhancement levels.
EnhancementInfo enhancementStats;
uint8_t availableChapter=1;
for(int enhancementLevel=0;enhancementLevel<=10;enhancementLevel++){
datafile&dat=data[key]["StatValues["+std::to_string(enhancementLevel)+"]"];
for(int attrIndex=0;ItemAttribute&attr:statValueList){
enhancementStats.SetAttribute(enhancementLevel,attr,dat.GetReal(attrIndex));
attrIndex++;
}
std::vector<ItemPair>itemsRequired;
uint32_t goldCost=0;
if(enhancementLevel!=0){ //The first level does not require any crafting, skip this level.
while(data[key]["Crafting"][std::format("Level[{}]",enhancementLevel)].HasProperty(std::format("Item[{}]",itemsRequired.size()))){
datafile&item=data[key]["Crafting"][std::format("Level[{}]",enhancementLevel)][std::format("Item[{}]",itemsRequired.size())];
itemsRequired.push_back({item.GetString(0),item.GetInt(1)});
}
if(data[key]["Crafting"][std::format("Level[{}]",enhancementLevel)].HasProperty("Gold")){
goldCost=data[key]["Crafting"][std::format("Level[{}]",enhancementLevel)]["Gold"].GetInt();
}
if(data[key]["Crafting"][std::format("Level[{}]",enhancementLevel)].HasProperty("AvailableChapter")){
availableChapter=data[key]["Crafting"][std::format("Level[{}]",enhancementLevel)]["AvailableChapter"].GetInt();
}
enhancementStats.SetCraftingRequirements(enhancementLevel,itemsRequired,goldCost,availableChapter);
}else
{ //This item still could be craftable, provide a recipe for that if necessary.
while(data[key]["Crafting"].HasProperty(std::format("Item[{}]",itemsRequired.size()))){
datafile&item=data[key]["Crafting"][std::format("Item[{}]",itemsRequired.size())];
itemsRequired.push_back({item.GetString(0),item.GetInt(1)});
}
if(data[key]["Crafting"].HasProperty("Gold")){
goldCost=data[key]["Crafting"]["Gold"].GetInt();
}
if(data[key]["Crafting"].HasProperty("AvailableChapter")){
availableChapter=data[key]["Crafting"]["AvailableChapter"].GetInt();
}
enhancementStats.SetCraftingRequirements(enhancementLevel,itemsRequired,goldCost,availableChapter);
}
}
it.enhancement=enhancementStats;
}
if(data[key].HasProperty("Crafting")&&!data[key]["Crafting"].HasProperty("Level[1]")){
std::vector<ItemPair>itemsRequired;
uint32_t goldCost=0;
uint8_t availableChapter=1;
//If the item has a key called "Crafting" but did not specify a level number, then we assume this item just has one base level for normal crafting and thus we will add the requirements to craft this item under enhancement level 1.
while(data[key]["Crafting"].HasProperty(std::format("Item[{}]",itemsRequired.size()))){
datafile&item=data[key]["Crafting"][std::format("Item[{}]",itemsRequired.size())];
itemsRequired.push_back({item.GetString(0),item.GetInt(1)});
}
goldCost=data[key]["Crafting"]["Gold"].GetInt();
if(data[key]["Crafting"].HasProperty("AvailableChapter")){
availableChapter=data[key]["Crafting"]["AvailableChapter"].GetInt();
}
craftableConsumables.push_back(std::make_shared<Item>(1,key));
it.enhancement.SetAttribute(1,ItemAttribute::Get("Attack"),0.f);
it.enhancement.SetCraftingRequirements(1,itemsRequired,goldCost,availableChapter);
}
if(data[key].HasProperty("MinStats")){
if(data[key]["MinStats"].GetValueCount()!=statValueList.size())ERR(std::format("MinStats attribute count does not match statValueList attribute count {}!={}",data[key]["MinStats"].GetValueCount(),statValueList.size()));
for(int attrCount=0;ItemAttribute&attr:statValueList){
minStats.A(attr)=data[key]["MinStats"].GetReal(attrCount);
attrCount++;
}
}
if(data[key].HasProperty("MaxStats")){
if(data[key]["MaxStats"].GetValueCount()!=statValueList.size())ERR(std::format("MaxStats attribute count does not match statValueList attribute count {}!={}",data[key]["MaxStats"].GetValueCount(),statValueList.size()));
for(int attrCount=0;ItemAttribute&attr:statValueList){
maxStats.A(attr)=data[key]["MaxStats"].GetReal(attrCount);
attrCount++;
}
}
if(data[key].HasProperty("MinStats")^data[key].HasProperty("MaxStats"))ERR("Only one of MinStats/MaxStats was provided! Both are required!");
if(scriptName!=""){
if(scriptName=="RestoreDuringCast"){
useDuringCast=true;
}
if(!ITEM_SCRIPTS.count(scriptName)){
ERR("Could not load script "<<scriptName<<" for Item "<<key<<"!")
}
}
it.name=key;
it.description=description;
it.category=category;
it.castTime=castTime;
it.cooldownTime=cooldownTime;
it.slot=EquipSlot::NONE;
it.set=setName;
it.buyValue=buyValue;
it.sellValue=sellValue;
it.useDuringCast=useDuringCast;
it.useSound=useSound;
it.equippableClass=equippableClass;
if(slot.size()>0){
for(std::string&s:slot){
if(!nameToEquipSlot.count(s))ERR("WARNING! Tried to add item "<<it.name<<" to slot "<<s<<" which doesn't exist!");
it.slot|=nameToEquipSlot[s];
}
}
if(!ITEM_CATEGORIES.count(it.category)){
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;
ItemProps&props=it.customProps;
if(scriptName!=""){
props.scriptProps=&DATA["ItemScript"][scriptName];
props.customProps=&data[key];
}
it.useFunc=scriptName;
it.minStats=minStats;
it.maxStats=maxStats;
it.fragmentName=fragmentName;
it.fragmentIcon=fragmentIcon;
#pragma region Equipment Category Verification Tests
int equipmentCategories=0;
equipmentCategories+=it.IsWeapon();
equipmentCategories+=it.IsArmor();
equipmentCategories+=it.IsAccessory();
if(it.IsEquippable()&&equipmentCategories==0)ERR(std::format("WARNING! {} is not considered a weapon, armor, or accessory but is considered equippable!",it.Name()))
if(it.IsEquippable()&&equipmentCategories!=1)ERR(std::format("WARNING! {} is considered in {} categories among the [weapon, armor, or accessory] set but is considered equippable!",it.Name(),equipmentCategories))
if(it.IsEquippable()&&it.Category()!="Equipment"&&it.Category()!="Accessories")ERR(std::format("WARNING! {} is considered equippable, but is not in either the Equipment category or the Accessories category!",it.Name()))
if(it.Category()=="Equipment"&&!(it.IsWeapon()||it.IsArmor()))ERR(std::format("WARNING! {} is in the Equipment Category, but is not considered a Weapon or Armor!",it.Name()))
if(it.Category()=="Accessories"&&it.IsWeapon())ERR(std::format("WARNING! {} is in the Accessories Category, but is considered a Weapon!",it.Name()))
if(it.Category()=="Accessories"&&it.IsArmor())ERR(std::format("WARNING! {} is in the Accessories Category, but is considered Armor!",it.Name()))
if(it.Category()=="Accessories"&&!it.IsAccessory())ERR(std::format("WARNING! {} is in the Accessories Category, but not considered an Accessory!",it.Name()))
if(it.IsAccessory()&&!it.fragmentName.has_value())ERR(std::format("WARNING! Accessory {} does not have a \"Fragment Name\" set!",it.Name()));
#pragma endregion
#pragma region Equipment Sort Rules Verification Tests
if(category=="Equipment"){
if(std::find_if(ItemSortRules::primarySort.begin(),ItemSortRules::primarySort.end(),[&](const std::string&sort){return key.find(sort)!=std::string::npos;})==ItemSortRules::primarySort.end())ERR(std::format("WARNING! Item {} does not have valid equipment sorting primary rule! Check items.txt config key: 'Equipment Sort Order Primary'!",key))
if(std::find_if(ItemSortRules::secondarySort.begin(),ItemSortRules::secondarySort.end(),[&](const std::string&sort){return key.find(sort)!=std::string::npos;})==ItemSortRules::secondarySort.end())ERR(std::format("WARNING! Item {} does not have valid equipment sorting secondary rule! Check items.txt config key: 'Equipment Sort Order Secondary'!",key))
}
#pragma endregion
}
};
ReadItems(DATA["ItemDatabase"]);
ReadItems(DATA["Equipment"]);
std::sort(craftableConsumables.begin(),craftableConsumables.end(),[](std::shared_ptr<Item>&item1,std::shared_ptr<Item>&item2){return item1.get()->GetEnhancementInfo()[0].chapterAvailable<item2.get()->GetEnhancementInfo()[0].chapterAvailable;});
for(auto&[name,info]:ITEM_DATA){
Item tempItem{1,name};
if(tempItem.Description().length()==0)ERR("WARNING! Item "<<info.name<<" does not have a description!");
}
#pragma region Create Accessory Fragments
for(const auto&itemName:ITEM_CATEGORIES.at("Accessories")){
const std::string fragmentName{ITEM_DATA.at(itemName).fragmentName.value()};
ItemInfo&it{ITEM_DATA[fragmentName]};
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("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("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).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("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){
if(util::random()%colors.size()==0)return PixelLerp(in,{p.r,p.g,p.b},0.5f);
}
return in;
});
game->SetDrawTarget(nullptr);
GFX.at("item_img_directory"_S+fragmentName).Decal()->Update();
#pragma endregion
}
}
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));
}
#pragma endregion
ItemEnchantInfo::Initialize();
ITEM_DATA.SetInitialized();
ITEM_CATEGORIES.SetInitialized();
ITEM_CONVERSIONS.SetInitialized();
Menu::LockInListeners();
LOG(ITEM_DATA.size()<<" items have been loaded.");
LOG(ITEM_CATEGORIES.size()<<" item categories have been loaded.");
}
ItemProps::ItemProps(utils::datafile*scriptProps,utils::datafile*customProps)
:scriptProps(scriptProps),customProps(customProps){}
int ItemProps::GetIntProp(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{
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{
if(customProps->HasProperty(prop)) return (*customProps)[prop].GetString(index);
else return (*scriptProps)[prop].GetString(index);
};
const uint32_t ItemProps::PropCount(const std::string&prop)const{
if(customProps->HasProperty(prop)) return (*customProps)[prop].GetValueCount();
else return uint32_t((*scriptProps)[prop].GetValueCount());
}
const std::unordered_map<std::string,BuffOverTimeType::BuffOverTimeType>ItemInfo::NameToBuffType{
{"HP Restore",BuffOverTimeType::HP_RESTORATION},
{"HP % Restore",BuffOverTimeType::HP_PCT_RESTORATION},
{"MP Restore",BuffOverTimeType::MP_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()
:amt(0),it(nullptr),enhancementLevel(0){}
Item::Item(uint32_t amt,IT item,uint8_t enhancementLevel)
:amt(amt),enhancementLevel(enhancementLevel),it(&ITEM_DATA.at(item)){}
std::weak_ptr<Item>Inventory::AddItem(IT it,uint32_t amt,bool monsterDrop){
if(!ITEM_DATA.count(it))ERR("Item "<<it<<" does not exist in Item Database!");
std::weak_ptr<Item>itemPtr;
if(ITEM_DATA[it].IsEquippable()){ //Do not stack equips!
for(uint32_t i=0;i<amt;i++){
itemPtr=(*_inventory.insert({it,std::make_shared<Item>(1,it)})).second;
itemPtr.lock()->RandomizeStats();
InsertIntoSortedInv(itemPtr.lock());
InsertIntoStageInventoryCategory(std::make_shared<Item>(*itemPtr.lock()),monsterDrop);
}
}
else{
//There are two places to manipulate items in (Both the sorted inventory and the actual inventory)
if(!_inventory.count(it)){
itemPtr=(*_inventory.insert({it,std::make_shared<Item>(amt,it)})).second;
InsertIntoSortedInv(itemPtr.lock());
}else{
auto inventory=_inventory.equal_range(it);
if(std::accumulate(inventory.first,inventory.second,0,
[&](int counter,std::pair<IT,std::shared_ptr<Item>>item){
(*item.second).amt+=amt;
itemPtr=item.second;
return counter+1;})>1)ERR("WARNING! We should not have more than 1 instance of a stackable item!");
}
InsertIntoStageInventoryCategory(std::make_shared<Item>(amt,it),monsterDrop);
}
return itemPtr;
}
std::vector<std::shared_ptr<Item>>Inventory::CopyItem(IT it){
if(!_inventory.count(it))return{};
std::vector<std::shared_ptr<Item>>copiedItems;
auto inventory=_inventory.equal_range(it);
std::for_each(inventory.first,inventory.second,[&](std::pair<IT,std::shared_ptr<Item>>it){copiedItems.push_back(std::make_shared<Item>(*it.second));});
return copiedItems;
}
std::vector<std::weak_ptr<Item>>Inventory::GetItem(IT it){
if(!_inventory.count(it))return{};
std::vector<std::weak_ptr<Item>>items;
auto inventory=_inventory.equal_range(it);
std::for_each(inventory.first,inventory.second,[&](std::pair<IT,std::shared_ptr<Item>>it){items.push_back(it.second);});
return items;
}
uint32_t Inventory::GetItemCount(IT it){
if(!_inventory.count(it)){
return 0;
}else
{
auto inventory=_inventory.equal_range(it);
return std::accumulate(inventory.first,inventory.second,0,[](int val,std::pair<IT,std::shared_ptr<Item>>it){return val+(*it.second).Amt();});
}
}
//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){
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)){
return RemoveItem(GetItem(it)[0]);
}
}
return false;
}
//Returns true if the item has been consumed completely and there are 0 remaining of that type in our inventory.
bool Inventory::RemoveItem(std::weak_ptr<Item>itemRef,ITCategory inventory,uint32_t amt){
if(amt==0)ERR("WARNING! Trying to remove zero of an item makes no sense.");
#pragma region Calculate Inventory to Manipulate
std::vector<std::shared_ptr<Item>>&inv=sortedInv.at(inventory);
bool eraseFromLootWindow=false;
if(inventory=="Monster Loot") {
inv=sortedInv.at("Monster Loot");
eraseFromLootWindow=true;
}else
if(inventory=="Stage Loot"){
inv=sortedInv.at("Stage Loot");
eraseFromLootWindow=true;
}
int count=0;
for(std::weak_ptr<Item>item:inv){
if(item==itemRef)break;
count++;
}
#pragma endregion
uint32_t itemAmt=GetItemCount(itemRef.lock()->ActualName());
if(inventory=="Monster Loot"||inventory=="Stage Loot"){
itemAmt=inv.at(count)->Amt();
}
//There are two places to manipulate items in (Both the sorted inventory and the actual inventory)
if(!itemAmt)return false;
std::string itemName=itemRef.lock()->DisplayName();
if (amt>=itemAmt){
size_t invSize=inv.size();
inv.erase(inv.begin()+count); //Clears it from the detected sorted inventory as well!
if(invSize-1!=inv.size())ERR(std::format("WARNING! Did not properly erase {} from sorted inventory {}",itemName,inventory));
if(!eraseFromLootWindow){ //We must clear out the item AFTER we've updated context-sensitive inventories because they may be borrowing a ref from this structure!!!
_inventory.erase(itemRef.lock()->ActualName());
}
//Callback for GUI inventories.
Menu::InventorySlotsUpdated(inventory);
return true;
}else{
if(itemRef.lock()->IsEquippable()){ //Since equipment doesn't stack, if we have more than one piece we have to still remove that piece.
bool found=false;
size_t erased=std::erase_if(_inventory,[&](const std::pair<const IT,std::shared_ptr<Item>>data){
if(!found&&data.second==itemRef){
found=true;
return true;
}
return false;
});
if(erased!=1)ERR(std::format("Did not erase a single element, instead erased {} elements.",erased));
size_t invSize=inv.size();
inv.erase(inv.begin()+count); //Clears it from the detected sorted inventory as well!
if(invSize-1!=inv.size())ERR(std::format("WARNING! Did not properly erase {} from sorted inventory {}",itemName,inventory));
Menu::InventorySlotsUpdated(inventory);
return true;
}else{
itemRef.lock()->amt-=amt; //Don't touch the sorted inventory unless this is monster loot or stage loot because there's only "1" of this item in the entry list.
}
return false;
}
}
bool Inventory::RemoveItem(IT it,uint32_t amt){
if(GetItemCount(it)==0)return false;
std::weak_ptr<Item>foundItem{GetItem(it)[0]};
ITCategory cat = foundItem.lock()->Category();
return RemoveItem(foundItem, cat, amt);
}
//Returns true if the item has been consumed completely and there are 0 remaining of that type in our inventory.
bool Inventory::RemoveItem(std::weak_ptr<Item>itemRef,uint32_t amt){
ITCategory cat = itemRef.lock()->Category();
return RemoveItem(itemRef, cat, amt);
}
const std::vector<std::shared_ptr<Item>>&Inventory::get(ITCategory itemCategory){
return sortedInv.at(itemCategory);
}
void Inventory::InsertIntoSortedInv(std::shared_ptr<Item>itemRef){
sortedInv.at(itemRef->Category()).push_back(itemRef);
//This should be a callback to menus that we need to update the interface with another item slot since a new one has appeared.
Menu::InventorySlotsUpdated(itemRef->Category());
}
void Inventory::InsertIntoStageInventoryCategory(std::shared_ptr<Item>itemRef,const bool monsterDrop){
std::string stageInventoryCategory="Stage Loot";
if(monsterDrop){
stageInventoryCategory="Monster Loot";
}
std::vector<std::shared_ptr<Item>>&inv=sortedInv.at(stageInventoryCategory);
if(itemRef->IsEquippable()){ //We cannot stack items! They are always individual.
inv.push_back(itemRef);
}else{
std::vector<std::shared_ptr<Item>>::iterator it=std::find(inv.begin(),inv.end(),itemRef); //Uses operator== to compare if this item does exist in a stage/monster loot inventory already. We just make an in-place shared pointer of an item to compare with.
if(it!=inv.end()){
(*it)->amt+=itemRef->Amt();
}else{
inv.push_back(itemRef);
}
}
Menu::InventorySlotsUpdated(stageInventoryCategory);
}
bool Inventory::ExecuteAction(IT item){
if(ITEM_SCRIPTS.count(ITEM_DATA.at(item).useFunc)){
return ITEM_SCRIPTS.at(ITEM_DATA.at(item).useFunc)(game,ITEM_DATA[item].customProps);
}else{
return false;
}
}
bool Inventory::SwapItems(ITCategory itemCategory,uint32_t slot1,uint32_t slot2){
std::vector<std::shared_ptr<Item>>&inv=sortedInv.at(itemCategory);
int largestSlot=std::max(slot1,slot2);
if(inv.size()<=largestSlot){
//The inventory is too small, so expand out blank slots to accomodate.
inv.resize(largestSlot+size_t(1));
}
std::weak_ptr<Item>item1=inv.at(slot1);
std::weak_ptr<Item>item2=inv.at(slot2);
std::swap(item1,item2);
return true;
}
uint32_t Item::Amt()const{
return amt;
};
const std::string&Item::ActualName()const{
if(_IsBlank())return BLANK_ITEM_NAME;
return it->Name();
};
const std::string Item::DisplayName()const{
if(_IsBlank())return BLANK_ITEM_NAME;
std::string name=ActualName();
if(IsEquippable()&&EnhancementLevel()>0){
name+=" [#00FF00+"+std::to_string(EnhancementLevel())+"#FFFFFF]";
}
return GetDisplayNameColor().toHTMLColorCode()+name;
}
const bool ItemInfo::IsEquippable()const{
return slot!=EquipSlot::NONE&&(category=="Equipment"||category=="Accessories");
}
const bool Item::IsEquippable()const{
return it->IsEquippable();
}
const std::string Item::Description(CompactText compact)const{
std::string description=it->Description();
if(IsEquippable()){
if(HasRandomizedStats()){
description+='\n';
description+=randomizedStats.GetStatsString(it->GetMaxStats());
}
description+='\n';
description+=GetStats().GetStatsString(Stats::NO_MAX_HIGHLIGHT,compact);
std::optional<const::ItemSet*const>setOpt=ItemSet();
if(setOpt){
const::ItemSet*const set=setOpt.value();
if(compact==COMPACT){
description+="\n"+set->GetSetName()+" Set - ";
}else{
description+="\n\n"+set->GetSetName()+" Set\n";
}
const std::map<::ItemSet,int>&itemSetCounts=Inventory::GetEquippedItemSets();
for(bool first=true;const auto&[pieceCount,bonuses]:set->GetSetBonusBreakpoints()){
if(itemSetCounts.count(*set)&&pieceCount<=itemSetCounts.at(*set)){
description+="#56E346";
}
if(!first){
if(compact==COMPACT){
description+=" ";
}else{
description+="\n";
}
}
description+="("+std::to_string(pieceCount)+"): "+bonuses.GetStatsString(Stats::NO_MAX_HIGHLIGHT,compact)+"#FFFFFF";
first=false;
}
}
}
if(compact==CRAFTING_INFO){
if(IsCraftable()&&EnhancementLevel()+1<="Item.Item Max Enhancement Level"_I&&GetEnhancementInfo()[EnhancementLevel()+1].chapterAvailable<=game->GetCurrentChapter()){
description+="\n\nCrafting Requirements:\n---------\n";
size_t enhanceIndex=EnhancementLevel()+1;
if(IsEquippable()&&Inventory::GetItemCount(ActualName())==0)enhanceIndex=0;
const EnhancementLevelInfo&info=GetEnhancementInfo()[int(enhanceIndex)];
for(const auto&[name,amt]:info.craftingRequirement.GetItems()){
description+=std::format("{}{} x{} ({})\n",
Inventory::GetItemCount(name)<amt?"#FF0000":"#FFFFFF",
std::string(const_cast<IT&>(name)),
amt,
Inventory::GetItemCount(name));
}
description+="\n";
uint32_t goldCost=info.craftingRequirement.GetCost();
if(goldCost>0){
description+=std::format("{}Crafting Cost: {} {}",
game->GetPlayer()->GetMoney()<goldCost?"#FF0000":"#FFFFFF",
goldCost,
"Item.Currency Name"_S);
}
}
}
if(HasEnchant()){
description+=std::format("\n{}{}\n#E0E0E0{}#FFFFFF",GetEnchant().value().DisplayCol().toHTMLColorCode(),GetEnchant().value().Name(),GetEnchant().value().Description());
}
return description;
}
const ITCategory Item::Category()const{
return it->Category();
}
const::Renderable&Item::Icon()const{
return it->Icon();
}
const ItemScript&Item::OnUseAction()const{
return it->OnUseAction();
}
const std::string&ItemInfo::Name()const{
return name;
}
const std::string&ItemInfo::Description()const{
return description;
}
const ITCategory ItemInfo::Category()const{
return category;
}
const::Renderable&const ItemInfo::Icon()const{
return img.value().get();
}
const ItemScript&ItemInfo::OnUseAction()const{
return ITEM_SCRIPTS.at(useFunc);
}
const bool Item::IsBlank()const{
if(Item::IsBlankStaticCallCounter!=1)ERR("WARNING! You should not call the IsBlank() function directly! Use ISBLANK() macro instead!")
Item::IsBlankStaticCallCounter--;
return this==nullptr||amt==0||it==nullptr;
}
void Inventory::Clear(ITCategory itemCategory){
std::vector<std::shared_ptr<Item>>itemList=get(itemCategory); //We have to make a copy here because RemoveItem() will modify the list provided by get() inline.
if(itemCategory=="Monster Loot"||itemCategory=="Stage Loot"){
while(sortedInv[itemCategory].size()>0){
RemoveItem(sortedInv[itemCategory].front(),itemCategory,sortedInv[itemCategory].front()->Amt());
}
}else
{
for(std::shared_ptr<Item>&item:itemList){
RemoveItem(item,itemCategory,item->Amt());
}
}
}
ItemOverlay::ItemOverlay(ItemInfo item)
:it(item),width("ItemDrop.Item Drop Scale"_F*24+4+game->GetTextSizeProp(item.Name()).x*0.5f){
xOffset=-width;
}
void ItemOverlay::Update(){
for(ItemOverlay&item:items){
item.timer+=game->GetElapsedTime();
item.xOffset=std::min(item.xOffset+game->GetElapsedTime()*"ItemOverlay.Item Overlay Speed"_F,0.f);
}
std::erase_if(items,[](ItemOverlay&item){return item.timer>"ItemOverlay.Item Overlay Time"_F;});
}
void ItemOverlay::Draw(){
float itemScale="ItemDrop.Item Drop Scale"_F;
for(int counter=0;ItemOverlay&item:items){
vf2d pos={item.xOffset,96.f+counter*10};
Pixel darkCol=Menu::GetCurrentTheme().GetButtonCol();
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.Icon().Decal()),{itemScale,itemScale});
game->DrawShadowStringPropDecal(pos+vf2d{itemScale*24+2,2},item.it.Name(),WHITE,BLACK,{0.5f,0.7f});
counter++;
}
}
void ItemOverlay::AddToItemOverlay(const ItemInfo&it){
items.push_back(ItemOverlay{it});
std::for_each(items.begin(),items.end(),[](ItemOverlay&it){it.ResetTimer();});
}
const float ItemInfo::CastTime()const{
return castTime;
}
const float ItemInfo::CooldownTime()const{
return cooldownTime;
}
void ItemOverlay::ResetTimer(){
timer=0;
}
const float Item::CastTime()const{
return it->CastTime();
}
const float Item::CooldownTime()const{
return it->CooldownTime();
}
const EnhancementLevelInfo EnhancementInfo::operator[](int level)const{
return EnhancementLevelInfo{const_cast<Stats&>(enhancementStats.at(level)),const_cast<CraftingRequirement&>(craftingRequirements.at(level)),craftingRequirements.at(level).GetAvailableChapter()};
}
const std::optional<const ItemSet *const>ItemInfo::ItemSet()const{
if(ItemSet::sets.count(set)){
return &ItemSet::sets.at(set);
}
return {};
};
const Stats&ItemSet::operator[](int setPieces)const{
if(setPieces-1<=-1||setPieces-1>=setBonuses.size())ERR("Piece count is invalid! Expecting a value (1-8) but got "<<setPieces);
return setBonuses[setPieces-1];
};
void ItemSet::AddSetBonus(std::string setName,int pieceCount,Stats&bonuses){
if(pieceCount-1<=-1||pieceCount-1>=sets[setName].setBonuses.size())ERR("Piece count is invalid! Expecting a value (1-8) but got "<<pieceCount);
sets[setName].name=setName;
for(int i=pieceCount-1;i<sets[setName].setBonuses.size();i++){
sets[setName].setBonuses[i]+=bonuses;
}
sets[setName].setBonusBreakpoints.push_back({pieceCount,bonuses});
}
void Inventory::EquipItem(const std::weak_ptr<Item>it,EquipSlot slot){
if(!(it.lock()->it->slot&slot))return;
EquipSlot equippedSlot=GetSlotEquippedIn(it);
if(equippedSlot!=EquipSlot::NONE)UnequipItem(equippedSlot);
if(!GetEquip(slot).expired())UnequipItem(slot);
Inventory::equipment[slot]=it;
game->GetPlayer()->RecalculateEquipStats();
};
void Inventory::UnequipItem(EquipSlot slot){
Inventory::equipment[slot]=Item::BLANK;
game->GetPlayer()->RecalculateEquipStats();
};
EquipSlot Inventory::GetSlotEquippedIn(const std::weak_ptr<Item>it){
if(it.expired()||!it.lock()->IsEquippable())return EquipSlot::NONE;
for(int i=int(EquipSlot::HELMET);i<=int(EquipSlot::RING2);i<<=1){
EquipSlot slot=EquipSlot(i);
std::weak_ptr<Item>equip=GetEquip(slot);
if(equip.expired())continue;
if(&*equip.lock()==&*it.lock())return slot;
}
return EquipSlot::NONE;
};
std::weak_ptr<Item>Inventory::GetEquip(EquipSlot slot){
return Inventory::equipment[slot];
}
const EquipSlot Item::GetEquipSlot()const{
return it->Slot();
}
const EquipSlot ItemInfo::Slot()const{
return slot;
}
void EnhancementInfo::SetAttribute(int enhanceLevel,ItemAttribute attribute,float value){
while(enhancementStats.size()<=enhanceLevel){
enhancementStats.push_back({});
}
enhancementStats[enhanceLevel].A(attribute)=value;
}
void ItemInfo::InitializeSets(){
for(auto&[key,value]:DATA["ItemSet"].GetKeys()){
std::string setName=key;
datafile&setInfo=DATA["ItemSet"][setName];
for(int pieceCount=1;pieceCount<=8;pieceCount++){
if(setInfo.HasProperty(std::to_string(pieceCount))){
datafile&statInfo=setInfo[std::to_string(pieceCount)];
Stats bonuses;
for(auto&[key,value]:statInfo.GetKeys()){
const ItemAttribute&attr=ItemAttribute::Get(key);
bonuses.A(attr)=statInfo[key].GetInt(0);
}
ItemSet::AddSetBonus(setName,pieceCount,bonuses);
}
}
}
}
const Stats ItemInfo::GetStats(int enhancementLevel)const{
if(enhancement.size()<=enhancementLevel){
return {};
}
return enhancement[enhancementLevel].stats;
}
const Stats Item::GetStats()const{
if(it==nullptr)return{};
return it->GetStats(enhancementLevel);
};
const size_t EnhancementInfo::size()const{
return enhancementStats.size();
};
const std::optional<const ItemSet*const>Item::ItemSet()const{
return it->ItemSet();
};
const uint8_t Item::EnhancementLevel()const{
return enhancementLevel;
};
void Item::EnhanceItem(uint8_t qty){
if(qty==0)ERR("WARNING! You must specify at least 1 item to be enhanced!")
enhanceFunctionPrimed.Validate(ActualName(),EnhancementLevel(),qty);
for(uint8_t i=0;i<qty;i++){
if(GetEnhancementInfo().size()!=2){
if(enhancementLevel+1>"Item.Item Max Enhancement Level"_I)ERR("WARNING! Attempted to enhance "<<DisplayName()<<" beyond the cap of "<<"Item.Item Max Enhancement Level"_I);
enhancementLevel++;
#pragma region Achievements
STEAMUSERSTATS(
// Fully Decked Out Achievement
datafile&unlock=DATA.GetProperty("Achievement.Equip Unlocks.Fully Decked Out");
if(Inventory::EquipsFullyMaxedOut(unlock["Weapon Max Level"].GetInt(),unlock["Armor Max Level"].GetInt())){
SteamUserStats()->SetAchievement(unlock["API Name"].GetString().c_str());
SteamUserStats()->StoreStats();
}
// Equipment achievement unlocks
for(auto&[key,size]:DATA.GetProperty("Achievement.Equip Unlocks")){
datafile&unlock=DATA.GetProperty(std::format("Achievement.Equip Unlocks.{}",key));
if(!(unlock.HasProperty("Upgrade Requirement")&&unlock.HasProperty("Equip Slot")))continue; //Ignore any achievements that don't have an upgrade requirement/equipment slot defined.
if(EnhancementLevel()>=unlock["Upgrade Requirement"].GetInt()){
EquipSlot validSlots=EquipSlot::NONE;
for(const std::string&slot:unlock["Equip Slot"].GetValues()){
validSlots|=ItemInfo::StringToEquipSlot(slot); //Collect all the bits that this equipment can fall under.
}
if(GetEquipSlot()&validSlots){
//This piece of gear matches one of the provided slots.
SteamUserStats()->SetAchievement(unlock["API Name"].GetString().c_str());
SteamUserStats()->StoreStats();
}
}
}
)
#pragma endregion
const CraftingRequirement&consumedResources=GetEnhancementInfo()[EnhancementLevel()].craftingRequirement;
for(const auto&[name,amt]:consumedResources.GetItems()){
Inventory::RemoveItem(Inventory::GetItem(name)[0],amt);
}
game->GetPlayer()->RemoveMoney(consumedResources.GetCost());
SoundEffect::PlaySFX("Enhance Item",SoundEffect::CENTERED);
game->GetPlayer()->RecalculateEquipStats();
}else{ //This is a craftable, so we have to give the player the item they crafted.
Inventory::AddItem(ActualName());
const CraftingRequirement&consumedResources=GetEnhancementInfo()[1].craftingRequirement;
for(const auto&[name,amt]:consumedResources.GetItems()){
Inventory::RemoveItem(Inventory::GetItem(name)[0],amt);
}
game->GetPlayer()->RemoveMoney(consumedResources.GetCost());
}
}
SoundEffect::PlaySFX("Craft Item",SoundEffect::CENTERED);
};
const std::vector<std::pair<int,Stats>>&ItemSet::GetSetBonusBreakpoints()const{
return setBonusBreakpoints;
};
const std::map<ItemSet,int>Inventory::GetEquippedItemSets(){
std::map<ItemSet,int>setCounts;
for(int i=int(EquipSlot::HELMET);i<=int(EquipSlot::RING2);i<<=1){
EquipSlot slot=EquipSlot(i);
std::weak_ptr<Item>equip=Inventory::GetEquip(slot);
if(ISBLANK(equip))continue;
if(equip.lock()->ItemSet()){
setCounts[*equip.lock()->ItemSet().value()]++;
}
}
return setCounts;
}
const std::string Stats::GetStatsString(const Stats&maxStats,CompactText compact)const{
std::string description="";
for(bool first=true;const auto&[attr,val]:attributes){
if(!first){
if(compact==COMPACT){
description+=" ";
}else{
description+="\n";
}
}
std::string statNumber=std::to_string(int(val));
if(attr.ShowAsDecimal()){
statNumber=std::format("{:.2f}",val);
}
std::string col="";
if(maxStats.attributes.count(attr)&&int(val)>=int(maxStats.attributes.at(attr))){
col=Stats::GetShimmeringColor().toHTMLColorCode();
}
description+=col+std::string(attr.Name())+": "+statNumber+(attr.DisplayAsPercent()?"%":"")+"#FFFFFF";
first=false;
}
return description;
}
ItemInfo&ItemInfo::operator[](const IT&item){
if(!ITEM_DATA.count(const_cast<IT&>(item)))ERR("Item "<<const_cast<IT&>(item)<<" does not exist in the item database!");
return ITEM_DATA[const_cast<IT&>(item)];
}
const uint32_t ItemInfo::GetBuyValue()const{
return buyValue;
}
const uint32_t ItemInfo::GetSellValue()const{
return sellValue;
}
const bool ItemInfo::CanBeSold()const{
return GetSellValue()>0;
}
const bool ItemInfo::CanBePurchased()const{
return GetBuyValue()>0;
}
const uint32_t Item::BuyValue()const{
return it->GetBuyValue();
}
const uint32_t Item::SellValue()const{
return it->GetSellValue();
}
const bool Item::CanBeSold()const{
return it->CanBeSold();
}
const bool Item::CanBePurchased()const{
return it->CanBePurchased();
}
void Item::SetAmt(uint32_t newAmt){
amt=newAmt;
}
const std::weak_ptr<Item>Inventory::GetInventorySlot(ITCategory itemCategory,size_t slot){
return get(itemCategory).at(slot);
}
bool Item::IsBlank(std::shared_ptr<Item>item){
Item::IsBlankStaticCallCounter++;
return item->IsBlank();
};
bool Item::IsBlank(std::weak_ptr<Item>item){
if(!item.expired())Item::IsBlankStaticCallCounter++;else return true;
return item.lock()->IsBlank();
};
const bool Item::_IsBlank()const{
Item::IsBlankStaticCallCounter++;
return IsBlank();
};
const bool Item::UseDuringCast()const{
return it->UseDuringCast();
}
const bool ItemInfo::UseDuringCast()const{
return useDuringCast;
}
void Stats::InitializeDamageReductionTable(){
maxDamageReductionTable.Reset();
float totalReduction=0;
maxDamageReductionTable[0]=0;
for(int i=1;i<=1000;i++){
if(i<=10){
totalReduction+=1._Pct;
}else
if(i<=30){
totalReduction+=0.5_Pct;
}else
if(i<=70){
totalReduction+=0.25_Pct;
}else
if(i<=150){
totalReduction+=0.125_Pct;
}else
if(i<=310){
totalReduction+=0.0625_Pct;
}else
if(i<=950){
totalReduction+=0.03125_Pct;
}else{
totalReduction+=0.1_Pct;
}
maxDamageReductionTable[i]=totalReduction;
}
}
void EnhancementInfo::SetCraftingRequirements(const int enhanceLevel,const std::vector<ItemPair>&requiredItems,const uint32_t goldCost,const uint8_t availableChapter){
while(craftingRequirements.size()<=enhanceLevel){
craftingRequirements.push_back(CraftingRequirement({},0,availableChapter));
}
craftingRequirements[enhanceLevel]=CraftingRequirement(requiredItems,goldCost,availableChapter);
}
EnhancementLevelInfo::EnhancementLevelInfo(const Stats&stats,const CraftingRequirement&craftingRequirement,const uint8_t chapterAvailable)
:stats(stats),craftingRequirement(craftingRequirement),chapterAvailable(chapterAvailable){}
const bool Item::CanEnhanceItem(uint8_t qty)const{
if(qty==0)ERR("WARNING! Must specify at least 1 for the quantity!")
if(ISBLANK(std::make_shared<Item>(*this)))return false;
if(!GetEnhancementInfo().CanBeEnhanced())return false;
size_t enhanceIndex=std::min(EnhancementLevel()+1,"Item.Item Max Enhancement Level"_I);
if(IsEquippable()&&Inventory::GetItemCount(ActualName())==0)enhanceIndex=0;//Equipment we don't have we need to first craft.
if(game->GetCurrentChapter()<GetEnhancementInfo()[enhanceIndex].chapterAvailable)return false;
const EnhancementLevelInfo&enhanceInfo=GetEnhancementInfo()[enhanceIndex];
if(GetEnhancementInfo().size()>2){ //If the item has exactly 2 enhancement levels, then it's an item that can only simply be crafted. Therefore, don't do the max enhancement level check.
if(EnhancementLevel()>="Item.Item Max Enhancement Level"_I)return false;
}
if(game->GetPlayer()->GetMoney()<enhanceInfo.craftingRequirement.GetCost()*qty)return false;
for(const auto&[name,amt]:enhanceInfo.craftingRequirement.GetItems()){
if(Inventory::GetItemCount(name)<amt*qty){
return false;
}
}
enhanceFunctionPrimed.enhancementLevel=EnhancementLevel();
enhanceFunctionPrimed.item=ActualName();
enhanceFunctionPrimed.qty=qty;
return enhanceFunctionPrimed=true;
}
const EnhancementInfo&ItemInfo::GetEnhancementInfo()const{
return enhancement;
}
const EnhancementInfo&Item::GetEnhancementInfo()const{
return it->GetEnhancementInfo();
}
const bool EnhancementInfo::CanBeEnhanced()const{
return craftingRequirements.size()>0;
};
const bool ItemInfo::IsWeapon()const{
return slot&EquipSlot::WEAPON;
}
const bool ItemInfo::IsArmor()const{
return IsEquippable()&&!IsWeapon()&&!IsAccessory();
}
const bool ItemInfo::IsAccessory()const{
return slot&EquipSlot::RING1||slot&EquipSlot::RING2;
}
const bool Item::IsWeapon()const{
return it->IsWeapon();
}
const bool Item::IsArmor()const{
return it->IsArmor();
}
const bool Item::IsAccessory()const{
return it->IsAccessory();
}
const bool Item::IsCraftable()const{
return it->IsCraftable();
}
const bool ItemInfo::IsCraftable()const{
return GetEnhancementInfo().CanBeEnhanced();
}
const bool Item::EnhancementIsPossible()const{
return it->IsCraftable();
}
const Stats&Item::RandomStats()const{
return randomizedStats;
};
void Item::RandomizeStats(){
randomizedStats=it->RandomizeStats();
};
const Stats ItemInfo::GetMinStats()const{
return minStats;
}
const Stats ItemInfo::GetMaxStats()const{
return maxStats;
}
Stats ItemInfo::RandomizeStats(){
Stats randomRolls;
for(auto&[attr,minVal]:minStats){
float diff=maxStats.A(attr)-minVal;
if(attr.ShowAsDecimal()){
randomRolls.A(attr)=util::random(diff)+minVal;
}else{
randomRolls.A(attr)=floor(util::random(diff+1)+minVal); //We must add 1 because we are going to floor the value to prevent the edges from having half as likely probability as all other numbers.
}
}
return randomRolls;
}
const bool Item::HasRandomizedStats()const{
return randomizedStats.size()>0;
}
const EventName&ItemInfo::UseSound()const{
return useSound;
}
const EventName&Item::UseSound()const{
return it->UseSound();
}
const std::vector<std::shared_ptr<Item>>Inventory::GetInventory(){
std::vector<std::shared_ptr<Item>>itemList;
for(size_t itemCount=0;auto&[itemName,item]:Inventory::_inventory){
itemList.push_back(item);
}
return itemList;
}
const std::unordered_set<std::string>&ItemInfo::GetClass()const{
return equippableClass.size()==0?game->GetPlayer()->GetMyClass():equippableClass;
}
const std::unordered_set<std::string>&Item::GetClass()const{
return it->GetClass();
}
void Inventory::UpdateBlacksmithInventoryLists(){
auto weaponsList=Component<RowInventoryScrollableWindowComponent>(BLACKSMITH,"Weapon Inventory Display");
auto armorList=Component<RowInventoryScrollableWindowComponent>(BLACKSMITH,"Armor Inventory Display");
blacksmithInventory.clear();
#pragma region Retrieve all equipment that the player currently does not own
for(auto&[key,size]:DATA.GetProperty("Equipment")){
if(GetItemCount(key)==0){
blacksmithInventory.push_back(std::make_shared<Item>(1,key));
}
}
#pragma endregion
InventoryCreator::RowPlayerWeapons_InventorySlotsUpdate(*DYNAMIC_POINTER_CAST<InventoryScrollableWindowComponent>(weaponsList),"Equipment");
InventoryCreator::RowPlayerArmor_InventorySlotsUpdate(*DYNAMIC_POINTER_CAST<InventoryScrollableWindowComponent>(armorList),"Equipment");
}
uint16_t ItemSortRules::GetItemSortRanking(const std::weak_ptr<Item>&it){
uint16_t sortRank=0;
auto secondaryFind=std::find_if(secondarySort.begin(),secondarySort.end(),[&](const std::string&sort){
if(it.lock()->DisplayName().find(sort)!=std::string::npos){
return true;
}
return false;
});
sortRank+=std::distance(secondarySort.begin(),secondaryFind);
auto primaryFind=std::find_if(primarySort.begin(),primarySort.end(),[&](const std::string&sort){
if(it.lock()->DisplayName().find(sort)!=std::string::npos){
return true;
}
return false;
});
sortRank+=std::distance(primarySort.begin(),primaryFind)*(secondarySort.size()+1);
return sortRank;
}
uint16_t ItemSortRules::MaxSortRanking(){
return primarySort.size()*(secondarySort.size()+1)+secondarySort.size();
}
void Inventory::AddLoadoutItemUsed(IT item,int slot){
if(slot<0||slot>game->GetLoadoutSize()-1)ERR(std::format("WARNING! Invalid loadout slot number {} provided! Range is (0-{})",slot,game->GetLoadoutSize()-1));
loadoutItemsUsed[slot].first=item;
loadoutItemsUsed[slot].second++;
}
void Inventory::ResetLoadoutItemsUsed(){
for(int i=0;i<game->GetLoadoutSize();i++){
loadoutItemsUsed[i].first=""s;
loadoutItemsUsed[i].second=0;
}
}
void Inventory::GivePlayerLoadoutItemsUsed(){
for(int i=0;i<game->GetLoadoutSize();i++){
if(loadoutItemsUsed[i].second>0){
Inventory::AddItem(loadoutItemsUsed[i].first,loadoutItemsUsed[i].second);
game->SetLoadoutItem(i,loadoutItemsUsed[i].first);
}
}
ResetLoadoutItemsUsed();
}
const bool Item::IsLocked()const{
return locked;
}
void Item::Lock(){
locked=true;
}
void Item::Unlock(){
locked=false;
}
//Specifically for the "Fully Decked Out" achievement.
const bool Inventory::EquipsFullyMaxedOut(int maxWeaponLevel,int maxArmorLevel){
for(int i=int(EquipSlot::HELMET);i<=int(EquipSlot::RING2);i<<=1){
EquipSlot slot=EquipSlot(i);
if(!ISBLANK(Inventory::GetEquip(slot))){
std::shared_ptr<Item>equip=Inventory::GetEquip(slot).lock();
if(!(equip->IsAccessory()||
(equip->IsWeapon()&&equip->EnhancementLevel()>=maxWeaponLevel)||
(equip->IsArmor()&&equip->EnhancementLevel()>=maxArmorLevel))
){
return false;
}
}else return false;
}
return true;
}
const EquipSlot ItemInfo::StringToEquipSlot(std::string_view slotName){
return nameToEquipSlot[std::string(slotName)];
}
const Inventory::DisassembleResult Inventory::Disassemble(std::weak_ptr<Item>itemRef){
if(ISBLANK(itemRef))ERR(std::format("WARNING! Trying to feed a blank item into the Disassemble function! THIS SHOULD NOT BE HAPPENING!"));
const std::shared_ptr<Item>&currentItem{itemRef.lock()};
if(!currentItem->IsAccessory())ERR(std::format("WARNING! Trying to disassemble Item {} which is not an accessory! THIS SHOULD NOT BE HAPPENING!",currentItem->ActualName()));
if(currentItem->IsLocked())return DisassembleResult::FAILED;
Inventory::RemoveItem(itemRef);
Inventory::AddItem(currentItem->FragmentName(),"Fragment Disassemble Gain Amount"_I);
return DisassembleResult::SUCCESS;
}
const std::string&Item::FragmentName()const{
return ITEM_DATA[ActualName()].FragmentName();
}
const std::string&ItemInfo::FragmentName()const{
if(!fragmentName.has_value())ERR(std::format("WARNING! Item {} does not break down into a fragment (fragment name not set)!",Name()));
return fragmentName.value();
}
const bool Item::CanBeRefined()const{
if(!IsAccessory()||Inventory::GetItemCount(FragmentName())<"Fragment Refine Cost"_i[0]||game->GetPlayer()->GetMoney()<"Fragment Refine Cost"_i[1])return false;
for(const auto&[attr,val]:randomizedStats){
float maxVal{ITEM_DATA[ActualName()].GetMaxStats().A_Read(attr)};
if(val!=maxVal)return true;
}
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()->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)};
if(val!=maxVal)statsAvailableForRefining.push_back(attr);
}
ItemAttribute&chosenAttr{statsAvailableForRefining[util::random()%statsAvailableForRefining.size()]};
const float pctIncreaseAmount{"Refine Stat Increase Amount"_F/100.f};
const float maxStat{ITEM_DATA[ActualName()].GetMaxStats().A_Read(chosenAttr)};
const int increaseAmt{int(ceil(maxStat*pctIncreaseAmount))};
const float oldAmt{randomizedStats.A_Read(chosenAttr)};
randomizedStats.A(chosenAttr)=std::min(maxStat,oldAmt+increaseAmt);
const float newAmt{randomizedStats.A_Read(chosenAttr)};
game->GetPlayer()->RecalculateEquipStats();
return RefineResult{chosenAttr,newAmt-oldAmt};
}
void Item::_EnchantItem(const std::string_view enchantName){
enchant=ItemEnchant{enchantName};
game->GetPlayer()->RecalculateEquipStats();
}
const std::optional<ItemEnchant>&Item::GetEnchant()const{
return enchant;
}
const bool Item::HasEnchant()const{
return enchant.has_value();
}
const Pixel Item::GetDisplayNameColor()const{
Pixel col{WHITE};
if(HasEnchant())col=enchant.value().DisplayCol();
return col;
}
const bool Item::SelectedEquipIsDifferent(const std::weak_ptr<Item>equipAttemptingToEquip,const EquipSlot slotToEquipTo){
std::weak_ptr<Item>currentItem=Inventory::GetEquip(slotToEquipTo);
switch(slotToEquipTo){
case EquipSlot::RING1:
case EquipSlot::RING2:{
std::weak_ptr<Item>otherItem;
if(slotToEquipTo&EquipSlot::RING1)otherItem=Inventory::GetEquip(EquipSlot::RING2);
else
if(slotToEquipTo&EquipSlot::RING2)otherItem=Inventory::GetEquip(EquipSlot::RING1);
const auto EquipIsNotSameAsOppositeRingSlot=[&equip=equipAttemptingToEquip,&otherItem](){
return &*equip.lock()!=&*otherItem.lock();
};
const auto EquipIsNotSameAsCurrentRingSlot=[&equip=equipAttemptingToEquip,&currentItem](){
return &*equip.lock()!=&*currentItem.lock();
};
const auto OneOfBothRingsDoesNotHaveEnchant=[&equip=equipAttemptingToEquip,&otherItem](){
return !equip.lock()->HasEnchant()||!otherItem.lock()->HasEnchant();
};
const auto BothRingsAreCompatible=[&equip=equipAttemptingToEquip,&otherItem](){ //Check if enchants are compatible... Currently unused.
return true;
};
return (ISBLANK(otherItem)&&(OneOfBothRingsDoesNotHaveEnchant()||BothRingsAreCompatible())||
EquipIsNotSameAsOppositeRingSlot()&&
EquipIsNotSameAsCurrentRingSlot()&&
(OneOfBothRingsDoesNotHaveEnchant()||BothRingsAreCompatible()));
}break;
default:{
return &*equipAttemptingToEquip.lock()!=&*currentItem.lock();
}
}
}
const std::optional<std::string>&ItemInfo::FragmentIcon()const{
return fragmentIcon;
}
const Pixel Stats::GetShimmeringColor(){
return PixelLerp({255,196,60},{254,217,133},sin((70*game->GetRunTime())/2.f+0.5f));
}
const std::optional<std::string>&Item::FragmentIcon()const{
return ITEM_DATA.at(FragmentName()).FragmentIcon();
}
const ItemEnchantInfo 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!");
EnchantName randomEnchantName{ItemEnchant::RollRandomEnchant()};
_EnchantItem(randomEnchantName);
Inventory::RemoveItem(FragmentName(),"Fragment Enchant Cost"_i[0]);
game->GetPlayer()->RemoveMoney("Fragment Enchant Cost"_i[1]);
return ItemEnchantInfo::GetEnchant(randomEnchantName);
}