d2r-saver 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +126 -0
  3. package/data/data.json +1 -0
  4. package/data/strings.json +1 -0
  5. package/dist/core/binary-reader.d.ts +43 -0
  6. package/dist/core/binary-reader.d.ts.map +1 -0
  7. package/dist/core/binary-reader.js +130 -0
  8. package/dist/core/binary-reader.js.map +1 -0
  9. package/dist/core/binary-writer.d.ts +49 -0
  10. package/dist/core/binary-writer.d.ts.map +1 -0
  11. package/dist/core/binary-writer.js +156 -0
  12. package/dist/core/binary-writer.js.map +1 -0
  13. package/dist/core/checksum.d.ts +28 -0
  14. package/dist/core/checksum.d.ts.map +1 -0
  15. package/dist/core/checksum.js +62 -0
  16. package/dist/core/checksum.js.map +1 -0
  17. package/dist/core/huffman.d.ts +22 -0
  18. package/dist/core/huffman.d.ts.map +1 -0
  19. package/dist/core/huffman.js +66 -0
  20. package/dist/core/huffman.js.map +1 -0
  21. package/dist/core/index.d.ts +5 -0
  22. package/dist/core/index.d.ts.map +1 -0
  23. package/dist/core/index.js +5 -0
  24. package/dist/core/index.js.map +1 -0
  25. package/dist/formats/d2i-reader.d.ts +53 -0
  26. package/dist/formats/d2i-reader.d.ts.map +1 -0
  27. package/dist/formats/d2i-reader.js +208 -0
  28. package/dist/formats/d2i-reader.js.map +1 -0
  29. package/dist/formats/d2i-writer.d.ts +63 -0
  30. package/dist/formats/d2i-writer.d.ts.map +1 -0
  31. package/dist/formats/d2i-writer.js +229 -0
  32. package/dist/formats/d2i-writer.js.map +1 -0
  33. package/dist/formats/d2s-reader.d.ts +104 -0
  34. package/dist/formats/d2s-reader.d.ts.map +1 -0
  35. package/dist/formats/d2s-reader.js +257 -0
  36. package/dist/formats/d2s-reader.js.map +1 -0
  37. package/dist/formats/d2s-writer.d.ts +52 -0
  38. package/dist/formats/d2s-writer.d.ts.map +1 -0
  39. package/dist/formats/d2s-writer.js +407 -0
  40. package/dist/formats/d2s-writer.js.map +1 -0
  41. package/dist/formats/detect.d.ts +18 -0
  42. package/dist/formats/detect.d.ts.map +1 -0
  43. package/dist/formats/detect.js +40 -0
  44. package/dist/formats/detect.js.map +1 -0
  45. package/dist/formats/index.d.ts +8 -0
  46. package/dist/formats/index.d.ts.map +1 -0
  47. package/dist/formats/index.js +8 -0
  48. package/dist/formats/index.js.map +1 -0
  49. package/dist/formats/item-parser.d.ts +97 -0
  50. package/dist/formats/item-parser.d.ts.map +1 -0
  51. package/dist/formats/item-parser.js +627 -0
  52. package/dist/formats/item-parser.js.map +1 -0
  53. package/dist/formats/item-writer.d.ts +55 -0
  54. package/dist/formats/item-writer.d.ts.map +1 -0
  55. package/dist/formats/item-writer.js +514 -0
  56. package/dist/formats/item-writer.js.map +1 -0
  57. package/dist/game-data/game-data.d.ts +90 -0
  58. package/dist/game-data/game-data.d.ts.map +1 -0
  59. package/dist/game-data/game-data.js +355 -0
  60. package/dist/game-data/game-data.js.map +1 -0
  61. package/dist/game-data/index.d.ts +4 -0
  62. package/dist/game-data/index.d.ts.map +1 -0
  63. package/dist/game-data/index.js +3 -0
  64. package/dist/game-data/index.js.map +1 -0
  65. package/dist/game-data/loader.d.ts +23 -0
  66. package/dist/game-data/loader.d.ts.map +1 -0
  67. package/dist/game-data/loader.js +32 -0
  68. package/dist/game-data/loader.js.map +1 -0
  69. package/dist/game-data/types.d.ts +428 -0
  70. package/dist/game-data/types.d.ts.map +1 -0
  71. package/dist/game-data/types.js +10 -0
  72. package/dist/game-data/types.js.map +1 -0
  73. package/dist/index.d.ts +166 -0
  74. package/dist/index.d.ts.map +1 -0
  75. package/dist/index.js +197 -0
  76. package/dist/index.js.map +1 -0
  77. package/dist/inventory/dimensions.d.ts +42 -0
  78. package/dist/inventory/dimensions.d.ts.map +1 -0
  79. package/dist/inventory/dimensions.js +30 -0
  80. package/dist/inventory/dimensions.js.map +1 -0
  81. package/dist/inventory/grid.d.ts +53 -0
  82. package/dist/inventory/grid.d.ts.map +1 -0
  83. package/dist/inventory/grid.js +117 -0
  84. package/dist/inventory/grid.js.map +1 -0
  85. package/dist/inventory/index.d.ts +4 -0
  86. package/dist/inventory/index.d.ts.map +1 -0
  87. package/dist/inventory/index.js +4 -0
  88. package/dist/inventory/index.js.map +1 -0
  89. package/dist/inventory/placement.d.ts +68 -0
  90. package/dist/inventory/placement.d.ts.map +1 -0
  91. package/dist/inventory/placement.js +90 -0
  92. package/dist/inventory/placement.js.map +1 -0
  93. package/dist/items/index.d.ts +7 -0
  94. package/dist/items/index.d.ts.map +1 -0
  95. package/dist/items/index.js +7 -0
  96. package/dist/items/index.js.map +1 -0
  97. package/dist/items/item-dto.d.ts +47 -0
  98. package/dist/items/item-dto.d.ts.map +1 -0
  99. package/dist/items/item-dto.js +124 -0
  100. package/dist/items/item-dto.js.map +1 -0
  101. package/dist/items/item-icon.d.ts +27 -0
  102. package/dist/items/item-icon.d.ts.map +1 -0
  103. package/dist/items/item-icon.js +103 -0
  104. package/dist/items/item-icon.js.map +1 -0
  105. package/dist/items/item-serializer.d.ts +38 -0
  106. package/dist/items/item-serializer.d.ts.map +1 -0
  107. package/dist/items/item-serializer.js +92 -0
  108. package/dist/items/item-serializer.js.map +1 -0
  109. package/dist/items/item-stats-parser.d.ts +52 -0
  110. package/dist/items/item-stats-parser.d.ts.map +1 -0
  111. package/dist/items/item-stats-parser.js +671 -0
  112. package/dist/items/item-stats-parser.js.map +1 -0
  113. package/dist/items/item-types.d.ts +40 -0
  114. package/dist/items/item-types.d.ts.map +1 -0
  115. package/dist/items/item-types.js +198 -0
  116. package/dist/items/item-types.js.map +1 -0
  117. package/dist/items/item.d.ts +61 -0
  118. package/dist/items/item.d.ts.map +1 -0
  119. package/dist/items/item.js +335 -0
  120. package/dist/items/item.js.map +1 -0
  121. package/dist/operations/extract-item.d.ts +39 -0
  122. package/dist/operations/extract-item.d.ts.map +1 -0
  123. package/dist/operations/extract-item.js +204 -0
  124. package/dist/operations/extract-item.js.map +1 -0
  125. package/dist/operations/index.d.ts +4 -0
  126. package/dist/operations/index.d.ts.map +1 -0
  127. package/dist/operations/index.js +4 -0
  128. package/dist/operations/index.js.map +1 -0
  129. package/dist/operations/insert-item.d.ts +59 -0
  130. package/dist/operations/insert-item.d.ts.map +1 -0
  131. package/dist/operations/insert-item.js +210 -0
  132. package/dist/operations/insert-item.js.map +1 -0
  133. package/dist/operations/read-save.d.ts +25 -0
  134. package/dist/operations/read-save.d.ts.map +1 -0
  135. package/dist/operations/read-save.js +27 -0
  136. package/dist/operations/read-save.js.map +1 -0
  137. package/dist/types/constants.d.ts +76 -0
  138. package/dist/types/constants.d.ts.map +1 -0
  139. package/dist/types/constants.js +96 -0
  140. package/dist/types/constants.js.map +1 -0
  141. package/dist/types/errors.d.ts +47 -0
  142. package/dist/types/errors.d.ts.map +1 -0
  143. package/dist/types/errors.js +53 -0
  144. package/dist/types/errors.js.map +1 -0
  145. package/dist/types/index.d.ts +5 -0
  146. package/dist/types/index.d.ts.map +1 -0
  147. package/dist/types/index.js +3 -0
  148. package/dist/types/index.js.map +1 -0
  149. package/dist/types/item.d.ts +112 -0
  150. package/dist/types/item.d.ts.map +1 -0
  151. package/dist/types/item.js +6 -0
  152. package/dist/types/item.js.map +1 -0
  153. package/dist/types/save-file.d.ts +74 -0
  154. package/dist/types/save-file.d.ts.map +1 -0
  155. package/dist/types/save-file.js +5 -0
  156. package/dist/types/save-file.js.map +1 -0
  157. package/dist/types/trade-item.d.ts +37 -0
  158. package/dist/types/trade-item.d.ts.map +1 -0
  159. package/dist/types/trade-item.js +5 -0
  160. package/dist/types/trade-item.js.map +1 -0
  161. package/package.json +70 -0
