solanapolis 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 (202) hide show
  1. package/README.md +518 -0
  2. package/bin/solanapolis.js +197 -0
  3. package/convex/_generated/api.d.ts +175 -0
  4. package/convex/_generated/api.js +23 -0
  5. package/convex/_generated/dataModel.d.ts +60 -0
  6. package/convex/_generated/server.d.ts +143 -0
  7. package/convex/_generated/server.js +93 -0
  8. package/convex/agent/conversation.ts +352 -0
  9. package/convex/agent/embeddingsCache.ts +110 -0
  10. package/convex/agent/memory.ts +450 -0
  11. package/convex/agent/schema.ts +53 -0
  12. package/convex/aiChat.ts +54 -0
  13. package/convex/aiTown/agent.ts +382 -0
  14. package/convex/aiTown/agentDescription.ts +27 -0
  15. package/convex/aiTown/agentInputs.ts +155 -0
  16. package/convex/aiTown/agentOperations.ts +178 -0
  17. package/convex/aiTown/conversation.ts +395 -0
  18. package/convex/aiTown/conversationMembership.ts +38 -0
  19. package/convex/aiTown/game.ts +371 -0
  20. package/convex/aiTown/ids.ts +32 -0
  21. package/convex/aiTown/inputHandler.ts +9 -0
  22. package/convex/aiTown/inputs.ts +25 -0
  23. package/convex/aiTown/insertInput.ts +20 -0
  24. package/convex/aiTown/location.ts +32 -0
  25. package/convex/aiTown/main.ts +154 -0
  26. package/convex/aiTown/movement.ts +189 -0
  27. package/convex/aiTown/player.ts +310 -0
  28. package/convex/aiTown/playerDescription.ts +35 -0
  29. package/convex/aiTown/schema.ts +79 -0
  30. package/convex/aiTown/world.ts +65 -0
  31. package/convex/aiTown/worldMap.ts +74 -0
  32. package/convex/chat.ts +79 -0
  33. package/convex/constants.ts +78 -0
  34. package/convex/convex.config.ts +6 -0
  35. package/convex/crons.ts +89 -0
  36. package/convex/engine/abstractGame.ts +199 -0
  37. package/convex/engine/historicalObject.ts +355 -0
  38. package/convex/engine/schema.ts +56 -0
  39. package/convex/http.ts +36 -0
  40. package/convex/init.ts +110 -0
  41. package/convex/messages.ts +53 -0
  42. package/convex/npcCarAgents.ts +415 -0
  43. package/convex/schema.ts +61 -0
  44. package/convex/streaming.ts +23 -0
  45. package/convex/testing.ts +202 -0
  46. package/convex/tsconfig.json +18 -0
  47. package/convex/util/FastIntegerCompression.ts +221 -0
  48. package/convex/util/assertNever.ts +4 -0
  49. package/convex/util/asyncMap.ts +20 -0
  50. package/convex/util/compression.ts +71 -0
  51. package/convex/util/geometry.ts +132 -0
  52. package/convex/util/isSimpleObject.ts +11 -0
  53. package/convex/util/llm.ts +724 -0
  54. package/convex/util/minheap.ts +38 -0
  55. package/convex/util/object.ts +22 -0
  56. package/convex/util/sleep.ts +3 -0
  57. package/convex/util/types.ts +33 -0
  58. package/convex/util/xxhash.ts +228 -0
  59. package/convex/world.ts +257 -0
  60. package/data/animations/campfire.json +45 -0
  61. package/data/animations/gentlesparkle.json +37 -0
  62. package/data/animations/gentlesplash.json +61 -0
  63. package/data/animations/gentlewaterfall.json +61 -0
  64. package/data/animations/windmill.json +78 -0
  65. package/data/characters.ts +121 -0
  66. package/data/convertMap.js +74 -0
  67. package/data/gentle.js +330 -0
  68. package/data/spritesheets/f1.ts +75 -0
  69. package/data/spritesheets/f2.ts +75 -0
  70. package/data/spritesheets/f3.ts +75 -0
  71. package/data/spritesheets/f4.ts +75 -0
  72. package/data/spritesheets/f5.ts +75 -0
  73. package/data/spritesheets/f6.ts +75 -0
  74. package/data/spritesheets/f7.ts +75 -0
  75. package/data/spritesheets/f8.ts +75 -0
  76. package/data/spritesheets/p1.ts +59 -0
  77. package/data/spritesheets/p2.ts +59 -0
  78. package/data/spritesheets/p3.ts +59 -0
  79. package/data/spritesheets/player.ts +59 -0
  80. package/data/spritesheets/types.ts +26 -0
  81. package/eslint.config.mjs +37 -0
  82. package/next.config.ts +7 -0
  83. package/package.json +85 -0
  84. package/postcss.config.mjs +7 -0
  85. package/public/file.svg +1 -0
  86. package/public/globe.svg +1 -0
  87. package/public/helius-icon.svg +84 -0
  88. package/public/helius-logo.svg +85 -0
  89. package/public/next.svg +1 -0
  90. package/public/plane.glb +0 -0
  91. package/public/vercel.svg +1 -0
  92. package/public/window.svg +1 -0
  93. package/scripts/clear-city.ts +74 -0
  94. package/scripts/seed-wallets.ts +185 -0
  95. package/scripts/setup-webhook.ts +73 -0
  96. package/src/app/api/auth/callback/route.ts +6 -0
  97. package/src/app/api/auth/link-wallet/route.ts +6 -0
  98. package/src/app/api/auth/phantom/route.ts +6 -0
  99. package/src/app/api/broadcast-position/route.ts +59 -0
  100. package/src/app/api/leaderboard/route.ts +85 -0
  101. package/src/app/api/network-stats/route.ts +86 -0
  102. package/src/app/api/parcel-reward/route.ts +181 -0
  103. package/src/app/api/queue-status/route.ts +30 -0
  104. package/src/app/api/snapshots/route.ts +37 -0
  105. package/src/app/api/transactions/enhanced/route.ts +57 -0
  106. package/src/app/api/treasury/route.ts +83 -0
  107. package/src/app/api/wallet/[address]/balances/route.ts +124 -0
  108. package/src/app/api/wallet/[address]/identity/route.ts +32 -0
  109. package/src/app/api/wallet/[address]/route.ts +216 -0
  110. package/src/app/api/wallet/[address]/traded-tokens/route.ts +41 -0
  111. package/src/app/api/wallets/route.ts +68 -0
  112. package/src/app/api/webhooks/helius/route.ts +76 -0
  113. package/src/app/auth/callback/page.tsx +29 -0
  114. package/src/app/favicon.ico +0 -0
  115. package/src/app/globals.css +39 -0
  116. package/src/app/layout.tsx +43 -0
  117. package/src/app/page.tsx +16 -0
  118. package/src/components/AITownNPCs.tsx +206 -0
  119. package/src/components/ActivityFeed.tsx +189 -0
  120. package/src/components/AuthPanel.tsx +163 -0
  121. package/src/components/BeachScene.tsx +280 -0
  122. package/src/components/Building.tsx +138 -0
  123. package/src/components/CesiumFlight.tsx +1768 -0
  124. package/src/components/CesiumGlobe.tsx +616 -0
  125. package/src/components/CitizenCard.tsx +442 -0
  126. package/src/components/CitizenCardModal.tsx +153 -0
  127. package/src/components/CityGrid.tsx +313 -0
  128. package/src/components/CityLandmarks.tsx +427 -0
  129. package/src/components/CityScene.tsx +1289 -0
  130. package/src/components/CitySlotsBadge.tsx +68 -0
  131. package/src/components/CockpitHUD.tsx +460 -0
  132. package/src/components/ConvexWrapper.tsx +19 -0
  133. package/src/components/DubaiDistrict.tsx +630 -0
  134. package/src/components/FlightMiniMap.tsx +133 -0
  135. package/src/components/GameChat.tsx +383 -0
  136. package/src/components/GameHUD.tsx +393 -0
  137. package/src/components/Ground.tsx +14 -0
  138. package/src/components/HowItWorksModal.tsx +251 -0
  139. package/src/components/IngestionBanner.tsx +123 -0
  140. package/src/components/InstancedBuildings.tsx +316 -0
  141. package/src/components/InstancedCars.tsx +504 -0
  142. package/src/components/InstancedCityPlanes.tsx +259 -0
  143. package/src/components/InstancedHouses.tsx +246 -0
  144. package/src/components/InstancedLampPosts.tsx +201 -0
  145. package/src/components/InstancedResidentCars.tsx +357 -0
  146. package/src/components/InstancedRoadDashes.tsx +42 -0
  147. package/src/components/InstancedSkyscrapers.tsx +434 -0
  148. package/src/components/InstancedTrees.tsx +67 -0
  149. package/src/components/LeaderboardPanel.tsx +136 -0
  150. package/src/components/MultiplayerPlanes.tsx +128 -0
  151. package/src/components/NetworkStats.tsx +83 -0
  152. package/src/components/NewBuildingSpotlight.tsx +93 -0
  153. package/src/components/ParcelChallengeBanner.tsx +242 -0
  154. package/src/components/ParcelReward.tsx +191 -0
  155. package/src/components/Park.tsx +42 -0
  156. package/src/components/PhantomWrapper.tsx +22 -0
  157. package/src/components/PixelStreamViewer.tsx +335 -0
  158. package/src/components/PlaneMode.tsx +190 -0
  159. package/src/components/PlayerCar.tsx +211 -0
  160. package/src/components/PlayerPlane.tsx +255 -0
  161. package/src/components/ProjectileRenderer.tsx +249 -0
  162. package/src/components/QueueStatusBanner.tsx +86 -0
  163. package/src/components/RealPlayerTags.tsx +82 -0
  164. package/src/components/SceneLighting.tsx +382 -0
  165. package/src/components/SelectionBeam.tsx +59 -0
  166. package/src/components/SwapPanel.tsx +104 -0
  167. package/src/components/SwapParticles.tsx +237 -0
  168. package/src/components/TreasureGate.tsx +505 -0
  169. package/src/components/WalletPanel.tsx +421 -0
  170. package/src/components/WalletSearch.tsx +244 -0
  171. package/src/components/WelcomeOverlay.tsx +135 -0
  172. package/src/components/WindowTooltip.tsx +498 -0
  173. package/src/context/AuthContext.tsx +230 -0
  174. package/src/lib/bot-detection.ts +125 -0
  175. package/src/lib/building-math.ts +136 -0
  176. package/src/lib/building-shader.ts +253 -0
  177. package/src/lib/car-paths.ts +244 -0
  178. package/src/lib/car-system.ts +182 -0
  179. package/src/lib/city-constants.ts +29 -0
  180. package/src/lib/city-slots.ts +35 -0
  181. package/src/lib/city-zoning.ts +64 -0
  182. package/src/lib/collision-map.ts +147 -0
  183. package/src/lib/day-night.ts +252 -0
  184. package/src/lib/export-card.ts +28 -0
  185. package/src/lib/helius-webhook.ts +90 -0
  186. package/src/lib/helius.ts +74 -0
  187. package/src/lib/house-shader.ts +119 -0
  188. package/src/lib/mock-data.ts +56 -0
  189. package/src/lib/multiplayer-manager.ts +329 -0
  190. package/src/lib/plane-physics.ts +66 -0
  191. package/src/lib/player-car.ts +147 -0
  192. package/src/lib/player-plane.ts +200 -0
  193. package/src/lib/projectile-system.ts +272 -0
  194. package/src/lib/skyscraper-types.ts +52 -0
  195. package/src/lib/sound-engine.ts +464 -0
  196. package/src/lib/supabase-admin.ts +9 -0
  197. package/src/lib/supabase.ts +8 -0
  198. package/src/lib/swap-events.ts +70 -0
  199. package/src/middleware.ts +37 -0
  200. package/src/types/phantom.d.ts +16 -0
  201. package/src/types/wallet.ts +20 -0
  202. package/tsconfig.json +34 -0
