mudra-skills 1.0.0

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.
Files changed (149) hide show
  1. package/.claude-plugin/plugin.json +19 -0
  2. package/CLAUDE.md +35 -0
  3. package/bin/install.js +25 -0
  4. package/package.json +28 -0
  5. package/skills/mudra-master/SKILL.md +202 -0
  6. package/skills/mudra-master/mudra-preview/SKILL.md +151 -0
  7. package/skills/mudra-master/mudra-preview/assets/ar-menu.html +382 -0
  8. package/skills/mudra-master/mudra-preview/assets/document-scroller.html +468 -0
  9. package/skills/mudra-master/mudra-preview/assets/drum-machine.html +788 -0
  10. package/skills/mudra-master/mudra-preview/assets/emg-visualizer.html +390 -0
  11. package/skills/mudra-master/mudra-preview/assets/generative-art.html +408 -0
  12. package/skills/mudra-master/mudra-preview/assets/gesture-assistant.html +342 -0
  13. package/skills/mudra-master/mudra-preview/assets/gesture-speech.html +444 -0
  14. package/skills/mudra-master/mudra-preview/assets/hands-free-desktop.html +376 -0
  15. package/skills/mudra-master/mudra-preview/assets/model-rotator.html +365 -0
  16. package/skills/mudra-master/mudra-preview/assets/mudra-duel.html +3200 -0
  17. package/skills/mudra-master/mudra-preview/assets/mudra-monitor.html +1451 -0
  18. package/skills/mudra-master/mudra-preview/assets/mudra-ultimate-template.html +891 -0
  19. package/skills/mudra-master/mudra-preview/assets/music-sequencer.html +455 -0
  20. package/skills/mudra-master/mudra-preview/assets/neural-pong.html +380 -0
  21. package/skills/mudra-master/mudra-preview/assets/neural-snake.html +364 -0
  22. package/skills/mudra-master/mudra-preview/assets/presentation-controller.html +345 -0
  23. package/skills/mudra-master/mudra-preview/assets/pressure-painter.html +285 -0
  24. package/skills/mudra-master/mudra-preview/assets/runner.html +1812 -0
  25. package/skills/mudra-master/mudra-preview/assets/smart-home.html +461 -0
  26. package/skills/mudra-master/mudra-preview/assets/space-invaders.html +1154 -0
  27. package/skills/mudra-master/mudra-preview/assets/waterful-ring-toss.html +1624 -0
  28. package/skills/mudra-master/mudra-preview/references/agent_protocol.json +646 -0
  29. package/skills/mudra-master/mudra-preview/references/promt.md +16829 -0
  30. package/skills/mudra-master/mudra-xr/SKILL.md +245 -0
  31. package/skills/mudra-master/mudra-xr/assets/demos/3dgs-walkthrough.html +242 -0
  32. package/skills/mudra-master/mudra-xr/assets/demos/aisimulator.html +792 -0
  33. package/skills/mudra-master/mudra-xr/assets/demos/balloonpop.html +422 -0
  34. package/skills/mudra-master/mudra-xr/assets/demos/ballpit.html +284 -0
  35. package/skills/mudra-master/mudra-xr/assets/demos/drone.html +41 -0
  36. package/skills/mudra-master/mudra-xr/assets/demos/gemini-icebreakers.html +338 -0
  37. package/skills/mudra-master/mudra-xr/assets/demos/gemini-xrobject.html +302 -0
  38. package/skills/mudra-master/mudra-xr/assets/demos/math3d.html +188 -0
  39. package/skills/mudra-master/mudra-xr/assets/demos/measure.html +247 -0
  40. package/skills/mudra-master/mudra-xr/assets/demos/occlusion.html +251 -0
  41. package/skills/mudra-master/mudra-xr/assets/demos/rain.html +498 -0
  42. package/skills/mudra-master/mudra-xr/assets/demos/screenwiper.html +432 -0
  43. package/skills/mudra-master/mudra-xr/assets/demos/splash.html +519 -0
  44. package/skills/mudra-master/mudra-xr/assets/demos/webcam_gestures.html +253 -0
  45. package/skills/mudra-master/mudra-xr/assets/demos/xremoji.html +833 -0
  46. package/skills/mudra-master/mudra-xr/assets/demos/xrpoet.html +151 -0
  47. package/skills/mudra-master/mudra-xr/assets/samples/depthmap.html +266 -0
  48. package/skills/mudra-master/mudra-xr/assets/samples/depthmesh.html +100 -0
  49. package/skills/mudra-master/mudra-xr/assets/samples/game_rps.html +1099 -0
  50. package/skills/mudra-master/mudra-xr/assets/samples/gestures_custom.html +467 -0
  51. package/skills/mudra-master/mudra-xr/assets/samples/gestures_heuristic.html +256 -0
  52. package/skills/mudra-master/mudra-xr/assets/samples/lighting.html +163 -0
  53. package/skills/mudra-master/mudra-xr/assets/samples/mesh_detection.html +46 -0
  54. package/skills/mudra-master/mudra-xr/assets/samples/modelviewer.html +183 -0
  55. package/skills/mudra-master/mudra-xr/assets/samples/paint.html +133 -0
  56. package/skills/mudra-master/mudra-xr/assets/samples/planar-vst.html +45 -0
  57. package/skills/mudra-master/mudra-xr/assets/samples/reticle.html +95 -0
  58. package/skills/mudra-master/mudra-xr/assets/samples/skybox_agent.html +384 -0
  59. package/skills/mudra-master/mudra-xr/assets/samples/sound.html +390 -0
  60. package/skills/mudra-master/mudra-xr/assets/samples/ui.html +496 -0
  61. package/skills/mudra-master/mudra-xr/assets/samples/virtual-screens.html +808 -0
  62. package/skills/mudra-master/mudra-xr/assets/templates/0_basic.html +64 -0
  63. package/skills/mudra-master/mudra-xr/assets/templates/1_ui.html +107 -0
  64. package/skills/mudra-master/mudra-xr/assets/templates/2_hands.html +183 -0
  65. package/skills/mudra-master/mudra-xr/assets/templates/3_depth.html +89 -0
  66. package/skills/mudra-master/mudra-xr/assets/templates/4_stereo.html +84 -0
  67. package/skills/mudra-master/mudra-xr/assets/templates/5_camera.html +117 -0
  68. package/skills/mudra-master/mudra-xr/assets/templates/6_ai.html +154 -0
  69. package/skills/mudra-master/mudra-xr/assets/templates/7_ai_live.html +253 -0
  70. package/skills/mudra-master/mudra-xr/assets/templates/8_objects.html +95 -0
  71. package/skills/mudra-master/mudra-xr/assets/templates/9_xr-toggle.html +90 -0
  72. package/skills/mudra-master/mudra-xr/assets/templates/heuristic_hand_gestures.html +102 -0
  73. package/skills/mudra-master/mudra-xr/assets/templates/meshes.html +47 -0
  74. package/skills/mudra-master/mudra-xr/assets/templates/planes.html +47 -0
  75. package/skills/mudra-master/mudra-xr/assets/templates/uikit.html +187 -0
  76. package/skills/mudra-master/mudra-xr/references/agent_protocol.json +878 -0
  77. package/skills/mudra-master/mudra-xr/references/promt.md +1516 -0
  78. package/skills/mudra-preview/SKILL.md +151 -0
  79. package/skills/mudra-preview/assets/ar-menu.html +382 -0
  80. package/skills/mudra-preview/assets/document-scroller.html +468 -0
  81. package/skills/mudra-preview/assets/drum-machine.html +788 -0
  82. package/skills/mudra-preview/assets/emg-visualizer.html +390 -0
  83. package/skills/mudra-preview/assets/generative-art.html +408 -0
  84. package/skills/mudra-preview/assets/gesture-assistant.html +342 -0
  85. package/skills/mudra-preview/assets/gesture-speech.html +444 -0
  86. package/skills/mudra-preview/assets/hands-free-desktop.html +376 -0
  87. package/skills/mudra-preview/assets/model-rotator.html +365 -0
  88. package/skills/mudra-preview/assets/mudra-duel.html +3200 -0
  89. package/skills/mudra-preview/assets/mudra-monitor.html +1451 -0
  90. package/skills/mudra-preview/assets/mudra-ultimate-template.html +891 -0
  91. package/skills/mudra-preview/assets/music-sequencer.html +455 -0
  92. package/skills/mudra-preview/assets/neural-pong.html +380 -0
  93. package/skills/mudra-preview/assets/neural-snake.html +364 -0
  94. package/skills/mudra-preview/assets/presentation-controller.html +345 -0
  95. package/skills/mudra-preview/assets/pressure-painter.html +285 -0
  96. package/skills/mudra-preview/assets/runner.html +1812 -0
  97. package/skills/mudra-preview/assets/smart-home.html +461 -0
  98. package/skills/mudra-preview/assets/space-invaders.html +1154 -0
  99. package/skills/mudra-preview/assets/waterful-ring-toss.html +1624 -0
  100. package/skills/mudra-preview/references/agent_protocol.json +646 -0
  101. package/skills/mudra-preview/references/promt.md +16829 -0
  102. package/skills/mudra-xr/SKILL.md +245 -0
  103. package/skills/mudra-xr/assets/demos/3dgs-walkthrough.html +242 -0
  104. package/skills/mudra-xr/assets/demos/aisimulator.html +792 -0
  105. package/skills/mudra-xr/assets/demos/balloonpop.html +422 -0
  106. package/skills/mudra-xr/assets/demos/ballpit.html +284 -0
  107. package/skills/mudra-xr/assets/demos/drone.html +41 -0
  108. package/skills/mudra-xr/assets/demos/gemini-icebreakers.html +338 -0
  109. package/skills/mudra-xr/assets/demos/gemini-xrobject.html +302 -0
  110. package/skills/mudra-xr/assets/demos/math3d.html +188 -0
  111. package/skills/mudra-xr/assets/demos/measure.html +247 -0
  112. package/skills/mudra-xr/assets/demos/occlusion.html +251 -0
  113. package/skills/mudra-xr/assets/demos/rain.html +498 -0
  114. package/skills/mudra-xr/assets/demos/screenwiper.html +432 -0
  115. package/skills/mudra-xr/assets/demos/splash.html +519 -0
  116. package/skills/mudra-xr/assets/demos/webcam_gestures.html +253 -0
  117. package/skills/mudra-xr/assets/demos/xremoji.html +833 -0
  118. package/skills/mudra-xr/assets/demos/xrpoet.html +151 -0
  119. package/skills/mudra-xr/assets/samples/depthmap.html +266 -0
  120. package/skills/mudra-xr/assets/samples/depthmesh.html +100 -0
  121. package/skills/mudra-xr/assets/samples/game_rps.html +1099 -0
  122. package/skills/mudra-xr/assets/samples/gestures_custom.html +467 -0
  123. package/skills/mudra-xr/assets/samples/gestures_heuristic.html +256 -0
  124. package/skills/mudra-xr/assets/samples/lighting.html +163 -0
  125. package/skills/mudra-xr/assets/samples/mesh_detection.html +46 -0
  126. package/skills/mudra-xr/assets/samples/modelviewer.html +183 -0
  127. package/skills/mudra-xr/assets/samples/paint.html +133 -0
  128. package/skills/mudra-xr/assets/samples/planar-vst.html +45 -0
  129. package/skills/mudra-xr/assets/samples/reticle.html +95 -0
  130. package/skills/mudra-xr/assets/samples/skybox_agent.html +384 -0
  131. package/skills/mudra-xr/assets/samples/sound.html +390 -0
  132. package/skills/mudra-xr/assets/samples/ui.html +496 -0
  133. package/skills/mudra-xr/assets/samples/virtual-screens.html +808 -0
  134. package/skills/mudra-xr/assets/templates/0_basic.html +64 -0
  135. package/skills/mudra-xr/assets/templates/1_ui.html +107 -0
  136. package/skills/mudra-xr/assets/templates/2_hands.html +183 -0
  137. package/skills/mudra-xr/assets/templates/3_depth.html +89 -0
  138. package/skills/mudra-xr/assets/templates/4_stereo.html +84 -0
  139. package/skills/mudra-xr/assets/templates/5_camera.html +117 -0
  140. package/skills/mudra-xr/assets/templates/6_ai.html +154 -0
  141. package/skills/mudra-xr/assets/templates/7_ai_live.html +253 -0
  142. package/skills/mudra-xr/assets/templates/8_objects.html +95 -0
  143. package/skills/mudra-xr/assets/templates/9_xr-toggle.html +90 -0
  144. package/skills/mudra-xr/assets/templates/heuristic_hand_gestures.html +102 -0
  145. package/skills/mudra-xr/assets/templates/meshes.html +47 -0
  146. package/skills/mudra-xr/assets/templates/planes.html +47 -0
  147. package/skills/mudra-xr/assets/templates/uikit.html +187 -0
  148. package/skills/mudra-xr/references/agent_protocol.json +878 -0
  149. package/skills/mudra-xr/references/promt.md +1516 -0
