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 }