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,357 @@
1
+ "use client";
2
+
3
+ import { useRef, useEffect, useMemo, useCallback } from "react";
4
+ import { useFrame, ThreeEvent } from "@react-three/fiber";
5
+ import * as THREE from "three";
6
+ import { PlacedWallet } from "@/types/wallet";
7
+ import {
8
+ getBuildingDimensions,
9
+ getInstanceSeed,
10
+ getWalletWorldPosition,
11
+ } from "@/lib/building-math";
12
+ import { generatePathFromBuilding, CarPath } from "@/lib/car-paths";
13
+ import { MAX_CARS } from "@/lib/city-slots";
14
+ import { WindowHoverInfo } from "./WindowTooltip";
15
+
16
+ interface InstancedResidentCarsProps {
17
+ wallets: PlacedWallet[];
18
+ timeRef: React.MutableRefObject<number>;
19
+ onHoverCar?: (info: WindowHoverInfo | null) => void;
20
+ onClickCar?: (wallet: PlacedWallet, position: [number, number, number]) => void;
21
+ positionsRef?: React.MutableRefObject<Array<{ x: number; y: number; z: number }>>;
22
+ }
23
+
24
+ /** Per-resident car runtime state */
25
+ interface ResidentCar {
26
+ wallet: PlacedWallet;
27
+ path: CarPath;
28
+ pathIdx: number; // current segment index
29
+ segmentT: number; // progress along current segment [0,1]
30
+ speed: number; // world units per second
31
+ x: number;
32
+ z: number;
33
+ heading: number;
34
+ color: THREE.Color;
35
+ }
36
+
37
+ const _dummy = new THREE.Object3D();
38
+
39
+ // Car body dimensions (slightly smaller than swap cars to visually distinguish)
40
+ const CAR_W = 1.0;
41
+ const CAR_H = 0.45;
42
+ const CAR_L = 1.9;
43
+ const CABIN_H = 0.35;
44
+ const WHEEL_R = 0.15;
45
+ const WHEEL_W = 0.08;
46
+
47
+ // Warm resident-car color palette
48
+ const RESIDENT_COLORS = [
49
+ "#3b82f6", "#8b5cf6", "#06b6d4", "#10b981", "#f59e0b",
50
+ "#ef4444", "#ec4899", "#6366f1", "#14b8a6", "#f97316",
51
+ "#84cc16", "#a855f7", "#0ea5e9", "#e11d48", "#d946ef",
52
+ "#22c55e", "#f43f5e", "#7c3aed", "#0891b2", "#ca8a04",
53
+ ];
54
+
55
+ function getCarColor(seed: number): THREE.Color {
56
+ const idx = Math.floor(seed * RESIDENT_COLORS.length) % RESIDENT_COLORS.length;
57
+ return new THREE.Color(RESIDENT_COLORS[idx]);
58
+ }
59
+
60
+ export default function InstancedResidentCars({
61
+ wallets,
62
+ timeRef,
63
+ onHoverCar,
64
+ onClickCar,
65
+ positionsRef,
66
+ }: InstancedResidentCarsProps) {
67
+ const count = Math.min(wallets.length, MAX_CARS);
68
+ const bodyRef = useRef<THREE.InstancedMesh>(null);
69
+ const cabinRef = useRef<THREE.InstancedMesh>(null);
70
+ const wheelRef = useRef<THREE.InstancedMesh>(null);
71
+ const markerRef = useRef<THREE.InstancedMesh>(null);
72
+ const hoveredIdx = useRef<number | null>(null);
73
+ const elapsedRef = useRef(0);
74
+
75
+ // Initialize resident car states
76
+ const carsRef = useRef<ResidentCar[]>([]);
77
+
78
+ useEffect(() => {
79
+ const cars: ResidentCar[] = [];
80
+ for (let i = 0; i < count; i++) {
81
+ const w = wallets[i];
82
+ const seed = getInstanceSeed(w.address);
83
+ const path = generatePathFromBuilding(w.blockRow, w.blockCol);
84
+
85
+ const wps = path.waypoints;
86
+
87
+ cars.push({
88
+ wallet: w,
89
+ path,
90
+ pathIdx: 0,
91
+ segmentT: 0,
92
+ speed: 3 + seed * 5, // 3–8 units/sec
93
+ x: wps[0].x,
94
+ z: wps[0].z,
95
+ heading: 0,
96
+ color: getCarColor(seed),
97
+ });
98
+ }
99
+ carsRef.current = cars;
100
+ }, [wallets, count]);
101
+
102
+ // Materials
103
+ const bodyMat = useMemo(() => new THREE.MeshStandardMaterial({
104
+ metalness: 0.5,
105
+ roughness: 0.4,
106
+ }), []);
107
+
108
+ const cabinMat = useMemo(() => new THREE.MeshStandardMaterial({
109
+ color: "#aaddff",
110
+ metalness: 0.8,
111
+ roughness: 0.1,
112
+ transparent: true,
113
+ opacity: 0.5,
114
+ }), []);
115
+
116
+ const wheelMat = useMemo(() => new THREE.MeshStandardMaterial({
117
+ color: "#222",
118
+ metalness: 0.3,
119
+ roughness: 0.8,
120
+ }), []);
121
+
122
+ const markerMat = useMemo(() => new THREE.MeshStandardMaterial({
123
+ color: "#6366f1",
124
+ emissive: "#6366f1",
125
+ emissiveIntensity: 0.5,
126
+ transparent: true,
127
+ opacity: 0.6,
128
+ }), []);
129
+
130
+ // Animate cars
131
+ useFrame((_, delta) => {
132
+ const cars = carsRef.current;
133
+ if (cars.length === 0) return;
134
+ const body = bodyRef.current;
135
+ const cabin = cabinRef.current;
136
+ const wheel = wheelRef.current;
137
+ const marker = markerRef.current;
138
+ if (!body || !cabin || !wheel || !marker) return;
139
+
140
+ const dt = Math.min(delta, 0.05);
141
+ elapsedRef.current += dt;
142
+ const dayScale = 0.95 + 0.05 * Math.sin(timeRef.current * Math.PI * 2);
143
+
144
+ for (let i = 0; i < cars.length; i++) {
145
+ const car = cars[i];
146
+ const wps = car.path.waypoints;
147
+ const numPts = wps.length;
148
+
149
+ // Advance along path
150
+ const segLen = segmentLengthWp(wps, car.pathIdx);
151
+ const segSpeed = segLen > 0 ? (car.speed * dayScale) / segLen : 1;
152
+ car.segmentT += segSpeed * dt;
153
+
154
+ while (car.segmentT >= 1 && car.pathIdx < numPts - 2) {
155
+ car.segmentT -= 1;
156
+ car.pathIdx++;
157
+ }
158
+
159
+ // Loop back to start
160
+ if (car.pathIdx >= numPts - 2 && car.segmentT >= 1) {
161
+ // Generate a new path from same building
162
+ const newPath = generatePathFromBuilding(car.wallet.blockRow, car.wallet.blockCol);
163
+ car.path = newPath;
164
+ car.pathIdx = 0;
165
+ car.segmentT = 0;
166
+ }
167
+
168
+ // Interpolate position
169
+ const pIdx = Math.min(car.pathIdx, numPts - 2);
170
+ const a = wps[pIdx];
171
+ const b = wps[pIdx + 1];
172
+ const t = Math.min(car.segmentT, 1);
173
+ car.x = a.x + (b.x - a.x) * t;
174
+ car.z = a.z + (b.z - a.z) * t;
175
+
176
+ // Heading
177
+ const dx = b.x - a.x;
178
+ const dz = b.z - a.z;
179
+ if (Math.abs(dx) > 0.001 || Math.abs(dz) > 0.001) {
180
+ car.heading = Math.atan2(dx, dz);
181
+ }
182
+
183
+ const y = 0.25; // ground level
184
+
185
+ // Body
186
+ _dummy.position.set(car.x, y, car.z);
187
+ _dummy.rotation.set(0, car.heading, 0);
188
+ _dummy.scale.set(CAR_W, CAR_H, CAR_L);
189
+ _dummy.updateMatrix();
190
+ body.setMatrixAt(i, _dummy.matrix);
191
+
192
+ // Cabin
193
+ _dummy.position.set(car.x, y + CAR_H * 0.5 + CABIN_H * 0.5, car.z);
194
+ _dummy.scale.set(CAR_W * 0.75, CABIN_H, CAR_L * 0.5);
195
+ _dummy.updateMatrix();
196
+ cabin.setMatrixAt(i, _dummy.matrix);
197
+
198
+ // Marker (floating diamond above car)
199
+ const bobY = 2.0 + Math.sin(elapsedRef.current * 2 + i * 1.3) * 0.3;
200
+ _dummy.position.set(car.x, y + bobY, car.z);
201
+ _dummy.rotation.set(0, elapsedRef.current * 1.5, Math.PI / 4);
202
+ _dummy.scale.set(0.2, 0.2, 0.2);
203
+ _dummy.updateMatrix();
204
+ marker.setMatrixAt(i, _dummy.matrix);
205
+
206
+ // Color
207
+ body.setColorAt(i, car.color);
208
+ }
209
+
210
+ body.instanceMatrix.needsUpdate = true;
211
+ cabin.instanceMatrix.needsUpdate = true;
212
+ marker.instanceMatrix.needsUpdate = true;
213
+ if (body.instanceColor) body.instanceColor.needsUpdate = true;
214
+
215
+ // Write positions for HUD/projectiles
216
+ if (positionsRef) {
217
+ const posOut: Array<{ x: number; y: number; z: number }> = [];
218
+ for (let i = 0; i < cars.length; i++) {
219
+ posOut.push({ x: cars[i].x, y: 0.25, z: cars[i].z });
220
+ }
221
+ positionsRef.current = posOut;
222
+ }
223
+
224
+ // Wheels (4 per car)
225
+ for (let i = 0; i < cars.length; i++) {
226
+ const car = cars[i];
227
+ const cos = Math.cos(car.heading);
228
+ const sin = Math.sin(car.heading);
229
+ const offsets = [
230
+ [-CAR_W * 0.4, CAR_L * 0.3],
231
+ [CAR_W * 0.4, CAR_L * 0.3],
232
+ [-CAR_W * 0.4, -CAR_L * 0.3],
233
+ [CAR_W * 0.4, -CAR_L * 0.3],
234
+ ];
235
+
236
+ for (let w = 0; w < 4; w++) {
237
+ const idx = i * 4 + w;
238
+ if (idx >= count * 4) break;
239
+ const [lx, lz] = offsets[w];
240
+ const wx = car.x + lx * cos - lz * sin;
241
+ const wz = car.z + lx * sin + lz * cos;
242
+ _dummy.position.set(wx, WHEEL_R, wz);
243
+ _dummy.rotation.set(0, car.heading, Math.PI / 2);
244
+ _dummy.scale.set(WHEEL_R * 2, WHEEL_W, WHEEL_R * 2);
245
+ _dummy.updateMatrix();
246
+ wheel.setMatrixAt(idx, _dummy.matrix);
247
+ }
248
+ }
249
+ wheel.instanceMatrix.needsUpdate = true;
250
+ });
251
+
252
+ // Pointer handlers
253
+ const handlePointerMove = useCallback((e: ThreeEvent<PointerEvent>) => {
254
+ e.stopPropagation();
255
+ const id = e.instanceId;
256
+ if (id === undefined || id >= count) { onHoverCar?.(null); return; }
257
+
258
+ if (hoveredIdx.current !== id) {
259
+ hoveredIdx.current = id;
260
+ document.body.style.cursor = "pointer";
261
+ }
262
+
263
+ const car = carsRef.current[id];
264
+ if (!car) return;
265
+ const w = car.wallet;
266
+ onHoverCar?.({
267
+ address: w.address,
268
+ tokenIndex: 0,
269
+ screenX: e.nativeEvent.clientX,
270
+ screenY: e.nativeEvent.clientY,
271
+ mode: "building",
272
+ identityName: w.identityName,
273
+ identityType: w.identityType,
274
+ identityCategory: w.identityCategory,
275
+ });
276
+ }, [count, onHoverCar]);
277
+
278
+ const handlePointerOut = useCallback(() => {
279
+ hoveredIdx.current = null;
280
+ document.body.style.cursor = "default";
281
+ onHoverCar?.(null);
282
+ }, [onHoverCar]);
283
+
284
+ const handleClick = useCallback((e: ThreeEvent<MouseEvent>) => {
285
+ e.stopPropagation();
286
+ const id = e.instanceId;
287
+ if (id === undefined || id >= count) return;
288
+ const car = carsRef.current[id];
289
+ if (!car) return;
290
+ const w = car.wallet;
291
+ const dims = getBuildingDimensions(w);
292
+ const pos = getWalletWorldPosition(w, dims);
293
+ onClickCar?.(w, pos);
294
+ }, [count, onClickCar]);
295
+
296
+ if (count === 0) return null;
297
+
298
+ return (
299
+ <group>
300
+ {/* Car bodies */}
301
+ <instancedMesh
302
+ ref={bodyRef}
303
+ args={[undefined!, undefined!, count]}
304
+ frustumCulled={false}
305
+ onPointerMove={handlePointerMove}
306
+ onPointerOut={handlePointerOut}
307
+ onClick={handleClick}
308
+ >
309
+ <boxGeometry args={[1, 1, 1]} />
310
+ <primitive object={bodyMat} attach="material" />
311
+ </instancedMesh>
312
+
313
+ {/* Cabins */}
314
+ <instancedMesh
315
+ ref={cabinRef}
316
+ args={[undefined!, undefined!, count]}
317
+ frustumCulled={false}
318
+ raycast={() => { }}
319
+ >
320
+ <boxGeometry args={[1, 1, 1]} />
321
+ <primitive object={cabinMat} attach="material" />
322
+ </instancedMesh>
323
+
324
+ {/* Wheels (4 per car) */}
325
+ <instancedMesh
326
+ ref={wheelRef}
327
+ args={[undefined!, undefined!, count * 4]}
328
+ frustumCulled={false}
329
+ raycast={() => { }}
330
+ >
331
+ <cylinderGeometry args={[1, 1, 1, 8]} />
332
+ <primitive object={wheelMat} attach="material" />
333
+ </instancedMesh>
334
+
335
+ {/* Floating markers above each car */}
336
+ <instancedMesh
337
+ ref={markerRef}
338
+ args={[undefined!, undefined!, count]}
339
+ frustumCulled={false}
340
+ raycast={() => { }}
341
+ >
342
+ <octahedronGeometry args={[1, 0]} />
343
+ <primitive object={markerMat} attach="material" />
344
+ </instancedMesh>
345
+ </group>
346
+ );
347
+ }
348
+
349
+ /** Distance between two waypoints */
350
+ function segmentLengthWp(wps: { x: number; z: number }[], idx: number): number {
351
+ if (idx >= wps.length - 1) return 0;
352
+ const a = wps[idx];
353
+ const b = wps[idx + 1];
354
+ const dx = b.x - a.x;
355
+ const dz = b.z - a.z;
356
+ return Math.sqrt(dx * dx + dz * dz);
357
+ }
@@ -0,0 +1,42 @@
1
+ "use client";
2
+
3
+ import { useRef, useEffect, useMemo } from "react";
4
+ import * as THREE from "three";
5
+
6
+ interface DashData {
7
+ x: number;
8
+ z: number;
9
+ rotY: number; // 0 for vertical roads, PI/2 for horizontal
10
+ }
11
+
12
+ /**
13
+ * Renders all road center-line dashes as a single InstancedMesh.
14
+ */
15
+ export default function InstancedRoadDashes({ dashes }: { dashes: DashData[] }) {
16
+ const meshRef = useRef<THREE.InstancedMesh>(null);
17
+ const dummy = useMemo(() => new THREE.Object3D(), []);
18
+
19
+ useEffect(() => {
20
+ const mesh = meshRef.current;
21
+ if (!mesh) return;
22
+
23
+ for (let i = 0; i < dashes.length; i++) {
24
+ dummy.position.set(dashes[i].x, 0.04, dashes[i].z);
25
+ dummy.rotation.set(-Math.PI / 2, 0, dashes[i].rotY);
26
+ dummy.scale.set(1, 1, 1);
27
+ dummy.updateMatrix();
28
+ mesh.setMatrixAt(i, dummy.matrix);
29
+ }
30
+
31
+ mesh.instanceMatrix.needsUpdate = true;
32
+ }, [dashes, dummy]);
33
+
34
+ if (dashes.length === 0) return null;
35
+
36
+ return (
37
+ <instancedMesh ref={meshRef} args={[undefined!, undefined!, dashes.length]} frustumCulled={false}>
38
+ <planeGeometry args={[0.15, 1.2]} />
39
+ <meshStandardMaterial color="#6a6a80" />
40
+ </instancedMesh>
41
+ );
42
+ }