@@ -0,0 +1,671 @@
1
+ /**
2
+ * Item stat parser — determines mod sources for parsed items.
3
+ *
4
+ * Ported from d2planner/src/logic/parser.js.
5
+ * Provides: parseModStats, parseUniqueStats, parseRunewordStats.
6
+ *
7
+ * These functions analyze item stats read from the binary save file
8
+ * and determine which affixes (mods) produced those stats, along with
9
+ * the variable values (uniqueValues) for unique/set/runeword items.
10
+ *
11
+ * Uses GameData instead of global Data singleton.
12
+ */
13
+ import { GameData } from '../game-data/game-data.js';
14
+ import { modsToStats, uniqueStats, runewordStats } from './item.js';
15
+ import { itemGetTypes, itemCheckMod, itemGetType } from './item-types.js';
16
+ // ─── Internal: itemListMods ─────────────────────────────────────
17
+ /**
18
+ * List all applicable mods for a given item (by base code and quality).
19
+ * Returns groups: prefix, suffix, auto, crafted, superior, staff.
20
+ */
21
+ function itemListMods(gd, id, quality, magic) {
22
+ const item = gd.items[id];
23
+ if (!item)
24
+ return {};
25
+ const types = itemGetTypes(gd, id);
26
+ function listMods(modList, name, gid, supt) {
27
+ return Object.keys(modList).filter(modId => {
28
+ const mod = modList[modId];
29
+ if (supt != null && !mod[supt])
30
+ return false;
31
+ if (gid != null && mod.group !== gid)
32
+ return false;
33
+ if (name != null) {
34
+ const modName = (gd.locale.strings[mod.name] || mod.name);
35
+ if (modName !== name)
36
+ return false;
37
+ }
38
+ return itemCheckMod(gd, mod, item, types, quality);
39
+ });
40
+ }
41
+ const result = {};
42
+ // Magic prefix/suffix
43
+ if (quality === 4 || quality === 6 || quality === 8) { // MAGIC, RARE, CRAFTED
44
+ result.prefix = listMods(gd.magicPrefix, magic ? (magic.prefix || '') : null, undefined, undefined);
45
+ result.suffix = listMods(gd.magicSuffix, magic ? (magic.suffix || '') : null, undefined, undefined);
46
+ }
47
+ // Auto prefix
48
+ const itemAny = item;
49
+ if (itemAny.autoprefix) {
50
+ result.auto = listMods(gd.autoMagic, null, itemAny.autoprefix, undefined);
51
+ }
52
+ // Crafted mods
53
+ if (quality === 8) { // CRAFTED
54
+ result.crafted = listMods(gd.crafted, null, undefined, undefined);
55
+ }
56
+ // Superior / runeword quality mods
57
+ if (quality === 3 || quality === 9) { // SUPERIOR or RUNEWORD
58
+ const supt = itemGetType(types);
59
+ if (supt) {
60
+ result.superior = listMods(gd.qualityItems, null, undefined, supt);
61
+ }
62
+ }
63
+ // Staff mods (class-specific)
64
+ if (gd.itemTypes[item.type]?.staffmods && quality !== 7 && quality !== 5) { // not UNIQUE/SET
65
+ result.staff = listMods(gd.staffMods, null, undefined, undefined);
66
+ }
67
+ return result;
68
+ }
69
+ // ─── Internal: subtractStats ────────────────────────────────────
70
+ function subtractStats(dst, src, orig) {
71
+ for (const id in src) {
72
+ if (orig && orig[id] && orig[id] < 0) {
73
+ dst[id] = (dst[id] || 0) - src[id];
74
+ continue;
75
+ }
76
+ if (id === 'coldlength')
77
+ continue;
78
+ if (!dst[id])
79
+ return false;
80
+ if (typeof dst[id] === 'object') {
81
+ const d = dst[id];
82
+ if (Math.max(Math.abs(d.min), Math.abs(d.max)) < Math.abs(src[id]))
83
+ return false;
84
+ d.min -= src[id];
85
+ d.max -= src[id];
86
+ }
87
+ else {
88
+ if (Math.abs(dst[id]) < Math.abs(src[id]))
89
+ return false;
90
+ dst[id] -= src[id];
91
+ if (!dst[id])
92
+ delete dst[id];
93
+ }
94
+ }
95
+ return true;
96
+ }
97
+ /**
98
+ * Brute-force search for the combination of mods that produce the observed stats.
99
+ */
100
+ function parseMods(gd, stats, modVals, quality, presetMods) {
101
+ // Build target vector
102
+ const target = {};
103
+ switch (quality) {
104
+ case 3: // SUPERIOR
105
+ target.n_superior = { min: 1, max: 1 };
106
+ break;
107
+ case 4: // MAGIC
108
+ target.n_prefix = { min: 0, max: 1 };
109
+ target.n_suffix = { min: 0, max: 1 };
110
+ target.n_affix = { min: 1, max: 2 };
111
+ break;
112
+ case 6: // RARE
113
+ target.n_prefix = { min: 0, max: 3 };
114
+ target.n_suffix = { min: 0, max: 3 };
115
+ target.n_affix = { min: 3, max: 6 };
116
+ break;
117
+ case 8: // CRAFTED
118
+ target.n_crafted = { min: 1, max: 1 };
119
+ target.n_prefix = { min: 0, max: 3 };
120
+ target.n_suffix = { min: 0, max: 3 };
121
+ target.n_affix = { min: 1, max: 4 };
122
+ break;
123
+ case 9: // RUNEWORD
124
+ target.n_superior = { min: 0, max: 1 };
125
+ break;
126
+ }
127
+ if (presetMods) {
128
+ for (const id of presetMods) {
129
+ target[`m_${id}`] = { min: 1, max: 1 };
130
+ }
131
+ }
132
+ if (Object.keys(modVals).some(id => gd.autoMagic[id])) {
133
+ target.n_auto = { min: 1, max: 1 };
134
+ }
135
+ if (modVals.runeword) {
136
+ target.n_runeword = { min: 1, max: 1 };
137
+ }
138
+ for (const id in stats) {
139
+ if (typeof stats[id] === 'object') {
140
+ const obj = stats[id];
141
+ target[id] = { min: obj.min, max: obj.max };
142
+ }
143
+ else {
144
+ const v = stats[id];
145
+ target[id] = { min: v, max: v };
146
+ }
147
+ }
148
+ // Build source vectors
149
+ const groups = {};
150
+ const modLimits = {};
151
+ const negative = new Set();
152
+ for (const [id, mstat] of Object.entries(modVals)) {
153
+ const modsrc = {};
154
+ if (gd.magicPrefix[id]) {
155
+ modsrc.n_prefix = { min: 1, max: 1 };
156
+ modsrc.n_affix = { min: 1, max: 1 };
157
+ }
158
+ if (gd.magicSuffix[id]) {
159
+ modsrc.n_suffix = { min: 1, max: 1 };
160
+ modsrc.n_affix = { min: 1, max: 1 };
161
+ }
162
+ if (gd.crafted?.[id]) {
163
+ modsrc.n_crafted = { min: 1, max: 1 };
164
+ }
165
+ if (gd.autoMagic[id]) {
166
+ modsrc.n_auto = { min: 1, max: 1 };
167
+ }
168
+ if (gd.qualityItems[id]) {
169
+ modsrc.n_superior = { min: 1, max: 1 };
170
+ }
171
+ if (id === 'runeword') {
172
+ modsrc.n_runeword = { min: 1, max: 1 };
173
+ }
174
+ if (presetMods?.includes(id)) {
175
+ modsrc[`m_${id}`] = { min: 1, max: 1 };
176
+ }
177
+ const usedIndices = {};
178
+ let repeats = 0;
179
+ for (const [sid, value] of Object.entries(mstat)) {
180
+ if (sid === 'coldlength')
181
+ continue;
182
+ const chargedMatch = sid.match(/item_charged_skill#(\d+)#/);
183
+ if (chargedMatch) {
184
+ const nstat = Object.keys(stats).find(s => s.includes(chargedMatch[0])) || sid;
185
+ const nval = (stats[nstat] ?? value);
186
+ modsrc[nstat] = { min: nval, max: nval };
187
+ }
188
+ else if (typeof value === 'number') {
189
+ modsrc[sid] = { min: value, max: value };
190
+ if (value < 0)
191
+ negative.add(sid);
192
+ }
193
+ else if (typeof value === 'string') {
194
+ const m = value.match(/^\{(\d)\}$/);
195
+ if (m) {
196
+ const mod = gd.mods[id];
197
+ const min = mod[`mod${m[1]}min`];
198
+ const max = mod[`mod${m[1]}max`];
199
+ const index = parseInt(m[1]) - 1;
200
+ modsrc[sid] = { min, max, index };
201
+ if (usedIndices[index])
202
+ repeats += 1;
203
+ usedIndices[index] = true;
204
+ if (min < 0)
205
+ negative.add(sid);
206
+ }
207
+ }
208
+ }
209
+ if (Object.keys(modsrc).every(sid => target[sid])) {
210
+ const gid = gd.mods[id]?.group || 0;
211
+ if (!groups[gid])
212
+ groups[gid] = { mods: {}, repeats: 0, max: {} };
213
+ groups[gid].mods[id] = modsrc;
214
+ groups[gid].repeats = Math.max(groups[gid].repeats, repeats);
215
+ modLimits[id] = modsrc;
216
+ }
217
+ }
218
+ // Negate negative stat targets and sources
219
+ const negate = ({ min, max, index }) => {
220
+ const r = { min: -max, max: -min };
221
+ if (index != null)
222
+ r.index = index;
223
+ return r;
224
+ };
225
+ for (const id of negative) {
226
+ if (!target[id])
227
+ continue;
228
+ target[id] = negate(target[id]);
229
+ for (const mod of Object.values(modLimits)) {
230
+ if (mod[id])
231
+ mod[id] = negate(mod[id]);
232
+ }
233
+ }
234
+ // Build source groups and partial sums
235
+ const source = Object.values(groups);
236
+ for (const group of source) {
237
+ group.max = {};
238
+ for (const mod of Object.values(group.mods)) {
239
+ for (const id in target) {
240
+ mod[id] = mod[id] || { min: 0, max: 0 };
241
+ group.max[id] = Math.max(group.max[id] || 0, mod[id].max);
242
+ }
243
+ }
244
+ }
245
+ source.sort((a, b) => a.repeats - b.repeats);
246
+ // Reverse partial sums for pruning
247
+ const reverse = new Array(source.length + 1);
248
+ reverse[source.length] = {};
249
+ for (const id in target)
250
+ reverse[source.length][id] = 0;
251
+ for (let i = source.length - 1; i >= 0; --i) {
252
+ reverse[i] = { ...reverse[i + 1] };
253
+ for (const id in source[i].max) {
254
+ reverse[i][id] += source[i].max[id];
255
+ }
256
+ }
257
+ // Recursive brute-force
258
+ const result = [];
259
+ const ids = Object.keys(target);
260
+ function iterate(i, tmin, tmax) {
261
+ if (i >= source.length) {
262
+ return ids.every(id => tmin[id] <= 0 && tmax[id] >= 0);
263
+ }
264
+ const next = reverse[i + 1];
265
+ for (const m in source[i].mods) {
266
+ const src = source[i].mods[m];
267
+ const pmin = [];
268
+ const pmax = [];
269
+ for (const id in stats) {
270
+ const { min, max, index } = src[id];
271
+ if (index != null) {
272
+ const cmin = Math.max(min, tmin[id] - next[id]);
273
+ const cmax = Math.min(max, tmax[id]);
274
+ pmin[index] = pmin[index] == null ? cmin : Math.max(pmin[index], cmin);
275
+ pmax[index] = pmax[index] == null ? cmax : Math.min(pmax[index], cmax);
276
+ }
277
+ }
278
+ if (ids.every(id => src[id].min <= tmax[id] && src[id].max + next[id] >= tmin[id]) &&
279
+ Object.keys(pmin).every(idx => pmin[parseInt(idx)] <= pmax[parseInt(idx)])) {
280
+ const nmin = { ...tmin };
281
+ const nmax = { ...tmax };
282
+ for (const id in src) {
283
+ const { min, max, index } = src[id];
284
+ if (index != null) {
285
+ nmin[id] -= pmax[index];
286
+ nmax[id] -= pmin[index];
287
+ }
288
+ else {
289
+ nmin[id] -= max;
290
+ nmax[id] -= min;
291
+ }
292
+ }
293
+ result.push(m);
294
+ if (iterate(i + 1, nmin, nmax))
295
+ return true;
296
+ result.pop();
297
+ }
298
+ }
299
+ if (ids.every(id => next[id] >= tmin[id])) {
300
+ return iterate(i + 1, tmin, tmax);
301
+ }
302
+ return false;
303
+ }
304
+ const tmin = {};
305
+ const tmax = {};
306
+ for (const id in target) {
307
+ tmin[id] = target[id].min;
308
+ tmax[id] = target[id].max;
309
+ }
310
+ if (iterate(0, tmin, tmax)) {
311
+ result.reverse();
312
+ const rmin = {};
313
+ const rmax = {};
314
+ const nmin = {};
315
+ const nmax = {};
316
+ for (const sid in stats) {
317
+ rmin[sid] = rmax[sid] = 0;
318
+ nmin[sid] = typeof stats[sid] === 'object' ? stats[sid].min : stats[sid];
319
+ nmax[sid] = typeof stats[sid] === 'object' ? stats[sid].max : stats[sid];
320
+ for (const id of result) {
321
+ rmin[sid] += modLimits[id][sid].min;
322
+ rmax[sid] += modLimits[id][sid].max;
323
+ }
324
+ }
325
+ const output = {};
326
+ for (const id of result) {
327
+ const values = [];
328
+ const modi = gd.mods[id];
329
+ if (modi.mod1max != null)
330
+ values[0] = modi.mod1max;
331
+ if (modi.mod2max != null)
332
+ values[1] = modi.mod2max;
333
+ if (modi.mod3max != null)
334
+ values[2] = modi.mod3max;
335
+ if (modi.mod4max != null)
336
+ values[3] = modi.mod4max;
337
+ if (modi.mod5max != null)
338
+ values[4] = modi.mod5max;
339
+ if (modi.mod6max != null)
340
+ values[5] = modi.mod6max;
341
+ if (modi.mod7max != null)
342
+ values[6] = modi.mod7max;
343
+ const pmin = [];
344
+ const pmax = [];
345
+ for (const sid in stats) {
346
+ const { min, max, index } = modLimits[id][sid];
347
+ if (index != null) {
348
+ const cmin = Math.max(min, nmin[sid] + max - rmax[sid]);
349
+ const cmax = Math.min(max, nmax[sid] + min - rmin[sid]);
350
+ pmin[index] = pmin[index] == null ? cmin : Math.max(pmin[index], cmin);
351
+ pmax[index] = pmax[index] == null ? cmax : Math.min(pmax[index], cmax);
352
+ }
353
+ }
354
+ for (const sid in stats) {
355
+ const { min, max, index } = modLimits[id][sid];
356
+ if (index != null) {
357
+ if (pmin[index] > pmax[index])
358
+ return null;
359
+ const value = (pmin[index] + pmax[index]) >> 1;
360
+ nmin[sid] -= value;
361
+ nmax[sid] -= value;
362
+ values[index] = negative.has(sid) ? -value : value;
363
+ }
364
+ else {
365
+ nmin[sid] -= min;
366
+ nmax[sid] -= max;
367
+ }
368
+ rmin[sid] -= min;
369
+ rmax[sid] -= max;
370
+ }
371
+ output[id] = values;
372
+ }
373
+ return output;
374
+ }
375
+ return null;
376
+ }
377
+ // ─── parseParamStats ────────────────────────────────────────────
378
+ /**
379
+ * Post-process binary stats into a format suitable for mod matching.
380
+ * Handles descfunc 13/27 stats (class skill bonuses, single-skill bonuses).
381
+ * Ported from d2planner binary/index.js → parseParamStats().
382
+ */
383
+ export function parseParamStats(gd, stats, src) {
384
+ const result = {};
385
+ for (const idFull in src) {
386
+ const parts = idFull.split('#');
387
+ const id = parts[0];
388
+ const stat = gd.itemStatCost[id];
389
+ // Non-descfunc stats: try to match directly
390
+ if (!stat?.descfunc) {
391
+ if (stats[idFull] != null) {
392
+ result[idFull] = stats[idFull];
393
+ }
394
+ else if (typeof src[idFull] === 'number') {
395
+ // Fixed value from mod definition — keep if matches
396
+ result[idFull] = src[idFull];
397
+ }
398
+ continue;
399
+ }
400
+ // descfunc 13: class skill stats (e.g. "+2 to Amazon Skill Levels")
401
+ if (stat.descfunc === 13 && parts[2] === 'x') {
402
+ // The param is the value, param2 is the class
403
+ const value = parts[1];
404
+ for (let cls = 0; cls < GameData.strClassSkillsStat.length; ++cls) {
405
+ const key = `${id}#${value}#${cls}`;
406
+ if (stats[key] != null) {
407
+ result[key] = stats[key];
408
+ break;
409
+ }
410
+ }
411
+ continue;
412
+ }
413
+ // descfunc 27: single-skill stats (e.g. "+3 to Frozen Orb (Sorceress Only)")
414
+ if (stat.descfunc === 27 && parts[2] === 'x') {
415
+ const value = parts[1];
416
+ for (const skillId in gd.skills) {
417
+ const key = `${id}#${skillId}#${value}`;
418
+ if (stats[key] != null) {
419
+ result[key] = stats[key];
420
+ break;
421
+ }
422
+ }
423
+ continue;
424
+ }
425
+ // Standard stat: copy from binary stats
426
+ if (stats[idFull] != null) {
427
+ result[idFull] = stats[idFull];
428
+ }
429
+ }
430
+ // Check that all required src stats were resolved
431
+ for (const idFull in src) {
432
+ const parts = idFull.split('#');
433
+ const id = parts[0];
434
+ const stat = gd.itemStatCost[id];
435
+ if (stat?.descfunc && result[idFull] == null) {
436
+ // Try finding by prefix (for parameterized stats)
437
+ const found = Object.keys(result).some(k => k.startsWith(id + '#'));
438
+ if (!found && parts[2] !== 'x') {
439
+ // Required stat missing
440
+ return null;
441
+ }
442
+ }
443
+ }
444
+ return result;
445
+ }
446
+ // ─── parseUniqueStats ───────────────────────────────────────────
447
+ /**
448
+ * Determine the variable values for a unique/set item by analyzing binary stats.
449
+ * Returns `{uniqueValues, propertyIndex}` (matching d2planner HEAD) or null
450
+ * when the stats don't match the definition.
451
+ *
452
+ * `propertyIndex` maps each propertyGroup property name to the picked option
453
+ * index (warlock items use this for "pick a skill tab" style rolls).
454
+ */
455
+ export function parseUniqueStats(gd, item, getStats) {
456
+ const uniq = (gd.uniqueItems[item.unique] || gd.setItems[item.unique]);
457
+ if (!uniq)
458
+ return null;
459
+ // Build source stats with template values
460
+ const values = [];
461
+ for (let i = 0; i < 12; ++i) {
462
+ if (!uniq[`prop${i + 1}`])
463
+ continue;
464
+ const min = uniq[`min${i + 1}`];
465
+ const max = uniq[`max${i + 1}`];
466
+ values.push(min === max && max !== 0 ? max : `{${i}}`);
467
+ }
468
+ const statsSrc = uniqueStats(gd, {}, uniq, values, item);
469
+ const uniqueValues = [];
470
+ const propertyIndex = {};
471
+ const stats = getStats(statsSrc);
472
+ if (!stats)
473
+ return null;
474
+ for (const id in statsSrc) {
475
+ const descFunc = gd.itemStatCost[id]?.descfunc;
476
+ if (descFunc || stats[id] != null) {
477
+ if (stats[id] == null)
478
+ return null;
479
+ if ((descFunc < 6 || descFunc > 10) && typeof statsSrc[id] === 'number') {
480
+ if (typeof stats[id] === 'object') {
481
+ const s = stats[id];
482
+ if (statsSrc[id] < s.min || statsSrc[id] > s.max)
483
+ return null;
484
+ }
485
+ else {
486
+ if (statsSrc[id] !== stats[id])
487
+ return null;
488
+ }
489
+ }
490
+ }
491
+ if (typeof statsSrc[id] === 'string') {
492
+ const m = statsSrc[id].match(/\{(\d+)\}/);
493
+ if (m) {
494
+ const index = parseInt(m[1]);
495
+ const min = (uniqueValues[index] != null ? uniqueValues[index] : uniq[`min${index + 1}`]);
496
+ const max = (uniqueValues[index] != null ? uniqueValues[index] : uniq[`max${index + 1}`]);
497
+ if (min === 0 && max === 0) {
498
+ uniqueValues[index] = typeof stats[id] === 'object'
499
+ ? ((stats[id].min + stats[id].max) >> 1)
500
+ : (stats[id] ?? 0);
501
+ }
502
+ else if (typeof stats[id] === 'object') {
503
+ const s = stats[id];
504
+ if (min > s.max || max < s.min)
505
+ return null;
506
+ uniqueValues[index] = (Math.max(s.min, min) + Math.min(s.max, max)) >> 1;
507
+ }
508
+ else {
509
+ if (stats[id] < min || stats[id] > max)
510
+ return null;
511
+ uniqueValues[index] = stats[id];
512
+ }
513
+ }
514
+ }
515
+ }
516
+ // Fill non-variable slots. For propertyGroup props (pg), pick the actual
517
+ // option index by scanning the group's mods for one whose mod1 stat is
518
+ // present in `stats`; otherwise default to the def's `max`.
519
+ for (let i = 0; i < 12; ++i) {
520
+ const prop = uniq[`prop${i + 1}`];
521
+ if (!prop)
522
+ continue;
523
+ if (uniqueValues[i] != null)
524
+ continue;
525
+ const pg = gd.propertyGroups?.[prop];
526
+ if (pg) {
527
+ let foundOptIdx = 0;
528
+ for (let j = 1; gd.mods[`${prop}:${j}`]; j++) {
529
+ const m = gd.mods[`${prop}:${j}`];
530
+ const baseStat = gd.properties[m.mod1code]?.stat1;
531
+ const key = m.mod1param != null ? `${baseStat}#${m.mod1param}` : baseStat;
532
+ if (baseStat && key != null && stats[key] != null) {
533
+ foundOptIdx = j - 1;
534
+ break;
535
+ }
536
+ }
537
+ propertyIndex[prop] = foundOptIdx;
538
+ uniqueValues[i] = foundOptIdx;
539
+ }
540
+ else {
541
+ uniqueValues[i] = uniq[`max${i + 1}`];
542
+ }
543
+ }
544
+ return { uniqueValues, propertyIndex };
545
+ }
546
+ // ─── parseRunewordStats ─────────────────────────────────────────
547
+ /**
548
+ * Determine the mod sources and uniqueValues for a runeword item.
549
+ */
550
+ export function parseRunewordStats(gd, item, socketStats, getStats, presetMods) {
551
+ const runeword = gd.runes[item.unique];
552
+ if (!runeword)
553
+ return null;
554
+ const mods = itemListMods(gd, item.base, item.quality);
555
+ const allStats = { ...socketStats };
556
+ const modStats = {};
557
+ for (const type in mods) {
558
+ for (const id of mods[type]) {
559
+ modStats[id] = modsToStats(gd, {}, { [id]: ['{1}', '{2}', '{3}', '{4}'] });
560
+ delete modStats[id].poison_count;
561
+ Object.assign(allStats, modStats[id]);
562
+ }
563
+ }
564
+ // Build runeword mod
565
+ const rwmod = { group: 'runeword' };
566
+ const rwvals = [];
567
+ for (let i = 1; i <= 7; ++i) {
568
+ if (runeword[`t1code${i}`]) {
569
+ rwmod[`mod${i}code`] = runeword[`t1code${i}`];
570
+ rwmod[`mod${i}param`] = runeword[`t1param${i}`];
571
+ const min = rwmod[`mod${i}min`] = runeword[`t1min${i}`];
572
+ const max = rwmod[`mod${i}max`] = runeword[`t1max${i}`];
573
+ rwvals.push(min === max || max === 0 ? max : `{${i}}`);
574
+ }
575
+ }
576
+ modStats.runeword = runewordStats(gd, {}, runeword, rwvals, item);
577
+ Object.assign(allStats, modStats.runeword);
578
+ const stats = getStats(allStats);
579
+ if (!stats)
580
+ return null;
581
+ if (socketStats && !subtractStats(stats, socketStats))
582
+ return null;
583
+ // Temporarily add runeword mod to gd.mods for parseMods
584
+ gd.mods.runeword = rwmod;
585
+ const parsed = parseMods(gd, stats, modStats, item.quality, presetMods);
586
+ delete gd.mods.runeword;
587
+ if (!parsed)
588
+ return null;
589
+ const result = {};
590
+ for (const id in parsed) {
591
+ if (gd.autoMagic[id]) {
592
+ if (!result.auto)
593
+ result.auto = {};
594
+ result.auto[id] = parsed[id];
595
+ }
596
+ else if (gd.qualityItems[id]) {
597
+ if (!result.superior)
598
+ result.superior = {};
599
+ result.superior[id] = parsed[id];
600
+ }
601
+ else if (gd.staffMods[id]) {
602
+ if (!result.staff)
603
+ result.staff = {};
604
+ result.staff[id] = parsed[id];
605
+ }
606
+ else if (id === 'runeword') {
607
+ result.uniqueValues = parsed[id];
608
+ }
609
+ }
610
+ if (!result.uniqueValues)
611
+ return null;
612
+ return result;
613
+ }
614
+ // ─── parseModStats ──────────────────────────────────────────────
615
+ /**
616
+ * Determine the mod sources for a magic/rare/crafted/superior item.
617
+ */
618
+ export function parseModStats(gd, item, magic, socketStats, levelreq, getStats, presetMods) {
619
+ const mods = itemListMods(gd, item.base, item.quality, magic);
620
+ const statsSrc = { ...socketStats };
621
+ const modStats = {};
622
+ for (const type in mods) {
623
+ for (const id of mods[type]) {
624
+ const mod = gd.mods[id];
625
+ if (levelreq != null && !gd.autoMagic[id] &&
626
+ (mod.classlevelreq || mod.levelreq) > levelreq)
627
+ continue;
628
+ modStats[id] = modsToStats(gd, {}, { [id]: ['{1}', '{2}', '{3}', '{4}'] });
629
+ delete modStats[id].poison_count;
630
+ Object.assign(statsSrc, modStats[id]);
631
+ }
632
+ }
633
+ const stats = getStats(statsSrc);
634
+ if (!stats)
635
+ return null;
636
+ if (socketStats && !subtractStats(stats, socketStats))
637
+ return null;
638
+ const parsed = parseMods(gd, stats, modStats, item.quality, presetMods);
639
+ if (!parsed)
640
+ return null;
641
+ const result = {};
642
+ for (const id in parsed) {
643
+ if (gd.magicPrefix[id] || gd.magicSuffix[id]) {
644
+ if (!result.mods)
645
+ result.mods = {};
646
+ result.mods[id] = parsed[id];
647
+ }
648
+ else if (gd.autoMagic[id]) {
649
+ if (!result.auto)
650
+ result.auto = {};
651
+ result.auto[id] = parsed[id];
652
+ }
653
+ else if (gd.crafted[id]) {
654
+ if (!result.crafted)
655
+ result.crafted = {};
656
+ result.crafted[id] = parsed[id];
657
+ }
658
+ else if (gd.qualityItems[id]) {
659
+ if (!result.superior)
660
+ result.superior = {};
661
+ result.superior[id] = parsed[id];
662
+ }
663
+ else if (gd.staffMods[id]) {
664
+ if (!result.staff)
665
+ result.staff = {};
666
+ result.staff[id] = parsed[id];
667
+ }
668
+ }
669
+ return result;
670
+ }
671
+ //# sourceMappingURL=item-stats-parser.js.map