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.
1405 lines
51 KiB
1405 lines
51 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_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}),img(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;
|
|
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{ //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.Decal();
|
|
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;
|
|
|
|
#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[fragmentName].Create(24,24);
|
|
if(!game->TestingModeEnabled()){
|
|
#pragma region Collect colors from source ring image
|
|
std::set<Pixel>colors;
|
|
for(int y=0;y<24;y++){
|
|
for(int x=0;x<24;x++){
|
|
colors.insert(ITEM_DATA.at(itemName).img->sprite->GetPixel(x,y));
|
|
}
|
|
}
|
|
colors.erase(BLANK);
|
|
#pragma endregion
|
|
#pragma region Generate fragment with randomly sampled pixels from the source ring image
|
|
game->SetDrawTarget(GFX.at(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(fragmentName).Decal()->Update();
|
|
#pragma endregion
|
|
}
|
|
it.img=GFX.at(fragmentName).Decal();
|
|
it.name=fragmentName;
|
|
it.description="Fragment Description"_S;
|
|
it.category="Materials";
|
|
LOG(std::format("Item Fragment {} generated...",fragmentName));
|
|
}
|
|
#pragma endregion
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
//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 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);
|
|
}
|
|
}
|
|
}
|
|
return description;
|
|
}
|
|
const ITCategory Item::Category()const{
|
|
return it->Category();
|
|
}
|
|
const::Decal*const Item::Decal()const{
|
|
return it->Decal();
|
|
}
|
|
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::Decal*const ItemInfo::Decal()const{
|
|
return img;
|
|
}
|
|
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.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()->SetMoney(game->GetPlayer()->GetMoney()-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()->SetMoney(game->GetPlayer()->GetMoney()-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))){
|
|
Pixel shimmeringCol=PixelLerp({255,196,60},{254,217,133},sin((70*game->GetRunTime())/2.f+0.5f));
|
|
col=util::PixelToHTMLColorCode(shimmeringCol);
|
|
}
|
|
|
|
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)];
|
|
}
|
|
|
|
void 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>¤tItem{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()));
|
|
Inventory::RemoveItem(itemRef);
|
|
Inventory::AddItem(currentItem->FragmentName(),"Fragment Disassemble Gain Amount"_I);
|
|
}
|
|
|
|
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();
|
|
} |