@@ -0,0 +1,1812 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Intent Runner: The Ghost Trail</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ background: #0a0a0f;
16
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
17
+ overflow: hidden;
18
+ color: #fff;
19
+ }
20
+
21
+ #game-container {
22
+ position: relative;
23
+ width: 100vw;
24
+ height: 100vh;
25
+ overflow: hidden;
26
+ }
27
+
28
+ #game-canvas {
29
+ position: absolute;
30
+ top: 0;
31
+ left: 0;
32
+ width: 100%;
33
+ height: 100%;
34
+ }
35
+
36
+ /* HUD Styles */
37
+ #hud {
38
+ position: absolute;
39
+ top: 0;
40
+ left: 0;
41
+ right: 0;
42
+ padding: 15px 30px;
43
+ display: flex;
44
+ justify-content: space-between;
45
+ align-items: flex-start;
46
+ pointer-events: none;
47
+ z-index: 10;
48
+ }
49
+
50
+ .hud-left, .hud-right {
51
+ display: flex;
52
+ flex-direction: column;
53
+ gap: 8px;
54
+ }
55
+
56
+ .hud-center {
57
+ display: flex;
58
+ flex-direction: column;
59
+ align-items: center;
60
+ }
61
+
62
+ .hud-item {
63
+ display: flex;
64
+ flex-direction: column;
65
+ background: rgba(0, 0, 0, 0.5);
66
+ padding: 8px 12px;
67
+ border-radius: 8px;
68
+ border: 1px solid rgba(255, 255, 255, 0.1);
69
+ }
70
+
71
+ .hud-label {
72
+ font-size: 10px;
73
+ text-transform: uppercase;
74
+ letter-spacing: 1px;
75
+ color: rgba(255, 255, 255, 0.6);
76
+ }
77
+
78
+ .hud-value {
79
+ font-size: 18px;
80
+ font-weight: bold;
81
+ font-family: 'Courier New', monospace;
82
+ }
83
+
84
+ .hud-value.glow {
85
+ color: #00ffff;
86
+ text-shadow: 0 0 10px #00ffff, 0 0 20px #00ffff;
87
+ }
88
+
89
+ .intent-lead {
90
+ border: 1px solid rgba(0, 255, 255, 0.3);
91
+ background: rgba(0, 255, 255, 0.1);
92
+ }
93
+
94
+ #lives-container {
95
+ display: flex;
96
+ gap: 10px;
97
+ font-size: 28px;
98
+ }
99
+
100
+ .heart {
101
+ color: #ff4757;
102
+ text-shadow: 0 0 10px #ff4757;
103
+ transition: all 0.3s ease;
104
+ }
105
+
106
+ .heart.lost {
107
+ color: #333;
108
+ text-shadow: none;
109
+ transform: scale(0.8);
110
+ }
111
+
112
+ /* Connection Status */
113
+ #connection-status {
114
+ position: absolute;
115
+ bottom: 20px;
116
+ left: 20px;
117
+ display: flex;
118
+ align-items: center;
119
+ gap: 8px;
120
+ font-size: 12px;
121
+ color: rgba(255, 255, 255, 0.6);
122
+ z-index: 10;
123
+ }
124
+
125
+ .status-dot {
126
+ width: 10px;
127
+ height: 10px;
128
+ border-radius: 50%;
129
+ transition: all 0.3s ease;
130
+ }
131
+
132
+ .status-dot.connected {
133
+ background: #2ed573;
134
+ box-shadow: 0 0 10px #2ed573;
135
+ }
136
+
137
+ .status-dot.disconnected {
138
+ background: #ff4757;
139
+ box-shadow: 0 0 10px #ff4757;
140
+ }
141
+
142
+ /* Delta Flash */
143
+ #delta-flash {
144
+ position: absolute;
145
+ top: 50%;
146
+ left: 50%;
147
+ transform: translate(-50%, -50%);
148
+ font-size: 48px;
149
+ font-weight: bold;
150
+ font-family: 'Courier New', monospace;
151
+ color: #00ffff;
152
+ text-shadow: 0 0 20px #00ffff, 0 0 40px #00ffff;
153
+ opacity: 0;
154
+ pointer-events: none;
155
+ z-index: 20;
156
+ transition: opacity 0.1s ease;
157
+ }
158
+
159
+ #delta-flash.show {
160
+ opacity: 1;
161
+ animation: flashPulse 0.8s ease-out forwards;
162
+ }
163
+
164
+ @keyframes flashPulse {
165
+ 0% {
166
+ opacity: 1;
167
+ transform: translate(-50%, -50%) scale(1);
168
+ }
169
+ 50% {
170
+ opacity: 0.8;
171
+ transform: translate(-50%, -50%) scale(1.1);
172
+ }
173
+ 100% {
174
+ opacity: 0;
175
+ transform: translate(-50%, -50%) scale(0.9);
176
+ }
177
+ }
178
+
179
+ /* Screens */
180
+ .screen {
181
+ position: absolute;
182
+ top: 0;
183
+ left: 0;
184
+ width: 100%;
185
+ height: 100%;
186
+ display: flex;
187
+ flex-direction: column;
188
+ justify-content: center;
189
+ align-items: center;
190
+ background: rgba(10, 10, 15, 0.95);
191
+ z-index: 100;
192
+ transition: opacity 0.5s ease;
193
+ }
194
+
195
+ .screen.hidden {
196
+ opacity: 0;
197
+ pointer-events: none;
198
+ }
199
+
200
+ /* Title Screen */
201
+ #title-screen h1 {
202
+ font-size: 72px;
203
+ font-weight: 300;
204
+ letter-spacing: 8px;
205
+ text-transform: uppercase;
206
+ background: linear-gradient(135deg, #00ffff, #a855f7);
207
+ -webkit-background-clip: text;
208
+ -webkit-text-fill-color: transparent;
209
+ background-clip: text;
210
+ margin-bottom: 10px;
211
+ }
212
+
213
+ #title-screen h2 {
214
+ font-size: 24px;
215
+ font-weight: 300;
216
+ color: rgba(255, 255, 255, 0.6);
217
+ letter-spacing: 4px;
218
+ margin-bottom: 30px;
219
+ }
220
+
221
+ .tagline {
222
+ font-size: 16px;
223
+ color: rgba(255, 255, 255, 0.4);
224
+ margin-bottom: 40px;
225
+ }
226
+
227
+ .controls-info {
228
+ margin-bottom: 30px;
229
+ }
230
+
231
+ /* Threshold Setting */
232
+ .threshold-setting {
233
+ margin-bottom: 30px;
234
+ padding: 20px 30px;
235
+ background: rgba(0, 255, 255, 0.05);
236
+ border: 1px solid rgba(0, 255, 255, 0.2);
237
+ border-radius: 12px;
238
+ min-width: 300px;
239
+ }
240
+
241
+ .threshold-setting label {
242
+ display: block;
243
+ font-size: 14px;
244
+ color: rgba(255, 255, 255, 0.7);
245
+ margin-bottom: 12px;
246
+ text-transform: uppercase;
247
+ letter-spacing: 1px;
248
+ }
249
+
250
+ .slider-container {
251
+ display: flex;
252
+ align-items: center;
253
+ gap: 15px;
254
+ margin-bottom: 15px;
255
+ }
256
+
257
+ #threshold-slider {
258
+ flex: 1;
259
+ -webkit-appearance: none;
260
+ appearance: none;
261
+ height: 8px;
262
+ background: rgba(255, 255, 255, 0.1);
263
+ border-radius: 4px;
264
+ outline: none;
265
+ cursor: pointer;
266
+ }
267
+
268
+ #threshold-slider::-webkit-slider-thumb {
269
+ -webkit-appearance: none;
270
+ appearance: none;
271
+ width: 20px;
272
+ height: 20px;
273
+ background: #00ffff;
274
+ border-radius: 50%;
275
+ cursor: pointer;
276
+ box-shadow: 0 0 10px rgba(0, 255, 255, 0.5);
277
+ transition: transform 0.2s ease;
278
+ }
279
+
280
+ #threshold-slider::-webkit-slider-thumb:hover {
281
+ transform: scale(1.2);
282
+ }
283
+
284
+ #threshold-slider::-moz-range-thumb {
285
+ width: 20px;
286
+ height: 20px;
287
+ background: #00ffff;
288
+ border-radius: 50%;
289
+ cursor: pointer;
290
+ border: none;
291
+ box-shadow: 0 0 10px rgba(0, 255, 255, 0.5);
292
+ }
293
+
294
+ #threshold-value {
295
+ font-family: 'Courier New', monospace;
296
+ font-size: 18px;
297
+ font-weight: bold;
298
+ color: #00ffff;
299
+ min-width: 45px;
300
+ text-align: right;
301
+ }
302
+
303
+ #pressure-indicator {
304
+ position: relative;
305
+ height: 24px;
306
+ background: rgba(255, 255, 255, 0.1);
307
+ border-radius: 12px;
308
+ overflow: hidden;
309
+ margin-bottom: 10px;
310
+ }
311
+
312
+ #pressure-bar {
313
+ position: absolute;
314
+ left: 0;
315
+ top: 0;
316
+ height: 100%;
317
+ width: 0%;
318
+ background: linear-gradient(90deg, #00ffff, #a855f7);
319
+ border-radius: 12px;
320
+ transition: width 0.05s ease;
321
+ }
322
+
323
+ #pressure-bar.triggered {
324
+ background: linear-gradient(90deg, #2ed573, #00ffff);
325
+ }
326
+
327
+ #threshold-marker {
328
+ position: absolute;
329
+ top: 0;
330
+ height: 100%;
331
+ width: 3px;
332
+ background: #fff;
333
+ box-shadow: 0 0 8px rgba(255, 255, 255, 0.8);
334
+ left: 30%;
335
+ transition: left 0.1s ease;
336
+ }
337
+
338
+ .threshold-hint {
339
+ font-size: 11px;
340
+ color: rgba(255, 255, 255, 0.4);
341
+ font-style: italic;
342
+ }
343
+
344
+ .controls-info p {
345
+ margin: 10px 0;
346
+ font-size: 14px;
347
+ color: rgba(255, 255, 255, 0.7);
348
+ }
349
+
350
+ .key {
351
+ display: inline-block;
352
+ padding: 6px 12px;
353
+ border-radius: 6px;
354
+ font-family: 'Courier New', monospace;
355
+ font-weight: bold;
356
+ margin-right: 10px;
357
+ }
358
+
359
+ .ghost-key {
360
+ background: rgba(0, 255, 255, 0.2);
361
+ border: 1px solid #00ffff;
362
+ color: #00ffff;
363
+ }
364
+
365
+ .physical-key {
366
+ background: rgba(255, 255, 255, 0.1);
367
+ border: 1px solid rgba(255, 255, 255, 0.5);
368
+ color: #fff;
369
+ }
370
+
371
+ .game-btn {
372
+ padding: 15px 50px;
373
+ font-size: 18px;
374
+ font-weight: bold;
375
+ text-transform: uppercase;
376
+ letter-spacing: 2px;
377
+ border: 2px solid #00ffff;
378
+ background: transparent;
379
+ color: #00ffff;
380
+ cursor: pointer;
381
+ transition: all 0.3s ease;
382
+ margin-top: 20px;
383
+ }
384
+
385
+ .game-btn:hover {
386
+ background: #00ffff;
387
+ color: #0a0a0f;
388
+ box-shadow: 0 0 30px rgba(0, 255, 255, 0.5);
389
+ }
390
+
391
+ #title-connection-status {
392
+ margin-top: 30px;
393
+ display: flex;
394
+ align-items: center;
395
+ gap: 8px;
396
+ font-size: 14px;
397
+ color: rgba(255, 255, 255, 0.5);
398
+ }
399
+
400
+ /* Countdown */
401
+ #countdown {
402
+ background: rgba(10, 10, 15, 0.8);
403
+ }
404
+
405
+ #countdown-number {
406
+ font-size: 200px;
407
+ font-weight: bold;
408
+ color: #00ffff;
409
+ text-shadow: 0 0 50px #00ffff;
410
+ animation: countdownPulse 1s ease-in-out infinite;
411
+ }
412
+
413
+ @keyframes countdownPulse {
414
+ 0%, 100% { transform: scale(1); opacity: 1; }
415
+ 50% { transform: scale(1.1); opacity: 0.8; }
416
+ }
417
+
418
+ /* End Screen */
419
+ #end-screen h1 {
420
+ font-size: 48px;
421
+ margin-bottom: 30px;
422
+ color: #fff;
423
+ }
424
+
425
+ .stats-container {
426
+ display: flex;
427
+ gap: 60px;
428
+ margin-bottom: 30px;
429
+ }
430
+
431
+ .stats-column {
432
+ min-width: 250px;
433
+ }
434
+
435
+ .stats-column h3 {
436
+ font-size: 18px;
437
+ margin-bottom: 15px;
438
+ padding-bottom: 10px;
439
+ border-bottom: 2px solid;
440
+ }
441
+
442
+ .ghost-title {
443
+ color: #00ffff;
444
+ border-color: #00ffff !important;
445
+ }
446
+
447
+ .physical-title {
448
+ color: #fff;
449
+ border-color: rgba(255, 255, 255, 0.5) !important;
450
+ }
451
+
452
+ .stat-item {
453
+ display: flex;
454
+ justify-content: space-between;
455
+ padding: 8px 0;
456
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
457
+ }
458
+
459
+ .stat-label {
460
+ color: rgba(255, 255, 255, 0.6);
461
+ font-size: 14px;
462
+ }
463
+
464
+ .stat-value {
465
+ font-family: 'Courier New', monospace;
466
+ font-weight: bold;
467
+ }
468
+
469
+ .advantage-summary {
470
+ background: rgba(0, 255, 255, 0.1);
471
+ border: 1px solid rgba(0, 255, 255, 0.3);
472
+ border-radius: 12px;
473
+ padding: 25px 40px;
474
+ margin-bottom: 25px;
475
+ }
476
+
477
+ .advantage-summary h3 {
478
+ color: #00ffff;
479
+ margin-bottom: 20px;
480
+ text-align: center;
481
+ font-size: 20px;
482
+ }
483
+
484
+ .advantage-stats {
485
+ display: flex;
486
+ gap: 40px;
487
+ }
488
+
489
+ .advantage-item {
490
+ display: flex;
491
+ flex-direction: column;
492
+ align-items: center;
493
+ min-width: 120px;
494
+ }
495
+
496
+ .advantage-value {
497
+ font-size: 32px;
498
+ font-weight: bold;
499
+ font-family: 'Courier New', monospace;
500
+ color: #00ffff;
501
+ text-shadow: 0 0 10px rgba(0, 255, 255, 0.5);
502
+ }
503
+
504
+ .advantage-label {
505
+ font-size: 11px;
506
+ text-transform: uppercase;
507
+ letter-spacing: 1px;
508
+ color: rgba(255, 255, 255, 0.5);
509
+ margin-top: 5px;
510
+ }
511
+
512
+ .summary-message {
513
+ font-size: 16px;
514
+ color: rgba(255, 255, 255, 0.8);
515
+ max-width: 500px;
516
+ text-align: center;
517
+ line-height: 1.6;
518
+ margin-bottom: 20px;
519
+ }
520
+
521
+ /* Message Flash */
522
+ #message-flash {
523
+ position: absolute;
524
+ bottom: 30%;
525
+ left: 50%;
526
+ transform: translateX(-50%);
527
+ font-size: 24px;
528
+ font-weight: bold;
529
+ color: #ff4757;
530
+ text-shadow: 0 0 20px rgba(255, 71, 87, 0.8);
531
+ opacity: 0;
532
+ pointer-events: none;
533
+ z-index: 15;
534
+ text-align: center;
535
+ white-space: nowrap;
536
+ }
537
+
538
+ #message-flash.show {
539
+ animation: messageFlash 2s ease-out forwards;
540
+ }
541
+
542
+ @keyframes messageFlash {
543
+ 0% { opacity: 0; transform: translateX(-50%) translateY(20px); }
544
+ 20% { opacity: 1; transform: translateX(-50%) translateY(0); }
545
+ 80% { opacity: 1; }
546
+ 100% { opacity: 0; }
547
+ }
548
+
549
+ /* Screen shake */
550
+ .shake {
551
+ animation: shake 0.3s ease-in-out;
552
+ }
553
+
554
+ @keyframes shake {
555
+ 0%, 100% { transform: translateX(0); }
556
+ 20% { transform: translateX(-10px); }
557
+ 40% { transform: translateX(10px); }
558
+ 60% { transform: translateX(-5px); }
559
+ 80% { transform: translateX(5px); }
560
+ }
561
+ </style>
562
+ </head>
563
+ <body>
564
+ <a href="/gallery" style="position:fixed;top:12px;left:12px;z-index:9999;padding:6px 14px;background:rgba(0,0,0,0.6);color:#77eae9;text-decoration:none;font-size:13px;font-family:system-ui,sans-serif;border-radius:8px;backdrop-filter:blur(8px);border:1px solid rgba(119,234,233,0.2);transition:background 0.2s;"onmouseenter="this.style.background='rgba(0,0,0,0.8)'"onmouseleave="this.style.background='rgba(0,0,0,0.6)'">&larr; Gallery</a>
565
+ <div id="game-container">
566
+ <canvas id="game-canvas"></canvas>
567
+
568
+ <!-- HUD -->
569
+ <div id="hud">
570
+ <div class="hud-left">
571
+ <div class="hud-item">
572
+ <span class="hud-label">Distance</span>
573
+ <span id="distance-value" class="hud-value">0m</span>
574
+ </div>
575
+ <div class="hud-item">
576
+ <span class="hud-label">Speed</span>
577
+ <span id="speed-value" class="hud-value">8.0 m/s</span>
578
+ </div>
579
+ <div class="hud-item intent-lead">
580
+ <span class="hud-label">Intent Lead</span>
581
+ <span id="lead-value" class="hud-value glow">+0.0m</span>
582
+ </div>
583
+ </div>
584
+ <div class="hud-center">
585
+ <div id="lives-container">
586
+ <span class="heart">♥</span>
587
+ <span class="heart">♥</span>
588
+ <span class="heart">♥</span>
589
+ </div>
590
+ </div>
591
+ <div class="hud-right">
592
+ <div class="hud-item">
593
+ <span class="hud-label">Mudra Avg</span>
594
+ <span id="mudra-avg" class="hud-value">--ms</span>
595
+ </div>
596
+ <div class="hud-item">
597
+ <span class="hud-label">Mouse Avg</span>
598
+ <span id="mouse-avg" class="hud-value">--ms</span>
599
+ </div>
600
+ <div class="hud-item">
601
+ <span class="hud-label">Obstacles</span>
602
+ <span id="obstacles-value" class="hud-value">0</span>
603
+ </div>
604
+ </div>
605
+ </div>
606
+
607
+ <!-- Connection Status -->
608
+ <div id="connection-status">
609
+ <span id="mudra-status" class="status-dot disconnected"></span>
610
+ <span id="mudra-status-text">Mudra: Disconnected</span>
611
+ </div>
612
+
613
+ <!-- Delta Flash -->
614
+ <div id="delta-flash"></div>
615
+
616
+ <!-- Title Screen -->
617
+ <div id="title-screen" class="screen">
618
+ <h1>Intent Runner</h1>
619
+ <h2>The Ghost Trail</h2>
620
+ <p class="tagline">See the gap between thought and action</p>
621
+ <div class="controls-info">
622
+ <p><span class="key ghost-key">Mudra SNC</span> = Ghost (Intent)</p>
623
+ <p><span class="key physical-key">Mouse Click</span> = Runner (Action)</p>
624
+ </div>
625
+ <div class="threshold-setting">
626
+ <label for="threshold-slider">SNC Threshold</label>
627
+ <div class="slider-container">
628
+ <input type="range" id="threshold-slider" min="0.1" max="0.9" step="0.05" value="0.3">
629
+ <span id="threshold-value">0.30</span>
630
+ </div>
631
+ <div id="pressure-indicator">
632
+ <div id="pressure-bar"></div>
633
+ <div id="threshold-marker"></div>
634
+ </div>
635
+ <span class="threshold-hint">Flex to test - bar shows live SNC magnitude</span>
636
+ </div>
637
+ <button id="start-btn" class="game-btn">Start Run</button>
638
+ <div id="title-connection-status">
639
+ <span id="title-mudra-dot" class="status-dot disconnected"></span>
640
+ <span id="title-mudra-text">Mudra: Connecting...</span>
641
+ </div>
642
+ </div>
643
+
644
+ <!-- Countdown -->
645
+ <div id="countdown" class="screen hidden">
646
+ <span id="countdown-number">3</span>
647
+ </div>
648
+
649
+ <!-- End Screen -->
650
+ <div id="end-screen" class="screen hidden">
651
+ <h1 id="end-title">Run Complete</h1>
652
+ <div class="stats-container">
653
+ <div class="stats-column">
654
+ <h3 class="ghost-title">Intent (Mudra)</h3>
655
+ <div class="stat-item">
656
+ <span class="stat-label">Average Reaction</span>
657
+ <span id="end-mudra-avg" class="stat-value">--ms</span>
658
+ </div>
659
+ <div class="stat-item">
660
+ <span class="stat-label">Fastest Reaction</span>
661
+ <span id="end-mudra-fastest" class="stat-value">--ms</span>
662
+ </div>
663
+ <div class="stat-item">
664
+ <span class="stat-label">Obstacles Cleared</span>
665
+ <span id="end-mudra-cleared" class="stat-value">0</span>
666
+ </div>
667
+ </div>
668
+ <div class="stats-column">
669
+ <h3 class="physical-title">Action (Mouse)</h3>
670
+ <div class="stat-item">
671
+ <span class="stat-label">Average Reaction</span>
672
+ <span id="end-mouse-avg" class="stat-value">--ms</span>
673
+ </div>
674
+ <div class="stat-item">
675
+ <span class="stat-label">Fastest Reaction</span>
676
+ <span id="end-mouse-fastest" class="stat-value">--ms</span>
677
+ </div>
678
+ <div class="stat-item">
679
+ <span class="stat-label">Obstacles Cleared</span>
680
+ <span id="end-mouse-cleared" class="stat-value">0</span>
681
+ </div>
682
+ </div>
683
+ </div>
684
+ <div class="advantage-summary">
685
+ <h3>Intent Advantage</h3>
686
+ <div class="advantage-stats">
687
+ <div class="advantage-item">
688
+ <span class="advantage-value" id="total-time-saved">0ms</span>
689
+ <span class="advantage-label">Total Time Saved</span>
690
+ </div>
691
+ <div class="advantage-item">
692
+ <span class="advantage-value" id="distance-equivalent">0m</span>
693
+ <span class="advantage-label">Distance Equivalent</span>
694
+ </div>
695
+ <div class="advantage-item">
696
+ <span class="advantage-value" id="ghost-saves">0</span>
697
+ <span class="advantage-label">Ghost Saves</span>
698
+ </div>
699
+ <div class="advantage-item">
700
+ <span class="advantage-value" id="avg-delta">0ms</span>
701
+ <span class="advantage-label">Avg Delta/Obstacle</span>
702
+ </div>
703
+ </div>
704
+ </div>
705
+ <p id="summary-message" class="summary-message"></p>
706
+ <button id="restart-btn" class="game-btn">Run Again</button>
707
+ </div>
708
+ </div>
709
+
710
+ <script>
711
+ // Intent Runner: The Ghost Trail
712
+ // A visualization of the gap between neural intent and physical action
713
+
714
+ class IntentRunner {
715
+ constructor() {
716
+ this.canvas = document.getElementById('game-canvas');
717
+ this.ctx = this.canvas.getContext('2d');
718
+
719
+ // Game state
720
+ this.gameStatus = 'idle'; // 'idle', 'countdown', 'running', 'ended'
721
+ this.lives = 3;
722
+ this.distance = 0;
723
+ this.speed = 8; // m/s
724
+ this.baseSpeed = 8;
725
+ this.maxSpeed = 16;
726
+ this.speedIncrement = 0.5;
727
+ this.speedIncrementInterval = 10; // seconds
728
+ this.lastSpeedIncrement = 0;
729
+
730
+ // Timing
731
+ this.totalAdvantage = 0; // cumulative ms saved
732
+ this.ghostLead = 0; // visual distance in pixels
733
+ this.reactions = [];
734
+ this.ghostSaves = 0;
735
+
736
+ // Obstacles
737
+ this.obstacles = [];
738
+ this.obstacleInterval = 2500; // ms
739
+ this.minObstacleInterval = 800;
740
+ this.lastObstacleSpawn = 0;
741
+ this.obstaclesPassed = 0;
742
+
743
+ // Finish line
744
+ this.finishDistance = 500; // meters
745
+ this.finishLineX = null; // Will be set when close to finish
746
+ this.victory = false;
747
+
748
+ // Runners
749
+ this.runnerY = 0;
750
+ this.groundY = 0;
751
+ this.runnerX = 200;
752
+ this.ghostOffset = 0;
753
+
754
+ // Jump state
755
+ this.physicalJumping = false;
756
+ this.physicalJumpVelocity = 0;
757
+ this.physicalJumpHeight = 0;
758
+ this.ghostJumping = false;
759
+ this.ghostJumpVelocity = 0;
760
+ this.ghostJumpHeight = 0;
761
+ this.jumpForce = 18;
762
+ this.gravity = 0.8;
763
+
764
+ // Animation
765
+ this.runFrame = 0;
766
+ this.frameCount = 0;
767
+ this.physicalStumble = 0; // Stumble animation timer
768
+ this.ghostStumble = 0;
769
+
770
+ // Input tracking
771
+ this.currentObstacle = null;
772
+ this.mudraTime = null;
773
+ this.mouseTime = null;
774
+ this.reactionWindowOpen = false;
775
+
776
+ // Mudra connection
777
+ this.mudraConnected = false;
778
+ this.ws = null;
779
+
780
+ // Pressure detection
781
+ this.pressureThreshold = 0.3; // Trigger jump when pressure exceeds this
782
+ this.pressureActive = false; // Prevents repeated triggers while held
783
+ this.lastPressure = 0; // For spike detection
784
+ this.lastPressureTime = 0; // Timestamp of last pressure reading
785
+
786
+ // Stats
787
+ this.mudraReactions = [];
788
+ this.mouseReactions = [];
789
+ this.ghostCleared = 0;
790
+ this.physicalCleared = 0;
791
+
792
+ // Visual effects
793
+ this.particles = [];
794
+ this.speedLines = [];
795
+ this.screenShake = 0;
796
+ this.deltaFlashTimeout = null;
797
+
798
+ // Grid for background
799
+ this.gridOffset = 0;
800
+
801
+ this.init();
802
+ }
803
+
804
+ init() {
805
+ this.resize();
806
+ window.addEventListener('resize', () => this.resize());
807
+
808
+ // Input handlers
809
+ document.addEventListener('click', (e) => this.handleMouseClick(e));
810
+ document.addEventListener('keydown', (e) => {
811
+ if (e.code === 'Space') {
812
+ e.preventDefault();
813
+ this.handleMouseClick(e);
814
+ }
815
+ });
816
+
817
+ // UI handlers
818
+ document.getElementById('start-btn').addEventListener('click', () => this.startGame());
819
+ document.getElementById('restart-btn').addEventListener('click', () => this.restartGame());
820
+
821
+ // Threshold slider
822
+ const thresholdSlider = document.getElementById('threshold-slider');
823
+ const thresholdValue = document.getElementById('threshold-value');
824
+ const thresholdMarker = document.getElementById('threshold-marker');
825
+
826
+ thresholdSlider.addEventListener('input', (e) => {
827
+ this.pressureThreshold = parseFloat(e.target.value);
828
+ thresholdValue.textContent = this.pressureThreshold.toFixed(2);
829
+ thresholdMarker.style.left = `${this.pressureThreshold * 100}%`;
830
+ });
831
+
832
+ // Initialize marker position
833
+ thresholdMarker.style.left = `${this.pressureThreshold * 100}%`;
834
+
835
+ // Connect to Mudra
836
+ this.connectMudra();
837
+
838
+ // Start render loop
839
+ this.lastTime = performance.now();
840
+ this.gameLoop();
841
+ }
842
+
843
+ resize() {
844
+ this.canvas.width = window.innerWidth;
845
+ this.canvas.height = window.innerHeight;
846
+ this.groundY = this.canvas.height * 0.7;
847
+ this.runnerY = this.groundY;
848
+ }
849
+
850
+ connectMudra() {
851
+ try {
852
+ this.ws = new WebSocket('ws://127.0.0.1:8766');
853
+
854
+ this.ws.onopen = () => {
855
+ console.log('Connected to Mudra Companion');
856
+ // Subscribe to SNC (neuromuscular EMG) events
857
+ this.ws.send(JSON.stringify({
858
+ command: 'subscribe',
859
+ signal: 'snc'
860
+ }));
861
+ };
862
+
863
+ this.ws.onmessage = (event) => {
864
+ try {
865
+ const data = JSON.parse(event.data);
866
+
867
+ // Log all messages for debugging
868
+ console.log('Mudra message:', data.type, data.data);
869
+
870
+ if (data.type === 'connection_status' && data.data?.status === 'connected') {
871
+ this.setMudraConnected(true);
872
+ } else if (data.type === 'snc') {
873
+ // Handle SNC (neuromuscular EMG) signal
874
+ // SNC data is typically an array of values in range -1 to 1
875
+ const rawData = data.data;
876
+ let magnitude = 0;
877
+
878
+ // Try to extract numeric values from various formats
879
+ let values = [];
880
+
881
+ if (typeof rawData === 'number') {
882
+ values = [rawData];
883
+ } else if (Array.isArray(rawData)) {
884
+ // Flatten if nested array
885
+ values = rawData.flat().filter(v => typeof v === 'number');
886
+ } else if (typeof rawData === 'object' && rawData !== null) {
887
+ // Try to find array or number in object
888
+ const possibleKeys = ['value', 'values', 'data', 'snc', 'magnitude', 'samples'];
889
+ for (const key of possibleKeys) {
890
+ if (rawData[key] !== undefined) {
891
+ const val = rawData[key];
892
+ if (Array.isArray(val)) {
893
+ values = val.flat().filter(v => typeof v === 'number');
894
+ } else if (typeof val === 'number') {
895
+ values = [val];
896
+ }
897
+ if (values.length > 0) break;
898
+ }
899
+ }
900
+ // If still empty, try all numeric values in object
901
+ if (values.length === 0) {
902
+ values = Object.values(rawData).filter(v => typeof v === 'number');
903
+ }
904
+ }
905
+
906
+ // Calculate magnitude from values
907
+ if (values.length > 0) {
908
+ // RMS (root mean square) for EMG signal
909
+ const sumSquares = values.reduce((sum, v) => sum + v * v, 0);
910
+ magnitude = Math.sqrt(sumSquares / values.length);
911
+ }
912
+
913
+ // SNC is in range -1 to 1, so magnitude should be 0 to 1
914
+ // But clamp just in case
915
+ magnitude = Math.max(0, Math.min(1, magnitude));
916
+
917
+ // Update live indicator on title screen
918
+ this.updatePressureIndicator(magnitude);
919
+
920
+ // Store for tracking
921
+ this.lastPressure = magnitude;
922
+ this.lastPressureTime = performance.now();
923
+
924
+ if (magnitude > this.pressureThreshold && !this.pressureActive) {
925
+ // Signal exceeded threshold - trigger jump
926
+ this.pressureActive = true;
927
+ this.handleMudraInput(performance.now());
928
+ } else if (magnitude <= this.pressureThreshold) {
929
+ // Signal dropped - allow next trigger
930
+ this.pressureActive = false;
931
+ }
932
+ }
933
+ } catch (e) {
934
+ console.error('Error parsing Mudra message:', e);
935
+ }
936
+ };
937
+
938
+ this.ws.onclose = () => {
939
+ console.log('Mudra connection closed');
940
+ this.setMudraConnected(false);
941
+ // Attempt reconnection after 3 seconds
942
+ setTimeout(() => this.connectMudra(), 3000);
943
+ };
944
+
945
+ this.ws.onerror = (error) => {
946
+ console.error('Mudra WebSocket error:', error);
947
+ this.setMudraConnected(false);
948
+ };
949
+ } catch (e) {
950
+ console.error('Failed to connect to Mudra:', e);
951
+ this.setMudraConnected(false);
952
+ }
953
+ }
954
+
955
+ setMudraConnected(connected) {
956
+ this.mudraConnected = connected;
957
+ const dots = document.querySelectorAll('.status-dot');
958
+ const texts = [
959
+ document.getElementById('mudra-status-text'),
960
+ document.getElementById('title-mudra-text')
961
+ ];
962
+
963
+ dots.forEach(dot => {
964
+ dot.classList.toggle('connected', connected);
965
+ dot.classList.toggle('disconnected', !connected);
966
+ });
967
+
968
+ texts.forEach(text => {
969
+ if (text) {
970
+ text.textContent = connected ? 'Mudra: Connected' : 'Mudra: Disconnected';
971
+ }
972
+ });
973
+ }
974
+
975
+ updatePressureIndicator(pressure) {
976
+ const pressureBar = document.getElementById('pressure-bar');
977
+ if (pressureBar) {
978
+ // Clamp pressure to 0-1 range and convert to percentage
979
+ const clampedPressure = Math.max(0, Math.min(1, pressure));
980
+ pressureBar.style.width = `${clampedPressure * 100}%`;
981
+
982
+ // Add visual feedback when threshold is exceeded
983
+ if (pressure > this.pressureThreshold) {
984
+ pressureBar.classList.add('triggered');
985
+ } else {
986
+ pressureBar.classList.remove('triggered');
987
+ }
988
+ }
989
+ }
990
+
991
+ handleMudraInput(timestamp) {
992
+ if (this.gameStatus !== 'running') return;
993
+ if (this.ghostJumping) return; // Already jumping
994
+
995
+ // Record reaction time if in reaction window
996
+ if (this.reactionWindowOpen && this.currentObstacle && !this.currentObstacle.ghostJumped) {
997
+ const reactionTime = timestamp - this.currentObstacle.reactionWindowStart;
998
+ this.mudraReactions.push(reactionTime);
999
+ this.mudraTime = timestamp;
1000
+ this.currentObstacle.ghostJumped = true;
1001
+ }
1002
+
1003
+ // Trigger ghost jump
1004
+ this.ghostJumping = true;
1005
+ this.ghostJumpVelocity = this.jumpForce;
1006
+
1007
+ // Add particles for ghost jump
1008
+ this.addJumpParticles(this.runnerX + this.ghostOffset, this.runnerY, true);
1009
+ }
1010
+
1011
+ handleMouseClick(e) {
1012
+ if (this.gameStatus !== 'running') return;
1013
+ if (this.physicalJumping) return; // Already jumping
1014
+
1015
+ const timestamp = performance.now();
1016
+
1017
+ // Record reaction time if in reaction window
1018
+ if (this.reactionWindowOpen && this.currentObstacle && !this.currentObstacle.physicalJumped) {
1019
+ const reactionTime = timestamp - this.currentObstacle.reactionWindowStart;
1020
+ this.mouseReactions.push(reactionTime);
1021
+ this.mouseTime = timestamp;
1022
+ this.currentObstacle.physicalJumped = true;
1023
+
1024
+ // Calculate delta if both inputs received
1025
+ if (this.mudraTime !== null) {
1026
+ const delta = this.mouseTime - this.mudraTime;
1027
+ if (delta > 0) {
1028
+ this.totalAdvantage += delta;
1029
+ this.reactions.push({
1030
+ mudraTime: this.mudraTime,
1031
+ mouseTime: this.mouseTime,
1032
+ delta: delta
1033
+ });
1034
+ this.showDeltaFlash(delta);
1035
+ }
1036
+ }
1037
+ }
1038
+
1039
+ // Trigger physical jump
1040
+ this.physicalJumping = true;
1041
+ this.physicalJumpVelocity = this.jumpForce;
1042
+
1043
+ // Add particles for physical jump
1044
+ this.addJumpParticles(this.runnerX, this.runnerY, false);
1045
+ }
1046
+
1047
+ showDeltaFlash(delta) {
1048
+ const flash = document.getElementById('delta-flash');
1049
+ flash.textContent = `+${Math.round(delta)}ms`;
1050
+ flash.classList.remove('show');
1051
+ void flash.offsetWidth; // Force reflow
1052
+ flash.classList.add('show');
1053
+
1054
+ if (this.deltaFlashTimeout) {
1055
+ clearTimeout(this.deltaFlashTimeout);
1056
+ }
1057
+ this.deltaFlashTimeout = setTimeout(() => {
1058
+ flash.classList.remove('show');
1059
+ }, 800);
1060
+ }
1061
+
1062
+ addJumpParticles(x, y, isGhost) {
1063
+ const color = isGhost ? { r: 0, g: 255, b: 255 } : { r: 255, g: 255, b: 255 };
1064
+ for (let i = 0; i < 10; i++) {
1065
+ this.particles.push({
1066
+ x: x,
1067
+ y: y,
1068
+ vx: (Math.random() - 0.5) * 8,
1069
+ vy: Math.random() * -5 - 2,
1070
+ life: 1,
1071
+ color: color,
1072
+ isGhost: isGhost
1073
+ });
1074
+ }
1075
+ }
1076
+
1077
+ startGame() {
1078
+ document.getElementById('title-screen').classList.add('hidden');
1079
+ document.getElementById('countdown').classList.remove('hidden');
1080
+ this.gameStatus = 'countdown';
1081
+
1082
+ let count = 3;
1083
+ const countdownEl = document.getElementById('countdown-number');
1084
+
1085
+ const countdownInterval = setInterval(() => {
1086
+ count--;
1087
+ if (count > 0) {
1088
+ countdownEl.textContent = count;
1089
+ } else if (count === 0) {
1090
+ countdownEl.textContent = 'GO!';
1091
+ } else {
1092
+ clearInterval(countdownInterval);
1093
+ document.getElementById('countdown').classList.add('hidden');
1094
+ this.gameStatus = 'running';
1095
+ this.lastObstacleSpawn = performance.now();
1096
+ this.lastSpeedIncrement = performance.now();
1097
+ }
1098
+ }, 1000);
1099
+ }
1100
+
1101
+ restartGame() {
1102
+ // Reset all state
1103
+ this.lives = 3;
1104
+ this.distance = 0;
1105
+ this.speed = this.baseSpeed;
1106
+ this.totalAdvantage = 0;
1107
+ this.ghostLead = 0;
1108
+ this.reactions = [];
1109
+ this.ghostSaves = 0;
1110
+ this.obstacles = [];
1111
+ this.obstaclesPassed = 0;
1112
+ this.obstacleInterval = 2500;
1113
+ this.mudraReactions = [];
1114
+ this.mouseReactions = [];
1115
+ this.ghostCleared = 0;
1116
+ this.physicalCleared = 0;
1117
+ this.particles = [];
1118
+ this.speedLines = [];
1119
+ this.physicalJumping = false;
1120
+ this.physicalJumpHeight = 0;
1121
+ this.ghostJumping = false;
1122
+ this.ghostJumpHeight = 0;
1123
+ this.physicalStumble = 0;
1124
+ this.ghostStumble = 0;
1125
+ this.currentObstacle = null;
1126
+ this.mudraTime = null;
1127
+ this.mouseTime = null;
1128
+ this.reactionWindowOpen = false;
1129
+ this.finishLineX = null;
1130
+ this.victory = false;
1131
+
1132
+ // Reset UI
1133
+ this.updateLivesDisplay();
1134
+ document.getElementById('end-screen').classList.add('hidden');
1135
+
1136
+ this.startGame();
1137
+ }
1138
+
1139
+ endGame(isVictory = false) {
1140
+ this.gameStatus = 'ended';
1141
+ this.victory = isVictory;
1142
+ this.showEndScreen();
1143
+ }
1144
+
1145
+ showEndScreen() {
1146
+ const endScreen = document.getElementById('end-screen');
1147
+ const endTitle = document.getElementById('end-title');
1148
+
1149
+ // Update title based on victory or defeat
1150
+ if (endTitle) {
1151
+ endTitle.textContent = this.victory ? 'Victory!' : 'Run Complete';
1152
+ endTitle.style.color = this.victory ? '#2ed573' : '#fff';
1153
+ }
1154
+
1155
+ // Calculate stats
1156
+ const mudraAvg = this.mudraReactions.length > 0
1157
+ ? Math.round(this.mudraReactions.reduce((a, b) => a + b, 0) / this.mudraReactions.length)
1158
+ : '--';
1159
+ const mouseAvg = this.mouseReactions.length > 0
1160
+ ? Math.round(this.mouseReactions.reduce((a, b) => a + b, 0) / this.mouseReactions.length)
1161
+ : '--';
1162
+ const mudraFastest = this.mudraReactions.length > 0
1163
+ ? Math.round(Math.min(...this.mudraReactions))
1164
+ : '--';
1165
+ const mouseFastest = this.mouseReactions.length > 0
1166
+ ? Math.round(Math.min(...this.mouseReactions))
1167
+ : '--';
1168
+
1169
+ // Update stats display
1170
+ document.getElementById('end-mudra-avg').textContent = mudraAvg !== '--' ? `${mudraAvg}ms` : '--ms';
1171
+ document.getElementById('end-mouse-avg').textContent = mouseAvg !== '--' ? `${mouseAvg}ms` : '--ms';
1172
+ document.getElementById('end-mudra-fastest').textContent = mudraFastest !== '--' ? `${mudraFastest}ms` : '--ms';
1173
+ document.getElementById('end-mouse-fastest').textContent = mouseFastest !== '--' ? `${mouseFastest}ms` : '--ms';
1174
+ document.getElementById('end-mudra-cleared').textContent = this.ghostCleared;
1175
+ document.getElementById('end-mouse-cleared').textContent = this.physicalCleared;
1176
+
1177
+ // Advantage summary
1178
+ const avgSpeed = (this.baseSpeed + this.speed) / 2;
1179
+ const distanceEquiv = (this.totalAdvantage / 1000) * avgSpeed;
1180
+ const avgDelta = this.reactions.length > 0
1181
+ ? Math.round(this.reactions.reduce((a, b) => a + b.delta, 0) / this.reactions.length)
1182
+ : 0;
1183
+
1184
+ document.getElementById('total-time-saved').textContent = `${Math.round(this.totalAdvantage)}ms`;
1185
+ document.getElementById('distance-equivalent').textContent = `${distanceEquiv.toFixed(1)}m`;
1186
+ document.getElementById('ghost-saves').textContent = this.ghostSaves;
1187
+ document.getElementById('avg-delta').textContent = `${avgDelta}ms`;
1188
+
1189
+ // Summary message
1190
+ let message = '';
1191
+ if (this.victory) {
1192
+ message = `You crossed the finish line! `;
1193
+ if (this.reactions.length > 0 && avgDelta > 0) {
1194
+ message += `Your intent was ${avgDelta}ms faster on average, saving ${distanceEquiv.toFixed(1)} meters.`;
1195
+ }
1196
+ } else if (this.reactions.length > 0 && avgDelta > 0) {
1197
+ message = `Your intent was ${avgDelta}ms faster on average. Over this run, thinking beat doing by ${distanceEquiv.toFixed(1)} meters.`;
1198
+ } else if (!this.mudraConnected) {
1199
+ message = `Mudra wasn't connected. Connect to see the gap between thought and action.`;
1200
+ } else {
1201
+ message = `Keep practicing! Try to react with both intent and action to see the difference.`;
1202
+ }
1203
+ document.getElementById('summary-message').textContent = message;
1204
+
1205
+ endScreen.classList.remove('hidden');
1206
+ }
1207
+
1208
+ updateLivesDisplay() {
1209
+ const hearts = document.querySelectorAll('.heart');
1210
+ hearts.forEach((heart, i) => {
1211
+ heart.classList.toggle('lost', i >= this.lives);
1212
+ });
1213
+ }
1214
+
1215
+ spawnObstacle() {
1216
+ const obstacle = {
1217
+ x: this.canvas.width + 100,
1218
+ width: 30 + Math.random() * 20,
1219
+ height: 60 + Math.random() * 30,
1220
+ reactionWindowStart: null,
1221
+ ghostJumped: false,
1222
+ physicalJumped: false,
1223
+ ghostCleared: false,
1224
+ physicalCleared: false,
1225
+ ghostHit: false,
1226
+ physicalHit: false,
1227
+ processed: false
1228
+ };
1229
+ this.obstacles.push(obstacle);
1230
+ }
1231
+
1232
+ update(deltaTime) {
1233
+ if (this.gameStatus !== 'running') return;
1234
+
1235
+ const now = performance.now();
1236
+
1237
+ // Update distance
1238
+ this.distance += this.speed * (deltaTime / 1000);
1239
+
1240
+ // Check for finish line
1241
+ const pixelsPerMeter = 50;
1242
+ const distanceToFinish = this.finishDistance - this.distance;
1243
+
1244
+ // Spawn finish line when close (within screen width + buffer)
1245
+ if (distanceToFinish > 0 && distanceToFinish < 30 && this.finishLineX === null) {
1246
+ this.finishLineX = this.canvas.width + distanceToFinish * pixelsPerMeter;
1247
+ }
1248
+
1249
+ // Update finish line position
1250
+ if (this.finishLineX !== null) {
1251
+ this.finishLineX -= this.speed * pixelsPerMeter * (deltaTime / 1000);
1252
+
1253
+ // Check if runner crossed finish line
1254
+ if (this.finishLineX < this.runnerX) {
1255
+ this.endGame(true); // Victory!
1256
+ return;
1257
+ }
1258
+ }
1259
+
1260
+ // Stop spawning obstacles when close to finish
1261
+ const shouldSpawnObstacles = distanceToFinish > 50; // Stop spawning 50m before finish
1262
+
1263
+ // Update speed over time
1264
+ if (now - this.lastSpeedIncrement > this.speedIncrementInterval * 1000) {
1265
+ if (this.speed < this.maxSpeed) {
1266
+ this.speed = Math.min(this.speed + this.speedIncrement, this.maxSpeed);
1267
+ // Also decrease obstacle interval
1268
+ this.obstacleInterval = Math.max(
1269
+ this.minObstacleInterval,
1270
+ this.obstacleInterval - 100
1271
+ );
1272
+ }
1273
+ this.lastSpeedIncrement = now;
1274
+ }
1275
+
1276
+ // Update ghost lead based on total advantage
1277
+ // Convert ms to distance: distance = time(s) * speed(m/s) * pixels_per_meter
1278
+ this.ghostLead = (this.totalAdvantage / 1000) * this.speed * pixelsPerMeter;
1279
+ this.ghostOffset = Math.min(this.ghostLead, 300); // Cap visual offset
1280
+
1281
+ // Spawn obstacles (but not near finish line)
1282
+ if (now - this.lastObstacleSpawn > this.obstacleInterval && shouldSpawnObstacles) {
1283
+ this.spawnObstacle();
1284
+ this.lastObstacleSpawn = now;
1285
+ }
1286
+
1287
+ // Update obstacles
1288
+ const obstacleSpeed = this.speed * pixelsPerMeter * (deltaTime / 1000);
1289
+
1290
+ for (let i = this.obstacles.length - 1; i >= 0; i--) {
1291
+ const obs = this.obstacles[i];
1292
+ obs.x -= obstacleSpeed;
1293
+
1294
+ // Open reaction window when obstacle enters reaction zone
1295
+ const reactionZone = this.runnerX + 400;
1296
+ if (obs.x <= reactionZone && obs.reactionWindowStart === null) {
1297
+ obs.reactionWindowStart = now;
1298
+ this.currentObstacle = obs;
1299
+ this.reactionWindowOpen = true;
1300
+ this.mudraTime = null;
1301
+ this.mouseTime = null;
1302
+ }
1303
+
1304
+ // Check collisions
1305
+ const ghostX = this.runnerX + this.ghostOffset;
1306
+ const runnerWidth = 40;
1307
+ const runnerHeight = 60;
1308
+ const jumpThreshold = obs.height - 10;
1309
+
1310
+ // Ghost collision check - is ghost overlapping obstacle?
1311
+ const ghostOverlapping = obs.x < ghostX + runnerWidth && obs.x + obs.width > ghostX;
1312
+ if (ghostOverlapping && !obs.ghostCleared && !obs.ghostHit) {
1313
+ if (this.ghostJumpHeight >= jumpThreshold) {
1314
+ // Ghost jumped high enough - cleared!
1315
+ obs.ghostCleared = true;
1316
+ this.ghostCleared++;
1317
+ } else {
1318
+ // Ghost hit the obstacle
1319
+ obs.ghostHit = true;
1320
+ this.ghostStumble = 30; // Stumble animation frames
1321
+ }
1322
+ }
1323
+
1324
+ // Physical runner collision check - is runner overlapping obstacle?
1325
+ const physicalOverlapping = obs.x < this.runnerX + runnerWidth && obs.x + obs.width > this.runnerX;
1326
+ if (physicalOverlapping && !obs.physicalCleared && !obs.physicalHit) {
1327
+ if (this.physicalJumpHeight >= jumpThreshold) {
1328
+ // Runner jumped high enough - cleared!
1329
+ obs.physicalCleared = true;
1330
+ this.physicalCleared++;
1331
+ } else {
1332
+ // Runner hit the obstacle!
1333
+ obs.physicalHit = true;
1334
+ obs.processed = true;
1335
+ this.lives--;
1336
+ this.updateLivesDisplay();
1337
+ this.screenShake = 15;
1338
+ this.physicalStumble = 30; // Stumble animation frames
1339
+
1340
+ // Check if ghost saved (ghost cleared but physical didn't)
1341
+ if (obs.ghostCleared) {
1342
+ this.ghostSaves++;
1343
+ this.showMessage("Intent cleared it. Body didn't.");
1344
+ }
1345
+
1346
+ if (this.lives <= 0) {
1347
+ this.endGame();
1348
+ return;
1349
+ }
1350
+ }
1351
+ }
1352
+
1353
+ // Close reaction window and count obstacle when it passes
1354
+ if (obs.x + obs.width < this.runnerX && !obs.processed) {
1355
+ obs.processed = true;
1356
+ this.obstaclesPassed++;
1357
+ this.reactionWindowOpen = false;
1358
+ this.currentObstacle = null;
1359
+ }
1360
+
1361
+ // Remove off-screen obstacles
1362
+ if (obs.x + obs.width < -100) {
1363
+ this.obstacles.splice(i, 1);
1364
+ }
1365
+ }
1366
+
1367
+ // Update jumps
1368
+ if (this.physicalJumping) {
1369
+ this.physicalJumpHeight += this.physicalJumpVelocity;
1370
+ this.physicalJumpVelocity -= this.gravity;
1371
+
1372
+ if (this.physicalJumpHeight <= 0) {
1373
+ this.physicalJumpHeight = 0;
1374
+ this.physicalJumping = false;
1375
+ this.physicalJumpVelocity = 0;
1376
+ }
1377
+ }
1378
+
1379
+ if (this.ghostJumping) {
1380
+ this.ghostJumpHeight += this.ghostJumpVelocity;
1381
+ this.ghostJumpVelocity -= this.gravity;
1382
+
1383
+ if (this.ghostJumpHeight <= 0) {
1384
+ this.ghostJumpHeight = 0;
1385
+ this.ghostJumping = false;
1386
+ this.ghostJumpVelocity = 0;
1387
+ }
1388
+ }
1389
+
1390
+ // Update particles
1391
+ for (let i = this.particles.length - 1; i >= 0; i--) {
1392
+ const p = this.particles[i];
1393
+ p.x += p.vx;
1394
+ p.y += p.vy;
1395
+ p.vy += 0.2;
1396
+ p.life -= 0.02;
1397
+
1398
+ if (p.life <= 0) {
1399
+ this.particles.splice(i, 1);
1400
+ }
1401
+ }
1402
+
1403
+ // Update speed lines
1404
+ if (Math.random() < 0.3) {
1405
+ this.speedLines.push({
1406
+ x: this.canvas.width,
1407
+ y: Math.random() * this.canvas.height,
1408
+ length: 50 + Math.random() * 100,
1409
+ speed: this.speed * 10
1410
+ });
1411
+ }
1412
+
1413
+ for (let i = this.speedLines.length - 1; i >= 0; i--) {
1414
+ this.speedLines[i].x -= this.speedLines[i].speed * (deltaTime / 1000);
1415
+ if (this.speedLines[i].x + this.speedLines[i].length < 0) {
1416
+ this.speedLines.splice(i, 1);
1417
+ }
1418
+ }
1419
+
1420
+ // Update grid offset for scrolling effect
1421
+ this.gridOffset = (this.gridOffset + obstacleSpeed) % 100;
1422
+
1423
+ // Update screen shake
1424
+ if (this.screenShake > 0) {
1425
+ this.screenShake *= 0.9;
1426
+ if (this.screenShake < 0.1) this.screenShake = 0;
1427
+ }
1428
+
1429
+ // Update stumble timers
1430
+ if (this.physicalStumble > 0) this.physicalStumble--;
1431
+ if (this.ghostStumble > 0) this.ghostStumble--;
1432
+
1433
+ // Update animation frame
1434
+ this.frameCount++;
1435
+ if (this.frameCount % 6 === 0) {
1436
+ this.runFrame = (this.runFrame + 1) % 4;
1437
+ }
1438
+
1439
+ // Update HUD
1440
+ this.updateHUD();
1441
+ }
1442
+
1443
+ showMessage(text) {
1444
+ let flash = document.getElementById('message-flash');
1445
+ if (!flash) {
1446
+ flash = document.createElement('div');
1447
+ flash.id = 'message-flash';
1448
+ document.getElementById('game-container').appendChild(flash);
1449
+ }
1450
+ flash.textContent = text;
1451
+ flash.classList.remove('show');
1452
+ void flash.offsetWidth;
1453
+ flash.classList.add('show');
1454
+ }
1455
+
1456
+ updateHUD() {
1457
+ document.getElementById('distance-value').textContent = `${Math.floor(this.distance)}m / ${this.finishDistance}m`;
1458
+ document.getElementById('speed-value').textContent = `${this.speed.toFixed(1)} m/s`;
1459
+ document.getElementById('lead-value').textContent = `+${(this.ghostLead / 50).toFixed(1)}m`;
1460
+ document.getElementById('obstacles-value').textContent = this.obstaclesPassed;
1461
+
1462
+ if (this.mudraReactions.length > 0) {
1463
+ const avg = Math.round(this.mudraReactions.reduce((a, b) => a + b, 0) / this.mudraReactions.length);
1464
+ document.getElementById('mudra-avg').textContent = `${avg}ms`;
1465
+ }
1466
+
1467
+ if (this.mouseReactions.length > 0) {
1468
+ const avg = Math.round(this.mouseReactions.reduce((a, b) => a + b, 0) / this.mouseReactions.length);
1469
+ document.getElementById('mouse-avg').textContent = `${avg}ms`;
1470
+ }
1471
+ }
1472
+
1473
+ render() {
1474
+ const ctx = this.ctx;
1475
+
1476
+ // Apply screen shake
1477
+ ctx.save();
1478
+ if (this.screenShake > 0) {
1479
+ ctx.translate(
1480
+ (Math.random() - 0.5) * this.screenShake,
1481
+ (Math.random() - 0.5) * this.screenShake
1482
+ );
1483
+ }
1484
+
1485
+ // Clear canvas
1486
+ ctx.fillStyle = '#0a0a0f';
1487
+ ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
1488
+
1489
+ // Draw grid background
1490
+ this.drawGrid();
1491
+
1492
+ // Draw speed lines
1493
+ this.drawSpeedLines();
1494
+
1495
+ // Draw ground
1496
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
1497
+ ctx.lineWidth = 2;
1498
+ ctx.beginPath();
1499
+ ctx.moveTo(0, this.groundY);
1500
+ ctx.lineTo(this.canvas.width, this.groundY);
1501
+ ctx.stroke();
1502
+
1503
+ // Draw obstacles
1504
+ this.drawObstacles();
1505
+
1506
+ // Draw finish line
1507
+ if (this.finishLineX !== null) {
1508
+ this.drawFinishLine();
1509
+ }
1510
+
1511
+ // Draw tether between runners
1512
+ if (this.ghostOffset > 10) {
1513
+ this.drawTether();
1514
+ }
1515
+
1516
+ // Draw ghost runner (behind, but visually ahead)
1517
+ this.drawRunner(
1518
+ this.runnerX + this.ghostOffset,
1519
+ this.groundY - this.ghostJumpHeight,
1520
+ true
1521
+ );
1522
+
1523
+ // Draw physical runner
1524
+ this.drawRunner(
1525
+ this.runnerX,
1526
+ this.groundY - this.physicalJumpHeight,
1527
+ false
1528
+ );
1529
+
1530
+ // Draw particles
1531
+ this.drawParticles();
1532
+
1533
+ ctx.restore();
1534
+ }
1535
+
1536
+ drawGrid() {
1537
+ const ctx = this.ctx;
1538
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
1539
+ ctx.lineWidth = 1;
1540
+
1541
+ // Vertical lines (moving for parallax)
1542
+ for (let x = -this.gridOffset; x < this.canvas.width + 100; x += 100) {
1543
+ ctx.beginPath();
1544
+ ctx.moveTo(x, this.groundY - 200);
1545
+ ctx.lineTo(x, this.groundY);
1546
+ ctx.stroke();
1547
+ }
1548
+
1549
+ // Horizontal lines
1550
+ for (let y = this.groundY - 200; y <= this.groundY; y += 50) {
1551
+ ctx.beginPath();
1552
+ ctx.moveTo(0, y);
1553
+ ctx.lineTo(this.canvas.width, y);
1554
+ ctx.stroke();
1555
+ }
1556
+ }
1557
+
1558
+ drawSpeedLines() {
1559
+ const ctx = this.ctx;
1560
+ const intensity = (this.speed - this.baseSpeed) / (this.maxSpeed - this.baseSpeed);
1561
+
1562
+ ctx.strokeStyle = `rgba(255, 255, 255, ${0.1 + intensity * 0.2})`;
1563
+ ctx.lineWidth = 1;
1564
+
1565
+ for (const line of this.speedLines) {
1566
+ ctx.beginPath();
1567
+ ctx.moveTo(line.x, line.y);
1568
+ ctx.lineTo(line.x + line.length, line.y);
1569
+ ctx.stroke();
1570
+ }
1571
+ }
1572
+
1573
+ drawObstacles() {
1574
+ const ctx = this.ctx;
1575
+
1576
+ for (const obs of this.obstacles) {
1577
+ // Main obstacle
1578
+ ctx.fillStyle = '#ff4757';
1579
+ ctx.fillRect(obs.x, this.groundY - obs.height, obs.width, obs.height);
1580
+
1581
+ // Highlight
1582
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
1583
+ ctx.fillRect(obs.x, this.groundY - obs.height, obs.width, 5);
1584
+
1585
+ // Glow
1586
+ ctx.shadowColor = '#ff4757';
1587
+ ctx.shadowBlur = 20;
1588
+ ctx.fillStyle = 'transparent';
1589
+ ctx.fillRect(obs.x, this.groundY - obs.height, obs.width, obs.height);
1590
+ ctx.shadowBlur = 0;
1591
+ }
1592
+ }
1593
+
1594
+ drawFinishLine() {
1595
+ const ctx = this.ctx;
1596
+ const x = this.finishLineX;
1597
+ const flagHeight = 150;
1598
+ const flagWidth = 80;
1599
+ const poleHeight = flagHeight + 50;
1600
+
1601
+ // Draw pole
1602
+ ctx.fillStyle = '#fff';
1603
+ ctx.fillRect(x - 3, this.groundY - poleHeight, 6, poleHeight);
1604
+
1605
+ // Draw checkered flag
1606
+ const squareSize = 10;
1607
+ for (let row = 0; row < flagHeight / squareSize; row++) {
1608
+ for (let col = 0; col < flagWidth / squareSize; col++) {
1609
+ const isWhite = (row + col) % 2 === 0;
1610
+ ctx.fillStyle = isWhite ? '#fff' : '#000';
1611
+ ctx.fillRect(
1612
+ x + 3 + col * squareSize,
1613
+ this.groundY - poleHeight + row * squareSize,
1614
+ squareSize,
1615
+ squareSize
1616
+ );
1617
+ }
1618
+ }
1619
+
1620
+ // Draw "FINISH" text
1621
+ ctx.font = 'bold 24px Courier New';
1622
+ ctx.fillStyle = '#2ed573';
1623
+ ctx.textAlign = 'center';
1624
+ ctx.shadowColor = '#2ed573';
1625
+ ctx.shadowBlur = 20;
1626
+ ctx.fillText('FINISH', x + flagWidth / 2, this.groundY - poleHeight - 20);
1627
+ ctx.shadowBlur = 0;
1628
+
1629
+ // Draw finish line on ground
1630
+ ctx.strokeStyle = '#2ed573';
1631
+ ctx.lineWidth = 4;
1632
+ ctx.setLineDash([10, 10]);
1633
+ ctx.beginPath();
1634
+ ctx.moveTo(x, this.groundY - 200);
1635
+ ctx.lineTo(x, this.groundY);
1636
+ ctx.stroke();
1637
+ ctx.setLineDash([]);
1638
+
1639
+ // Distance remaining indicator
1640
+ const distanceToFinish = Math.max(0, this.finishDistance - this.distance);
1641
+ if (distanceToFinish > 0 && distanceToFinish < 100) {
1642
+ ctx.font = 'bold 18px Courier New';
1643
+ ctx.fillStyle = '#2ed573';
1644
+ ctx.textAlign = 'center';
1645
+ ctx.fillText(`${Math.ceil(distanceToFinish)}m to go!`, x, this.groundY + 40);
1646
+ }
1647
+ }
1648
+
1649
+ drawTether() {
1650
+ const ctx = this.ctx;
1651
+ const startX = this.runnerX + 20;
1652
+ const startY = this.groundY - this.physicalJumpHeight - 30;
1653
+ const endX = this.runnerX + this.ghostOffset + 20;
1654
+ const endY = this.groundY - this.ghostJumpHeight - 30;
1655
+
1656
+ // Create gradient for tether
1657
+ const gradient = ctx.createLinearGradient(startX, startY, endX, endY);
1658
+ gradient.addColorStop(0, 'rgba(255, 255, 255, 0.5)');
1659
+ gradient.addColorStop(1, 'rgba(0, 255, 255, 0.8)');
1660
+
1661
+ ctx.strokeStyle = gradient;
1662
+ ctx.lineWidth = 3;
1663
+ ctx.setLineDash([5, 5]);
1664
+
1665
+ ctx.beginPath();
1666
+ ctx.moveTo(startX, startY);
1667
+ ctx.lineTo(endX, endY);
1668
+ ctx.stroke();
1669
+
1670
+ ctx.setLineDash([]);
1671
+
1672
+ // Draw distance label
1673
+ const midX = (startX + endX) / 2;
1674
+ const midY = Math.min(startY, endY) - 20;
1675
+ const leadDistance = (this.ghostLead / 50).toFixed(1);
1676
+
1677
+ ctx.font = 'bold 14px Courier New';
1678
+ ctx.fillStyle = '#00ffff';
1679
+ ctx.textAlign = 'center';
1680
+ ctx.shadowColor = '#00ffff';
1681
+ ctx.shadowBlur = 10;
1682
+ ctx.fillText(`+${leadDistance}m`, midX, midY);
1683
+ ctx.shadowBlur = 0;
1684
+ }
1685
+
1686
+ drawRunner(x, y, isGhost) {
1687
+ const ctx = this.ctx;
1688
+ const width = 40;
1689
+ const height = 60;
1690
+
1691
+ // Check if stumbling
1692
+ const isStumbling = isGhost ? this.ghostStumble > 0 : this.physicalStumble > 0;
1693
+
1694
+ ctx.save();
1695
+
1696
+ // Apply stumble effect (red tint and wobble)
1697
+ if (isStumbling) {
1698
+ const wobble = Math.sin(this.frameCount * 0.5) * 5;
1699
+ ctx.translate(wobble, 0);
1700
+ }
1701
+
1702
+ if (isGhost) {
1703
+ // Ghost effect
1704
+ ctx.globalAlpha = isStumbling ? 0.4 : 0.7;
1705
+ ctx.shadowColor = isStumbling ? '#ff4757' : '#00ffff';
1706
+ ctx.shadowBlur = 30;
1707
+
1708
+ // Motion blur effect
1709
+ for (let i = 3; i > 0; i--) {
1710
+ ctx.globalAlpha = 0.1 * i;
1711
+ this.drawRunnerBody(x - i * 8, y, width, height, isGhost, isStumbling);
1712
+ }
1713
+
1714
+ ctx.globalAlpha = isStumbling ? 0.4 : 0.7;
1715
+ }
1716
+
1717
+ this.drawRunnerBody(x, y, width, height, isGhost, isStumbling);
1718
+
1719
+ ctx.restore();
1720
+ }
1721
+
1722
+ drawRunnerBody(x, y, width, height, isGhost, isStumbling = false) {
1723
+ const ctx = this.ctx;
1724
+ const color = isStumbling ? '#ff4757' : (isGhost ? '#00ffff' : '#ffffff');
1725
+ const secondaryColor = isStumbling ? '#ff6b7a' : (isGhost ? '#a855f7' : '#cccccc');
1726
+
1727
+ // Body
1728
+ ctx.fillStyle = color;
1729
+ ctx.fillRect(x, y - height, width * 0.6, height * 0.5);
1730
+
1731
+ // Head
1732
+ ctx.beginPath();
1733
+ ctx.arc(x + width * 0.3, y - height - 10, 12, 0, Math.PI * 2);
1734
+ ctx.fill();
1735
+
1736
+ // Legs (animated)
1737
+ const legOffset = Math.sin(this.runFrame * Math.PI / 2) * 15;
1738
+
1739
+ ctx.strokeStyle = color;
1740
+ ctx.lineWidth = 6;
1741
+ ctx.lineCap = 'round';
1742
+
1743
+ // Back leg
1744
+ ctx.beginPath();
1745
+ ctx.moveTo(x + width * 0.3, y - height * 0.5);
1746
+ ctx.lineTo(x + width * 0.3 - legOffset, y);
1747
+ ctx.stroke();
1748
+
1749
+ // Front leg
1750
+ ctx.beginPath();
1751
+ ctx.moveTo(x + width * 0.3, y - height * 0.5);
1752
+ ctx.lineTo(x + width * 0.3 + legOffset, y);
1753
+ ctx.stroke();
1754
+
1755
+ // Arms (animated opposite to legs)
1756
+ ctx.strokeStyle = secondaryColor;
1757
+ ctx.lineWidth = 4;
1758
+
1759
+ // Back arm
1760
+ ctx.beginPath();
1761
+ ctx.moveTo(x + width * 0.3, y - height * 0.7);
1762
+ ctx.lineTo(x + width * 0.3 + legOffset * 0.7, y - height * 0.4);
1763
+ ctx.stroke();
1764
+
1765
+ // Front arm
1766
+ ctx.beginPath();
1767
+ ctx.moveTo(x + width * 0.3, y - height * 0.7);
1768
+ ctx.lineTo(x + width * 0.3 - legOffset * 0.7, y - height * 0.4);
1769
+ ctx.stroke();
1770
+ }
1771
+
1772
+ drawParticles() {
1773
+ const ctx = this.ctx;
1774
+
1775
+ for (const p of this.particles) {
1776
+ ctx.globalAlpha = p.life;
1777
+ ctx.fillStyle = `rgb(${p.color.r}, ${p.color.g}, ${p.color.b})`;
1778
+
1779
+ if (p.isGhost) {
1780
+ ctx.shadowColor = `rgb(${p.color.r}, ${p.color.g}, ${p.color.b})`;
1781
+ ctx.shadowBlur = 10;
1782
+ }
1783
+
1784
+ ctx.beginPath();
1785
+ ctx.arc(p.x, p.y, 3 * p.life, 0, Math.PI * 2);
1786
+ ctx.fill();
1787
+
1788
+ ctx.shadowBlur = 0;
1789
+ }
1790
+
1791
+ ctx.globalAlpha = 1;
1792
+ }
1793
+
1794
+ gameLoop() {
1795
+ const now = performance.now();
1796
+ const deltaTime = now - this.lastTime;
1797
+ this.lastTime = now;
1798
+
1799
+ this.update(deltaTime);
1800
+ this.render();
1801
+
1802
+ requestAnimationFrame(() => this.gameLoop());
1803
+ }
1804
+ }
1805
+
1806
+ // Initialize game when DOM is ready
1807
+ document.addEventListener('DOMContentLoaded', () => {
1808
+ new IntentRunner();
1809
+ });
1810
+ </script>
1811
+ </body>
1812
+ </html>