-
Notifications
You must be signed in to change notification settings - Fork 1
/
item_cluster_jewel.py
522 lines (393 loc) · 15 KB
/
item_cluster_jewel.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
# Python
import logging
import json
from functools import cached_property
# Self
from item import item_t
import stat_parsing
import logger
import copy
import passive_skill_tree
from _exceptions import EligibilityException
# TODO: Convert to cached properties
# load in a function to aid garbage collection
def init():
with open('data/cluster_jewels.json', 'r') as f:
raw_data = json.load(f)
global data
data = {}
# Initialized hardcoded size indices
# This value is not from the game data but is present in PoB, so we supplement here
size_indices = {
'Large': 2,
'Medium': 1,
'Small': 0
}
for key, value in list(raw_data.items()):
value['size_index'] = size_indices[value['size']]
data[value['name']] = value
for skill_data in value['passive_skills']:
# Add enchant value
skill_data['enchant'] = []
for stat_id, value in list(skill_data['stats'].items()):
stat = stat_parsing.stat_t(None, {stat_id: value})
stat_str = stat.string
# blargh, phys damage stat translates to "global phys damage" even though thats now how it works ingame
# spaghet
if stat_id == "physical_damage_+%":
stat_str = "{0}% increased Physical Damage".format(value)
elif stat_id == "critical_strike_chance_+%":
stat_str = "{0}% increased Critical Strike Chance".format(value)
skill_data['enchant'].append("Added Small Passive Skills grant: {}".format(stat_str))
del stat
with open('data/cluster_jewel_notables.json', 'r') as f:
notable_data = json.load(f)
# Create these dicts
global notable_sort_order
notable_sort_order = {}
global cluster_notable_map
cluster_notable_map = {}
global cluster_keystone_map
cluster_keystone_map = {}
passive_skill_warnings = 0
for notable in notable_data:
# Create sort order entry
notable_sort_order[notable['name']] = notable_data.index(notable)
# Create notable / keystone map entry
try:
matching_nodes = passive_skill_tree.find_nodes_by_name(notable['name'])
except KeyError:
logging.warning("No passive found for {} ({})".format(notable['name'], notable['jewel_stat']))
continue
'''
In some cases, multiple nodes are present with the same name (ie "Intensity").
To find the cluster jewel ones, we'll filter out nodes that have an assigned
"group" as this relates to their position on the skill tree (which cluster jewel notables do not have).
'''
filtered_nodes = list(filter(lambda n: 'group' not in n, matching_nodes))
assert len(filtered_nodes) == 1
if 'isKeystone' in filtered_nodes[0] and filtered_nodes[0]['isKeystone']:
cluster_keystone_map[notable['jewel_stat']] = (notable['name'], filtered_nodes[0]['skill'])
else:
'''
Jun 29 2020
The data for the new cluster jewel notables added in patch 3.11 is fucked up and aren't
designated as notables, so with that in mind we'll spew a concise warning and carry on.
In a more sane world this would mean something fucky has happened and we should terminate.
'''
if not ('isNotable' in filtered_nodes[0] and filtered_nodes[0]['isNotable']):
logging.debug("Cluster jewel notable '{}' passive is not designated as a keystone or notable.".format(notable['name']))
passive_skill_warnings += 1
cluster_notable_map[notable['jewel_stat']] = (notable['name'], filtered_nodes[0]['skill'])
#logging.info("'{}' mapped to passive {} ({})".format(notable['jewel_stat'], notable['name'], filtered_nodes[0]['skill']))
if passive_skill_warnings > 0:
logging.warning("Warning: Found {} cluster jewel notables whose passive is not designated as a keystone or notable.".format(passive_skill_warnings))
# define bases for constructor selection in item.py
global bases
bases = [x['name'] for x in data.values()]
class cluster_node_t(object):
def __init__(self, subgraph):
self.subgraph = subgraph
self.jewel = subgraph.jewel
@cached_property
def index(self):
nodes = [n for n in list(self.subgraph.nodes.items()) if n[1] == self]
assert len(nodes) == 1
#logging.info("{} node index is {}".format(self, nodes[0][0]))
return nodes[0][0]
'''
Generate an ID for the node the same way PoB does. This is an unofficial method of ID generation.
Reference:
https://github.com/PathOfBuildingCommunity/PathOfBuilding/blob/acf47d219f9d463a3286d97129ee4f6448f62444/Classes/PassiveSpec.lua#L637
'''
def get_id(self):
if self.subgraph.parent_socket is None:
return None
'''
Make id for this subgraph (and nodes)
0-3: Node index (0-11)
4-5: Group size (0-2)
6-8: Large index (0-5)
9-10: Medium index (0-2)
11-15: Unused
16: 1 (signal bit, to prevent conflict with node hashes)
'''
id = 65536
# Step 1: Node index
id += self.index
# Step 2: Group size
id += self.jewel.data['size_index'] << 4
# Step 3: Large index
node = self.subgraph.parent_socket
# Climb the node tree until we find the parent socket / grandparent socket etc that is large
while node['expansionJewel']['size'] < 2:
parent_id = int(node['expansionJewel']['parent'])
node = passive_skill_tree.nodes[parent_id]
assert node['expansionJewel']['size'] == 2
# Add its index
id += node['expansionJewel']['index'] << 6
# Step 4: Medium index
# If the parent socket is medium or smaller
if self.subgraph.parent_socket['expansionJewel']['size'] <= 1:
node = self.subgraph.parent_socket
# Climb the node tree until we find the parent socket / grandparent socket etc that is medium
while node['expansionJewel']['size'] < 1:
parent_id = int(node['expansionJewel']['parent'])
node = passive_skill_tree.nodes[parent_id]
assert node['expansionJewel']['size'] == 1
# Add its index
id += node['expansionJewel']['index'] << 9
'''
Example bitmask
65856 320 00101000000
node index: 0 -------0000
group size: 0 -----00----
large index: 5 --101------
medium index: 0 00---------
'''
return id
@cached_property
def allocated(self):
if self.subgraph.parent_socket is None:
return False
return self.get_id() in self.jewel.build.passives_by_id
@cached_property
def stats(self):
pass
@cached_property
def name(self):
pass
class cluster_small_node_t(cluster_node_t):
@cached_property
def stats(self):
return stat_parsing.combined_stats_t(None, stats_dict=self.jewel.skill['stats'], passive=self)
@cached_property
def name(self):
if self.jewel.skill:
self.jewel.skill['name']
else:
raise EligibilityException("Cluster jewel with name '{}' doesn't grant a recognisable passive effect. Check the enchant/implicit text and make sure it is recognized by Path of Building.".format(self.jewel))
class cluster_data_node_t(cluster_node_t):
def __init__(self, subgraph, passive_id):
self.passive_id = passive_id
super(cluster_data_node_t, self).__init__(subgraph)
@cached_property
def passive(self):
return passive_skill_tree.nodes[self.passive_id]
@cached_property
def stats(self):
stat_str = '\n'.join(self.passive['stats'])
return stat_parsing.combined_stats_t(stat_str, passive=self)
@cached_property
def name(self):
return self.passive['name']
# this is basically a helper-constructor that automatically finds the right passive id for the socket
class cluster_socket_t(cluster_data_node_t):
def __init__(self, subgraph, socket_index):
passive_id = self.find_socket(subgraph.proxy_group, socket_index)
super(cluster_socket_t, self).__init__(subgraph, passive_id)
@classmethod
def find_socket(cls, group, socket_index):
# Find the given socket index in the group
for node_id in group['nodes']:
node = passive_skill_tree.nodes[int(node_id)]
if 'expansionJewel' in node and node['expansionJewel']['index'] == socket_index:
return int(node_id)
def get_id(self):
return self.passive_id
class subgraph_t():
def __init__(self, jewel, socket_id):
self.jewel = jewel
self.parent_socket = copy.deepcopy(passive_skill_tree.nodes[socket_id])
self.skill = jewel.skill
self.__init_proxy_group__()
self.__init_nodes__()
#for node in self.nodes.values():
# logging.info("Cluster jewel passive node {} ({}) allocation is: {}".format(node.name, node.get_id(), node.allocated))
@cached_property
def data(self):
return self.jewel.data
def __init_proxy_group__(self):
node_id = int(self.parent_socket['expansionJewel']['proxy'])
self.proxy_node = copy.deepcopy(passive_skill_tree.nodes[node_id])
self.proxy_group = copy.deepcopy(passive_skill_tree.groups[self.proxy_node['group']])
def __init_nodes__(self):
#logging.info("Initializing nodes for socket {} ({})".format(self.parent_socket['skill'], self.jewel))
# Special handling for keystones
if self.jewel.keystone_id:
self.nodes = { 0: cluster_data_node_t(self, self.jewel.keystone_id) }
return
indices = {}
node_count = self.jewel.node_count
# First pass: sockets
socket_count = self.jewel.socket_count
if self.data['size'] == "Large" and socket_count == 1:
# Large clusters always have the single jewel at index 6
node_index = 6
assert node_index not in indices
indices[node_index] = cluster_socket_t(self, 1)
else:
assert socket_count <= len(self.data['socket_indices']) and "Too many sockets!"
get_jewels = [ 0, 2, 1 ]
for i in range(0, socket_count):
node_index = self.data['socket_indices'][i]
assert node_index not in indices
indices[node_index] = cluster_socket_t(self, get_jewels[i])
# Second pass: notables
notable_count = self.jewel.notable_count
notable_list = self.jewel.notable_list
# assign notables to indices
notable_index_list = []
for node_index in self.data['notable_indices']:
if len(notable_index_list) == notable_count:
break
if self.data['size'] == "Medium":
if socket_count == 0 and notable_count == 2:
# Special rule for two notables in a Medium cluster
if node_index == 6:
node_index = 4
elif node_index == 10:
node_index = 8
elif node_count == 4:
# Special rule for notables in a 4-node Medium cluster
if node_index == 10:
node_index = 9
elif node_index == 2:
node_index = 3
if node_index not in indices:
notable_index_list.append(node_index)
notable_index_list.sort()
for base_node in notable_list:
index = notable_list.index(base_node)
if index >= len(notable_index_list):
# Jewel has more notables than is possible
# Mirror PoB's approach and just silently ignore excess notables
break
# Get the index
node_index = notable_index_list[index]
assert node_index not in indices
indices[node_index] = cluster_data_node_t(self, base_node[1])
# Third pass: small fill
small_count = node_count - socket_count - notable_count
# Gather small indices
small_index_list = []
for node_index in self.data['small_indices']:
if len(small_index_list) == small_count:
break
if self.data['size'] == "Medium":
# Special rules for small nodes in Medium clusters
if node_count == 5 and node_index == 4:
node_index = 3
elif node_count == 4:
if node_index == 8:
node_index = 9
elif node_index == 4:
node_index = 3
if node_index not in indices:
small_index_list.append(node_index)
# Create the small nodes
for index in range(0, small_count):
# Get the index
node_index = small_index_list[index]
# TODO: inject the cluster jewel added mods here
assert node_index not in indices
indices[node_index] = cluster_small_node_t(self)
#logging.info("indices: {}".format(indices))
assert indices[0] and "No entrance to subgraph"
self.nodes = indices
class cluster_jewel_t(item_t):
def __init__(self, build, item_xml):
super(cluster_jewel_t, self).__init__(build, item_xml)
self.__init_notables__()
self.__init_skill__()
self.__init_keystone__()
self.__init_subgraphs__()
self.__update_build_passives__()
def __str__(self):
return "{} {} [{}]".format(self.name, self.base, self.id)
@cached_property
def node_count(self):
if 'local_jewel_expansion_passive_node_count' in self.stats.dict():
return int(self.stats.dict()['local_jewel_expansion_passive_node_count'])
else:
return self.socket_count + self.notable_count + self.nothingness_count
@cached_property
def socket_count(self):
if 'local_jewel_expansion_jewels_count_override' in self.stats.dict():
return int(self.stats.dict()['local_jewel_expansion_jewels_count_override'])
if 'local_jewel_expansion_jewels_count' in self.stats.dict():
return int(self.stats.dict()['local_jewel_expansion_jewels_count'])
else:
return 0
@cached_property
def notable_count(self):
return len(self.notable_stats)
@cached_property
def nothingness_count(self):
if 'local_unique_jewel_grants_x_empty_passives' in self.stats.dict():
# Voices
return int(self.stats.dict()['local_unique_jewel_grants_x_empty_passives'])
elif 'local_affliction_jewel_display_small_nodes_grant_nothing' in self.stats.dict():
# Megalomaniac
# Make sure the number of points is specified, or this won't work (stack overflow)
assert 'local_jewel_expansion_passive_node_count' in self.stats.dict()
return self.node_count - self.socket_count - self.notable_count
else:
return 0
@cached_property
def data(self):
return data[self.base]
def __init_skill__(self):
if self.nothingness_count > 0:
self.skill = {
"name": "Nothingness",
"tag": None,
"stats": []
}
return
rows = self.xml.text.split('\n')
self.skill = None
for skill_data in self.data['passive_skills']:
for row in rows:
if skill_data['enchant'][0].lower() in row.lower():
self.skill = copy.deepcopy(skill_data)
break
if self.skill:
break
#if self.skill:
# logging.info("{} skill is {} ({})".format(self, self.skill['name'], self.skill['id']))
if not self.skill:
log_level = logging.DEBUG if self.rarity == "UNIQUE" else logging.WARNING
logging.log(log_level, "{} has no skill".format(self))
def __init_keystone__(self):
for stat in self.stats.dict():
if stat in cluster_keystone_map:
self.keystone_id = cluster_keystone_map[stat][1]
#self.keystone = passive_skill_tree.nodes[self.keystone_id]
return
self.keystone_id = None
#self.keystone = None
def __init_notables__(self):
self.notable_stats = []
for stat in self.stats.dict():
if stat in cluster_notable_map:
self.notable_stats.append(stat)
# make notable list from stats
self.notable_list = []
for stat in self.notable_stats:
self.notable_list.append(cluster_notable_map[stat])
self.notable_list.sort(key=lambda n: notable_sort_order[n[0]])
logging.log(logger.DEBUG_ALL, "Sorted notable order: {}".format(self.notable_list))
def __init_subgraphs__(self):
self.subgraphs = []
for socket in self.build.xml.findall('Tree/Spec/Sockets/Socket'):
if int(socket.attrib['itemId']) == self.id:
node_id = int(socket.attrib['nodeId'])
# only generate a subgraph for that socket if the socket is allocated
if node_id in self.build.passives_by_id:
self.subgraphs.append(subgraph_t(self, node_id))
def __update_build_passives__(self):
for subgraph in self.subgraphs:
for node in list(subgraph.nodes.values()):
if node.allocated:
self.build.passives_by_name[node.name] = node.get_id()