1 /// Useful functions that are not part of nwscript 2 module nwn.nwscript.extensions; 3 4 import std.conv: to, ConvException; 5 import std.math; 6 import std.meta; 7 import std.traits; 8 9 static import nwn.gff; 10 static import nwn.fastgff; 11 import nwn.types; 12 import nwn.nwscript.resources; 13 14 15 /// Checks if T is either a nwn.gff.GffStruct or a nwn.fastgff.GffStruct 16 template isGffStruct(T){ 17 enum isGffStruct = is(T: nwn.gff.GffStruct) || is(T: const(nwn.fastgff.GffStruct)); 18 } 19 20 /// Imports either nwn.gff or nwn.fastgff, that provide T 21 package template ImportGffLib(T){ 22 enum ImportGffLib = "import " ~ moduleName!T ~ ";"; 23 } 24 25 /// Calculate an item cost, without taking into account its additional cost 26 uint calcItemCost(ST)(ref ST oItem) if(isGffStruct!ST) { 27 mixin(ImportGffLib!ST); 28 // GetItemCost 29 // num: malusCost 30 // num2: bonusCost 31 // num3: chargeCost 32 // num4: unitPrice 33 // num5: finalPrice 34 const baseItemType = oItem["BaseItem"].get!GffInt; 35 36 float malusCost = 0f; 37 float bonusCost = 0f; 38 float chargeCost = 0f; 39 getPropertiesCost(oItem, bonusCost, malusCost, chargeCost); 40 41 float unitPrice = getItemBaseCost(oItem); 42 unitPrice += chargeCost; 43 unitPrice += 1000f * (bonusCost * bonusCost); 44 unitPrice -= 1000f * malusCost * malusCost; 45 unitPrice *= getTwoDA("baseitems").get("itemmultiplier", baseItemType, 1f); 46 47 const stack = oItem["StackSize"].get!GffWord; 48 49 int finalPrice = cast(int)(unitPrice) * (stack > 0 ? stack : 1); 50 51 if (finalPrice < 0) 52 finalPrice = 0; 53 54 if (finalPrice == 0 && unitPrice > 0f && unitPrice <= 1f) 55 finalPrice = 1; 56 57 return finalPrice; 58 } 59 60 unittest{ 61 import std.meta; 62 initTwoDAPaths(["unittest/2da"]); 63 64 foreach(GFF ; AliasSeq!(nwn.gff.Gff, nwn.fastgff.FastGff)){ 65 auto item = new GFF(cast(ubyte[])import("test_cost_armor.uti")).root; 66 assert(calcItemCost(item) == 789_774); 67 68 auto item2 = new GFF(cast(ubyte[])import("test_cost_bow.uti")).root; 69 assert(calcItemCost(item2) == 375_303); 70 } 71 72 } 73 74 /// Returns the item additional cost as defined in the blueprint, multiplied by the number of stacked items 75 int getItemModifyCost(ST)(ref ST oItem) if(isGffStruct!ST) { 76 mixin(ImportGffLib!ST); 77 const stack = oItem["StackSize"].get!GffWord; 78 return oItem["ModifyCost"].to!int * (stack > 0 ? stack : 1); 79 } 80 81 82 private uint getItemBaseCost(ST)(ref ST oItem) if(isGffStruct!ST) { 83 mixin(ImportGffLib!ST); 84 const baseItemType = oItem["BaseItem"].get!GffInt; 85 if(baseItemType != 16){// != OBJECT_TYPE_ARMOR 86 return getTwoDA("baseitems").get("basecost", baseItemType).to!uint; 87 } 88 89 auto armor2da = getTwoDA("armorrulestats"); 90 const armorRulesType = oItem["ArmorRulesType"].get!GffByte; 91 if(armorRulesType < armor2da.rows){ 92 return armor2da.get("cost", armorRulesType).to!uint; 93 } 94 return 0; 95 } 96 97 private void getPropertiesCost(ST)(ref ST oItem, out float bonusCost, out float malusCost, out float spellChargeCost) if(isGffStruct!ST) { 98 mixin(ImportGffLib!ST); 99 // A_2: bonusCost 100 // A_3: malusCost 101 // A_4: spellChargeCost 102 // num: highestCastSpellCost 103 // num2: secondHighestCastSpellCost 104 // num3: totalCastSpellCost 105 // num4: specialCostAdjust 106 // num5: fTypeCost 107 // num6: fSubTypeCost 108 // num7: fCostValueCost 109 // num8: cost 110 // num9: cost 111 bonusCost = 0; 112 malusCost = 0; 113 spellChargeCost = 0; 114 115 float highestCastSpellCost = 0f; 116 float secondHighestCastSpellCost = 0f; 117 float totalCastSpellCost = 0f; 118 119 foreach(ref prop ; oItem["PropertiesList"].get!GffList){ 120 auto ip = prop.toNWItemproperty; 121 122 float specialCostAdjust = 0f; 123 float fTypeCost = getTwoDA("itempropdef").get("cost", ip.type, 0f); 124 125 float fSubTypeCost = 0.0; 126 string sSubTypeTable = getTwoDA("itempropdef").get("subtyperesref", ip.type); 127 if(sSubTypeTable !is null) 128 fSubTypeCost = getTwoDA(sSubTypeTable).get("cost", ip.subType, 0f); 129 float fCostValueCost = getCostValueCost(ip, specialCostAdjust); 130 131 if(ip.type == 15){ 132 // CastSpell 133 float cost = (fTypeCost + fCostValueCost) * fSubTypeCost; 134 if (specialCostAdjust >= 1f && oItem["Charges"].get!GffByte >= 1) 135 { 136 cost = cost * oItem["Charges"].to!float / 50f; 137 } 138 totalCastSpellCost += cost / 2f; 139 if (cost > highestCastSpellCost) 140 { 141 secondHighestCastSpellCost = highestCastSpellCost; 142 highestCastSpellCost = cost; 143 } 144 } 145 else{ 146 float cost; 147 // Either the ip.type has Cost > 0 or ip.subType has Cost > 0 148 if (fabs(fTypeCost) > 0f) 149 cost = fTypeCost * fCostValueCost; 150 else 151 cost = fSubTypeCost * fCostValueCost; 152 153 if(isIprpMalus(ip) || cost < 0f) 154 malusCost += cost; 155 else 156 bonusCost += cost; 157 } 158 } 159 160 foreach(ref red ; oItem["DmgReduction"].get!GffList){ 161 auto damageRed2da = getTwoDA("iprp_damagereduction"); 162 163 bonusCost += damageRed2da.get("Cost", red["DamageAmount"].get!GffInt).toNWFloat; 164 } 165 166 if (totalCastSpellCost > 0f) 167 { 168 spellChargeCost = totalCastSpellCost + highestCastSpellCost / 2f + secondHighestCastSpellCost / 4f; 169 } 170 171 } 172 173 private bool isIprpMalus(NWItemproperty ip) 174 { 175 switch (ip.type) 176 { 177 case 10://EnhancementPenalty 178 case 21://DamagePenalty 179 case 24://Damage_Vulnerability 180 case 27://DecreaseAbilityScore 181 case 28://DecreaseAC 182 case 29://DecreasedSkill 183 case 49://ReducedSavingThrows 184 case 50://ReducedSpecificSavingThrow 185 return true; 186 default: return false; 187 } 188 } 189 190 191 private float getCostValueCost(NWItemproperty ip, out float specialCostAdjust){ 192 specialCostAdjust = 0.0; 193 float cost = 0.0; 194 195 auto costTableRef = getTwoDA("itempropdef").get("CostTableResRef", ip.type); 196 if(costTableRef !is null){ 197 auto costTableID = costTableRef.to!uint; 198 if(costTableID == 3){ // IPRP_CHARGECOST 199 switch (ip.costValue) 200 { 201 case 2: specialCostAdjust = 5.0; break; // 5_Charges/Use 202 case 3: specialCostAdjust = 4.0; break; // 4_Charges/Use 203 case 4: specialCostAdjust = 3.0; break; // 3_Charges/Use 204 case 5: specialCostAdjust = 2.0; break; // 2_Charges/Use 205 case 6: specialCostAdjust = 1.0; break; // 1_Charge/Use 206 default: break; 207 } 208 } 209 210 string costTable = getTwoDA("iprp_costtable").get("Name", costTableID); 211 cost = getTwoDA(costTable).get("Cost", ip.costValue, 0f); 212 } 213 214 if (fabs(cost) <= float.epsilon) 215 cost = cost < 0.0 ? -1.0 : 1.0; 216 217 return cost; 218 } 219 220 221 222 223 224 // Converts an NWItemproperty into a developer-friendly string (without TLK translations) 225 string toPrettyString(in NWItemproperty ip){ 226 immutable propNameLabel = getTwoDA("itempropdef").get("Label", ip.type); 227 228 immutable subTypeTable = getTwoDA("itempropdef").get("SubTypeResRef", ip.type); 229 string subTypeLabel; 230 if(subTypeTable !is null){ 231 auto subType2da = getTwoDA(subTypeTable); 232 if("label" in subType2da) 233 subTypeLabel = subType2da.get("label", ip.subType); 234 else 235 subTypeLabel = subType2da.get("NameString", ip.subType); 236 } 237 238 immutable costValueTableIndex = getTwoDA("itempropdef").get("CostTableResRef", ip.type); 239 immutable costValueTable = costValueTableIndex is null? null : getTwoDA("iprp_costtable").get("Name", costValueTableIndex.to!uint); 240 241 immutable costValueLabel = costValueTable is null? null : getTwoDA(costValueTable).get("Label", ip.costValue); 242 243 return propNameLabel 244 ~(subTypeLabel is null? null : "."~subTypeLabel) 245 ~(costValueLabel is null? null : "("~costValueLabel~")"); 246 } 247 248 /// Converts an itemproperty into its in-game description 249 string toGameString(in NWItemproperty ip){ 250 const resolv = getStrRefResolver(); 251 const props2DA = getTwoDA("itempropdef"); 252 const costTable2DA = getTwoDA("iprp_costtable"); 253 const paramTable2DA = getTwoDA("iprp_paramtable"); 254 255 string propName; 256 string subType; 257 string costValue; 258 string paramValue; 259 260 propName = resolv[props2DA.get("GameStrRef", ip.type, 0)]; 261 262 string subTypeTable = props2DA.get("SubTypeResRef", ip.type, ""); 263 if(subTypeTable != ""){ 264 int strref = getTwoDA(subTypeTable).get("Name", ip.subType, 0); 265 if(strref > 0) 266 subType = resolv[strref]; 267 } 268 string costValueTableId = props2DA.get("CostTableResRef", ip.type, ""); 269 if(costValueTableId != ""){ 270 string costValueTable = costTable2DA.get("Name", costValueTableId.to!int); 271 int strref = getTwoDA(costValueTable).get("Name", ip.costValue, 0); 272 if(strref > 0) 273 costValue = resolv[strref]; 274 } 275 276 string paramTableId = props2DA.get("Param1ResRef", ip.type); 277 if(paramTableId != ""){ 278 string paramTable = paramTable2DA.get("TableResRef", paramTableId.to!int); 279 int strref = getTwoDA(paramTable).get("Name", ip.p1, 0); 280 if(strref > 0) 281 paramValue = resolv[strref]; 282 } 283 return propName 284 ~ (subType !is null ? " " ~ subType : null) 285 ~ (costValue !is null ? " " ~ costValue : null) 286 ~ (paramValue !is null ? " " ~ paramValue : null); 287 } 288 289 // Converts a GFF struct to an item property 290 NWItemproperty toNWItemproperty(ST)(in ST node) if(isGffStruct!ST) { 291 mixin(ImportGffLib!ST); 292 return NWItemproperty( 293 node["PropertyName"].get!GffWord, 294 node["Subtype"].get!GffWord, 295 node["CostValue"].get!GffWord, 296 node["Param1Value"].get!GffByte, 297 ); 298 } 299 300 NWFloat toNWFloat(T)(in T value){ 301 // TODO: not efficient (exception allocation) 302 try return value.to!NWFloat; 303 catch(ConvException){} 304 return 0.0; 305 } 306 307 NWInt toNWInt(T)(in T value){ 308 // TODO: not efficient (exception allocation) 309 try return value.to!NWInt; 310 catch(ConvException){} 311 return 0; 312 }