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.
- package/.claude-plugin/plugin.json +19 -0
- package/CLAUDE.md +35 -0
- package/bin/install.js +25 -0
- package/package.json +28 -0
- package/skills/mudra-master/SKILL.md +202 -0
- package/skills/mudra-master/mudra-preview/SKILL.md +151 -0
- package/skills/mudra-master/mudra-preview/assets/ar-menu.html +382 -0
- package/skills/mudra-master/mudra-preview/assets/document-scroller.html +468 -0
- package/skills/mudra-master/mudra-preview/assets/drum-machine.html +788 -0
- package/skills/mudra-master/mudra-preview/assets/emg-visualizer.html +390 -0
- package/skills/mudra-master/mudra-preview/assets/generative-art.html +408 -0
- package/skills/mudra-master/mudra-preview/assets/gesture-assistant.html +342 -0
- package/skills/mudra-master/mudra-preview/assets/gesture-speech.html +444 -0
- package/skills/mudra-master/mudra-preview/assets/hands-free-desktop.html +376 -0
- package/skills/mudra-master/mudra-preview/assets/model-rotator.html +365 -0
- package/skills/mudra-master/mudra-preview/assets/mudra-duel.html +3200 -0
- package/skills/mudra-master/mudra-preview/assets/mudra-monitor.html +1451 -0
- package/skills/mudra-master/mudra-preview/assets/mudra-ultimate-template.html +891 -0
- package/skills/mudra-master/mudra-preview/assets/music-sequencer.html +455 -0
- package/skills/mudra-master/mudra-preview/assets/neural-pong.html +380 -0
- package/skills/mudra-master/mudra-preview/assets/neural-snake.html +364 -0
- package/skills/mudra-master/mudra-preview/assets/presentation-controller.html +345 -0
- package/skills/mudra-master/mudra-preview/assets/pressure-painter.html +285 -0
- package/skills/mudra-master/mudra-preview/assets/runner.html +1812 -0
- package/skills/mudra-master/mudra-preview/assets/smart-home.html +461 -0
- package/skills/mudra-master/mudra-preview/assets/space-invaders.html +1154 -0
- package/skills/mudra-master/mudra-preview/assets/waterful-ring-toss.html +1624 -0
- package/skills/mudra-master/mudra-preview/references/agent_protocol.json +646 -0
- package/skills/mudra-master/mudra-preview/references/promt.md +16829 -0
- package/skills/mudra-master/mudra-xr/SKILL.md +245 -0
- package/skills/mudra-master/mudra-xr/assets/demos/3dgs-walkthrough.html +242 -0
- package/skills/mudra-master/mudra-xr/assets/demos/aisimulator.html +792 -0
- package/skills/mudra-master/mudra-xr/assets/demos/balloonpop.html +422 -0
- package/skills/mudra-master/mudra-xr/assets/demos/ballpit.html +284 -0
- package/skills/mudra-master/mudra-xr/assets/demos/drone.html +41 -0
- package/skills/mudra-master/mudra-xr/assets/demos/gemini-icebreakers.html +338 -0
- package/skills/mudra-master/mudra-xr/assets/demos/gemini-xrobject.html +302 -0
- package/skills/mudra-master/mudra-xr/assets/demos/math3d.html +188 -0
- package/skills/mudra-master/mudra-xr/assets/demos/measure.html +247 -0
- package/skills/mudra-master/mudra-xr/assets/demos/occlusion.html +251 -0
- package/skills/mudra-master/mudra-xr/assets/demos/rain.html +498 -0
- package/skills/mudra-master/mudra-xr/assets/demos/screenwiper.html +432 -0
- package/skills/mudra-master/mudra-xr/assets/demos/splash.html +519 -0
- package/skills/mudra-master/mudra-xr/assets/demos/webcam_gestures.html +253 -0
- package/skills/mudra-master/mudra-xr/assets/demos/xremoji.html +833 -0
- package/skills/mudra-master/mudra-xr/assets/demos/xrpoet.html +151 -0
- package/skills/mudra-master/mudra-xr/assets/samples/depthmap.html +266 -0
- package/skills/mudra-master/mudra-xr/assets/samples/depthmesh.html +100 -0
- package/skills/mudra-master/mudra-xr/assets/samples/game_rps.html +1099 -0
- package/skills/mudra-master/mudra-xr/assets/samples/gestures_custom.html +467 -0
- package/skills/mudra-master/mudra-xr/assets/samples/gestures_heuristic.html +256 -0
- package/skills/mudra-master/mudra-xr/assets/samples/lighting.html +163 -0
- package/skills/mudra-master/mudra-xr/assets/samples/mesh_detection.html +46 -0
- package/skills/mudra-master/mudra-xr/assets/samples/modelviewer.html +183 -0
- package/skills/mudra-master/mudra-xr/assets/samples/paint.html +133 -0
- package/skills/mudra-master/mudra-xr/assets/samples/planar-vst.html +45 -0
- package/skills/mudra-master/mudra-xr/assets/samples/reticle.html +95 -0
- package/skills/mudra-master/mudra-xr/assets/samples/skybox_agent.html +384 -0
- package/skills/mudra-master/mudra-xr/assets/samples/sound.html +390 -0
- package/skills/mudra-master/mudra-xr/assets/samples/ui.html +496 -0
- package/skills/mudra-master/mudra-xr/assets/samples/virtual-screens.html +808 -0
- package/skills/mudra-master/mudra-xr/assets/templates/0_basic.html +64 -0
- package/skills/mudra-master/mudra-xr/assets/templates/1_ui.html +107 -0
- package/skills/mudra-master/mudra-xr/assets/templates/2_hands.html +183 -0
- package/skills/mudra-master/mudra-xr/assets/templates/3_depth.html +89 -0
- package/skills/mudra-master/mudra-xr/assets/templates/4_stereo.html +84 -0
- package/skills/mudra-master/mudra-xr/assets/templates/5_camera.html +117 -0
- package/skills/mudra-master/mudra-xr/assets/templates/6_ai.html +154 -0
- package/skills/mudra-master/mudra-xr/assets/templates/7_ai_live.html +253 -0
- package/skills/mudra-master/mudra-xr/assets/templates/8_objects.html +95 -0
- package/skills/mudra-master/mudra-xr/assets/templates/9_xr-toggle.html +90 -0
- package/skills/mudra-master/mudra-xr/assets/templates/heuristic_hand_gestures.html +102 -0
- package/skills/mudra-master/mudra-xr/assets/templates/meshes.html +47 -0
- package/skills/mudra-master/mudra-xr/assets/templates/planes.html +47 -0
- package/skills/mudra-master/mudra-xr/assets/templates/uikit.html +187 -0
- package/skills/mudra-master/mudra-xr/references/agent_protocol.json +878 -0
- package/skills/mudra-master/mudra-xr/references/promt.md +1516 -0
- package/skills/mudra-preview/SKILL.md +151 -0
- package/skills/mudra-preview/assets/ar-menu.html +382 -0
- package/skills/mudra-preview/assets/document-scroller.html +468 -0
- package/skills/mudra-preview/assets/drum-machine.html +788 -0
- package/skills/mudra-preview/assets/emg-visualizer.html +390 -0
- package/skills/mudra-preview/assets/generative-art.html +408 -0
- package/skills/mudra-preview/assets/gesture-assistant.html +342 -0
- package/skills/mudra-preview/assets/gesture-speech.html +444 -0
- package/skills/mudra-preview/assets/hands-free-desktop.html +376 -0
- package/skills/mudra-preview/assets/model-rotator.html +365 -0
- package/skills/mudra-preview/assets/mudra-duel.html +3200 -0
- package/skills/mudra-preview/assets/mudra-monitor.html +1451 -0
- package/skills/mudra-preview/assets/mudra-ultimate-template.html +891 -0
- package/skills/mudra-preview/assets/music-sequencer.html +455 -0
- package/skills/mudra-preview/assets/neural-pong.html +380 -0
- package/skills/mudra-preview/assets/neural-snake.html +364 -0
- package/skills/mudra-preview/assets/presentation-controller.html +345 -0
- package/skills/mudra-preview/assets/pressure-painter.html +285 -0
- package/skills/mudra-preview/assets/runner.html +1812 -0
- package/skills/mudra-preview/assets/smart-home.html +461 -0
- package/skills/mudra-preview/assets/space-invaders.html +1154 -0
- package/skills/mudra-preview/assets/waterful-ring-toss.html +1624 -0
- package/skills/mudra-preview/references/agent_protocol.json +646 -0
- package/skills/mudra-preview/references/promt.md +16829 -0
- package/skills/mudra-xr/SKILL.md +245 -0
- package/skills/mudra-xr/assets/demos/3dgs-walkthrough.html +242 -0
- package/skills/mudra-xr/assets/demos/aisimulator.html +792 -0
- package/skills/mudra-xr/assets/demos/balloonpop.html +422 -0
- package/skills/mudra-xr/assets/demos/ballpit.html +284 -0
- package/skills/mudra-xr/assets/demos/drone.html +41 -0
- package/skills/mudra-xr/assets/demos/gemini-icebreakers.html +338 -0
- package/skills/mudra-xr/assets/demos/gemini-xrobject.html +302 -0
- package/skills/mudra-xr/assets/demos/math3d.html +188 -0
- package/skills/mudra-xr/assets/demos/measure.html +247 -0
- package/skills/mudra-xr/assets/demos/occlusion.html +251 -0
- package/skills/mudra-xr/assets/demos/rain.html +498 -0
- package/skills/mudra-xr/assets/demos/screenwiper.html +432 -0
- package/skills/mudra-xr/assets/demos/splash.html +519 -0
- package/skills/mudra-xr/assets/demos/webcam_gestures.html +253 -0
- package/skills/mudra-xr/assets/demos/xremoji.html +833 -0
- package/skills/mudra-xr/assets/demos/xrpoet.html +151 -0
- package/skills/mudra-xr/assets/samples/depthmap.html +266 -0
- package/skills/mudra-xr/assets/samples/depthmesh.html +100 -0
- package/skills/mudra-xr/assets/samples/game_rps.html +1099 -0
- package/skills/mudra-xr/assets/samples/gestures_custom.html +467 -0
- package/skills/mudra-xr/assets/samples/gestures_heuristic.html +256 -0
- package/skills/mudra-xr/assets/samples/lighting.html +163 -0
- package/skills/mudra-xr/assets/samples/mesh_detection.html +46 -0
- package/skills/mudra-xr/assets/samples/modelviewer.html +183 -0
- package/skills/mudra-xr/assets/samples/paint.html +133 -0
- package/skills/mudra-xr/assets/samples/planar-vst.html +45 -0
- package/skills/mudra-xr/assets/samples/reticle.html +95 -0
- package/skills/mudra-xr/assets/samples/skybox_agent.html +384 -0
- package/skills/mudra-xr/assets/samples/sound.html +390 -0
- package/skills/mudra-xr/assets/samples/ui.html +496 -0
- package/skills/mudra-xr/assets/samples/virtual-screens.html +808 -0
- package/skills/mudra-xr/assets/templates/0_basic.html +64 -0
- package/skills/mudra-xr/assets/templates/1_ui.html +107 -0
- package/skills/mudra-xr/assets/templates/2_hands.html +183 -0
- package/skills/mudra-xr/assets/templates/3_depth.html +89 -0
- package/skills/mudra-xr/assets/templates/4_stereo.html +84 -0
- package/skills/mudra-xr/assets/templates/5_camera.html +117 -0
- package/skills/mudra-xr/assets/templates/6_ai.html +154 -0
- package/skills/mudra-xr/assets/templates/7_ai_live.html +253 -0
- package/skills/mudra-xr/assets/templates/8_objects.html +95 -0
- package/skills/mudra-xr/assets/templates/9_xr-toggle.html +90 -0
- package/skills/mudra-xr/assets/templates/heuristic_hand_gestures.html +102 -0
- package/skills/mudra-xr/assets/templates/meshes.html +47 -0
- package/skills/mudra-xr/assets/templates/planes.html +47 -0
- package/skills/mudra-xr/assets/templates/uikit.html +187 -0
- package/skills/mudra-xr/references/agent_protocol.json +878 -0
- 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)'">← 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>
|