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.
- package/README.md +518 -0
- package/bin/solanapolis.js +197 -0
- package/convex/_generated/api.d.ts +175 -0
- package/convex/_generated/api.js +23 -0
- package/convex/_generated/dataModel.d.ts +60 -0
- package/convex/_generated/server.d.ts +143 -0
- package/convex/_generated/server.js +93 -0
- package/convex/agent/conversation.ts +352 -0
- package/convex/agent/embeddingsCache.ts +110 -0
- package/convex/agent/memory.ts +450 -0
- package/convex/agent/schema.ts +53 -0
- package/convex/aiChat.ts +54 -0
- package/convex/aiTown/agent.ts +382 -0
- package/convex/aiTown/agentDescription.ts +27 -0
- package/convex/aiTown/agentInputs.ts +155 -0
- package/convex/aiTown/agentOperations.ts +178 -0
- package/convex/aiTown/conversation.ts +395 -0
- package/convex/aiTown/conversationMembership.ts +38 -0
- package/convex/aiTown/game.ts +371 -0
- package/convex/aiTown/ids.ts +32 -0
- package/convex/aiTown/inputHandler.ts +9 -0
- package/convex/aiTown/inputs.ts +25 -0
- package/convex/aiTown/insertInput.ts +20 -0
- package/convex/aiTown/location.ts +32 -0
- package/convex/aiTown/main.ts +154 -0
- package/convex/aiTown/movement.ts +189 -0
- package/convex/aiTown/player.ts +310 -0
- package/convex/aiTown/playerDescription.ts +35 -0
- package/convex/aiTown/schema.ts +79 -0
- package/convex/aiTown/world.ts +65 -0
- package/convex/aiTown/worldMap.ts +74 -0
- package/convex/chat.ts +79 -0
- package/convex/constants.ts +78 -0
- package/convex/convex.config.ts +6 -0
- package/convex/crons.ts +89 -0
- package/convex/engine/abstractGame.ts +199 -0
- package/convex/engine/historicalObject.ts +355 -0
- package/convex/engine/schema.ts +56 -0
- package/convex/http.ts +36 -0
- package/convex/init.ts +110 -0
- package/convex/messages.ts +53 -0
- package/convex/npcCarAgents.ts +415 -0
- package/convex/schema.ts +61 -0
- package/convex/streaming.ts +23 -0
- package/convex/testing.ts +202 -0
- package/convex/tsconfig.json +18 -0
- package/convex/util/FastIntegerCompression.ts +221 -0
- package/convex/util/assertNever.ts +4 -0
- package/convex/util/asyncMap.ts +20 -0
- package/convex/util/compression.ts +71 -0
- package/convex/util/geometry.ts +132 -0
- package/convex/util/isSimpleObject.ts +11 -0
- package/convex/util/llm.ts +724 -0
- package/convex/util/minheap.ts +38 -0
- package/convex/util/object.ts +22 -0
- package/convex/util/sleep.ts +3 -0
- package/convex/util/types.ts +33 -0
- package/convex/util/xxhash.ts +228 -0
- package/convex/world.ts +257 -0
- package/data/animations/campfire.json +45 -0
- package/data/animations/gentlesparkle.json +37 -0
- package/data/animations/gentlesplash.json +61 -0
- package/data/animations/gentlewaterfall.json +61 -0
- package/data/animations/windmill.json +78 -0
- package/data/characters.ts +121 -0
- package/data/convertMap.js +74 -0
- package/data/gentle.js +330 -0
- package/data/spritesheets/f1.ts +75 -0
- package/data/spritesheets/f2.ts +75 -0
- package/data/spritesheets/f3.ts +75 -0
- package/data/spritesheets/f4.ts +75 -0
- package/data/spritesheets/f5.ts +75 -0
- package/data/spritesheets/f6.ts +75 -0
- package/data/spritesheets/f7.ts +75 -0
- package/data/spritesheets/f8.ts +75 -0
- package/data/spritesheets/p1.ts +59 -0
- package/data/spritesheets/p2.ts +59 -0
- package/data/spritesheets/p3.ts +59 -0
- package/data/spritesheets/player.ts +59 -0
- package/data/spritesheets/types.ts +26 -0
- package/eslint.config.mjs +37 -0
- package/next.config.ts +7 -0
- package/package.json +85 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/helius-icon.svg +84 -0
- package/public/helius-logo.svg +85 -0
- package/public/next.svg +1 -0
- package/public/plane.glb +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/scripts/clear-city.ts +74 -0
- package/scripts/seed-wallets.ts +185 -0
- package/scripts/setup-webhook.ts +73 -0
- package/src/app/api/auth/callback/route.ts +6 -0
- package/src/app/api/auth/link-wallet/route.ts +6 -0
- package/src/app/api/auth/phantom/route.ts +6 -0
- package/src/app/api/broadcast-position/route.ts +59 -0
- package/src/app/api/leaderboard/route.ts +85 -0
- package/src/app/api/network-stats/route.ts +86 -0
- package/src/app/api/parcel-reward/route.ts +181 -0
- package/src/app/api/queue-status/route.ts +30 -0
- package/src/app/api/snapshots/route.ts +37 -0
- package/src/app/api/transactions/enhanced/route.ts +57 -0
- package/src/app/api/treasury/route.ts +83 -0
- package/src/app/api/wallet/[address]/balances/route.ts +124 -0
- package/src/app/api/wallet/[address]/identity/route.ts +32 -0
- package/src/app/api/wallet/[address]/route.ts +216 -0
- package/src/app/api/wallet/[address]/traded-tokens/route.ts +41 -0
- package/src/app/api/wallets/route.ts +68 -0
- package/src/app/api/webhooks/helius/route.ts +76 -0
- package/src/app/auth/callback/page.tsx +29 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +39 -0
- package/src/app/layout.tsx +43 -0
- package/src/app/page.tsx +16 -0
- package/src/components/AITownNPCs.tsx +206 -0
- package/src/components/ActivityFeed.tsx +189 -0
- package/src/components/AuthPanel.tsx +163 -0
- package/src/components/BeachScene.tsx +280 -0
- package/src/components/Building.tsx +138 -0
- package/src/components/CesiumFlight.tsx +1768 -0
- package/src/components/CesiumGlobe.tsx +616 -0
- package/src/components/CitizenCard.tsx +442 -0
- package/src/components/CitizenCardModal.tsx +153 -0
- package/src/components/CityGrid.tsx +313 -0
- package/src/components/CityLandmarks.tsx +427 -0
- package/src/components/CityScene.tsx +1289 -0
- package/src/components/CitySlotsBadge.tsx +68 -0
- package/src/components/CockpitHUD.tsx +460 -0
- package/src/components/ConvexWrapper.tsx +19 -0
- package/src/components/DubaiDistrict.tsx +630 -0
- package/src/components/FlightMiniMap.tsx +133 -0
- package/src/components/GameChat.tsx +383 -0
- package/src/components/GameHUD.tsx +393 -0
- package/src/components/Ground.tsx +14 -0
- package/src/components/HowItWorksModal.tsx +251 -0
- package/src/components/IngestionBanner.tsx +123 -0
- package/src/components/InstancedBuildings.tsx +316 -0
- package/src/components/InstancedCars.tsx +504 -0
- package/src/components/InstancedCityPlanes.tsx +259 -0
- package/src/components/InstancedHouses.tsx +246 -0
- package/src/components/InstancedLampPosts.tsx +201 -0
- package/src/components/InstancedResidentCars.tsx +357 -0
- package/src/components/InstancedRoadDashes.tsx +42 -0
- package/src/components/InstancedSkyscrapers.tsx +434 -0
- package/src/components/InstancedTrees.tsx +67 -0
- package/src/components/LeaderboardPanel.tsx +136 -0
- package/src/components/MultiplayerPlanes.tsx +128 -0
- package/src/components/NetworkStats.tsx +83 -0
- package/src/components/NewBuildingSpotlight.tsx +93 -0
- package/src/components/ParcelChallengeBanner.tsx +242 -0
- package/src/components/ParcelReward.tsx +191 -0
- package/src/components/Park.tsx +42 -0
- package/src/components/PhantomWrapper.tsx +22 -0
- package/src/components/PixelStreamViewer.tsx +335 -0
- package/src/components/PlaneMode.tsx +190 -0
- package/src/components/PlayerCar.tsx +211 -0
- package/src/components/PlayerPlane.tsx +255 -0
- package/src/components/ProjectileRenderer.tsx +249 -0
- package/src/components/QueueStatusBanner.tsx +86 -0
- package/src/components/RealPlayerTags.tsx +82 -0
- package/src/components/SceneLighting.tsx +382 -0
- package/src/components/SelectionBeam.tsx +59 -0
- package/src/components/SwapPanel.tsx +104 -0
- package/src/components/SwapParticles.tsx +237 -0
- package/src/components/TreasureGate.tsx +505 -0
- package/src/components/WalletPanel.tsx +421 -0
- package/src/components/WalletSearch.tsx +244 -0
- package/src/components/WelcomeOverlay.tsx +135 -0
- package/src/components/WindowTooltip.tsx +498 -0
- package/src/context/AuthContext.tsx +230 -0
- package/src/lib/bot-detection.ts +125 -0
- package/src/lib/building-math.ts +136 -0
- package/src/lib/building-shader.ts +253 -0
- package/src/lib/car-paths.ts +244 -0
- package/src/lib/car-system.ts +182 -0
- package/src/lib/city-constants.ts +29 -0
- package/src/lib/city-slots.ts +35 -0
- package/src/lib/city-zoning.ts +64 -0
- package/src/lib/collision-map.ts +147 -0
- package/src/lib/day-night.ts +252 -0
- package/src/lib/export-card.ts +28 -0
- package/src/lib/helius-webhook.ts +90 -0
- package/src/lib/helius.ts +74 -0
- package/src/lib/house-shader.ts +119 -0
- package/src/lib/mock-data.ts +56 -0
- package/src/lib/multiplayer-manager.ts +329 -0
- package/src/lib/plane-physics.ts +66 -0
- package/src/lib/player-car.ts +147 -0
- package/src/lib/player-plane.ts +200 -0
- package/src/lib/projectile-system.ts +272 -0
- package/src/lib/skyscraper-types.ts +52 -0
- package/src/lib/sound-engine.ts +464 -0
- package/src/lib/supabase-admin.ts +9 -0
- package/src/lib/supabase.ts +8 -0
- package/src/lib/swap-events.ts +70 -0
- package/src/middleware.ts +37 -0
- package/src/types/phantom.d.ts +16 -0
- package/src/types/wallet.ts +20 -0
- 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
|
+
}
|