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,249 @@
1
+ "use client";
2
+
3
+ import { useRef, useMemo, useEffect, MutableRefObject } from "react";
4
+ import { useFrame } from "@react-three/fiber";
5
+ import * as THREE from "three";
6
+ import { ProjectileManager, HitEvent } from "@/lib/projectile-system";
7
+ import { playGunshot, playExplosion, playScoreChime, initAudio } from "@/lib/sound-engine";
8
+
9
+ interface ProjectileRendererProps {
10
+ manager: ProjectileManager;
11
+ /** Positions of all resident planes (for hit detection) */
12
+ planePositionsRef: MutableRefObject<Array<{ x: number; y: number; z: number }>>;
13
+ /** Positions of all resident cars (for hit detection) */
14
+ carPositionsRef: MutableRefObject<Array<{ x: number; y: number; z: number }>>;
15
+ /** Is the player actively in car or plane mode? */
16
+ active: boolean;
17
+ /** Player position ref (car or plane) */
18
+ playerPosRef: MutableRefObject<[number, number, number] | null>;
19
+ /** Player heading ref */
20
+ playerHeadingRef: MutableRefObject<number>;
21
+ /** Player pitch (planes only, 0 for cars) */
22
+ playerPitchRef?: MutableRefObject<number>;
23
+ /** "car" | "plane" */
24
+ mode: "car" | "plane";
25
+ }
26
+
27
+ const MAX_VISIBLE = 50;
28
+ const MAX_EXPLOSIONS = 20;
29
+ const _dummy = new THREE.Object3D();
30
+
31
+ export default function ProjectileRenderer({
32
+ manager,
33
+ planePositionsRef,
34
+ carPositionsRef,
35
+ active,
36
+ playerPosRef,
37
+ playerHeadingRef,
38
+ playerPitchRef,
39
+ mode,
40
+ }: ProjectileRendererProps) {
41
+ const bulletRef = useRef<THREE.InstancedMesh>(null);
42
+ const trailRef = useRef<THREE.InstancedMesh>(null);
43
+ const explosionRef = useRef<THREE.InstancedMesh>(null);
44
+ const fireKeyRef = useRef(false);
45
+ const prevHitCountRef = useRef(0);
46
+ const lastFireSoundRef = useRef(0);
47
+
48
+ // Materials
49
+ const bulletMat = useMemo(() => new THREE.MeshStandardMaterial({
50
+ color: "#ff4400",
51
+ emissive: "#ff6600",
52
+ emissiveIntensity: 3.0,
53
+ }), []);
54
+
55
+ const trailMat = useMemo(() => new THREE.MeshStandardMaterial({
56
+ color: "#ff8800",
57
+ emissive: "#ff6600",
58
+ emissiveIntensity: 2.0,
59
+ transparent: true,
60
+ opacity: 0.5,
61
+ }), []);
62
+
63
+ const explosionMat = useMemo(() => new THREE.MeshStandardMaterial({
64
+ color: "#ffaa00",
65
+ emissive: "#ff4400",
66
+ emissiveIntensity: 4.0,
67
+ transparent: true,
68
+ opacity: 0.8,
69
+ }), []);
70
+
71
+ // Keyboard: fire on F or left-click
72
+ useEffect(() => {
73
+ if (!active) return;
74
+
75
+ function onKeyDown(e: KeyboardEvent) {
76
+ if (e.key === "f" || e.key === "F") fireKeyRef.current = true;
77
+ }
78
+ function onKeyUp(e: KeyboardEvent) {
79
+ if (e.key === "f" || e.key === "F") fireKeyRef.current = false;
80
+ }
81
+ function onMouseDown(e: MouseEvent) {
82
+ if (e.button === 0) fireKeyRef.current = true;
83
+ }
84
+ function onMouseUp(e: MouseEvent) {
85
+ if (e.button === 0) fireKeyRef.current = false;
86
+ }
87
+
88
+ window.addEventListener("keydown", onKeyDown);
89
+ window.addEventListener("keyup", onKeyUp);
90
+ window.addEventListener("mousedown", onMouseDown);
91
+ window.addEventListener("mouseup", onMouseUp);
92
+
93
+ return () => {
94
+ window.removeEventListener("keydown", onKeyDown);
95
+ window.removeEventListener("keyup", onKeyUp);
96
+ window.removeEventListener("mousedown", onMouseDown);
97
+ window.removeEventListener("mouseup", onMouseUp);
98
+ fireKeyRef.current = false;
99
+ };
100
+ }, [active]);
101
+
102
+ useFrame((_, delta) => {
103
+ if (!active) return;
104
+ const pos = playerPosRef.current;
105
+ if (!pos) return;
106
+
107
+ // Fire projectiles
108
+ if (fireKeyRef.current) {
109
+ const pitch = playerPitchRef?.current ?? 0;
110
+ manager.fire(pos[0], pos[1], pos[2], playerHeadingRef.current, pitch, mode);
111
+ // Gunshot sound (throttled to ~5/sec max)
112
+ const now = performance.now();
113
+ if (now - lastFireSoundRef.current > 200) {
114
+ initAudio();
115
+ playGunshot();
116
+ lastFireSoundRef.current = now;
117
+ }
118
+ }
119
+
120
+ manager.update(
121
+ delta,
122
+ planePositionsRef.current,
123
+ carPositionsRef.current,
124
+ );
125
+
126
+ // Play explosion + score chime on new hits
127
+ if (manager.hits.length > prevHitCountRef.current) {
128
+ playExplosion();
129
+ playScoreChime();
130
+ }
131
+ prevHitCountRef.current = manager.hits.length;
132
+
133
+ // Render bullets
134
+ const bullets = bulletRef.current;
135
+ const trails = trailRef.current;
136
+ const alive = manager.getAlive();
137
+
138
+ if (bullets && trails) {
139
+ for (let i = 0; i < MAX_VISIBLE; i++) {
140
+ if (i < alive.length) {
141
+ const p = alive[i];
142
+ // Bullet head
143
+ _dummy.position.set(p.x, p.y, p.z);
144
+ _dummy.scale.set(0.25, 0.25, 0.25);
145
+ _dummy.rotation.set(0, 0, 0);
146
+ _dummy.updateMatrix();
147
+ bullets.setMatrixAt(i, _dummy.matrix);
148
+
149
+ // Trail (stretched behind)
150
+ const speed = Math.sqrt(p.vx * p.vx + p.vy * p.vy + p.vz * p.vz);
151
+ const trailLen = Math.min(speed * 0.02, 4.0);
152
+ const headingAngle = Math.atan2(p.vx, p.vz);
153
+ const pitchAngle = Math.atan2(-p.vy, Math.sqrt(p.vx * p.vx + p.vz * p.vz));
154
+ _dummy.position.set(
155
+ p.x - p.vx * 0.008,
156
+ p.y - p.vy * 0.008,
157
+ p.z - p.vz * 0.008,
158
+ );
159
+ _dummy.scale.set(0.08, 0.08, trailLen);
160
+ _dummy.rotation.set(pitchAngle, headingAngle, 0, "YXZ");
161
+ _dummy.updateMatrix();
162
+ trails.setMatrixAt(i, _dummy.matrix);
163
+ } else {
164
+ // Hide unused instances
165
+ _dummy.position.set(0, -1000, 0);
166
+ _dummy.scale.set(0, 0, 0);
167
+ _dummy.updateMatrix();
168
+ bullets.setMatrixAt(i, _dummy.matrix);
169
+ trails.setMatrixAt(i, _dummy.matrix);
170
+ }
171
+ }
172
+ bullets.instanceMatrix.needsUpdate = true;
173
+ trails.instanceMatrix.needsUpdate = true;
174
+ }
175
+
176
+ // Render explosions at hit locations
177
+ const explosions = explosionRef.current;
178
+ if (explosions) {
179
+ const hits = manager.hits;
180
+ const now = performance.now();
181
+ for (let i = 0; i < MAX_EXPLOSIONS; i++) {
182
+ if (i < hits.length) {
183
+ const h = hits[i];
184
+ const age = (now - h.timestamp) / 1000; // seconds
185
+ const life = 1 - Math.min(age / 1.0, 1);
186
+ const scale = 0.5 + age * 4; // expand quickly
187
+ _dummy.position.set(h.x, h.y, h.z);
188
+ _dummy.scale.set(scale * life, scale * life, scale * life);
189
+ _dummy.rotation.set(age * 3, age * 5, 0);
190
+ _dummy.updateMatrix();
191
+ explosions.setMatrixAt(i, _dummy.matrix);
192
+ } else {
193
+ _dummy.position.set(0, -1000, 0);
194
+ _dummy.scale.set(0, 0, 0);
195
+ _dummy.updateMatrix();
196
+ explosions.setMatrixAt(i, _dummy.matrix);
197
+ }
198
+ }
199
+ explosions.instanceMatrix.needsUpdate = true;
200
+ }
201
+ });
202
+
203
+ if (!active) return null;
204
+
205
+ return (
206
+ <group>
207
+ {/* Bullet heads */}
208
+ <instancedMesh
209
+ ref={bulletRef}
210
+ args={[undefined!, undefined!, MAX_VISIBLE]}
211
+ frustumCulled={false}
212
+ >
213
+ <sphereGeometry args={[1, 6, 6]} />
214
+ <primitive object={bulletMat} attach="material" />
215
+ </instancedMesh>
216
+
217
+ {/* Bullet trails */}
218
+ <instancedMesh
219
+ ref={trailRef}
220
+ args={[undefined!, undefined!, MAX_VISIBLE]}
221
+ frustumCulled={false}
222
+ >
223
+ <boxGeometry args={[1, 1, 1]} />
224
+ <primitive object={trailMat} attach="material" />
225
+ </instancedMesh>
226
+
227
+ {/* Explosion flashes */}
228
+ <instancedMesh
229
+ ref={explosionRef}
230
+ args={[undefined!, undefined!, MAX_EXPLOSIONS]}
231
+ frustumCulled={false}
232
+ >
233
+ <icosahedronGeometry args={[1, 1]} />
234
+ <primitive object={explosionMat} attach="material" />
235
+ </instancedMesh>
236
+
237
+ {/* Muzzle flash light */}
238
+ {playerPosRef.current && (
239
+ <pointLight
240
+ position={playerPosRef.current}
241
+ color="#ff6600"
242
+ intensity={fireKeyRef.current ? 8 : 0}
243
+ distance={15}
244
+ decay={2}
245
+ />
246
+ )}
247
+ </group>
248
+ );
249
+ }
@@ -0,0 +1,86 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+
5
+ interface QueueCounts {
6
+ queued: number;
7
+ processing: number;
8
+ complete: number;
9
+ failed: number;
10
+ }
11
+
12
+ export default function QueueStatusBanner() {
13
+ const [counts, setCounts] = useState<QueueCounts | null>(null);
14
+
15
+ useEffect(() => {
16
+ let active = true;
17
+
18
+ async function poll() {
19
+ try {
20
+ const res = await fetch("/api/queue-status");
21
+ if (res.ok && active) setCounts(await res.json());
22
+ } catch { /* ignore */ }
23
+ }
24
+
25
+ poll();
26
+ const id = setInterval(poll, 10_000);
27
+ return () => { active = false; clearInterval(id); };
28
+ }, []);
29
+
30
+ if (!counts) return null;
31
+
32
+ const total = counts.queued + counts.processing + counts.complete + counts.failed;
33
+ const pending = counts.queued + counts.processing;
34
+
35
+ // Hide when everything is done
36
+ if (pending === 0) return null;
37
+
38
+ const pct = total > 0 ? Math.round((counts.complete / total) * 100) : 0;
39
+
40
+ return (
41
+ <div className="absolute top-[5.5rem] left-3 sm:top-[7.5rem] sm:left-5 z-10 bg-black/50 backdrop-blur-xl border border-white/[0.08] rounded-2xl px-4 py-3 w-[220px]">
42
+ {/* Header */}
43
+ <div className="flex items-center gap-2 mb-2.5">
44
+ <span className="relative flex h-2 w-2">
45
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[#E35930] opacity-75" />
46
+ <span className="relative inline-flex rounded-full h-2 w-2 bg-[#E35930]" />
47
+ </span>
48
+ <span className="text-xs font-medium text-white/70">Indexing Pipeline</span>
49
+ </div>
50
+
51
+ {/* Progress bar */}
52
+ <div className="w-full h-1.5 bg-white/[0.06] rounded-full overflow-hidden mb-2.5">
53
+ <div
54
+ className="h-full bg-[#E35930] rounded-full transition-all duration-700"
55
+ style={{ width: `${pct}%` }}
56
+ />
57
+ </div>
58
+
59
+ {/* Stats */}
60
+ <div className="space-y-1.5 text-[11px]">
61
+ <div className="flex justify-between">
62
+ <span className="text-white/40">Complete</span>
63
+ <span className="text-white/70 font-mono">{counts.complete}/{total}</span>
64
+ </div>
65
+ {counts.processing > 0 && (
66
+ <div className="flex justify-between">
67
+ <span className="text-white/40">Processing</span>
68
+ <span className="text-[#E35930] font-mono">{counts.processing}</span>
69
+ </div>
70
+ )}
71
+ {counts.queued > 0 && (
72
+ <div className="flex justify-between">
73
+ <span className="text-white/40">Queued</span>
74
+ <span className="text-yellow-400/80 font-mono">{counts.queued}</span>
75
+ </div>
76
+ )}
77
+ </div>
78
+
79
+ {/* Throughput info */}
80
+ <div className="mt-2.5 pt-2.5 border-t border-white/[0.06] space-y-1 text-[10px] text-white/30">
81
+ <p>Dynamic parallel slicing</p>
82
+ <p>Rate-limited at 90 req/s</p>
83
+ </div>
84
+ </div>
85
+ );
86
+ }
@@ -0,0 +1,82 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { Html } from "@react-three/drei";
5
+ import * as THREE from "three";
6
+ import type { RemotePlayer } from "@/lib/multiplayer-manager";
7
+
8
+ interface RealPlayerTagsProps {
9
+ active: boolean;
10
+ remotePlayers: Map<string, RemotePlayer>;
11
+ }
12
+
13
+ function shortWallet(wallet: string): string {
14
+ return wallet.length <= 10 ? wallet : `${wallet.slice(0, 4)}…${wallet.slice(-4)}`;
15
+ }
16
+
17
+ export default function RealPlayerTags({ active, remotePlayers }: RealPlayerTagsProps) {
18
+ const [players, setPlayers] = useState<RemotePlayer[]>([]);
19
+
20
+ useEffect(() => {
21
+ if (!active) {
22
+ setPlayers([]);
23
+ return;
24
+ }
25
+
26
+ const sync = () => {
27
+ setPlayers(
28
+ Array.from(remotePlayers.values()).filter((p) => p.mode !== "spectating"),
29
+ );
30
+ };
31
+
32
+ sync();
33
+ const interval = window.setInterval(sync, 120);
34
+ return () => window.clearInterval(interval);
35
+ }, [active, remotePlayers]);
36
+
37
+ if (!active || players.length === 0) return null;
38
+
39
+ const now = Date.now();
40
+
41
+ return (
42
+ <>
43
+ {players.map((p) => {
44
+ const isCar = p.mode === "car";
45
+ const color = isCar ? "#38bdf8" : "#ec4899";
46
+ const yOffset = isCar ? 2.3 : 4.0;
47
+ const stale = now - p.lastSeen > 3000;
48
+
49
+ return (
50
+ <group key={p.wallet} position={[p.x, p.y + yOffset, p.z]}>
51
+ {/* Floating marker core */}
52
+ <mesh>
53
+ <sphereGeometry args={[0.18, 12, 12]} />
54
+ <meshBasicMaterial color={color} transparent opacity={stale ? 0.35 : 0.95} />
55
+ </mesh>
56
+
57
+ {/* Ring helps visibility from distance */}
58
+ <mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.3, 0]}>
59
+ <ringGeometry args={[0.3, 0.42, 20]} />
60
+ <meshBasicMaterial
61
+ color={color}
62
+ side={THREE.DoubleSide}
63
+ transparent
64
+ opacity={stale ? 0.2 : 0.6}
65
+ />
66
+ </mesh>
67
+
68
+ {/* Floating billboard label */}
69
+ <Html center position={[0, 0.55, 0]} distanceFactor={14} style={{ pointerEvents: "none" }}>
70
+ <div className="px-2 py-1 rounded-md border border-white/20 bg-black/75 backdrop-blur-sm text-center leading-tight shadow-lg">
71
+ <div className="text-[9px] font-bold tracking-wide text-white">REAL PLAYER</div>
72
+ <div className={`text-[9px] font-mono ${isCar ? "text-sky-300" : "text-pink-300"}`}>
73
+ {isCar ? "🚗 DRIVER" : "✈ PILOT"} · {shortWallet(p.wallet)}
74
+ </div>
75
+ </div>
76
+ </Html>
77
+ </group>
78
+ );
79
+ })}
80
+ </>
81
+ );
82
+ }