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,504 @@
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 { SwapEvent } from "@/lib/swap-events";
8
+ import { CarSystem } from "@/lib/car-system";
9
+ import { generatePathFromBuilding, generatePathPool, CarPath } from "@/lib/car-paths";
10
+ import { lampIntensity } from "@/lib/day-night";
11
+ import { getBuildingDimensions, getWalletWorldPosition } from "@/lib/building-math";
12
+ import { WindowHoverInfo } from "./WindowTooltip";
13
+
14
+ const MAX_CARS = 60;
15
+ const WHEELS_PER_CAR = 4;
16
+ const TOTAL_WHEELS = MAX_CARS * WHEELS_PER_CAR;
17
+
18
+ // Car body dimensions
19
+ const BODY_W = 0.7; // width (Z)
20
+ const BODY_H = 0.35; // height (Y)
21
+ const BODY_L = 1.5; // length (X along travel)
22
+
23
+ // Cabin dimensions
24
+ const CAB_W = 0.6;
25
+ const CAB_H = 0.25;
26
+ const CAB_L = 0.65;
27
+ const CAB_OFFSET_Y = (BODY_H + CAB_H) / 2;
28
+ const CAB_OFFSET_FORWARD = -0.1; // slightly toward rear
29
+
30
+ // Wheel dimensions
31
+ const WHEEL_R = 0.12;
32
+ const WHEEL_H = 0.1;
33
+ const WHEEL_POSITIONS = [
34
+ { dx: 0.45, dz: 0.3 }, // front-right
35
+ { dx: 0.45, dz: -0.3 }, // front-left
36
+ { dx: -0.45, dz: 0.3 }, // rear-right
37
+ { dx: -0.45, dz: -0.3 }, // rear-left
38
+ ];
39
+
40
+ // Invisible hitbox multiplier — makes clicks much easier
41
+ const HITBOX_SCALE = 3;
42
+
43
+ // Headlight decal
44
+ const HEADLIGHT_RADIUS = 3;
45
+ const HEADLIGHT_OFFSET_FORWARD = 2;
46
+
47
+ interface TrackedCarInfo {
48
+ slotIndex: number;
49
+ walletAddress: string;
50
+ signature: string;
51
+ tokenIn: string | null;
52
+ tokenOut: string | null;
53
+ amountSol: number | null;
54
+ }
55
+
56
+ interface InstancedCarsProps {
57
+ swapQueueRef: React.MutableRefObject<SwapEvent[]>;
58
+ wallets: PlacedWallet[];
59
+ timeRef: React.MutableRefObject<number>;
60
+ onHoverSwap?: (info: WindowHoverInfo | null) => void;
61
+ onClickCar?: (info: TrackedCarInfo) => void;
62
+ trackedCarSlot?: number | null;
63
+ trackedCarPosRef?: React.MutableRefObject<[number, number, number] | null>;
64
+ onCarDied?: (slotIndex: number) => void;
65
+ selectedAddress?: string | null;
66
+ swapBurstRef?: React.MutableRefObject<Array<{ x: number; y: number; z: number; color: string }>>;
67
+ }
68
+
69
+ // Floating arrow marker for highlighted cars
70
+ const MARKER_HEIGHT_ABOVE_CAR = 2.5;
71
+ const MARKER_BOB_AMPLITUDE = 0.3;
72
+ const MARKER_BOB_SPEED = 3;
73
+
74
+ export type { TrackedCarInfo };
75
+
76
+ const _dummy = new THREE.Object3D();
77
+ const _color = new THREE.Color();
78
+
79
+ // Headlight decal shaders (matches InstancedLampPosts)
80
+ const decalVertexShader = /* glsl */ `
81
+ varying vec2 vUv;
82
+ void main() {
83
+ vUv = uv;
84
+ gl_Position = projectionMatrix * modelViewMatrix * instanceMatrix * vec4(position, 1.0);
85
+ }
86
+ `;
87
+
88
+ const decalFragmentShader = /* glsl */ `
89
+ uniform float uIntensity;
90
+ uniform vec3 uColor;
91
+ varying vec2 vUv;
92
+ void main() {
93
+ vec2 center = vUv - 0.5;
94
+ float dist = length(center) * 2.0;
95
+ float falloff = 1.0 - smoothstep(0.0, 1.0, dist);
96
+ falloff *= falloff;
97
+ gl_FragColor = vec4(uColor, falloff * uIntensity * 0.3);
98
+ }
99
+ `;
100
+
101
+ export default function InstancedCars({
102
+ swapQueueRef,
103
+ wallets,
104
+ timeRef,
105
+ onHoverSwap,
106
+ onClickCar,
107
+ trackedCarSlot,
108
+ trackedCarPosRef,
109
+ onCarDied,
110
+ selectedAddress,
111
+ swapBurstRef,
112
+ }: InstancedCarsProps) {
113
+ const bodyRef = useRef<THREE.InstancedMesh>(null);
114
+ const cabinRef = useRef<THREE.InstancedMesh>(null);
115
+ const wheelRef = useRef<THREE.InstancedMesh>(null);
116
+ const decalRef = useRef<THREE.InstancedMesh>(null);
117
+ const hitboxRef = useRef<THREE.InstancedMesh>(null);
118
+ const markerRef = useRef<THREE.InstancedMesh>(null);
119
+ const hoveredSlot = useRef<number | null>(null);
120
+ const elapsedRef = useRef(0);
121
+
122
+ const decalMaterialRef = useRef<THREE.ShaderMaterial | null>(null);
123
+ if (!decalMaterialRef.current) {
124
+ decalMaterialRef.current = new THREE.ShaderMaterial({
125
+ vertexShader: decalVertexShader,
126
+ fragmentShader: decalFragmentShader,
127
+ transparent: true,
128
+ depthWrite: false,
129
+ blending: THREE.AdditiveBlending,
130
+ polygonOffset: true,
131
+ polygonOffsetFactor: -1,
132
+ polygonOffsetUnits: -1,
133
+ uniforms: {
134
+ uIntensity: { value: 0 },
135
+ uColor: { value: new THREE.Color("#ffeedd") },
136
+ },
137
+ });
138
+ }
139
+ const decalMaterial = decalMaterialRef.current;
140
+
141
+ const carSystem = useMemo(() => new CarSystem(), []);
142
+
143
+ // Wallet address -> PlacedWallet lookup
144
+ const walletMap = useMemo(() => {
145
+ const map = new Map<string, PlacedWallet>();
146
+ for (const w of wallets) map.set(w.address, w);
147
+ return map;
148
+ }, [wallets]);
149
+
150
+ // Pre-generate fallback path pool
151
+ const pathPool = useMemo(() => generatePathPool(20), []);
152
+ const pathPoolIdx = useRef(0);
153
+
154
+ useEffect(() => {
155
+ return () => {
156
+ decalMaterial.dispose();
157
+ };
158
+ }, [decalMaterial]);
159
+
160
+ // Initialize all instances to zero scale (invisible)
161
+ useEffect(() => {
162
+ const body = bodyRef.current;
163
+ const cabin = cabinRef.current;
164
+ const wheel = wheelRef.current;
165
+ const decal = decalRef.current;
166
+ const hitbox = hitboxRef.current;
167
+ const marker = markerRef.current;
168
+ if (!body || !cabin || !wheel || !decal || !hitbox || !marker) return;
169
+
170
+ _dummy.scale.set(0, 0, 0);
171
+ _dummy.position.set(0, -100, 0);
172
+ _dummy.rotation.set(0, 0, 0);
173
+ _dummy.updateMatrix();
174
+
175
+ for (let i = 0; i < MAX_CARS; i++) {
176
+ body.setMatrixAt(i, _dummy.matrix);
177
+ cabin.setMatrixAt(i, _dummy.matrix);
178
+ decal.setMatrixAt(i, _dummy.matrix);
179
+ hitbox.setMatrixAt(i, _dummy.matrix);
180
+ marker.setMatrixAt(i, _dummy.matrix);
181
+
182
+ _color.set("#333333");
183
+ body.setColorAt(i, _color);
184
+ cabin.setColorAt(i, _color);
185
+ }
186
+
187
+ for (let i = 0; i < TOTAL_WHEELS; i++) {
188
+ wheel.setMatrixAt(i, _dummy.matrix);
189
+ }
190
+
191
+ body.instanceMatrix.needsUpdate = true;
192
+ cabin.instanceMatrix.needsUpdate = true;
193
+ wheel.instanceMatrix.needsUpdate = true;
194
+ decal.instanceMatrix.needsUpdate = true;
195
+ hitbox.instanceMatrix.needsUpdate = true;
196
+ marker.instanceMatrix.needsUpdate = true;
197
+ if (body.instanceColor) body.instanceColor.needsUpdate = true;
198
+ if (cabin.instanceColor) cabin.instanceColor.needsUpdate = true;
199
+
200
+ // Set a fixed bounding sphere so raycasting always tests individual instances.
201
+ hitbox.boundingSphere = new THREE.Sphere(new THREE.Vector3(0, 0, 0), 400);
202
+ }, []);
203
+
204
+ // Main update loop
205
+ useFrame((_, delta) => {
206
+ const body = bodyRef.current;
207
+ const cabin = cabinRef.current;
208
+ const wheel = wheelRef.current;
209
+ const decal = decalRef.current;
210
+ const hitbox = hitboxRef.current;
211
+ const marker = markerRef.current;
212
+ if (!body || !cabin || !wheel || !decal || !hitbox || !marker) return;
213
+ elapsedRef.current += delta;
214
+
215
+ // 1. Drain swap queue
216
+ const burstColors = ["#E35930", "#6366f1", "#22c56e", "#f59e0b", "#ec4899"];
217
+ const pending = swapQueueRef.current.splice(0);
218
+ for (const evt of pending) {
219
+ const w = walletMap.get(evt.walletAddress);
220
+ let path: CarPath;
221
+ if (w) {
222
+ path = generatePathFromBuilding(w.blockRow, w.blockCol);
223
+ // Push particle burst at the building location
224
+ if (swapBurstRef) {
225
+ const dims = getBuildingDimensions(w);
226
+ const pos = getWalletWorldPosition(w, dims);
227
+ const color = burstColors[Math.floor(Math.random() * burstColors.length)];
228
+ swapBurstRef.current.push({
229
+ x: pos[0],
230
+ y: pos[1] + dims.height / 2 + 1,
231
+ z: pos[2],
232
+ color,
233
+ });
234
+ }
235
+ } else {
236
+ path = pathPool[pathPoolIdx.current % pathPool.length];
237
+ pathPoolIdx.current++;
238
+ }
239
+ carSystem.spawnCar(
240
+ path,
241
+ evt.walletAddress,
242
+ evt.signature,
243
+ evt.tokenIn,
244
+ evt.tokenOut,
245
+ evt.amountSol,
246
+ );
247
+ }
248
+
249
+ // 2. Update car system
250
+ carSystem.update(delta);
251
+
252
+ // 3. Update instance matrices
253
+ const intensity = lampIntensity(timeRef.current);
254
+ decalMaterialRef.current!.uniforms.uIntensity.value = intensity;
255
+
256
+ for (let i = 0; i < MAX_CARS; i++) {
257
+ const car = carSystem.cars[i];
258
+ if (!car || car.phase === "dead") {
259
+ // Hide this slot
260
+ _dummy.scale.set(0, 0, 0);
261
+ _dummy.position.set(0, -100, 0);
262
+ _dummy.rotation.set(0, 0, 0);
263
+ _dummy.updateMatrix();
264
+
265
+ body.setMatrixAt(i, _dummy.matrix);
266
+ cabin.setMatrixAt(i, _dummy.matrix);
267
+ decal.setMatrixAt(i, _dummy.matrix);
268
+ hitbox.setMatrixAt(i, _dummy.matrix);
269
+ marker.setMatrixAt(i, _dummy.matrix);
270
+
271
+ for (let w = 0; w < WHEELS_PER_CAR; w++) {
272
+ wheel.setMatrixAt(i * WHEELS_PER_CAR + w, _dummy.matrix);
273
+ }
274
+ continue;
275
+ }
276
+
277
+ const pos = carSystem.getCarPosition(car);
278
+ const isHighlighted = selectedAddress != null && car.walletAddress === selectedAddress;
279
+ const s = pos.scale;
280
+ const sinR = Math.sin(pos.rotY);
281
+ const cosR = Math.cos(pos.rotY);
282
+
283
+ // Body — Z is the long (travel) axis, X is the width
284
+ _dummy.position.set(pos.x, pos.y, pos.z);
285
+ _dummy.rotation.set(0, pos.rotY, 0);
286
+ _dummy.scale.set(BODY_W * s, BODY_H * s, BODY_L * s);
287
+ _dummy.updateMatrix();
288
+ body.setMatrixAt(i, _dummy.matrix);
289
+
290
+ // Hitbox — same position/rotation, scaled up for easier clicking
291
+ _dummy.scale.set(BODY_W * HITBOX_SCALE * s, BODY_H * HITBOX_SCALE * s, BODY_L * HITBOX_SCALE * s);
292
+ _dummy.updateMatrix();
293
+ hitbox.setMatrixAt(i, _dummy.matrix);
294
+
295
+ _color.copy(car.color);
296
+ body.setColorAt(i, _color);
297
+
298
+ // Cabin — Z is the long (travel) axis
299
+ const cabX = pos.x + sinR * CAB_OFFSET_FORWARD;
300
+ const cabZ = pos.z + cosR * CAB_OFFSET_FORWARD;
301
+ _dummy.position.set(cabX, pos.y + CAB_OFFSET_Y * s, cabZ);
302
+ _dummy.rotation.set(0, pos.rotY, 0);
303
+ _dummy.scale.set(CAB_W * s, CAB_H * s, CAB_L * s);
304
+ _dummy.updateMatrix();
305
+ cabin.setMatrixAt(i, _dummy.matrix);
306
+
307
+ _color.copy(car.color).multiplyScalar(0.8);
308
+ cabin.setColorAt(i, _color);
309
+
310
+ // Wheels
311
+ for (let w = 0; w < WHEELS_PER_CAR; w++) {
312
+ const wp = WHEEL_POSITIONS[w];
313
+ // Rotate wheel offsets by car rotation
314
+ const wx = pos.x + (sinR * wp.dx + cosR * wp.dz) * s;
315
+ const wz = pos.z + (cosR * wp.dx - sinR * wp.dz) * s;
316
+ const wy = 0.02 + WHEEL_R * s;
317
+
318
+ _dummy.position.set(wx, wy, wz);
319
+ // Wheel rotated 90deg on Z so cylinder lies on side, then align with car heading
320
+ _dummy.rotation.set(0, pos.rotY, Math.PI / 2);
321
+ _dummy.scale.set(WHEEL_R * s, WHEEL_H * s, WHEEL_R * s);
322
+ _dummy.updateMatrix();
323
+ wheel.setMatrixAt(i * WHEELS_PER_CAR + w, _dummy.matrix);
324
+ }
325
+
326
+ // Headlight decal — flat on ground, offset ahead of car
327
+ const hlX = pos.x + sinR * HEADLIGHT_OFFSET_FORWARD;
328
+ const hlZ = pos.z + cosR * HEADLIGHT_OFFSET_FORWARD;
329
+ _dummy.position.set(hlX, 0.03, hlZ);
330
+ _dummy.rotation.set(-Math.PI / 2, 0, 0);
331
+ _dummy.scale.set(HEADLIGHT_RADIUS * s, HEADLIGHT_RADIUS * s, 1);
332
+ _dummy.updateMatrix();
333
+ decal.setMatrixAt(i, _dummy.matrix);
334
+
335
+ // Arrow marker — floating above highlighted cars, bobbing
336
+ if (isHighlighted) {
337
+ const bob = Math.sin(elapsedRef.current * MARKER_BOB_SPEED) * MARKER_BOB_AMPLITUDE;
338
+ _dummy.position.set(pos.x, pos.y + MARKER_HEIGHT_ABOVE_CAR + bob, pos.z);
339
+ _dummy.rotation.set(Math.PI, 0, 0); // cone points downward
340
+ _dummy.scale.set(0.4, 0.6, 0.4);
341
+ _dummy.updateMatrix();
342
+ marker.setMatrixAt(i, _dummy.matrix);
343
+ } else {
344
+ _dummy.scale.set(0, 0, 0);
345
+ _dummy.position.set(0, -100, 0);
346
+ _dummy.rotation.set(0, 0, 0);
347
+ _dummy.updateMatrix();
348
+ marker.setMatrixAt(i, _dummy.matrix);
349
+ }
350
+ }
351
+
352
+ body.instanceMatrix.needsUpdate = true;
353
+ cabin.instanceMatrix.needsUpdate = true;
354
+ wheel.instanceMatrix.needsUpdate = true;
355
+ decal.instanceMatrix.needsUpdate = true;
356
+ hitbox.instanceMatrix.needsUpdate = true;
357
+ marker.instanceMatrix.needsUpdate = true;
358
+ if (body.instanceColor) body.instanceColor.needsUpdate = true;
359
+ if (cabin.instanceColor) cabin.instanceColor.needsUpdate = true;
360
+
361
+ // Update tracked car position ref for camera following
362
+ if (trackedCarSlot != null && trackedCarPosRef) {
363
+ const trackedCar = carSystem.cars[trackedCarSlot];
364
+ if (trackedCar && trackedCar.phase !== "dead") {
365
+ const tp = carSystem.getCarPosition(trackedCar);
366
+ trackedCarPosRef.current = [tp.x, tp.y, tp.z];
367
+ } else {
368
+ trackedCarPosRef.current = null;
369
+ onCarDied?.(trackedCarSlot);
370
+ }
371
+ }
372
+ });
373
+
374
+ // Click handler
375
+ const handleClick = useCallback(
376
+ (e: ThreeEvent<MouseEvent>) => {
377
+ e.stopPropagation();
378
+ const id = e.instanceId;
379
+ if (id === undefined || !onClickCar) return;
380
+ const car = carSystem.getCarSwapInfo(id);
381
+ if (!car) return;
382
+ onClickCar({
383
+ slotIndex: id,
384
+ walletAddress: car.walletAddress,
385
+ signature: car.signature,
386
+ tokenIn: car.tokenIn,
387
+ tokenOut: car.tokenOut,
388
+ amountSol: car.amountSol,
389
+ });
390
+ },
391
+ [carSystem, onClickCar],
392
+ );
393
+
394
+ // Hover handlers on body mesh
395
+ const handlePointerMove = useCallback(
396
+ (e: ThreeEvent<PointerEvent>) => {
397
+ e.stopPropagation();
398
+ const id = e.instanceId;
399
+ if (id === undefined) {
400
+ onHoverSwap?.(null);
401
+ return;
402
+ }
403
+
404
+ hoveredSlot.current = id;
405
+ document.body.style.cursor = "pointer";
406
+
407
+ const car = carSystem.getCarSwapInfo(id);
408
+ if (!car || !onHoverSwap) return;
409
+
410
+ onHoverSwap({
411
+ address: car.walletAddress,
412
+ tokenIndex: 0,
413
+ screenX: e.nativeEvent.clientX,
414
+ screenY: e.nativeEvent.clientY,
415
+ mode: "swap",
416
+ swapSignature: car.signature,
417
+ swapTokenIn: car.tokenIn ?? undefined,
418
+ swapTokenOut: car.tokenOut ?? undefined,
419
+ swapAmountSol: car.amountSol ?? undefined,
420
+ });
421
+ },
422
+ [carSystem, onHoverSwap],
423
+ );
424
+
425
+ const handlePointerOut = useCallback(() => {
426
+ hoveredSlot.current = null;
427
+ document.body.style.cursor = "default";
428
+ onHoverSwap?.(null);
429
+ }, [onHoverSwap]);
430
+
431
+ return (
432
+ <>
433
+ {/* Invisible hitbox — 3x larger for easy clicking/hovering */}
434
+ <instancedMesh
435
+ ref={hitboxRef}
436
+ args={[undefined!, undefined!, MAX_CARS]}
437
+ frustumCulled={false}
438
+ onPointerMove={handlePointerMove}
439
+ onPointerOut={handlePointerOut}
440
+ onClick={handleClick}
441
+ >
442
+ <boxGeometry args={[1, 1, 1]} />
443
+ <meshBasicMaterial transparent opacity={0} depthWrite={false} />
444
+ </instancedMesh>
445
+
446
+ {/* Car body */}
447
+ <instancedMesh
448
+ ref={bodyRef}
449
+ args={[undefined!, undefined!, MAX_CARS]}
450
+ frustumCulled={false}
451
+ >
452
+ <boxGeometry args={[1, 1, 1]} />
453
+ <meshStandardMaterial roughness={0.6} metalness={0.3} />
454
+ </instancedMesh>
455
+
456
+ {/* Cabin */}
457
+ <instancedMesh
458
+ ref={cabinRef}
459
+ args={[undefined!, undefined!, MAX_CARS]}
460
+ frustumCulled={false}
461
+ >
462
+ <boxGeometry args={[1, 1, 1]} />
463
+ <meshStandardMaterial roughness={0.5} metalness={0.2} />
464
+ </instancedMesh>
465
+
466
+ {/* Wheels */}
467
+ <instancedMesh
468
+ ref={wheelRef}
469
+ args={[undefined!, undefined!, TOTAL_WHEELS]}
470
+ frustumCulled={false}
471
+ >
472
+ <cylinderGeometry args={[1, 1, 1, 8]} />
473
+ <meshStandardMaterial color="#222222" roughness={0.8} metalness={0.1} />
474
+ </instancedMesh>
475
+
476
+ {/* Headlight decals */}
477
+ <instancedMesh
478
+ ref={decalRef}
479
+ args={[undefined!, undefined!, MAX_CARS]}
480
+ frustumCulled={false}
481
+ renderOrder={-1}
482
+ material={decalMaterial}
483
+ >
484
+ <circleGeometry args={[1, 32]} />
485
+ </instancedMesh>
486
+
487
+ {/* Arrow markers — floating above highlighted cars */}
488
+ <instancedMesh
489
+ ref={markerRef}
490
+ args={[undefined!, undefined!, MAX_CARS]}
491
+ frustumCulled={false}
492
+ >
493
+ <coneGeometry args={[1, 1, 6]} />
494
+ <meshBasicMaterial
495
+ color="#8b5cf6"
496
+ transparent
497
+ opacity={0.7}
498
+ side={THREE.DoubleSide}
499
+ depthWrite={false}
500
+ />
501
+ </instancedMesh>
502
+ </>
503
+ );
504
+ }