thevoidforge 21.0.0 → 21.0.1

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.
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Prophecy Visualizer — interactive SVG dependency graph for campaign missions.
3
+ *
4
+ * Reads campaign data from /api/danger-room/campaign and renders a node/edge graph.
5
+ * Nodes = missions. Edges = dependency order. Color = status.
6
+ * Clickable nodes show mission details.
7
+ */
8
+ (function () {
9
+ 'use strict';
10
+
11
+ // ── Constants ─────────────────────────────────
12
+
13
+ var NODE_RADIUS = 24;
14
+ var NODE_SPACING_X = 120;
15
+ var NODE_SPACING_Y = 80;
16
+ var PADDING = 40;
17
+
18
+ var STATUS_COLORS = {
19
+ COMPLETE: '#34d399',
20
+ ACTIVE: '#fbbf24',
21
+ BLOCKED: '#ef4444',
22
+ PENDING: '#555',
23
+ STRUCTURAL: '#6366f1'
24
+ };
25
+
26
+ var STATUS_LABELS = {
27
+ COMPLETE: 'Complete',
28
+ ACTIVE: 'In Progress',
29
+ BLOCKED: 'Blocked',
30
+ PENDING: 'Pending',
31
+ STRUCTURAL: 'Structural'
32
+ };
33
+
34
+ // ── SVG helpers ───────────────────────────────
35
+
36
+ function svgEl(tag, attrs) {
37
+ var el = document.createElementNS('http://www.w3.org/2000/svg', tag);
38
+ if (attrs) {
39
+ for (var key in attrs) {
40
+ if (Object.prototype.hasOwnProperty.call(attrs, key)) {
41
+ el.setAttribute(key, attrs[key]);
42
+ }
43
+ }
44
+ }
45
+ return el;
46
+ }
47
+
48
+ function escapeText(str) {
49
+ var div = document.createElement('div');
50
+ div.appendChild(document.createTextNode(str));
51
+ return div.innerHTML;
52
+ }
53
+
54
+ // ── Layout ────────────────────────────────────
55
+
56
+ function layoutNodes(missions) {
57
+ // Simple left-to-right grid layout
58
+ var cols = Math.ceil(Math.sqrt(missions.length));
59
+ return missions.map(function (m, i) {
60
+ var col = i % cols;
61
+ var row = Math.floor(i / cols);
62
+ return {
63
+ mission: m,
64
+ x: PADDING + col * NODE_SPACING_X + NODE_RADIUS,
65
+ y: PADDING + row * NODE_SPACING_Y + NODE_RADIUS
66
+ };
67
+ });
68
+ }
69
+
70
+ // ── Rendering ─────────────────────────────────
71
+
72
+ function renderGraph(container, campaignData) {
73
+ container.innerHTML = '';
74
+
75
+ if (!campaignData || !campaignData.missions || campaignData.missions.length === 0) {
76
+ container.innerHTML = '<div style="padding:20px;color:var(--text-dim);font-size:13px;">No campaign data — run /campaign to see the prophecy graph.</div>';
77
+ return;
78
+ }
79
+
80
+ var nodes = layoutNodes(campaignData.missions);
81
+ var cols = Math.ceil(Math.sqrt(campaignData.missions.length));
82
+ var rows = Math.ceil(campaignData.missions.length / cols);
83
+ var svgWidth = PADDING * 2 + cols * NODE_SPACING_X;
84
+ var svgHeight = PADDING * 2 + rows * NODE_SPACING_Y;
85
+
86
+ var svg = svgEl('svg', {
87
+ viewBox: '0 0 ' + svgWidth + ' ' + svgHeight,
88
+ width: '100%',
89
+ height: Math.min(svgHeight, 400) + 'px',
90
+ role: 'group',
91
+ 'aria-label': 'Campaign mission dependency graph'
92
+ });
93
+
94
+ // Draw edges (sequential dependency: mission N → mission N+1)
95
+ for (var i = 0; i < nodes.length - 1; i++) {
96
+ var from = nodes[i];
97
+ var to = nodes[i + 1];
98
+ var line = svgEl('line', {
99
+ x1: from.x,
100
+ y1: from.y,
101
+ x2: to.x,
102
+ y2: to.y,
103
+ stroke: '#444',
104
+ 'stroke-width': '2',
105
+ 'stroke-dasharray': '4,4'
106
+ });
107
+ svg.appendChild(line);
108
+ }
109
+
110
+ // Draw nodes
111
+ nodes.forEach(function (node) {
112
+ var m = node.mission;
113
+ var color = STATUS_COLORS[m.status] || STATUS_COLORS.PENDING;
114
+ var label = STATUS_LABELS[m.status] || m.status;
115
+
116
+ // Node group
117
+ var g = svgEl('g', {
118
+ 'data-mission': m.number,
119
+ style: 'cursor:pointer',
120
+ role: 'button',
121
+ tabindex: '0',
122
+ 'aria-label': 'Mission ' + m.number + ': ' + escapeText(m.name) + ' — ' + label
123
+ });
124
+
125
+ // Circle
126
+ var circle = svgEl('circle', {
127
+ cx: node.x,
128
+ cy: node.y,
129
+ r: NODE_RADIUS,
130
+ fill: color,
131
+ opacity: '0.2',
132
+ stroke: color,
133
+ 'stroke-width': '2'
134
+ });
135
+ g.appendChild(circle);
136
+
137
+ // Mission number
138
+ var text = svgEl('text', {
139
+ x: node.x,
140
+ y: node.y + 1,
141
+ 'text-anchor': 'middle',
142
+ 'dominant-baseline': 'central',
143
+ fill: color,
144
+ 'font-size': '14',
145
+ 'font-weight': '700'
146
+ });
147
+ text.textContent = m.number;
148
+ g.appendChild(text);
149
+
150
+ // Mission name label (below node)
151
+ var nameText = svgEl('text', {
152
+ x: node.x,
153
+ y: node.y + NODE_RADIUS + 14,
154
+ 'text-anchor': 'middle',
155
+ fill: '#999',
156
+ 'font-size': '9'
157
+ });
158
+ // Truncate long names
159
+ var displayName = m.name.length > 18 ? m.name.substring(0, 16) + '…' : m.name;
160
+ nameText.textContent = displayName;
161
+ g.appendChild(nameText);
162
+
163
+ // Status dot
164
+ var statusDot = svgEl('circle', {
165
+ cx: node.x + NODE_RADIUS - 4,
166
+ cy: node.y - NODE_RADIUS + 4,
167
+ r: '4',
168
+ fill: color
169
+ });
170
+ g.appendChild(statusDot);
171
+
172
+ // Focus indicator — highlight circle on keyboard focus
173
+ g.addEventListener('focus', function () { circle.setAttribute('stroke-width', '4'); });
174
+ g.addEventListener('blur', function () { circle.setAttribute('stroke-width', '2'); });
175
+
176
+ // Click handler — show details in the detail panel
177
+ g.addEventListener('click', function () { showDetail(m); });
178
+ g.addEventListener('keydown', function (e) {
179
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); showDetail(m); }
180
+ });
181
+
182
+ svg.appendChild(g);
183
+ });
184
+
185
+ container.appendChild(svg);
186
+
187
+ // Legend
188
+ var legend = document.createElement('div');
189
+ legend.style.cssText = 'display:flex;gap:12px;margin-top:8px;font-size:10px;color:var(--text-dim);flex-wrap:wrap;';
190
+ Object.keys(STATUS_COLORS).forEach(function (status) {
191
+ var item = document.createElement('span');
192
+ item.innerHTML = '<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:' +
193
+ STATUS_COLORS[status] + ';margin-right:4px;vertical-align:middle;"></span>' +
194
+ (STATUS_LABELS[status] || status);
195
+ legend.appendChild(item);
196
+ });
197
+ container.appendChild(legend);
198
+ }
199
+
200
+ // ── Detail panel ──────────────────────────────
201
+
202
+ function showDetail(mission) {
203
+ var panel = document.getElementById('prophecy-detail');
204
+ if (!panel) return;
205
+ var color = STATUS_COLORS[mission.status] || STATUS_COLORS.PENDING;
206
+ var label = STATUS_LABELS[mission.status] || escapeText(mission.status);
207
+ panel.innerHTML =
208
+ '<div style="font-weight:700;color:' + color + ';">Mission ' + escapeText(String(mission.number)) + '</div>' +
209
+ '<div style="margin:4px 0;font-size:13px;">' + escapeText(mission.name) + '</div>' +
210
+ '<div style="font-size:11px;color:var(--text-dim);">Status: ' + label + '</div>';
211
+ }
212
+
213
+ // ── Init ──────────────────────────────────────
214
+
215
+ // Expose render function for the main danger-room.js to call
216
+ window.renderProphecyGraph = renderGraph;
217
+ })();