Skip to content

Commit fefad51

Browse files
committed
feat(viz): refine progression mapping with node splitting and improved stability
1 parent fcbf595 commit fefad51

4 files changed

Lines changed: 126 additions & 88 deletions

File tree

modules/visualization/data_processor.py

Lines changed: 80 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -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

153189
def _identify_clusters(nodes):

modules/visualization/lattice_data.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
function processData() {
22
nodes = nodesData.map(d => ({
33
...d,
4-
x: d.tier * TIER_WIDTH + (Math.random() - 0.5) * 400,
5-
y: (window.innerHeight / 2) + (Math.random() - 0.5) * 2000,
4+
x: d.tier * TIER_WIDTH + (Math.random() - 0.5) * 50,
5+
y: (window.innerHeight / 2) + (Math.random() - 0.5) * 100,
66
vx: 0, vy: 0
77
}));
88

modules/visualization/lattice_simulation.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ function simulationStep() {
5656
});
5757

5858
nodes.forEach(n => {
59-
n.x += n.vx; n.y += n.vy; n.vx *= 0.7; n.vy *= 0.7;
59+
n.x += n.vx; n.y += n.vy;
60+
// Higher damping (0.5 instead of 0.7) for more stability
61+
n.vx *= 0.5; n.vy *= 0.5;
6062
n.el.setAttribute('transform', `translate(${n.x}, ${n.y})`);
6163
});
6264

simulation_report.md

Lines changed: 41 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Game Content Health Report
2-
Generated: 2026-04-29 09:00:02
2+
Generated: 2026-04-29 09:18:44
33

44
## 💀 Reachability Analysis
55
Total Quests Completed: 33
@@ -9,69 +9,69 @@ Routed Completion Time: 84.4m
99

1010
## ⚖️ Economic Sustainability
1111
### Sustainable Recurring Activities
12-
- Hunt Spiders
12+
- Refine Earth:Crystal Shards->Earth I
13+
- Hunt Bats
14+
- Refine Wood:Log->Herb
15+
- Refine Fire:Iron Ore->Fire I
1316
- Mine Iron Ore
14-
- Purify the Grove
15-
- Hunt Goblins
16-
- Study Ancient Texts
17-
- Refine Lightning:Ice Shard->Lightning I
18-
- Deep Sea Scavenge
17+
- Refine Fire:Basic Gem->Fire I
1918
- Scavenge Scrap
19+
- Hunt Goblins
2020
- Refine Lightning:Fire Shard->Lightning I
21-
- Refine Earth:Crystal Shards->Earth I
21+
- Refine Lightning:Ice Shard->Lightning I
2222
- Refine Life:Ancient Bark->Cure I
23-
- Harvest Sea-Life
24-
- Gather Moonberries
25-
- Hunt Bats
26-
- Refine Water:Blue Coral->Water I
27-
- Tutorial Section
28-
- Alchemy I:Basic Gem->Gold
29-
- Refine Mixology:Herb->Potion
30-
- Hunt Slimes
31-
- Hunt Sand-Sharks
32-
- Refine Wood:Log->Herb
33-
- Chop Wood
3423
- Shatter the Crystals
24+
- Power the Forge
25+
- Tutorial Section
26+
- Purify the Grove
27+
- Refine Ice:Moonberry->Ice I
28+
- Gather Moonberries
3529
- Archive Sifting
3630
- Refine Scrap:Web->Gold
31+
- Deep Sea Scavenge
3732
- Refine Ice:Mana Leaf->Ice I
33+
- Refine Water:Blue Coral->Water I
34+
- Alchemy I:Basic Gem->Gold
35+
- Harvest Sea-Life
36+
- Study Ancient Texts
37+
- Chop Wood
38+
- Hunt Slimes
3839
- High Altitude Survey
39-
- Refine Fire:Basic Gem->Fire I
40-
- Power the Forge
41-
- Refine Ice:Moonberry->Ice I
42-
- Refine Fire:Iron Ore->Fire I
40+
- Refine Mixology:Herb->Potion
41+
- Hunt Sand-Sharks
42+
- Hunt Spiders
4343

4444
### ⚠️ Unsustainable Activities (Reachable but starving)
45+
- Refine Haste:Lost Parchment->Haste I
4546
- Refine Life:Solar Essence->Cure I
46-
- Alchemy II:Sun-baked Scale->Gold
4747
- Refine Scrap:Slime->Gold
48-
- Refine Haste:Lost Parchment->Haste I
48+
- Alchemy II:Sun-baked Scale->Gold
4949
- Alchemy II:Potion->Gold
5050

5151
### Net Resource Rates (per second)
52-
- **Log**: 4.9887/s
53-
- **Mana Leaf**: 5.3223/s
54-
- **Lightning I**: 49.8873/s
55-
- **Water I**: 105.1123/s
5652
- **Ice Shard**: 34.2532/s
57-
- **Fire I**: 54.0445/s
5853
- **Leather**: 26.2781/s
59-
- **Fire Shard**: 31.2959/s
60-
- **Ancient Bark**: 6.2359/s
61-
- **Crystal Shards**: 7.4831/s
62-
- **Cure I**: 12.4718/s
63-
- **Iron Ore**: 16.5093/s
54+
- **Slime**: 4.1573/s
55+
- **Herb**: 19.9549/s
56+
- **Basic Gem**: 5.5225/s
57+
- **Mana Leaf**: 5.3223/s
58+
- **Fire I**: 54.0445/s
6459
- **Mythril Spark**: 0.4157/s
60+
- **Potion**: 4.9887/s
61+
- **Crystal Shards**: 7.4831/s
6562
- **Sun-baked Scale**: 0.8315/s
66-
- **Ice I**: 49.8873/s
6763
- **Moonberry**: 21.2893/s
68-
- **Herb**: 19.9549/s
69-
- **Basic Gem**: 5.5225/s
7064
- **Lost Parchment**: 2.0786/s
71-
- **Slime**: 4.1573/s
72-
- **Gold**: 7895.3115/s
73-
- **Potion**: 4.9887/s
7465
- **Earth I**: 24.9436/s
66+
- **Lightning I**: 49.8873/s
67+
- **Gold**: 7895.3115/s
68+
- **Ice I**: 49.8873/s
69+
- **Log**: 4.9887/s
70+
- **Ancient Bark**: 6.2359/s
71+
- **Water I**: 105.1123/s
72+
- **Fire Shard**: 31.2959/s
73+
- **Iron Ore**: 16.5093/s
74+
- **Cure I**: 12.4718/s
7575

7676
## 🔄 Feedback Loops
7777
✅ No unbounded growth loops detected (approximation).

0 commit comments

Comments
 (0)