11import json
22import os
33import sys
4+ import re
45
56GRAPH_FILE = "Mythril.Blazor/wwwroot/data/content_graph.json"
67
78def load_graph ():
89 with open (GRAPH_FILE , 'r' , encoding = 'utf-8' ) as f :
910 return json .load (f )
1011
12+ def clean_label (label ):
13+ if not label : return "Untitled"
14+ # Rigorous cleaning
15+ label = label .replace ('"' , "'" ).replace (":" , "-" ).replace ("[" , "(" ).replace ("]" , ")" )
16+ label = "" .join (c for c in label if c .isprintable () or c == " " )
17+ label = " " .join (label .split ()).strip ()
18+ return label if label else "Untitled"
19+
1120def generate_mermaid ():
1221 nodes = load_graph ()
1322
@@ -32,114 +41,127 @@ def generate_mermaid():
3241 cadence_map [edge ["targetId" ]] = node ["name" ]
3342
3443 # 2. Group nodes
35- by_location = {} # LocName -> [NodeLines]
36- by_cadence = {} # CadName -> [NodeLines]
37- globals = []
44+ by_location = {} # LocName -> [NodeIDs]
45+ by_cadence = {} # CadName -> [NodeIDs]
3846
39- lines = ["graph TD" ]
47+ lines = ["flowchart TD" ]
4048
4149 # Styles
42- lines .append ("classDef quest fill:#4b0082,stroke:#f9f,stroke-width:2px,color:#fff; " )
43- lines .append ("classDef location fill:#00008b,stroke:#ccf,stroke-width:2px,color:#fff; " )
44- lines .append ("classDef cadence fill:#006400,stroke:#cfc,stroke-width:2px,color:#fff; " )
45- lines .append ("classDef item fill:#444,stroke:#fff,stroke-width:1px,color:#fff; " )
46- lines .append ("classDef stat fill:#8b4513,stroke:#ff8c00,stroke-width:1px,color:#fff; " )
47- lines .append ("classDef ability fill:#2f4f4f,stroke:#00ced1,stroke-width:1px,color:#fff; " )
48- lines .append ("classDef refinement fill:#ff4500,stroke:#fff,stroke-width:1px,color:#fff; " )
49- lines .append ("classDef root fill:#8b8b00,stroke:#ff9,stroke-width:4px,color:#fff; " )
50+ lines .append ("classDef quest fill:#4b0082,stroke:#f9f,stroke-width:2px,color:#fff" )
51+ lines .append ("classDef location fill:#00008b,stroke:#ccf,stroke-width:2px,color:#fff" )
52+ lines .append ("classDef cadence fill:#006400,stroke:#cfc,stroke-width:2px,color:#fff" )
53+ lines .append ("classDef item fill:#444,stroke:#fff,stroke-width:1px,color:#fff" )
54+ lines .append ("classDef stat fill:#8b4513,stroke:#ff8c00,stroke-width:1px,color:#fff" )
55+ lines .append ("classDef ability fill:#2f4f4f,stroke:#00ced1,stroke-width:1px,color:#fff" )
56+ lines .append ("classDef refinement fill:#ff4500,stroke:#fff,stroke-width:1px,color:#fff" )
57+ lines .append ("classDef root fill:#8b8b00,stroke:#ff9,stroke-width:4px,color:#fff" )
5058
5159 edge_lines = []
60+ processed_ids = set ()
61+ node_defs = []
62+
63+ # Pre-define stat nodes
64+ for stat in ["Strength" , "Vitality" , "Magic" , "Speed" ]:
65+ stat_id = f"stat_{ stat .lower ()} "
66+ node_defs .append (f'{ stat_id } ["{ stat } "]:::stat' )
67+ processed_ids .add (stat_id )
5268
5369 for node in nodes :
5470 nid = node ["id" ].strip ()
5571 ntype = node ["type" ]
56- # Rigorous cleaning
57- raw_name = node ["name" ].replace ('"' , "'" ).replace (":" , "-" )
58- name = "" .join (c for c in raw_name if c .isprintable () or c == " " )
59- name = " " .join (name .split ()).strip ()
6072
6173 # IGNORE GOLD
6274 if nid == "item_gold" :
6375 continue
6476
77+ name = clean_label (node ["name" ])
6578 style = ntype .lower ()
6679 if nid == "quest_prologue" : style = "root"
6780
6881 node_def = f'{ nid } ["{ name } "]:::{ style } '
82+ node_defs .append (node_def )
83+ processed_ids .add (nid )
6984
7085 # Assign to group
7186 if ntype == "Quest" and nid in location_map :
72- raw_loc = location_map [nid ].replace (":" , "-" )
73- loc = "" .join (c for c in raw_loc if c .isprintable () or c == " " )
74- loc = " " .join (loc .split ()).strip ()
87+ loc = clean_label (location_map [nid ])
7588 if loc not in by_location : by_location [loc ] = []
76- by_location [loc ].append (node_def )
89+ by_location [loc ].append (nid )
7790 elif ntype == "Ability" and nid in cadence_map :
78- raw_cad = cadence_map [nid ].replace (":" , "-" )
79- cad = "" .join (c for c in raw_cad if c .isprintable () or c == " " )
80- cad = " " .join (cad .split ()).strip ()
91+ cad = clean_label (cadence_map [nid ])
8192 if cad not in by_cadence : by_cadence [cad ] = []
82- by_cadence [cad ].append (node_def )
83- else :
84- globals .append (node_def )
93+ by_cadence [cad ].append (nid )
8594
86- # Edges
95+ # Edges (same as before)
8796 in_edges = node .get ("in_edges" , {})
8897 out_edges = node .get ("out_edges" , {})
8998
9099 if "requires_quest" in in_edges :
91100 for req_id in in_edges ["requires_quest" ]:
92101 label = "Enables" if ntype == "Location" else "Requires"
93- edge_lines .append (f" { req_id } -->|{ label } | { nid } " )
102+ edge_lines .append (f' { req_id } -->|" { label } " | { nid } ' )
94103
95104 if "requires_ability" in in_edges :
96105 for req_id in in_edges ["requires_ability" ]:
97- edge_lines .append (f" { req_id } -. ->|Allows| { nid } " )
106+ edge_lines .append (f' { req_id } -->|" Allows" | { nid } ' )
98107
99108 if "requires_stat" in in_edges :
100109 for stat_name , val in in_edges ["requires_stat" ].items ():
101110 stat_id = f"stat_{ stat_name .lower ()} "
102- edge_lines .append (f" { stat_id } -. ->|Req { val } | { nid } " )
111+ edge_lines .append (f' { stat_id } -->|" Req { val } " | { nid } ' )
103112
104113 for unlock_type in ["unlocks_cadence" , "unlocks_ability" , "provides_ability" , "unlocks_location" ]:
105114 if unlock_type in out_edges :
106115 for edge in out_edges [unlock_type ]:
107- edge_lines .append (f" { nid } ==>| Unlocks| { edge [' targetId' ] } " )
116+ edge_lines .append (f' { nid } -->|" Unlocks" | { edge [" targetId" ] } ' )
108117
109118 for reward_type in ["rewards" , "produces" , "rewards_item" ]:
110119 if reward_type in out_edges :
111120 for edge in out_edges [reward_type ]:
112121 target_id = edge ["targetId" ]
113- # IGNORE GOLD EDGES
114122 if target_id == "item_gold" : continue
115-
116123 qty = edge .get ("quantity" , 1 )
117- edge_lines .append (f" { nid } --o|Gives { qty } | { target_id } " )
124+ edge_lines .append (f' { nid } --o|" Gives { qty } " | { target_id } ' )
118125
119126 if "consumes" in out_edges :
120127 for edge in out_edges ["consumes" ]:
121128 target_id = edge ["targetId" ]
122- # IGNORE GOLD EDGES
123129 if target_id == "item_gold" : continue
124-
125130 qty = edge .get ("quantity" , 1 )
126- edge_lines .append (f" { target_id } --x|Consumes { qty } | { nid } " )
131+ edge_lines .append (f' { target_id } --x|" Consumes { qty } " | { nid } ' )
127132
128133 # Build final output
129- for node_def in globals :
130- lines .append (node_def )
134+ lines .extend (node_defs )
131135
132136 for loc , nodes_in_loc in by_location .items ():
133- lines .append (f'subgraph "Location - { loc } "' )
137+ # Sanitize loc for ID
138+ loc_id = "loc_" + re .sub (r'\W+' , '_' , loc .lower ())
139+ lines .append (f'subgraph { loc_id } ["Location - { loc } "]' )
134140 lines .extend (nodes_in_loc )
135141 lines .append ("end" )
136142
137143 for cad , nodes_in_cad in by_cadence .items ():
138- lines .append (f'subgraph "Cadence - { cad } "' )
144+ # Sanitize cad for ID
145+ cad_id = "cad_" + re .sub (r'\W+' , '_' , cad .lower ())
146+ lines .append (f'subgraph { cad_id } ["Cadence - { cad } "]' )
139147 lines .extend (nodes_in_cad )
140148 lines .append ("end" )
141149
142- lines .extend (edge_lines )
150+ # Final filter to ensure no edges to missing nodes and no self-edges
151+ valid_edges = []
152+ for edge in edge_lines :
153+ # Simple extraction of IDs from "id1 --o|...| id2" or "id1 --x|...| id2" or "id1 -->|...| id2"
154+ match = re .search (r'^(\w+)\s*--[->ox]\s*\|".*?"\|\s*(\w+)$' , edge )
155+ if match :
156+ source_id = match .group (1 )
157+ target_id = match .group (2 )
158+ if source_id in processed_ids and target_id in processed_ids :
159+ if source_id != target_id :
160+ valid_edges .append (edge )
161+ else :
162+ print (f"Filtered out self-edge: { source_id } --> { target_id } " )
163+
164+ lines .extend (valid_edges )
143165
144166 return "\n " .join (lines )
145167
0 commit comments