@@ -77,77 +77,113 @@ def enrich_data(nodes):
7777 node_map = {n ["id" ]: n for n in nodes }
7878 sim_data = parse_simulation_report ()
7979
80- # 1. Build Adjacency and Edge Counts
80+ # 1. Build Adjacency for Tiering
8181 adj = {n ["id" ]: [] for n in nodes }
8282 edge_counts = {n ["id" ]: 0 for n in nodes }
83-
8483 for n in nodes :
85- for direction in ["out_edges" , "in_edges" ]:
86- if direction in n :
87- for rel_type , targets in n [direction ].items ():
88- for target in targets :
89- target_id = target if isinstance (target , str ) else target .get ("targetId" )
90- if target_id in node_map :
91- edge_counts [n ["id" ]] += 1
92- edge_counts [target_id ] += 1
93- if direction == "out_edges" :
94- adj [n ["id" ]].append (target_id )
84+ if "out_edges" in n :
85+ for rel_type , targets in n ["out_edges" ].items ():
86+ for target in targets :
87+ target_id = target if isinstance (target , str ) else target .get ("targetId" )
88+ if target_id in node_map :
89+ edge_counts [n ["id" ]] += 1
90+ edge_counts [target_id ] += 1
91+ if rel_type in ["consumes" , "requires_quest" , "requires_ability" ]:
92+ adj [target_id ].append (n ["id" ])
93+ else :
94+ adj [n ["id" ]].append (target_id )
95+ if "in_edges" in n :
96+ for rel_type , sources in n ["in_edges" ].items ():
97+ for source_id in sources :
98+ if source_id in node_map :
99+ edge_counts [n ["id" ]] += 1
100+ edge_counts [source_id ] += 1
101+ adj [source_id ].append (n ["id" ])
95102
96103 # 2. BFS for Tiers
97- tiers = {n ["id" ]: 0 for n in nodes }
98- roots = [ n [ "id" ] for n in nodes if n ["id" ] == "quest_prologue" ]
99- if not roots :
100- roots = [n ["id" ] for n in nodes if not n .get ("in_edges" )]
101-
104+ tiers = {n ["id" ]: 999 for n in nodes }
105+ prologue_node = next (( n for n in nodes if n ["id" ] == "quest_prologue" ), None )
106+ roots = [ prologue_node [ "id" ]] if prologue_node else [ n [ "id" ] for n in nodes if n [ "type" ] == "Quest" and not n . get ( "in_edges" , {}). get ( "requires_quest" )]
107+ if not roots : roots = [n ["id" ] for n in nodes if not n .get ("in_edges" )]
108+
102109 queue = deque ([(root , 0 ) for root in roots ])
103- visited_depth = { r : 0 for r in roots }
110+ for r in roots : tiers [ r ] = 0
104111
105112 while queue :
106113 curr_id , d = queue .popleft ()
107114 for neighbor in adj [curr_id ]:
108- if neighbor not in visited_depth or visited_depth [neighbor ] < d + 1 :
109- visited_depth [neighbor ] = d + 1
115+ if tiers [neighbor ] > d + 1 :
110116 tiers [neighbor ] = d + 1
111117 queue .append ((neighbor , d + 1 ))
112118
119+ for n in nodes :
120+ if tiers [n ["id" ]] == 999 : tiers [n ["id" ]] = 0
121+
122+ # 3. Node Splitting for Sustainable Resources
123+ sust_names = sim_data .get ("sustainable" , set ())
124+ new_nodes = []
125+ for n in nodes :
126+ if n ["type" ] == "Item" and n ["name" ] in sust_names :
127+ # Find producers (refinements/recurring quests) that are sustainable
128+ producers = [m for m in nodes if m ["type" ] in ["Refinement" , "Quest" ] and _matches_activity (m ["name" ], sust_names )]
129+ sust_producers = [p for p in producers if any (e .get ("targetId" ) == n ["id" ] for e in p .get ("out_edges" , {}).get ("produces" , []) + p .get ("out_edges" , {}).get ("rewards" , []))]
130+
131+ if sust_producers :
132+ min_sust_tier = min (tiers [p ["id" ]] for p in sust_producers )
133+ if min_sust_tier > tiers [n ["id" ]] + 1 :
134+ # Create a "Sustainable" variant
135+ sust_node = n .copy ()
136+ sust_node ["id" ] = f"{ n ['id' ]} _sustainable"
137+ sust_node ["name" ] = f"{ n ['name' ]} (Sustainable)"
138+ sust_node ["tier" ] = min_sust_tier + 1
139+ sust_node ["is_sustainable_instance" ] = True
140+ sust_node ["original_id" ] = n ["id" ]
141+ # Redirect edges from sustainable producers to this new node
142+ for p in sust_producers :
143+ if "out_edges" in p :
144+ for rel in ["produces" , "rewards" ]:
145+ if rel in p ["out_edges" ]:
146+ for edge in p ["out_edges" ][rel ]:
147+ if edge .get ("targetId" ) == n ["id" ]:
148+ edge ["targetId" ] = sust_node ["id" ]
149+ new_nodes .append (sust_node )
150+
151+ nodes .extend (new_nodes )
152+ node_map = {n ["id" ]: n for n in nodes }
153+
113154 # Force Slayer to end
114- max_bfs_tier = max (tiers .values ()) if tiers else 0
155+ max_bfs_tier = max (t for t in tiers .values () if t < 999 ) if any ( t < 999 for t in tiers . values ()) else 0
115156 for n in nodes :
116157 if n ["id" ] == "cadence_slayer" or "slayer" in n ["id" ].lower ():
117- tiers [n ["id" ]] = max_bfs_tier + 1
158+ n ["tier" ] = max_bfs_tier + 1
159+ else :
160+ n ["tier" ] = tiers .get (n ["id" ], 0 )
118161
119- # 3. Clusters
162+ # 4. Final Enrichment & Stable Ordering
120163 clusters , cluster_names = _identify_clusters (nodes )
121-
122- # Hub Detection Threshold
164+ nodes .sort (key = lambda x : (x .get ("tier" , 0 ), x ["type" ], x ["name" ]))
165+
166+ type_counts = {}
123167 HUB_THRESHOLD = 10
124-
125- sust_names = sim_data .get ("sustainable" , set ())
126- unsust_names = sim_data .get ("unsustainable" , set ())
168+ hubs = {n ["id" ] for n in nodes if edge_counts .get (n .get ("original_id" , n ["id" ]), 0 ) > HUB_THRESHOLD }
127169
128170 for n in nodes :
129- n [ "tier" ] = tiers .get (n [ "id" ] , 0 )
171+ t = n .get ("tier" , 0 )
130172 n ["cluster_id" ] = clusters .get (n ["id" ], "cluster_none" )
131- n ["is_hub" ] = edge_counts [ n ["id" ]] > HUB_THRESHOLD
173+ n ["is_hub" ] = n ["id" ] in hubs or n . get ( "original_id" ) in hubs
132174
133- # Milestone Detection
134- is_milestone = False
135- if n ["type" ] == "Quest" :
136- # Unlocks something important
137- out = n .get ("out_edges" , {})
138- if "unlocks_cadence" in out or "unlocks_location" in out :
139- is_milestone = True
140- if n ["id" ] == "quest_prologue" :
141- is_milestone = True
142- n ["is_milestone" ] = is_milestone
175+ tier_type = (t , n ["type" ])
176+ type_counts [tier_type ] = type_counts .get (tier_type , 0 ) + 1
177+ n ["tier_index" ] = type_counts [tier_type ]
143178
144- # Simulation Integration
145179 n ["simulation" ] = {
146- "sustainable" : _matches_activity (n ["name" ], sust_names ),
147- "unsustainable" : _matches_activity (n ["name" ], unsust_names ),
180+ "sustainable" : _matches_activity (n ["name" ], sust_names ) or n . get ( "is_sustainable_instance" ) ,
181+ "unsustainable" : _matches_activity (n ["name" ], sim_data . get ( "unsustainable" , set ()) ),
148182 "net_rate" : sim_data .get ("rates" , {}).get (n ["name" ], 0 )
149183 }
150-
184+
185+ n ["is_milestone" ] = (n ["type" ] == "Quest" and (n ["id" ] == "quest_prologue" or "unlocks_cadence" in n .get ("out_edges" , {}) or "unlocks_location" in n .get ("out_edges" , {})))
186+
151187 return nodes , cluster_names
152188
153189def _identify_clusters (nodes ):
0 commit comments