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,630 @@
1
+ "use client";
2
+
3
+ import { useRef, useMemo, useEffect, useState } from "react";
4
+ import { useFrame } from "@react-three/fiber";
5
+ import { Html } from "@react-three/drei";
6
+ import * as THREE from "three";
7
+
8
+ // ─── Dubai District Position (northeast corner, outside main grid) ──────────
9
+ const DUBAI_X = 260;
10
+ const DUBAI_Z = -260;
11
+
12
+ // ─── Seeded random ──────────────────────────────────────────────────────────
13
+ function sr(seed: number) {
14
+ const x = Math.sin(seed * 127.1 + 311.7) * 43758.5453;
15
+ return x - Math.floor(x);
16
+ }
17
+
18
+ // ─── Materials ──────────────────────────────────────────────────────────────
19
+ function useDubaiMats() {
20
+ return useMemo(() => ({
21
+ chrome: new THREE.MeshStandardMaterial({ color: "#c0c8d0", metalness: 0.95, roughness: 0.1 }),
22
+ goldGlass: new THREE.MeshStandardMaterial({ color: "#d4a843", metalness: 0.7, roughness: 0.15, transparent: true, opacity: 0.85 }),
23
+ darkGlass: new THREE.MeshStandardMaterial({ color: "#1a2a3a", metalness: 0.6, roughness: 0.05, transparent: true, opacity: 0.75 }),
24
+ sand: new THREE.MeshStandardMaterial({ color: "#e8d5a8", roughness: 0.95 }),
25
+ white: new THREE.MeshStandardMaterial({ color: "#f5f0ea", roughness: 0.4 }),
26
+ lamboYellow: new THREE.MeshStandardMaterial({ color: "#f5c518", metalness: 0.85, roughness: 0.15 }),
27
+ lamboRed: new THREE.MeshStandardMaterial({ color: "#cc1100", metalness: 0.85, roughness: 0.15 }),
28
+ neonPink: new THREE.MeshStandardMaterial({ color: "#ff1493", emissive: "#ff1493", emissiveIntensity: 2, toneMapped: false }),
29
+ neonBlue: new THREE.MeshStandardMaterial({ color: "#00bfff", emissive: "#00bfff", emissiveIntensity: 2, toneMapped: false }),
30
+ pool: new THREE.MeshStandardMaterial({ color: "#00ccff", transparent: true, opacity: 0.6, metalness: 0.3, roughness: 0.1 }),
31
+ }), []);
32
+ }
33
+
34
+ // ─── BURJ KHALIFA ───────────────────────────────────────────────────────────
35
+ function BurjKhalifa({ mats }: { mats: ReturnType<typeof useDubaiMats> }) {
36
+ const spireRef = useRef<THREE.Mesh>(null);
37
+
38
+ useFrame(({ clock }) => {
39
+ if (spireRef.current) {
40
+ const t = clock.getElapsedTime();
41
+ (spireRef.current.material as THREE.MeshStandardMaterial).emissiveIntensity =
42
+ 1.5 + Math.sin(t * 3) * 1.0;
43
+ }
44
+ });
45
+
46
+ return (
47
+ <group>
48
+ {/* Base podium */}
49
+ <mesh position={[0, 2, 0]} material={mats.white} castShadow>
50
+ <boxGeometry args={[18, 4, 18]} />
51
+ </mesh>
52
+ {/* Lower section */}
53
+ <mesh position={[0, 20, 0]} material={mats.chrome} castShadow>
54
+ <boxGeometry args={[12, 36, 12]} />
55
+ </mesh>
56
+ {/* Mid taper */}
57
+ <mesh position={[0, 48, 0]} material={mats.goldGlass} castShadow>
58
+ <boxGeometry args={[8, 20, 8]} />
59
+ </mesh>
60
+ {/* Upper taper */}
61
+ <mesh position={[0, 66, 0]} material={mats.chrome} castShadow>
62
+ <boxGeometry args={[5, 16, 5]} />
63
+ </mesh>
64
+ {/* Needle section */}
65
+ <mesh position={[0, 82, 0]} material={mats.chrome} castShadow>
66
+ <cylinderGeometry args={[0.3, 2.5, 16, 8]} />
67
+ </mesh>
68
+ {/* Spire tip — pulsing */}
69
+ <mesh ref={spireRef} position={[0, 94, 0]}>
70
+ <sphereGeometry args={[0.6, 8, 8]} />
71
+ <meshStandardMaterial color="#ff6600" emissive="#ff6600" emissiveIntensity={2} toneMapped={false} />
72
+ </mesh>
73
+ {/* Y-shaped wings */}
74
+ {[0, 120, 240].map((angle) => (
75
+ <mesh
76
+ key={angle}
77
+ position={[
78
+ Math.sin((angle * Math.PI) / 180) * 7,
79
+ 25,
80
+ Math.cos((angle * Math.PI) / 180) * 7,
81
+ ]}
82
+ rotation={[0, (angle * Math.PI) / 180, 0]}
83
+ material={mats.darkGlass}
84
+ castShadow
85
+ >
86
+ <boxGeometry args={[2, 30, 6]} />
87
+ </mesh>
88
+ ))}
89
+ {/* Window strips */}
90
+ {[12, 24, 36, 48, 60, 70].map((y, i) => (
91
+ <mesh key={i} position={[6.1, y, 0]} material={mats.darkGlass}>
92
+ <planeGeometry args={[0.5, 3]} />
93
+ </mesh>
94
+ ))}
95
+ </group>
96
+ );
97
+ }
98
+
99
+ // ─── LAMBORGHINI (parked) ───────────────────────────────────────────────────
100
+ function Lambo({ mats, color }: { mats: ReturnType<typeof useDubaiMats>; color: "yellow" | "red" }) {
101
+ const mat = color === "yellow" ? mats.lamboYellow : mats.lamboRed;
102
+ return (
103
+ <group>
104
+ {/* Body */}
105
+ <mesh position={[0, 0.4, 0]} material={mat} castShadow>
106
+ <boxGeometry args={[1.8, 0.5, 4.5]} />
107
+ </mesh>
108
+ {/* Windshield wedge */}
109
+ <mesh position={[0, 0.75, -0.3]} material={mats.darkGlass}>
110
+ <boxGeometry args={[1.6, 0.3, 1.5]} />
111
+ </mesh>
112
+ {/* Wheels */}
113
+ {[[-0.9, 0.2, 1.4], [0.9, 0.2, 1.4], [-0.9, 0.2, -1.4], [0.9, 0.2, -1.4]].map((p, i) => (
114
+ <mesh key={i} position={p as [number, number, number]} rotation={[0, 0, Math.PI / 2]}>
115
+ <cylinderGeometry args={[0.22, 0.22, 0.15, 12]} />
116
+ <meshStandardMaterial color="#222" metalness={0.5} roughness={0.4} />
117
+ </mesh>
118
+ ))}
119
+ {/* Headlights */}
120
+ <mesh position={[0.5, 0.5, 2.26]}>
121
+ <sphereGeometry args={[0.12, 6, 6]} />
122
+ <meshStandardMaterial color="#ffffaa" emissive="#ffffaa" emissiveIntensity={1.5} toneMapped={false} />
123
+ </mesh>
124
+ <mesh position={[-0.5, 0.5, 2.26]}>
125
+ <sphereGeometry args={[0.12, 6, 6]} />
126
+ <meshStandardMaterial color="#ffffaa" emissive="#ffffaa" emissiveIntensity={1.5} toneMapped={false} />
127
+ </mesh>
128
+ </group>
129
+ );
130
+ }
131
+
132
+ // ─── INFLUENCER (walking figure with selfie stick + phone) ──────────────────
133
+ function Influencer({
134
+ startX, startZ, seed, label, emoji,
135
+ }: {
136
+ startX: number; startZ: number; seed: number; label: string; emoji: string;
137
+ }) {
138
+ const groupRef = useRef<THREE.Group>(null);
139
+ const phase = useRef(sr(seed) * Math.PI * 2);
140
+ const radius = 6 + sr(seed + 1) * 10;
141
+ const speed = 0.15 + sr(seed + 2) * 0.2;
142
+ const skinTone = useMemo(() => {
143
+ const tones = ["#f5cba7", "#d4a574", "#8d5524", "#c68642", "#f1c27d"];
144
+ return tones[Math.floor(sr(seed + 3) * tones.length)];
145
+ }, [seed]);
146
+ const outfitColor = useMemo(() => {
147
+ const fits = ["#ff1493", "#00bfff", "#ffd700", "#ff4500", "#7b68ee", "#00ff7f"];
148
+ return fits[Math.floor(sr(seed + 4) * fits.length)];
149
+ }, [seed]);
150
+
151
+ useFrame(({ clock }) => {
152
+ if (!groupRef.current) return;
153
+ const t = clock.getElapsedTime();
154
+ phase.current += speed * 0.016;
155
+ const x = startX + Math.sin(phase.current) * radius;
156
+ const z = startZ + Math.cos(phase.current * 0.7) * radius;
157
+ groupRef.current.position.set(x, 0, z);
158
+ // Bounce walk
159
+ groupRef.current.position.y = Math.abs(Math.sin(t * 4 + seed)) * 0.15;
160
+ // Face direction
161
+ const dx = Math.cos(phase.current) * radius * speed;
162
+ const dz = -Math.sin(phase.current * 0.7) * radius * speed * 0.7;
163
+ if (Math.abs(dx) > 0.001 || Math.abs(dz) > 0.001) {
164
+ groupRef.current.rotation.y = Math.atan2(dx, dz);
165
+ }
166
+ });
167
+
168
+ return (
169
+ <group ref={groupRef}>
170
+ {/* Body */}
171
+ <mesh position={[0, 0.7, 0]} castShadow>
172
+ <capsuleGeometry args={[0.25, 0.5, 4, 8]} />
173
+ <meshStandardMaterial color={outfitColor} />
174
+ </mesh>
175
+ {/* Head */}
176
+ <mesh position={[0, 1.25, 0]} castShadow>
177
+ <sphereGeometry args={[0.2, 8, 8]} />
178
+ <meshStandardMaterial color={skinTone} />
179
+ </mesh>
180
+ {/* Sunglasses */}
181
+ <mesh position={[0, 1.3, 0.18]}>
182
+ <boxGeometry args={[0.35, 0.08, 0.05]} />
183
+ <meshStandardMaterial color="#111" />
184
+ </mesh>
185
+ {/* Selfie stick arm */}
186
+ <mesh position={[0.35, 1.4, 0.3]} rotation={[0.3, 0, 0.5]}>
187
+ <cylinderGeometry args={[0.02, 0.02, 1.2, 4]} />
188
+ <meshStandardMaterial color="#888" metalness={0.8} />
189
+ </mesh>
190
+ {/* Phone on stick */}
191
+ <mesh position={[0.7, 1.9, 0.6]} rotation={[0.3, 0, 0.5]}>
192
+ <boxGeometry args={[0.2, 0.35, 0.03]} />
193
+ <meshStandardMaterial color="#222" emissive="#334488" emissiveIntensity={0.5} />
194
+ </mesh>
195
+ {/* Name + emoji tag */}
196
+ <Html center position={[0, 1.8, 0]} distanceFactor={18} style={{ pointerEvents: "none" }}>
197
+ <div className="px-1.5 py-0.5 rounded bg-black/80 border border-pink-500/40 whitespace-nowrap backdrop-blur-sm">
198
+ <span className="text-[8px] font-bold text-pink-400">{emoji} {label}</span>
199
+ </div>
200
+ </Html>
201
+ </group>
202
+ );
203
+ }
204
+
205
+ // ─── PAPARAZZI FLASH ────────────────────────────────────────────────────────
206
+ function PaparazziFlashes() {
207
+ const flashesRef = useRef<THREE.InstancedMesh>(null);
208
+ const count = 12;
209
+ const dummy = useMemo(() => new THREE.Object3D(), []);
210
+ const flashData = useMemo(() =>
211
+ Array.from({ length: count }, (_, i) => ({
212
+ x: (sr(i * 7) - 0.5) * 40,
213
+ z: (sr(i * 13) - 0.5) * 40,
214
+ phase: sr(i * 19) * Math.PI * 2,
215
+ interval: 1.5 + sr(i * 23) * 3,
216
+ })),
217
+ [],
218
+ );
219
+
220
+ useFrame(({ clock }) => {
221
+ if (!flashesRef.current) return;
222
+ const t = clock.getElapsedTime();
223
+ for (let i = 0; i < count; i++) {
224
+ const f = flashData[i];
225
+ const flash = Math.sin((t + f.phase) * (Math.PI * 2 / f.interval));
226
+ const on = flash > 0.92; // brief burst
227
+ dummy.position.set(f.x, on ? 1.5 : -10, f.z);
228
+ dummy.scale.setScalar(on ? 1.5 + Math.random() * 0.5 : 0.01);
229
+ dummy.updateMatrix();
230
+ flashesRef.current.setMatrixAt(i, dummy.matrix);
231
+ flashesRef.current.setColorAt(i, new THREE.Color(on ? "#ffffff" : "#000000"));
232
+ }
233
+ flashesRef.current.instanceMatrix.needsUpdate = true;
234
+ if (flashesRef.current.instanceColor) flashesRef.current.instanceColor.needsUpdate = true;
235
+ });
236
+
237
+ return (
238
+ <instancedMesh ref={flashesRef} args={[undefined, undefined, count]}>
239
+ <sphereGeometry args={[0.4, 6, 6]} />
240
+ <meshBasicMaterial color="#ffffff" transparent opacity={0.9} toneMapped={false} />
241
+ </instancedMesh>
242
+ );
243
+ }
244
+
245
+ // ─── EXPLOSIONS / FIREWORKS ─────────────────────────────────────────────────
246
+ interface Boom {
247
+ id: number;
248
+ x: number;
249
+ z: number;
250
+ birth: number;
251
+ color: string;
252
+ scale: number;
253
+ type: "explosion" | "firework";
254
+ }
255
+
256
+ function Explosions() {
257
+ const [booms, setBooms] = useState<Boom[]>([]);
258
+ const nextId = useRef(0);
259
+
260
+ useEffect(() => {
261
+ const interval = setInterval(() => {
262
+ const id = nextId.current++;
263
+ const type = Math.random() > 0.4 ? "firework" : "explosion";
264
+ const colors = type === "firework"
265
+ ? ["#ff1493", "#00bfff", "#ffd700", "#7b68ee", "#ff4500", "#00ff7f"]
266
+ : ["#ff4500", "#ff6600", "#ffcc00"];
267
+ setBooms((prev) => [
268
+ ...prev.slice(-8),
269
+ {
270
+ id,
271
+ x: (Math.random() - 0.5) * 50,
272
+ z: (Math.random() - 0.5) * 50,
273
+ birth: Date.now(),
274
+ color: colors[Math.floor(Math.random() * colors.length)],
275
+ scale: 1 + Math.random() * 2,
276
+ type,
277
+ },
278
+ ]);
279
+ }, 800 + Math.random() * 1500);
280
+ return () => clearInterval(interval);
281
+ }, []);
282
+
283
+ return (
284
+ <group>
285
+ {booms.map((b) => (
286
+ <BoomEffect key={b.id} boom={b} onDone={() => setBooms((p) => p.filter((x) => x.id !== b.id))} />
287
+ ))}
288
+ </group>
289
+ );
290
+ }
291
+
292
+ function BoomEffect({ boom, onDone }: { boom: Boom; onDone: () => void }) {
293
+ const ref = useRef<THREE.Mesh>(null);
294
+ const particlesRef = useRef<THREE.Points>(null);
295
+ const age = useRef(0);
296
+ const maxAge = boom.type === "firework" ? 2.5 : 1.5;
297
+
298
+ const particleGeo = useMemo(() => {
299
+ const count = boom.type === "firework" ? 40 : 20;
300
+ const positions = new Float32Array(count * 3);
301
+ const velocities: number[] = [];
302
+ for (let i = 0; i < count; i++) {
303
+ positions[i * 3] = 0;
304
+ positions[i * 3 + 1] = 0;
305
+ positions[i * 3 + 2] = 0;
306
+ const theta = Math.random() * Math.PI * 2;
307
+ const phi = Math.random() * Math.PI;
308
+ const spd = 3 + Math.random() * 8;
309
+ velocities.push(
310
+ Math.sin(phi) * Math.cos(theta) * spd,
311
+ Math.cos(phi) * spd * 0.7 + 2,
312
+ Math.sin(phi) * Math.sin(theta) * spd,
313
+ );
314
+ }
315
+ const geo = new THREE.BufferGeometry();
316
+ geo.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3));
317
+ (geo as any)._velocities = velocities;
318
+ return geo;
319
+ }, [boom.type]);
320
+
321
+ useFrame((_, delta) => {
322
+ age.current += delta;
323
+ if (age.current > maxAge) {
324
+ onDone();
325
+ return;
326
+ }
327
+ const t = age.current / maxAge;
328
+
329
+ // Expanding glow sphere
330
+ if (ref.current) {
331
+ const s = boom.scale * (boom.type === "firework" ? t * 4 : (1 - t) * 3 + t * 6);
332
+ ref.current.scale.setScalar(s);
333
+ (ref.current.material as THREE.MeshBasicMaterial).opacity = Math.max(0, 1 - t * 1.2);
334
+ }
335
+
336
+ // Particles
337
+ if (particlesRef.current) {
338
+ const pos = particlesRef.current.geometry.attributes.position;
339
+ const vels = (particlesRef.current.geometry as any)._velocities as number[];
340
+ for (let i = 0; i < pos.count; i++) {
341
+ pos.setXYZ(
342
+ i,
343
+ vels[i * 3] * age.current,
344
+ vels[i * 3 + 1] * age.current - 4.9 * age.current * age.current,
345
+ vels[i * 3 + 2] * age.current,
346
+ );
347
+ }
348
+ pos.needsUpdate = true;
349
+ (particlesRef.current.material as THREE.PointsMaterial).opacity = Math.max(0, 1 - t);
350
+ }
351
+ });
352
+
353
+ const y = boom.type === "firework" ? 15 + Math.random() * 20 : 2;
354
+
355
+ return (
356
+ <group position={[boom.x, y, boom.z]}>
357
+ {/* Central flash */}
358
+ <mesh ref={ref}>
359
+ <sphereGeometry args={[0.5, 8, 8]} />
360
+ <meshBasicMaterial color={boom.color} transparent opacity={1} toneMapped={false} />
361
+ </mesh>
362
+ {/* Particles */}
363
+ <points ref={particlesRef} geometry={particleGeo}>
364
+ <pointsMaterial color={boom.color} size={0.4} transparent opacity={1} toneMapped={false} sizeAttenuation />
365
+ </points>
366
+ </group>
367
+ );
368
+ }
369
+
370
+ // ─── FLOATING EMOJI REACTIONS ───────────────────────────────────────────────
371
+ function FloatingEmojis() {
372
+ const [emojis, setEmojis] = useState<{ id: number; x: number; z: number; emoji: string; birth: number }[]>([]);
373
+ const nextId = useRef(0);
374
+
375
+ useEffect(() => {
376
+ const reactions = ["🔥", "💰", "😂", "💎", "🚀", "👑", "📸", "💸", "🤑", "🏎️", "💅", "✨", "🍾", "💀", "🫡"];
377
+ const interval = setInterval(() => {
378
+ setEmojis((prev) => [
379
+ ...prev.slice(-6),
380
+ {
381
+ id: nextId.current++,
382
+ x: (Math.random() - 0.5) * 35,
383
+ z: (Math.random() - 0.5) * 35,
384
+ emoji: reactions[Math.floor(Math.random() * reactions.length)],
385
+ birth: Date.now(),
386
+ },
387
+ ]);
388
+ }, 600 + Math.random() * 1200);
389
+ return () => clearInterval(interval);
390
+ }, []);
391
+
392
+ return (
393
+ <group>
394
+ {emojis.map((e) => (
395
+ <FloatingEmoji
396
+ key={e.id}
397
+ x={e.x}
398
+ z={e.z}
399
+ emoji={e.emoji}
400
+ birth={e.birth}
401
+ onDone={() => setEmojis((p) => p.filter((x) => x.id !== e.id))}
402
+ />
403
+ ))}
404
+ </group>
405
+ );
406
+ }
407
+
408
+ function FloatingEmoji({ x, z, emoji, birth, onDone }: {
409
+ x: number; z: number; emoji: string; birth: number; onDone: () => void;
410
+ }) {
411
+ const ref = useRef<THREE.Group>(null);
412
+
413
+ useFrame(() => {
414
+ if (!ref.current) return;
415
+ const age = (Date.now() - birth) / 1000;
416
+ if (age > 3) { onDone(); return; }
417
+ ref.current.position.y = 3 + age * 4;
418
+ ref.current.scale.setScalar(Math.max(0, 1 - age / 3));
419
+ });
420
+
421
+ return (
422
+ <group ref={ref} position={[x, 3, z]}>
423
+ <Html center distanceFactor={20} style={{ pointerEvents: "none" }}>
424
+ <div className="text-2xl select-none drop-shadow-lg">{emoji}</div>
425
+ </Html>
426
+ </group>
427
+ );
428
+ }
429
+
430
+ // ─── INFINITY POOL ──────────────────────────────────────────────────────────
431
+ function InfinityPool({ mats }: { mats: ReturnType<typeof useDubaiMats> }) {
432
+ const waterRef = useRef<THREE.Mesh>(null);
433
+
434
+ useFrame(({ clock }) => {
435
+ if (!waterRef.current) return;
436
+ waterRef.current.position.y = 0.15 + Math.sin(clock.getElapsedTime() * 1.5) * 0.03;
437
+ });
438
+
439
+ return (
440
+ <group position={[20, 0, 15]}>
441
+ {/* Pool basin */}
442
+ <mesh position={[0, -0.1, 0]} material={mats.white}>
443
+ <boxGeometry args={[12, 0.5, 6]} />
444
+ </mesh>
445
+ {/* Water surface */}
446
+ <mesh ref={waterRef} position={[0, 0.15, 0]} rotation={[-Math.PI / 2, 0, 0]} material={mats.pool}>
447
+ <planeGeometry args={[11.5, 5.5]} />
448
+ </mesh>
449
+ </group>
450
+ );
451
+ }
452
+
453
+ // ─── NEON SIGN ──────────────────────────────────────────────────────────────
454
+ function DubaiSign({ mats }: { mats: ReturnType<typeof useDubaiMats> }) {
455
+ return (
456
+ <group position={[0, 0, 25]}>
457
+ {/* Arch */}
458
+ <mesh position={[0, 5, 0]} material={mats.chrome}>
459
+ <torusGeometry args={[6, 0.3, 8, 32, Math.PI]} />
460
+ </mesh>
461
+ {/* Pillars */}
462
+ <mesh position={[-6, 2.5, 0]} material={mats.chrome}>
463
+ <cylinderGeometry args={[0.3, 0.3, 5, 8]} />
464
+ </mesh>
465
+ <mesh position={[6, 2.5, 0]} material={mats.chrome}>
466
+ <cylinderGeometry args={[0.3, 0.3, 5, 8]} />
467
+ </mesh>
468
+ {/* Sign text */}
469
+ <Html center position={[0, 7, 0.5]} distanceFactor={30} style={{ pointerEvents: "none" }}>
470
+ <div className="text-3xl font-black tracking-widest whitespace-nowrap"
471
+ style={{
472
+ background: "linear-gradient(90deg, #ff1493, #ffd700, #00bfff)",
473
+ WebkitBackgroundClip: "text",
474
+ WebkitTextFillColor: "transparent",
475
+ textShadow: "0 0 20px rgba(255,20,147,0.5)",
476
+ filter: "drop-shadow(0 0 10px rgba(255,215,0,0.4))",
477
+ }}
478
+ >
479
+ DUBAI MARINA
480
+ </div>
481
+ </Html>
482
+ {/* Neon strips */}
483
+ <mesh position={[0, 5, 0.4]}>
484
+ <torusGeometry args={[6.3, 0.08, 4, 32, Math.PI]} />
485
+ <meshStandardMaterial color="#ff1493" emissive="#ff1493" emissiveIntensity={3} toneMapped={false} />
486
+ </mesh>
487
+ </group>
488
+ );
489
+ }
490
+
491
+ // ─── PALM TREES ─────────────────────────────────────────────────────────────
492
+ function PalmTree({ x, z, seed }: { x: number; z: number; seed: number }) {
493
+ const lean = (sr(seed) - 0.5) * 0.3;
494
+ const height = 5 + sr(seed + 1) * 4;
495
+ const frondRef = useRef<THREE.Group>(null);
496
+
497
+ useFrame(({ clock }) => {
498
+ if (frondRef.current) {
499
+ frondRef.current.rotation.z = Math.sin(clock.getElapsedTime() * 0.8 + seed) * 0.05;
500
+ }
501
+ });
502
+
503
+ return (
504
+ <group position={[x, 0, z]} rotation={[0, 0, lean]}>
505
+ {/* Trunk */}
506
+ <mesh position={[0, height / 2, 0]}>
507
+ <cylinderGeometry args={[0.15, 0.25, height, 6]} />
508
+ <meshStandardMaterial color="#8B7355" roughness={0.9} />
509
+ </mesh>
510
+ {/* Fronds */}
511
+ <group ref={frondRef} position={[0, height, 0]}>
512
+ {Array.from({ length: 7 }, (_, i) => {
513
+ const a = (i / 7) * Math.PI * 2;
514
+ return (
515
+ <mesh key={i} position={[Math.cos(a) * 1.5, -0.3, Math.sin(a) * 1.5]} rotation={[0.6, a, 0]}>
516
+ <boxGeometry args={[0.6, 0.05, 3]} />
517
+ <meshStandardMaterial color="#2d5a27" />
518
+ </mesh>
519
+ );
520
+ })}
521
+ </group>
522
+ </group>
523
+ );
524
+ }
525
+
526
+ // ─── INFLUENCER DEFINITIONS ─────────────────────────────────────────────────
527
+ const INFLUENCERS = [
528
+ { label: "CryptoQueen", emoji: "👑", sx: -8, sz: 8, seed: 100 },
529
+ { label: "LamboLord", emoji: "🏎️", sx: 12, sz: -5, seed: 200 },
530
+ { label: "NFTBae", emoji: "💅", sx: -15, sz: -10, seed: 300 },
531
+ { label: "DeFiDaddy", emoji: "💰", sx: 5, sz: 15, seed: 400 },
532
+ { label: "MoonBoi", emoji: "🚀", sx: -5, sz: -18, seed: 500 },
533
+ { label: "YachtClub", emoji: "⛵", sx: 18, sz: 8, seed: 600 },
534
+ { label: "ShillMaster", emoji: "📢", sx: -18, sz: 0, seed: 700 },
535
+ { label: "RugPullRick", emoji: "💀", sx: 10, sz: -15, seed: 800 },
536
+ ];
537
+
538
+ // ─── MAIN COMPONENT ────────────────────────────────────────────────────────
539
+ export default function DubaiDistrict() {
540
+ const mats = useDubaiMats();
541
+
542
+ return (
543
+ <group position={[DUBAI_X, 0, DUBAI_Z]}>
544
+ {/* Sand island base */}
545
+ <mesh position={[0, -0.2, 0]} rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
546
+ <circleGeometry args={[55, 32]} />
547
+ <meshStandardMaterial color="#e8d5a8" roughness={0.95} />
548
+ </mesh>
549
+
550
+ {/* Burj Khalifa centerpiece */}
551
+ <BurjKhalifa mats={mats} />
552
+
553
+ {/* Neon entrance sign */}
554
+ <DubaiSign mats={mats} />
555
+
556
+ {/* Lambos parked out front */}
557
+ <group position={[-6, 0, 20]} rotation={[0, 0.3, 0]}>
558
+ <Lambo mats={mats} color="yellow" />
559
+ </group>
560
+ <group position={[6, 0, 18]} rotation={[0, -0.2, 0]}>
561
+ <Lambo mats={mats} color="red" />
562
+ </group>
563
+
564
+ {/* Infinity pool */}
565
+ <InfinityPool mats={mats} />
566
+
567
+ {/* Palm trees scattered around */}
568
+ {[
569
+ [-25, 10], [-20, -15], [25, -10], [18, 20], [-10, 22],
570
+ [30, 5], [-28, -5], [15, -22], [-5, -25], [22, 18],
571
+ ].map(([x, z], i) => (
572
+ <PalmTree key={i} x={x} z={z} seed={i * 37} />
573
+ ))}
574
+
575
+ {/* Influencers walking around taking selfies */}
576
+ {INFLUENCERS.map((inf) => (
577
+ <Influencer key={inf.label} startX={inf.sx} startZ={inf.sz} seed={inf.seed} label={inf.label} emoji={inf.emoji} />
578
+ ))}
579
+
580
+ {/* Camera flashes from paparazzi */}
581
+ <PaparazziFlashes />
582
+
583
+ {/* Explosions and fireworks constantly going off */}
584
+ <Explosions />
585
+
586
+ {/* Floating emoji reactions rising up */}
587
+ <FloatingEmojis />
588
+
589
+ {/* Spotlight beams rotating from the tower */}
590
+ <SpotlightBeams />
591
+
592
+ {/* District label */}
593
+ <Html center position={[0, 100, 0]} distanceFactor={60} style={{ pointerEvents: "none" }}>
594
+ <div className="text-center">
595
+ <div className="text-4xl mb-1">🏙️</div>
596
+ <div className="px-3 py-1 rounded-full bg-gradient-to-r from-pink-500/80 via-amber-400/80 to-cyan-400/80 backdrop-blur-sm border border-white/20">
597
+ <span className="text-sm font-black text-white tracking-wider drop-shadow-lg">
598
+ DUBAI MARINA
599
+ </span>
600
+ </div>
601
+ </div>
602
+ </Html>
603
+ </group>
604
+ );
605
+ }
606
+
607
+ // ─── ROTATING SPOTLIGHT BEAMS ───────────────────────────────────────────────
608
+ function SpotlightBeams() {
609
+ const beam1 = useRef<THREE.Mesh>(null);
610
+ const beam2 = useRef<THREE.Mesh>(null);
611
+
612
+ useFrame(({ clock }) => {
613
+ const t = clock.getElapsedTime();
614
+ if (beam1.current) beam1.current.rotation.y = t * 0.5;
615
+ if (beam2.current) beam2.current.rotation.y = -t * 0.3 + 2;
616
+ });
617
+
618
+ const beamGeo = useMemo(() => new THREE.CylinderGeometry(0.1, 4, 60, 8, 1, true), []);
619
+
620
+ return (
621
+ <group position={[0, 50, 0]}>
622
+ <mesh ref={beam1} geometry={beamGeo} rotation={[0, 0, 0.4]}>
623
+ <meshBasicMaterial color="#ff1493" transparent opacity={0.08} side={THREE.DoubleSide} toneMapped={false} />
624
+ </mesh>
625
+ <mesh ref={beam2} geometry={beamGeo} rotation={[0, 0, -0.4]}>
626
+ <meshBasicMaterial color="#00bfff" transparent opacity={0.08} side={THREE.DoubleSide} toneMapped={false} />
627
+ </mesh>
628
+ </group>
629
+ );
630
+ }