@@ -0,0 +1,393 @@
1
+ "use client";
2
+
3
+ import { useRef, useEffect, MutableRefObject, useCallback } from "react";
4
+ import { ProjectileManager } from "@/lib/projectile-system";
5
+ import { GRID_WORLD } from "@/lib/city-constants";
6
+ import type { RemotePlayer } from "@/lib/multiplayer-manager";
7
+
8
+ interface GameHUDProps {
9
+ active: boolean;
10
+ mode: "car" | "plane";
11
+ manager: ProjectileManager;
12
+ playerPosRef: MutableRefObject<[number, number, number] | null>;
13
+ playerHeadingRef: MutableRefObject<number>;
14
+ planePositionsRef: MutableRefObject<Array<{ x: number; y: number; z: number }>>;
15
+ carPositionsRef: MutableRefObject<Array<{ x: number; y: number; z: number }>>;
16
+ /** Real multiplayer players currently active in combat */
17
+ remotePlayers: Map<string, RemotePlayer>;
18
+ /** Building center positions for minimap dots */
19
+ buildingPositions: Array<{ x: number; z: number }>;
20
+ }
21
+
22
+ const MAP_SIZE = 200; // pixels
23
+ const MAP_PADDING = 16;
24
+ const WORLD_HALF = GRID_WORLD / 2 + 30; // slight margin
25
+ const SCALE = MAP_SIZE / (WORLD_HALF * 2);
26
+
27
+ /** Convert world coords to minimap pixel coords */
28
+ function toMapXY(wx: number, wz: number): [number, number] {
29
+ const mx = (wx + WORLD_HALF) * SCALE;
30
+ const my = (wz + WORLD_HALF) * SCALE;
31
+ return [mx, my];
32
+ }
33
+
34
+ export default function GameHUD({
35
+ active,
36
+ mode,
37
+ manager,
38
+ playerPosRef,
39
+ playerHeadingRef,
40
+ planePositionsRef,
41
+ carPositionsRef,
42
+ remotePlayers,
43
+ buildingPositions,
44
+ }: GameHUDProps) {
45
+ const canvasRef = useRef<HTMLCanvasElement>(null);
46
+ const animRef = useRef<number>(0);
47
+ const scoreDisplayRef = useRef(0);
48
+
49
+ const draw = useCallback(() => {
50
+ const canvas = canvasRef.current;
51
+ if (!canvas) return;
52
+ const ctx = canvas.getContext("2d");
53
+ if (!ctx) return;
54
+
55
+ const size = MAP_SIZE * 2; // retina
56
+ canvas.width = size;
57
+ canvas.height = size;
58
+ ctx.scale(2, 2);
59
+ const now = performance.now();
60
+
61
+ // Background
62
+ ctx.fillStyle = "rgba(10, 10, 18, 0.85)";
63
+ ctx.fillRect(0, 0, MAP_SIZE, MAP_SIZE);
64
+
65
+ // Grid lines
66
+ ctx.strokeStyle = "rgba(255,255,255,0.04)";
67
+ ctx.lineWidth = 0.5;
68
+ for (let i = 0; i <= 10; i++) {
69
+ const p = (i / 10) * MAP_SIZE;
70
+ ctx.beginPath(); ctx.moveTo(p, 0); ctx.lineTo(p, MAP_SIZE); ctx.stroke();
71
+ ctx.beginPath(); ctx.moveTo(0, p); ctx.lineTo(MAP_SIZE, p); ctx.stroke();
72
+ }
73
+
74
+ // Buildings — tiny gray dots
75
+ ctx.fillStyle = "rgba(255,255,255,0.12)";
76
+ for (const b of buildingPositions) {
77
+ const [mx, my] = toMapXY(b.x, b.z);
78
+ ctx.fillRect(mx - 0.5, my - 0.5, 1, 1);
79
+ }
80
+
81
+ // Resident cars — blue dots
82
+ ctx.fillStyle = "#6366f1";
83
+ for (const c of carPositionsRef.current) {
84
+ const [mx, my] = toMapXY(c.x, c.z);
85
+ ctx.beginPath();
86
+ ctx.arc(mx, my, 1.5, 0, Math.PI * 2);
87
+ ctx.fill();
88
+ }
89
+
90
+ // Resident planes — orange dots
91
+ ctx.fillStyle = "#f59e0b";
92
+ for (const p of planePositionsRef.current) {
93
+ const [mx, my] = toMapXY(p.x, p.z);
94
+ ctx.beginPath();
95
+ ctx.arc(mx, my, 2, 0, Math.PI * 2);
96
+ ctx.fill();
97
+ }
98
+
99
+ // Real humans (multiplayer) — bright pulsing markers with heading
100
+ let humanCars = 0;
101
+ let humanPlanes = 0;
102
+ for (const remote of remotePlayers.values()) {
103
+ if (remote.mode === "spectating") continue;
104
+
105
+ const [mx, my] = toMapXY(remote.x, remote.z);
106
+ if (mx < -6 || my < -6 || mx > MAP_SIZE + 6 || my > MAP_SIZE + 6) continue;
107
+
108
+ const isCar = remote.mode === "car";
109
+ if (isCar) humanCars += 1;
110
+ else humanPlanes += 1;
111
+
112
+ // Pulse ring to draw attention to human players
113
+ const pulsePhase = ((now + remote.lastSeen) % 1400) / 1400;
114
+ const pulseR = (isCar ? 4 : 4.8) + pulsePhase * 3;
115
+ const pulseAlpha = 0.55 * (1 - pulsePhase);
116
+ ctx.strokeStyle = isCar
117
+ ? `rgba(56, 189, 248, ${pulseAlpha})`
118
+ : `rgba(236, 72, 153, ${pulseAlpha})`;
119
+ ctx.lineWidth = 1;
120
+ ctx.beginPath();
121
+ ctx.arc(mx, my, pulseR, 0, Math.PI * 2);
122
+ ctx.stroke();
123
+
124
+ // Direction tick
125
+ const hx = mx + Math.sin(remote.heading) * 5.5;
126
+ const hy = my - Math.cos(remote.heading) * 5.5;
127
+ ctx.strokeStyle = isCar ? "rgba(56, 189, 248, 0.9)" : "rgba(236, 72, 153, 0.9)";
128
+ ctx.beginPath();
129
+ ctx.moveTo(mx, my);
130
+ ctx.lineTo(hx, hy);
131
+ ctx.stroke();
132
+
133
+ if (isCar) {
134
+ // Diamond = human driver
135
+ ctx.fillStyle = "#38bdf8";
136
+ ctx.beginPath();
137
+ ctx.moveTo(mx, my - 3.5);
138
+ ctx.lineTo(mx + 3.5, my);
139
+ ctx.lineTo(mx, my + 3.5);
140
+ ctx.lineTo(mx - 3.5, my);
141
+ ctx.closePath();
142
+ ctx.fill();
143
+ } else {
144
+ // Triangle = human pilot
145
+ ctx.save();
146
+ ctx.translate(mx, my);
147
+ ctx.rotate(remote.heading);
148
+ ctx.fillStyle = "#ec4899";
149
+ ctx.beginPath();
150
+ ctx.moveTo(0, -4.5);
151
+ ctx.lineTo(-3.2, 3.2);
152
+ ctx.lineTo(3.2, 3.2);
153
+ ctx.closePath();
154
+ ctx.fill();
155
+ ctx.restore();
156
+ }
157
+
158
+ // White stroke for readability on dark/light minimap areas
159
+ ctx.strokeStyle = "rgba(255, 255, 255, 0.8)";
160
+ ctx.lineWidth = 0.8;
161
+ ctx.beginPath();
162
+ ctx.arc(mx, my, 3.8, 0, Math.PI * 2);
163
+ ctx.stroke();
164
+ }
165
+
166
+ // Human count label
167
+ const totalHumans = humanCars + humanPlanes;
168
+ if (totalHumans > 0) {
169
+ ctx.fillStyle = "rgba(255, 255, 255, 0.9)";
170
+ ctx.font = "600 9px monospace";
171
+ ctx.textAlign = "left";
172
+ ctx.fillText(`HUMANS ${totalHumans}`, 6, 11);
173
+ }
174
+
175
+ // Active projectiles — red dots
176
+ ctx.fillStyle = "#ff4400";
177
+ for (const p of manager.getAlive()) {
178
+ const [mx, my] = toMapXY(p.x, p.z);
179
+ ctx.beginPath();
180
+ ctx.arc(mx, my, 1.5, 0, Math.PI * 2);
181
+ ctx.fill();
182
+ }
183
+
184
+ // Hit locations — expanding rings
185
+ ctx.strokeStyle = "#ff4400";
186
+ ctx.lineWidth = 1;
187
+ for (const h of manager.hits) {
188
+ const age = (now - h.timestamp) / 1000;
189
+ if (age > 2) continue;
190
+ const [mx, my] = toMapXY(h.x, h.z);
191
+ const radius = 3 + age * 15;
192
+ const alpha = 1 - age / 2;
193
+ ctx.globalAlpha = alpha;
194
+ ctx.beginPath();
195
+ ctx.arc(mx, my, radius, 0, Math.PI * 2);
196
+ ctx.stroke();
197
+ }
198
+ ctx.globalAlpha = 1;
199
+
200
+ // Player position — large arrow
201
+ const pos = playerPosRef.current;
202
+ if (pos) {
203
+ const [px, py] = toMapXY(pos[0], pos[2]);
204
+ const heading = playerHeadingRef.current;
205
+
206
+ ctx.save();
207
+ ctx.translate(px, py);
208
+ ctx.rotate(heading);
209
+
210
+ // FOV cone
211
+ ctx.fillStyle = "rgba(227, 89, 48, 0.12)";
212
+ ctx.beginPath();
213
+ ctx.moveTo(0, 0);
214
+ const coneLen = 40;
215
+ const coneAngle = 0.4;
216
+ ctx.lineTo(Math.sin(-coneAngle) * coneLen, -Math.cos(-coneAngle) * coneLen);
217
+ ctx.lineTo(0, -coneLen * 0.8);
218
+ ctx.lineTo(Math.sin(coneAngle) * coneLen, -Math.cos(coneAngle) * coneLen);
219
+ ctx.closePath();
220
+ ctx.fill();
221
+
222
+ // Player triangle
223
+ ctx.fillStyle = "#E35930";
224
+ ctx.beginPath();
225
+ ctx.moveTo(0, -6);
226
+ ctx.lineTo(-4, 4);
227
+ ctx.lineTo(4, 4);
228
+ ctx.closePath();
229
+ ctx.fill();
230
+
231
+ // White outline
232
+ ctx.strokeStyle = "rgba(255,255,255,0.5)";
233
+ ctx.lineWidth = 0.8;
234
+ ctx.stroke();
235
+
236
+ ctx.restore();
237
+
238
+ // Pulsing ring around player
239
+ const pulsePhase = (now % 2000) / 2000;
240
+ const pulseR = 6 + pulsePhase * 8;
241
+ const pulseAlpha = 1 - pulsePhase;
242
+ ctx.strokeStyle = `rgba(227, 89, 48, ${pulseAlpha * 0.4})`;
243
+ ctx.lineWidth = 1;
244
+ ctx.beginPath();
245
+ ctx.arc(px, py, pulseR, 0, Math.PI * 2);
246
+ ctx.stroke();
247
+ }
248
+
249
+ // Border
250
+ ctx.strokeStyle = "rgba(255,255,255,0.08)";
251
+ ctx.lineWidth = 1;
252
+ ctx.strokeRect(0.5, 0.5, MAP_SIZE - 1, MAP_SIZE - 1);
253
+
254
+ // Smooth score counter
255
+ scoreDisplayRef.current += (manager.score - scoreDisplayRef.current) * 0.1;
256
+
257
+ animRef.current = requestAnimationFrame(draw);
258
+ }, [manager, playerPosRef, playerHeadingRef, planePositionsRef, carPositionsRef, remotePlayers, buildingPositions]);
259
+
260
+ useEffect(() => {
261
+ if (!active) return;
262
+ animRef.current = requestAnimationFrame(draw);
263
+ return () => cancelAnimationFrame(animRef.current);
264
+ }, [active, draw]);
265
+
266
+ if (!active) return null;
267
+
268
+ const score = Math.round(scoreDisplayRef.current);
269
+ const alive = manager.getAlive().length;
270
+
271
+ return (
272
+ <>
273
+ {/* Minimap — top right, below auth buttons */}
274
+ <div
275
+ className="fixed z-50"
276
+ style={{
277
+ top: 60,
278
+ right: MAP_PADDING,
279
+ width: MAP_SIZE,
280
+ height: MAP_SIZE,
281
+ borderRadius: 12,
282
+ overflow: "hidden",
283
+ border: "1px solid rgba(255,255,255,0.08)",
284
+ boxShadow: "0 8px 32px rgba(0,0,0,0.5)",
285
+ }}
286
+ >
287
+ <canvas
288
+ ref={canvasRef}
289
+ style={{ width: MAP_SIZE, height: MAP_SIZE }}
290
+ />
291
+
292
+ {/* Map legend */}
293
+ <div className="absolute bottom-1.5 left-2 flex gap-2 items-center">
294
+ <div className="flex items-center gap-1">
295
+ <div className="w-1.5 h-1.5 rounded-full bg-[#f59e0b]" />
296
+ <span className="text-[8px] text-white/30">Planes</span>
297
+ </div>
298
+ <div className="flex items-center gap-1">
299
+ <div className="w-1.5 h-1.5 rounded-full bg-[#6366f1]" />
300
+ <span className="text-[8px] text-white/30">Cars</span>
301
+ </div>
302
+ <div className="flex items-center gap-1">
303
+ <div className="w-1.5 h-1.5 rotate-45 bg-sky-400" />
304
+ <span className="text-[8px] text-sky-300/80">Human car</span>
305
+ </div>
306
+ <div className="flex items-center gap-1">
307
+ <div className="w-0 h-0 border-l-[3px] border-r-[3px] border-b-[5px] border-l-transparent border-r-transparent border-b-pink-400" />
308
+ <span className="text-[8px] text-pink-300/80">Human plane</span>
309
+ </div>
310
+ <div className="flex items-center gap-1">
311
+ <div className="w-1.5 h-1.5 rounded-full bg-[#E35930]" />
312
+ <span className="text-[8px] text-white/30">You</span>
313
+ </div>
314
+ </div>
315
+ </div>
316
+
317
+ {/* Score + Ammo HUD — top center */}
318
+ <div className="fixed top-4 left-1/2 -translate-x-1/2 z-50 flex items-center gap-4 bg-black/60 backdrop-blur-xl border border-white/[0.08] rounded-2xl px-5 py-2.5">
319
+ {/* Crosshair icon */}
320
+ <div className="flex flex-col items-center">
321
+ <span className="text-[9px] text-white/25 uppercase tracking-wider">Mode</span>
322
+ <span className="text-sm font-bold text-[#E35930]">
323
+ {mode === "plane" ? "✈ AIR" : "🚗 GROUND"}
324
+ </span>
325
+ </div>
326
+
327
+ <div className="w-px h-8 bg-white/[0.08]" />
328
+
329
+ {/* Score */}
330
+ <div className="flex flex-col items-center">
331
+ <span className="text-[9px] text-white/25 uppercase tracking-wider">Score</span>
332
+ <span className="text-lg font-bold text-white tabular-nums" style={{ fontVariantNumeric: "tabular-nums" }}>
333
+ {score.toLocaleString()}
334
+ </span>
335
+ </div>
336
+
337
+ <div className="w-px h-8 bg-white/[0.08]" />
338
+
339
+ {/* Hits */}
340
+ <div className="flex flex-col items-center">
341
+ <span className="text-[9px] text-white/25 uppercase tracking-wider">Hits</span>
342
+ <span className="text-sm font-semibold text-[#22c55e] tabular-nums">
343
+ {manager.hits.length}
344
+ </span>
345
+ </div>
346
+
347
+ <div className="w-px h-8 bg-white/[0.08]" />
348
+
349
+ {/* Active projectiles */}
350
+ <div className="flex flex-col items-center">
351
+ <span className="text-[9px] text-white/25 uppercase tracking-wider">Active</span>
352
+ <span className="text-sm font-semibold text-[#f59e0b] tabular-nums">
353
+ {alive}
354
+ </span>
355
+ </div>
356
+
357
+ <div className="w-px h-8 bg-white/[0.08]" />
358
+
359
+ {/* Fire key hint */}
360
+ <div className="flex flex-col items-center">
361
+ <span className="text-[9px] text-white/25 uppercase tracking-wider">Fire</span>
362
+ <span className="text-[11px] text-white/50 font-medium">F / Click</span>
363
+ </div>
364
+ </div>
365
+
366
+ {/* Crosshair — center of screen */}
367
+ <div className="fixed inset-0 z-40 pointer-events-none flex items-center justify-center">
368
+ <div className="relative w-8 h-8">
369
+ {/* Cross lines */}
370
+ <div className="absolute top-1/2 left-0 w-full h-px bg-white/20" />
371
+ <div className="absolute left-1/2 top-0 h-full w-px bg-white/20" />
372
+ {/* Center dot */}
373
+ <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-1 h-1 rounded-full bg-[#E35930]" />
374
+ {/* Corner brackets */}
375
+ <div className="absolute top-0 left-0 w-2 h-2 border-t border-l border-white/25" />
376
+ <div className="absolute top-0 right-0 w-2 h-2 border-t border-r border-white/25" />
377
+ <div className="absolute bottom-0 left-0 w-2 h-2 border-b border-l border-white/25" />
378
+ <div className="absolute bottom-0 right-0 w-2 h-2 border-b border-r border-white/25" />
379
+ </div>
380
+ </div>
381
+
382
+ {/* Hit markers — flash on screen when you hit something */}
383
+ {manager.hits.filter(h => performance.now() - h.timestamp < 500).map((h, i) => (
384
+ <div
385
+ key={`hit-${h.timestamp}-${i}`}
386
+ className="fixed inset-0 z-40 pointer-events-none flex items-center justify-center"
387
+ >
388
+ <div className="text-[#ff4400] text-3xl font-bold animate-ping">✕</div>
389
+ </div>
390
+ ))}
391
+ </>
392
+ );
393
+ }
@@ -0,0 +1,14 @@
1
+ "use client";
2
+
3
+ interface GroundProps {
4
+ size: number;
5
+ }
6
+
7
+ export default function Ground({ size }: GroundProps) {
8
+ return (
9
+ <mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.1, 0]} receiveShadow>
10
+ <planeGeometry args={[size, size]} />
11
+ <meshStandardMaterial color="#4a4a65" />
12
+ </mesh>
13
+ );
14
+ }
@@ -0,0 +1,251 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { createPortal } from "react-dom";
5
+
6
+ const HELIUS_DOCS = "https://docs.helius.dev";
7
+
8
+ const DATA_SOURCES = [
9
+ {
10
+ category: "Buildings",
11
+ items: [
12
+ {
13
+ visual: "Building height",
14
+ dataPoint: "Transaction count",
15
+ api: "Enhanced Transactions",
16
+ endpoint: "getTransactionsForAddress",
17
+ doc: `${HELIUS_DOCS}/api-reference/enhanced-transactions`,
18
+ detail: "Log-scaled txn count mapped to 1-150 floors",
19
+ },
20
+ {
21
+ visual: "Building width",
22
+ dataPoint: "Volume traded (SOL)",
23
+ api: "Enhanced Transactions",
24
+ endpoint: "getTransactionsForAddress",
25
+ doc: `${HELIUS_DOCS}/api-reference/enhanced-transactions`,
26
+ detail: "Square root-scaled relative to highest volume wallet",
27
+ },
28
+ {
29
+ visual: "Lit windows",
30
+ dataPoint: "Unique tokens swapped & recency",
31
+ api: "Enhanced Transactions",
32
+ endpoint: "getTransactionsForAddress",
33
+ doc: `${HELIUS_DOCS}/api-reference/enhanced-transactions`,
34
+ detail: "Fill ratio from token diversity, brightness from last activity",
35
+ },
36
+ ],
37
+ },
38
+ {
39
+ category: "Wallet Info",
40
+ items: [
41
+ {
42
+ visual: "Wallet age",
43
+ dataPoint: "First funding timestamp",
44
+ api: "Wallet API",
45
+ endpoint: "getWalletFundedBy",
46
+ doc: `${HELIUS_DOCS}/api-reference/wallet-api`,
47
+ detail: "Days since first SOL transfer to this wallet",
48
+ },
49
+ {
50
+ visual: "Wallet identity",
51
+ dataPoint: "Known entity labels",
52
+ api: "Wallet API",
53
+ endpoint: "getWalletIdentity",
54
+ doc: `${HELIUS_DOCS}/api-reference/wallet-api`,
55
+ detail: "Recognizes exchanges, protocols, and notable wallets",
56
+ },
57
+ {
58
+ visual: "Token balances & prices",
59
+ dataPoint: "Fungible asset holdings",
60
+ api: "DAS API",
61
+ endpoint: "getAssetsByOwner",
62
+ doc: `${HELIUS_DOCS}/api-reference/digital-asset-standard`,
63
+ detail: "Live balances with metadata, images, and price info",
64
+ },
65
+ {
66
+ visual: "Token metadata",
67
+ dataPoint: "Name, symbol, image per mint",
68
+ api: "DAS API",
69
+ endpoint: "getAssetsByOwner",
70
+ doc: `${HELIUS_DOCS}/api-reference/digital-asset-standard`,
71
+ detail: "Token names, symbols, and images returned alongside balances",
72
+ },
73
+ ],
74
+ },
75
+ {
76
+ category: "Live Activity",
77
+ items: [
78
+ {
79
+ visual: "Cars on roads",
80
+ dataPoint: "Real-time swap events",
81
+ api: "Webhooks",
82
+ endpoint: "Enhanced Webhooks",
83
+ doc: `${HELIUS_DOCS}/api-reference/webhooks`,
84
+ detail: "SWAP events pushed to the server, cars spawn near the swapping wallet's building",
85
+ },
86
+ ],
87
+ },
88
+ ] as const;
89
+
90
+ const INFRA = [
91
+ { label: "RPC", value: "Helius Mainnet RPC", doc: `${HELIUS_DOCS}/api-reference/rpc` },
92
+ { label: "Database", value: "Supabase (PostgreSQL)", doc: null },
93
+ { label: "Realtime", value: "Supabase Realtime", doc: null },
94
+ { label: "Frontend", value: "React Three Fiber + Next.js", doc: null },
95
+ ] as const;
96
+
97
+ export default function HowItWorksModal() {
98
+ const [open, setOpen] = useState(false);
99
+
100
+ return (
101
+ <>
102
+ <button
103
+ onClick={() => setOpen(true)}
104
+ className="w-6 h-6 flex items-center justify-center rounded-lg text-white/30 hover:text-white/60 hover:bg-white/[0.06] transition-colors cursor-pointer"
105
+ aria-label="How it works"
106
+ >
107
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
108
+ <circle cx="8" cy="8" r="6.5" />
109
+ <path d="M6.5 6.5a1.5 1.5 0 1 1 1.5 1.5v1" />
110
+ <circle cx="8" cy="11.5" r="0.5" fill="currentColor" stroke="none" />
111
+ </svg>
112
+ </button>
113
+
114
+ {open && createPortal(
115
+ <div
116
+ className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
117
+ onClick={() => setOpen(false)}
118
+ >
119
+ <div
120
+ className="relative w-full max-w-lg mx-4 max-h-[80vh] overflow-y-auto bg-[#0e0e1a]/95 backdrop-blur-xl border border-white/[0.08] rounded-2xl p-6"
121
+ onClick={(e) => e.stopPropagation()}
122
+ >
123
+ {/* Header */}
124
+ <div className="flex items-center justify-between mb-5">
125
+ <div>
126
+ <h2 className="text-base font-semibold text-white">How It Works</h2>
127
+ <p className="text-xs text-white/35 mt-0.5">Where the data comes from</p>
128
+ </div>
129
+ <button
130
+ onClick={() => setOpen(false)}
131
+ className="w-7 h-7 flex items-center justify-center rounded-lg text-white/30 hover:text-white hover:bg-white/[0.06] transition-colors cursor-pointer"
132
+ >
133
+ <svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="1.5">
134
+ <path d="M1 1l8 8M9 1l-8 8" />
135
+ </svg>
136
+ </button>
137
+ </div>
138
+
139
+ {/* Install Helius */}
140
+ <div className="mb-5">
141
+ <h3 className="text-xs font-semibold text-white/40 uppercase tracking-wider mb-2.5">
142
+ Install Helius
143
+ </h3>
144
+ <div className="space-y-2">
145
+ {[
146
+ { label: "CLI", command: "npm install -g helius-cli" },
147
+ { label: "MCP", command: "claude mcp add helius npx helius-mcp@latest" },
148
+ { label: "Marketplace", command: "/plugin marketplace add helius-labs/core-ai" },
149
+ { label: "Plugin", command: "/plugin install helius@helius-labs" },
150
+ ].map((item) => (
151
+ <div
152
+ key={item.label}
153
+ className="bg-white/[0.03] border border-white/[0.06] rounded-xl px-3.5 py-3"
154
+ >
155
+ <div className="text-xs text-white/35 mb-1">{item.label}</div>
156
+ <code className="text-xs text-orange-300/80 font-mono">{item.command}</code>
157
+ </div>
158
+ ))}
159
+ </div>
160
+ </div>
161
+
162
+ {/* Data sources */}
163
+ <div className="space-y-5">
164
+ {DATA_SOURCES.map((group) => (
165
+ <div key={group.category}>
166
+ <h3 className="text-xs font-semibold text-white/40 uppercase tracking-wider mb-2.5">
167
+ {group.category}
168
+ </h3>
169
+ <div className="space-y-2">
170
+ {group.items.map((item) => (
171
+ <div
172
+ key={item.visual}
173
+ className="bg-white/[0.03] border border-white/[0.06] rounded-xl px-3.5 py-3"
174
+ >
175
+ <div className="flex items-start justify-between gap-3">
176
+ <div className="min-w-0">
177
+ <div className="text-sm text-white/80">{item.visual}</div>
178
+ <div className="text-xs text-white/35 mt-0.5">{item.detail}</div>
179
+ </div>
180
+ <a
181
+ href={item.doc}
182
+ target="_blank"
183
+ rel="noopener noreferrer"
184
+ className="shrink-0 px-2 py-1 bg-orange-500/10 border border-orange-500/15 rounded-lg text-xs text-orange-300/80 hover:text-orange-200 hover:bg-orange-500/15 transition-colors"
185
+ >
186
+ {item.endpoint}
187
+ </a>
188
+ </div>
189
+ </div>
190
+ ))}
191
+ </div>
192
+ </div>
193
+ ))}
194
+ </div>
195
+
196
+ {/* Infrastructure */}
197
+ <div className="mt-5 pt-4 border-t border-white/[0.06]">
198
+ <h3 className="text-xs font-semibold text-white/40 uppercase tracking-wider mb-2.5">
199
+ Infrastructure
200
+ </h3>
201
+ <div className="grid grid-cols-2 gap-2">
202
+ {INFRA.map((item) => (
203
+ <div
204
+ key={item.label}
205
+ className="bg-white/[0.03] border border-white/[0.06] rounded-xl px-3 py-2.5"
206
+ >
207
+ <div className="text-xs text-white/35">{item.label}</div>
208
+ {item.doc ? (
209
+ <a
210
+ href={item.doc}
211
+ target="_blank"
212
+ rel="noopener noreferrer"
213
+ className="text-sm text-white/70 hover:text-white transition-colors"
214
+ >
215
+ {item.value}
216
+ </a>
217
+ ) : (
218
+ <div className="text-sm text-white/70">{item.value}</div>
219
+ )}
220
+ </div>
221
+ ))}
222
+ </div>
223
+ </div>
224
+
225
+ {/* Footer */}
226
+ <div className="mt-5 pt-4 border-t border-white/[0.06] flex items-center justify-between">
227
+ <a
228
+ href="https://www.helius.dev/"
229
+ target="_blank"
230
+ rel="noopener noreferrer"
231
+ className="flex items-center gap-2 text-xs text-white/30 hover:text-white/50 transition-colors"
232
+ >
233
+ <img src="/helius-icon.svg" alt="Helius" className="w-4 h-4" />
234
+ <span>Powered by Helius</span>
235
+ </a>
236
+ <a
237
+ href={HELIUS_DOCS}
238
+ target="_blank"
239
+ rel="noopener noreferrer"
240
+ className="text-xs text-blue-400/60 hover:text-blue-400 transition-colors"
241
+ >
242
+ Full API Docs &rarr;
243
+ </a>
244
+ </div>
245
+ </div>
246
+ </div>,
247
+ document.body,
248
+ )}
249
+ </>
250
+ );
251
+ }