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,1289 @@
1
+ "use client";
2
+
3
+ import { useState, useRef, useEffect, useCallback } from "react";
4
+ import { Canvas, useFrame } from "@react-three/fiber";
5
+ import { OrbitControls } from "@react-three/drei";
6
+ import { OrbitControls as OrbitControlsImpl } from "three-stdlib";
7
+ import * as THREE from "three";
8
+ import CityGrid from "./CityGrid";
9
+ import SceneLighting from "./SceneLighting";
10
+ import SelectionBeam from "./SelectionBeam";
11
+ import WalletPanel from "./WalletPanel";
12
+ import AuthPanel from "./AuthPanel";
13
+ import WalletSearch from "./WalletSearch";
14
+ import { WalletBuilding, PlacedWallet } from "@/types/wallet";
15
+ import { getBuildingDimensions, getWalletWorldPosition } from "@/lib/building-math";
16
+ import WindowTooltip, { WindowHoverInfo } from "./WindowTooltip";
17
+ import InstancedCars, { TrackedCarInfo } from "./InstancedCars";
18
+ import SwapPanel from "./SwapPanel";
19
+ import { useSwapEvents } from "@/lib/swap-events";
20
+ import HowItWorksModal from "./HowItWorksModal";
21
+ import { useAuth } from "@/context/AuthContext";
22
+ import { lazy, Suspense, useMemo } from "react";
23
+ import PlayerCar from "./PlayerCar";
24
+ import PlayerPlane from "./PlayerPlane";
25
+ import { buildCollisionMap } from "@/lib/collision-map";
26
+ import ActivityFeed from "./ActivityFeed";
27
+ import NetworkStats from "./NetworkStats";
28
+ import SwapParticles from "./SwapParticles";
29
+ import NewBuildingSpotlight from "./NewBuildingSpotlight";
30
+ import InstancedCityPlanes from "./InstancedCityPlanes";
31
+ import InstancedResidentCars from "./InstancedResidentCars";
32
+ import CitySlotsBadge from "./CitySlotsBadge";
33
+ import { getSlotCounts } from "@/lib/city-slots";
34
+ import ProjectileRenderer from "./ProjectileRenderer";
35
+ import GameHUD from "./GameHUD";
36
+ import { ProjectileManager } from "@/lib/projectile-system";
37
+ import { MultiplayerManager } from "@/lib/multiplayer-manager";
38
+ import LeaderboardPanel from "./LeaderboardPanel";
39
+ import GameChat from "./GameChat";
40
+ import BeachScene from "./BeachScene";
41
+ import { PlaneEngine, initAudio, playClick } from "@/lib/sound-engine";
42
+ import ParcelReward from "./ParcelReward";
43
+ import ParcelChallengeBanner from "./ParcelChallengeBanner";
44
+ import TreasureGate from "./TreasureGate";
45
+ import CockpitHUD from "./CockpitHUD";
46
+ import RealPlayerTags from "./RealPlayerTags";
47
+ import CityLandmarks from "./CityLandmarks";
48
+ import AITownNPCs from "./AITownNPCs";
49
+ import DubaiDistrict from "./DubaiDistrict";
50
+
51
+ const CitizenCardModal = lazy(() => import("./CitizenCardModal"));
52
+ const CesiumFlight = lazy(() => import("./CesiumFlight"));
53
+ const CesiumGlobe = lazy(() => import("./CesiumGlobe"));
54
+
55
+ const _trackTarget = new THREE.Vector3();
56
+ const _prevTarget = new THREE.Vector3();
57
+ const _delta = new THREE.Vector3();
58
+ const _desiredCamPos = new THREE.Vector3();
59
+
60
+ // Chase camera constants
61
+ const CHASE_DISTANCE = 18;
62
+ const CHASE_HEIGHT = 10;
63
+ const CHASE_LOOK_AHEAD = 8;
64
+ const CHASE_CAM_LERP = 0.06;
65
+
66
+ // Flight camera constants
67
+ const FLIGHT_DISTANCE = 25;
68
+ const FLIGHT_HEIGHT = 8;
69
+ const FLIGHT_LOOK_AHEAD = 12;
70
+ const FLIGHT_CAM_LERP = 0.04;
71
+
72
+
73
+
74
+ function CameraControls({
75
+ targetPosition,
76
+ trackingRef,
77
+ isTracking,
78
+ playerCarMode,
79
+ flyMode,
80
+ headingRef,
81
+ }: {
82
+ targetPosition: [number, number, number] | null;
83
+ trackingRef: React.MutableRefObject<[number, number, number] | null>;
84
+ isTracking: boolean;
85
+ playerCarMode: boolean;
86
+ flyMode: boolean;
87
+ headingRef: React.MutableRefObject<number>;
88
+ }) {
89
+ const controlsRef = useRef<OrbitControlsImpl>(null);
90
+
91
+ useEffect(() => {
92
+ const controls = controlsRef.current;
93
+ if (!controls) return;
94
+
95
+ if (flyMode || playerCarMode) {
96
+ // Disable all user camera manipulation in chase-cam or flight-cam mode
97
+ controls.enableRotate = false;
98
+ controls.enablePan = false;
99
+ controls.enableZoom = false;
100
+ return;
101
+ }
102
+
103
+ if (isTracking) {
104
+ // Orbit mode for AI car tracking — useFrame handles position
105
+ controls.enableRotate = true;
106
+ controls.enablePan = false;
107
+ controls.enableZoom = true;
108
+ controls.mouseButtons = {
109
+ LEFT: THREE.MOUSE.ROTATE,
110
+ MIDDLE: THREE.MOUSE.DOLLY,
111
+ RIGHT: THREE.MOUSE.PAN,
112
+ };
113
+ return;
114
+ }
115
+
116
+ controls.enableZoom = true;
117
+
118
+ if (targetPosition) {
119
+ const target = new THREE.Vector3(...targetPosition);
120
+ const start = controls.target.clone();
121
+ const startTime = performance.now();
122
+ const duration = 400;
123
+
124
+ function animate() {
125
+ const elapsed = performance.now() - startTime;
126
+ const t = Math.min(elapsed / duration, 1);
127
+ const ease = 1 - Math.pow(1 - t, 3);
128
+
129
+ controls!.target.lerpVectors(start, target, ease);
130
+ controls!.update();
131
+
132
+ if (t < 1) {
133
+ requestAnimationFrame(animate);
134
+ }
135
+ }
136
+ animate();
137
+
138
+ controls.enableRotate = true;
139
+ controls.enablePan = false;
140
+ controls.mouseButtons = {
141
+ LEFT: THREE.MOUSE.ROTATE,
142
+ MIDDLE: THREE.MOUSE.DOLLY,
143
+ RIGHT: THREE.MOUSE.PAN,
144
+ };
145
+ } else {
146
+ controls.enableRotate = true;
147
+ controls.enablePan = true;
148
+ controls.mouseButtons = {
149
+ LEFT: THREE.MOUSE.PAN,
150
+ MIDDLE: THREE.MOUSE.DOLLY,
151
+ RIGHT: THREE.MOUSE.ROTATE,
152
+ };
153
+ }
154
+ }, [targetPosition, isTracking, playerCarMode, flyMode]);
155
+
156
+ useFrame(() => {
157
+ const controls = controlsRef.current;
158
+ if (!controls || !isTracking || !trackingRef.current) return;
159
+
160
+ const pos = trackingRef.current;
161
+
162
+ if (flyMode) {
163
+ // Flight camera: behind & slightly above the plane
164
+ const heading = headingRef.current;
165
+ _desiredCamPos.set(
166
+ pos[0] - Math.sin(heading) * FLIGHT_DISTANCE,
167
+ pos[1] + FLIGHT_HEIGHT,
168
+ pos[2] - Math.cos(heading) * FLIGHT_DISTANCE,
169
+ );
170
+ controls.object.position.lerp(_desiredCamPos, FLIGHT_CAM_LERP);
171
+
172
+ _trackTarget.set(
173
+ pos[0] + Math.sin(heading) * FLIGHT_LOOK_AHEAD,
174
+ pos[1],
175
+ pos[2] + Math.cos(heading) * FLIGHT_LOOK_AHEAD,
176
+ );
177
+ controls.target.lerp(_trackTarget, FLIGHT_CAM_LERP);
178
+ controls.update();
179
+ return;
180
+ }
181
+
182
+ if (playerCarMode) {
183
+ // Chase camera: position behind & above the car, look ahead of it
184
+ const heading = headingRef.current;
185
+ _desiredCamPos.set(
186
+ pos[0] - Math.sin(heading) * CHASE_DISTANCE,
187
+ pos[1] + CHASE_HEIGHT,
188
+ pos[2] - Math.cos(heading) * CHASE_DISTANCE,
189
+ );
190
+ controls.object.position.lerp(_desiredCamPos, CHASE_CAM_LERP);
191
+
192
+ // Look ahead of the car
193
+ _trackTarget.set(
194
+ pos[0] + Math.sin(heading) * CHASE_LOOK_AHEAD,
195
+ pos[1] + 1,
196
+ pos[2] + Math.cos(heading) * CHASE_LOOK_AHEAD,
197
+ );
198
+ controls.target.lerp(_trackTarget, CHASE_CAM_LERP);
199
+ controls.update();
200
+ return;
201
+ }
202
+
203
+ // AI car tracking — translate both target and camera by the same delta
204
+ _trackTarget.set(pos[0], pos[1], pos[2]);
205
+ _prevTarget.copy(controls.target);
206
+ controls.target.lerp(_trackTarget, 0.08);
207
+ _delta.subVectors(controls.target, _prevTarget);
208
+ controls.object.position.add(_delta);
209
+ controls.update();
210
+ });
211
+
212
+ return (
213
+ <OrbitControls
214
+ ref={controlsRef}
215
+ makeDefault
216
+ minDistance={10}
217
+ maxDistance={500}
218
+ maxPolarAngle={Math.PI / 2.1}
219
+ enableDamping
220
+ dampingFactor={0.05}
221
+ enableRotate={false}
222
+ enablePan={true}
223
+ panSpeed={1.5}
224
+ screenSpacePanning={false}
225
+ />
226
+ );
227
+ }
228
+
229
+ const TIME_PRESETS = [
230
+ { id: "sunrise", label: "Sunrise", time: 0.0 },
231
+ { id: "day", label: "Day", time: 0.25 },
232
+ { id: "sunset", label: "Sunset", time: 0.5 },
233
+ { id: "night", label: "Night", time: 0.75 },
234
+ ] as const;
235
+
236
+ export default function CityScene() {
237
+ const [selectedWallet, setSelectedWallet] = useState<WalletBuilding | null>(null);
238
+ const [selectedPosition, setSelectedPosition] = useState<[number, number, number] | null>(null);
239
+ const [wallets, setWallets] = useState<PlacedWallet[]>([]);
240
+ const [loading, setLoading] = useState(true);
241
+ const [windowHover, setWindowHover] = useState<WindowHoverInfo | null>(null);
242
+ const [showCard, setShowCard] = useState(false);
243
+ const [buildingToast, setBuildingToast] = useState<{ message: string; type: 'loading' | 'success' | 'error' } | null>(null);
244
+ const { profile, phantomAddress, phantomConnected, openPhantomModal } = useAuth();
245
+ const activeWalletAddress = phantomAddress ?? profile?.wallet_address ?? null;
246
+ const walletConnected = phantomConnected || !!activeWalletAddress;
247
+
248
+ // Treasure gate — show the play-to-win game before entering the city
249
+ const [treasureGatePlayed, setTreasureGatePlayed] = useState(() => {
250
+ if (typeof window !== 'undefined') {
251
+ return !!sessionStorage.getItem('treasure-gate-played');
252
+ }
253
+ return false;
254
+ });
255
+ const showTreasureGate = !!activeWalletAddress && !treasureGatePlayed && !loading;
256
+
257
+ function handleTreasureGateComplete() {
258
+ sessionStorage.setItem('treasure-gate-played', '1');
259
+ setTreasureGatePlayed(true);
260
+ // Auto-claim building after entering the city from treasure gate
261
+ // handleClaimBuilding has its own guards, so always attempt
262
+ setTimeout(() => {
263
+ if (activeWalletAddress) {
264
+ handleClaimBuilding();
265
+ }
266
+ }, 800);
267
+ }
268
+
269
+ // Day/night cycle state (refs to avoid per-frame re-renders)
270
+ // Start at sunset (0.50) for dramatic first impression — blazing orange sky
271
+ const timeRef = useRef(0.50);
272
+ const autoModeRef = useRef(true);
273
+ const [activePreset, setActivePreset] = useState("cycle");
274
+
275
+ // Swap events — mutable ref queue, no re-renders
276
+ const swapQueueRef = useSwapEvents(wallets);
277
+
278
+ // Particle burst queue for swap effects
279
+ const swapBurstRef = useRef<Array<{ x: number; y: number; z: number; color: string }>>([]);
280
+
281
+ // Tracked car state — trackedCar controls the panel UI,
282
+ // cameraFollowSlot controls which car the camera follows (persists after panel close)
283
+ const [trackedCar, setTrackedCar] = useState<TrackedCarInfo | null>(null);
284
+ const trackedCarPosRef = useRef<[number, number, number] | null>(null);
285
+ const [cameraFollowSlot, setCameraFollowSlot] = useState<number | null>(null);
286
+
287
+ // Player car mode
288
+ const [carMode, setCarMode] = useState(false);
289
+ const playerCarPosRef = useRef<[number, number, number] | null>(null);
290
+ const playerCarHeadingRef = useRef(0);
291
+ const carSpeedRef = useRef(0);
292
+
293
+ // In-city flight mode (Three.js)
294
+ const [flyMode, setFlyMode] = useState(false);
295
+ // World globe mode (Cesium globe)
296
+ const [worldFlyMode, setWorldFlyMode] = useState(false);
297
+ // Cesium real-world flight sim (separate from in-city fly)
298
+ const [cesiumFlightMode, setCesiumFlightMode] = useState(false);
299
+ const planePosRef = useRef<[number, number, number] | null>(null);
300
+ const planeHeadingRef = useRef(0);
301
+ const planeEngineRef = useRef<PlaneEngine | null>(null);
302
+
303
+ // Plane engine sound lifecycle
304
+ useEffect(() => {
305
+ if (flyMode) {
306
+ initAudio();
307
+ const eng = new PlaneEngine();
308
+ eng.start();
309
+ eng.setThrottle(0.5);
310
+ planeEngineRef.current = eng;
311
+ } else {
312
+ planeEngineRef.current?.stop();
313
+ planeEngineRef.current = null;
314
+ }
315
+ return () => {
316
+ planeEngineRef.current?.stop();
317
+ planeEngineRef.current = null;
318
+ };
319
+ }, [flyMode]);
320
+
321
+ // New building spotlights — track recently added wallet addresses
322
+ const [newSpotlights, setNewSpotlights] = useState<Array<{ address: string; position: [number, number, number]; height: number; id: number }>>([]);
323
+ const spotlightIdRef = useRef(0);
324
+
325
+ const collisionMap = useMemo(
326
+ () => (carMode ? buildCollisionMap(wallets) : null),
327
+ [wallets, carMode],
328
+ );
329
+
330
+ // Projectile system + position tracking for HUD
331
+ const [projectileManager] = useState(() => new ProjectileManager());
332
+ const planePositionsRef = useRef<Array<{ x: number; y: number; z: number }>>([]);
333
+ const residentCarPositionsRef = useRef<Array<{ x: number; y: number; z: number }>>([]);
334
+ const planePitchRef = useRef(0);
335
+ const planeSpeedRef = useRef(0);
336
+
337
+ // Building center positions for minimap (computed once when wallets change)
338
+ const buildingPositions = useMemo(() => {
339
+ return wallets.map(w => {
340
+ const dims = getBuildingDimensions(w);
341
+ const pos = getWalletWorldPosition(w, dims);
342
+ return { x: pos[0], z: pos[2] };
343
+ });
344
+ }, [wallets]);
345
+
346
+ // Active combat mode flag
347
+ const combatActive = carMode || flyMode;
348
+
349
+ // Multiplayer manager — handles realtime sync and session persistence
350
+ const [mpManager] = useState(() => new MultiplayerManager());
351
+
352
+ // Connect multiplayer when wallet is available
353
+ useEffect(() => {
354
+ if (activeWalletAddress) {
355
+ mpManager.connect(activeWalletAddress);
356
+ }
357
+ return () => { mpManager.disconnect(); };
358
+ // eslint-disable-next-line react-hooks/exhaustive-deps
359
+ }, [activeWalletAddress]);
360
+
361
+ // Keep multiplayer position/score getters up to date
362
+ useEffect(() => {
363
+ mpManager.getPosition = () => {
364
+ const pos = carMode ? playerCarPosRef.current : planePosRef.current;
365
+ if (!pos) return null;
366
+ return {
367
+ x: pos[0], y: pos[1], z: pos[2],
368
+ heading: carMode ? playerCarHeadingRef.current : planeHeadingRef.current,
369
+ };
370
+ };
371
+ mpManager.getScore = () => projectileManager.score;
372
+ }, [carMode, mpManager, projectileManager]);
373
+
374
+ const prevWalletAddresses = useRef<Set<string>>(new Set());
375
+
376
+ const fetchWallets = useCallback(async (): Promise<PlacedWallet[]> => {
377
+ try {
378
+ const res = await fetch(`/api/wallets?t=${Date.now()}`);
379
+ if (!res.ok) return [];
380
+ const data = await res.json();
381
+ const fresh: PlacedWallet[] = data.wallets ?? [];
382
+
383
+ // Detect newly added wallets and add spotlights
384
+ const prevAddrs = prevWalletAddresses.current;
385
+ for (const w of fresh) {
386
+ if (!prevAddrs.has(w.address) && prevAddrs.size > 0) {
387
+ const dims = getBuildingDimensions(w);
388
+ const pos = getWalletWorldPosition(w, dims);
389
+ const id = ++spotlightIdRef.current;
390
+ setNewSpotlights(prev => [...prev, { address: w.address, position: pos, height: dims.height, id }]);
391
+ // Auto-remove after 8 seconds
392
+ setTimeout(() => {
393
+ setNewSpotlights(prev => prev.filter(s => s.id !== id));
394
+ }, 8000);
395
+ }
396
+ }
397
+ prevWalletAddresses.current = new Set(fresh.map(w => w.address));
398
+
399
+ setWallets(fresh);
400
+ return fresh;
401
+ } catch {
402
+ return [];
403
+ } finally {
404
+ setLoading(false);
405
+ }
406
+ }, []);
407
+
408
+ // Fetch placed wallets on mount
409
+ useEffect(() => {
410
+ fetchWallets();
411
+ }, [fetchWallets]);
412
+
413
+ // Check if wallet needs building when Phantom connects
414
+ const [buildingInProgress, setBuildingInProgress] = useState(false);
415
+ const [claimArrival, setClaimArrival] = useState<{ address: string; phase: 'driving' | 'reveal' } | null>(null);
416
+ const [hasBuilding, setHasBuilding] = useState(false);
417
+ const [onChainVerify, setOnChainVerify] = useState<{
418
+ phase: 'hashing' | 'verifying' | 'confirmed' | 'failed';
419
+ hash: string;
420
+ blockSlot: number;
421
+ steps: { label: string; done: boolean }[];
422
+ } | null>(null);
423
+
424
+ useEffect(() => {
425
+ if (!activeWalletAddress) {
426
+ setHasBuilding(false);
427
+ return;
428
+ }
429
+ const exists = wallets.find(w => w.address === activeWalletAddress);
430
+ setHasBuilding(!!exists);
431
+ // eslint-disable-next-line react-hooks/exhaustive-deps
432
+ }, [activeWalletAddress, wallets]);
433
+
434
+ /** Generate a fake tx hash from wallet address for visual effect */
435
+ function genTxHash(addr: string): string {
436
+ let h = 0xdeadbeef;
437
+ for (let i = 0; i < addr.length; i++) h = Math.imul(h ^ addr.charCodeAt(i), 2654435761);
438
+ const hex = Array.from({ length: 64 }, (_, i) => ((h >> (i % 28)) & 0xf).toString(16)).join('');
439
+ return hex;
440
+ }
441
+
442
+ /** User clicks "Claim My Building" — on-chain verify animation + car arrival */
443
+ async function handleClaimBuilding() {
444
+ if (!activeWalletAddress || buildingInProgress) return;
445
+ setBuildingInProgress(true);
446
+
447
+ const txHash = genTxHash(activeWalletAddress);
448
+ const blockSlot = 280_000_000 + Math.floor(Math.random() * 1_000_000);
449
+ const steps = [
450
+ { label: 'Reading wallet history', done: false },
451
+ { label: 'Hashing on-chain data', done: false },
452
+ { label: 'Verifying block inclusion', done: false },
453
+ { label: 'Confirming parcel assignment', done: false },
454
+ ];
455
+
456
+ // Phase 1: On-chain verification animation
457
+ setOnChainVerify({ phase: 'hashing', hash: txHash, blockSlot, steps });
458
+
459
+ // Animate steps one by one
460
+ for (let i = 0; i < steps.length; i++) {
461
+ await new Promise(r => setTimeout(r, 800 + Math.random() * 400));
462
+ setOnChainVerify(prev => {
463
+ if (!prev) return prev;
464
+ const updated = prev.steps.map((s, j) => j <= i ? { ...s, done: true } : s);
465
+ return { ...prev, steps: updated, phase: i < 2 ? 'hashing' : 'verifying' };
466
+ });
467
+ }
468
+
469
+ // Phase 2: Actually fetch wallet / create building
470
+ try {
471
+ const res = await fetch(`/api/wallet/${activeWalletAddress}`);
472
+ if (res.ok) {
473
+ const data = await res.json();
474
+ setBuildingToast({ message: data.isNew ? 'Building your parcel...' : 'Building found!', type: 'loading' });
475
+
476
+ // Show confirmed
477
+ setOnChainVerify(prev => prev ? { ...prev, phase: 'confirmed' } : prev);
478
+ await new Promise(r => setTimeout(r, 1200));
479
+ setOnChainVerify(null);
480
+
481
+ const freshWallets = await fetchWallets();
482
+ const placed = freshWallets.find((w: PlacedWallet) => w.address === activeWalletAddress);
483
+ if (placed) {
484
+ // Start the car arrival animation
485
+ setClaimArrival({ address: activeWalletAddress, phase: 'driving' });
486
+ const dims = getBuildingDimensions(placed);
487
+ const pos = getWalletWorldPosition(placed, dims);
488
+ handleSelectWallet(placed, pos);
489
+ setBuildingToast({ message: 'Arriving at your building...', type: 'loading' });
490
+
491
+ // Car drives in (3s)
492
+ setTimeout(() => {
493
+ setClaimArrival(prev => prev ? { ...prev, phase: 'reveal' } : null);
494
+ setBuildingToast({ message: 'Welcome to Solanapolis, citizen!', type: 'success' });
495
+ }, 3000);
496
+
497
+ // Show ID card after reveal (5s total)
498
+ setTimeout(() => {
499
+ setClaimArrival(null);
500
+ setBuildingToast(null);
501
+ setShowCard(true);
502
+ }, 5500);
503
+ } else {
504
+ setBuildingToast({ message: 'Building created!', type: 'success' });
505
+ setTimeout(() => setBuildingToast(null), 4000);
506
+ }
507
+ } else {
508
+ setOnChainVerify(prev => prev ? { ...prev, phase: 'failed' } : prev);
509
+ await new Promise(r => setTimeout(r, 1500));
510
+ setOnChainVerify(null);
511
+ const err = await res.json().catch(() => ({ error: 'Failed' }));
512
+ setBuildingToast({ message: err.error || 'Could not create building', type: 'error' });
513
+ setTimeout(() => setBuildingToast(null), 5000);
514
+ }
515
+ } catch {
516
+ setOnChainVerify(prev => prev ? { ...prev, phase: 'failed' } : prev);
517
+ await new Promise(r => setTimeout(r, 1500));
518
+ setOnChainVerify(null);
519
+ setBuildingToast({ message: 'Network error — try refreshing', type: 'error' });
520
+ setTimeout(() => setBuildingToast(null), 5000);
521
+ } finally {
522
+ setBuildingInProgress(false);
523
+ }
524
+ }
525
+
526
+
527
+
528
+ function handleTimePreset(id: string, time: number) {
529
+ timeRef.current = time;
530
+ autoModeRef.current = false;
531
+ setActivePreset(id);
532
+ }
533
+
534
+ function handleCycle() {
535
+ autoModeRef.current = true;
536
+ setActivePreset("cycle");
537
+ }
538
+
539
+
540
+ function handleSelectWallet(wallet: WalletBuilding, position: [number, number, number]) {
541
+ setCarMode(false);
542
+ setFlyMode(false);
543
+ setWorldFlyMode(false);
544
+ playerCarPosRef.current = null;
545
+ planePosRef.current = null;
546
+ setSelectedWallet(wallet);
547
+ setSelectedPosition(position);
548
+ setTrackedCar(null);
549
+ setCameraFollowSlot(null);
550
+ trackedCarPosRef.current = null;
551
+ }
552
+
553
+ function handleDeselect() {
554
+ setSelectedWallet(null);
555
+ setSelectedPosition(null);
556
+ setTrackedCar(null);
557
+ setCameraFollowSlot(null);
558
+ trackedCarPosRef.current = null;
559
+ setFlyMode(false);
560
+ setWorldFlyMode(false);
561
+ planePosRef.current = null;
562
+ }
563
+
564
+ useEffect(() => {
565
+ function onKeyDown(e: KeyboardEvent) {
566
+ if (e.key === "Escape") {
567
+ if (cesiumFlightMode) {
568
+ setCesiumFlightMode(false);
569
+ return;
570
+ }
571
+ if (worldFlyMode) {
572
+ setWorldFlyMode(false);
573
+ return;
574
+ }
575
+ if (flyMode) {
576
+ setFlyMode(false);
577
+ planePosRef.current = null;
578
+ return;
579
+ }
580
+ if (carMode) {
581
+ setCarMode(false);
582
+ playerCarPosRef.current = null;
583
+ return;
584
+ }
585
+ handleDeselect();
586
+ }
587
+ }
588
+ window.addEventListener("keydown", onKeyDown);
589
+ return () => window.removeEventListener("keydown", onKeyDown);
590
+ }, [carMode, flyMode, worldFlyMode, cesiumFlightMode]);
591
+
592
+ function handleClickAddress(address: string) {
593
+ const wallet = wallets.find(w => w.address === address);
594
+ if (!wallet) return;
595
+ const dims = getBuildingDimensions(wallet);
596
+ const pos = getWalletWorldPosition(wallet, dims);
597
+ handleSelectWallet(wallet, pos);
598
+ }
599
+
600
+ function handleClickCar(info: TrackedCarInfo) {
601
+ setCarMode(false);
602
+ setFlyMode(false);
603
+ setWorldFlyMode(false);
604
+ playerCarPosRef.current = null;
605
+ planePosRef.current = null;
606
+ setTrackedCar(info);
607
+ setCameraFollowSlot(info.slotIndex);
608
+ setSelectedWallet(null);
609
+ setSelectedPosition(null);
610
+ setWindowHover(null);
611
+ }
612
+
613
+ function handleCarDied(slotIndex: number) {
614
+ setTrackedCar(prev => prev?.slotIndex === slotIndex ? null : prev);
615
+ setCameraFollowSlot(prev => prev === slotIndex ? null : prev);
616
+ trackedCarPosRef.current = null;
617
+ }
618
+
619
+ function endCombatSession() {
620
+ mpManager.endSession(
621
+ projectileManager.score,
622
+ projectileManager.hits.filter(h => h.targetType === "plane").length,
623
+ projectileManager.hits.filter(h => h.targetType === "car").length,
624
+ projectileManager.projectiles.length,
625
+ );
626
+ projectileManager.reset();
627
+ }
628
+
629
+ return (
630
+ <div className="relative w-full h-screen">
631
+ {windowHover && <WindowTooltip {...windowHover} />}
632
+
633
+ {/* Top left — branding */}
634
+ <div className="absolute top-3 left-3 sm:top-5 sm:left-5 z-10 bg-black/50 backdrop-blur-xl border border-white/[0.08] rounded-2xl px-3.5 py-2.5 sm:px-5 sm:py-3.5">
635
+ <div className="flex items-center gap-2.5">
636
+ <img src="/helius-icon.svg" alt="Helius" className="w-6 h-6 sm:w-7 sm:h-7" />
637
+ <div>
638
+ <h1 className="text-lg sm:text-xl font-bold tracking-tight leading-tight" style={{ color: "#E35930" }}>SOLANApolis</h1>
639
+ <p className="text-[9px] text-white/30 tracking-wider leading-none hidden sm:block">powered by Helius on Solana</p>
640
+ </div>
641
+ </div>
642
+ <div className="flex items-center gap-2 mt-0.5">
643
+ <CitySlotsBadge counts={getSlotCounts(wallets.length)} loading={loading} />
644
+ <HowItWorksModal />
645
+ {(() => {
646
+ const myAddr = profile?.wallet_address || activeWalletAddress;
647
+ const myWallet = myAddr
648
+ ? wallets.find((w) => w.address === myAddr && (!w.ingestionStatus || w.ingestionStatus === "complete"))
649
+ : null;
650
+ if (!myWallet) return null;
651
+ return (
652
+ <>
653
+ <button
654
+ onClick={() => {
655
+ const dims = getBuildingDimensions(myWallet as PlacedWallet);
656
+ const pos = getWalletWorldPosition(myWallet as PlacedWallet, dims);
657
+ handleSelectWallet(myWallet, pos);
658
+ }}
659
+ className="px-2 py-0.5 bg-purple-500/15 hover:bg-purple-500/25 border border-purple-400/20 rounded-lg text-[10px] font-medium text-purple-300 transition-colors cursor-pointer"
660
+ >
661
+ My Building
662
+ </button>
663
+ <button
664
+ onClick={() => setShowCard(true)}
665
+ className="px-2 py-0.5 bg-[#E35930]/15 hover:bg-[#E35930]/25 border border-[#E35930]/20 rounded-lg text-[10px] font-medium text-[#E35930] transition-colors cursor-pointer"
666
+ >
667
+ ID Card
668
+ </button>
669
+ </>
670
+ );
671
+ })()}
672
+ </div>
673
+ </div>
674
+
675
+ {/* Building creation toast */}
676
+ {buildingToast && (
677
+ <div className="fixed top-28 left-1/2 -translate-x-1/2 z-40 animate-[fadeSlideDown_0.4s_ease-out]">
678
+ <div className={`flex items-center gap-3 px-5 py-3 rounded-2xl backdrop-blur-2xl border shadow-xl text-sm font-medium ${buildingToast.type === 'loading'
679
+ ? 'bg-purple-900/80 border-purple-500/30 text-purple-200 shadow-purple-900/30'
680
+ : buildingToast.type === 'success'
681
+ ? 'bg-emerald-900/80 border-emerald-500/30 text-emerald-200 shadow-emerald-900/30'
682
+ : 'bg-red-900/80 border-red-500/30 text-red-200 shadow-red-900/30'
683
+ }`}>
684
+ {buildingToast.type === 'loading' && (
685
+ <div className="w-4 h-4 border-2 border-purple-400/30 border-t-purple-300 rounded-full animate-spin" />
686
+ )}
687
+ <span>{buildingToast.message}</span>
688
+ </div>
689
+ </div>
690
+ )}
691
+
692
+ {/* Claim My Building — persistent button when wallet connected but no building */}
693
+ {walletConnected && activeWalletAddress && !hasBuilding && !claimArrival && (
694
+ <div className="fixed bottom-24 left-1/2 -translate-x-1/2 z-40 animate-[fadeSlideUp_0.5s_ease-out]">
695
+ <button
696
+ onClick={handleClaimBuilding}
697
+ disabled={buildingInProgress}
698
+ className="group relative flex items-center gap-3 px-6 py-3.5 rounded-2xl text-sm font-bold text-white transition-all cursor-pointer active:scale-[0.97] disabled:opacity-70 disabled:cursor-wait"
699
+ style={{
700
+ background: 'linear-gradient(135deg, #E35930 0%, #c94420 50%, #E35930 100%)',
701
+ backgroundSize: '200% 200%',
702
+ animation: buildingInProgress ? 'none' : 'claimShimmer 3s ease infinite',
703
+ boxShadow: '0 0 30px rgba(227,89,48,0.4), 0 0 60px rgba(227,89,48,0.15)',
704
+ }}
705
+ >
706
+ {/* Pulsing glow ring */}
707
+ <div className="absolute -inset-1 rounded-2xl opacity-60 blur-md" style={{ background: 'linear-gradient(135deg, #E35930, #8b5cf6)' }} />
708
+ <div className="relative flex items-center gap-3">
709
+ {buildingInProgress ? (
710
+ <div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
711
+ ) : (
712
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
713
+ <path d="M3 21h18" /><path d="M5 21V7l8-4v18" /><path d="M19 21V11l-6-4" />
714
+ <path d="M9 9v.01" /><path d="M9 12v.01" /><path d="M9 15v.01" /><path d="M9 18v.01" />
715
+ </svg>
716
+ )}
717
+ <span className="text-base tracking-tight">
718
+ {buildingInProgress ? 'Building...' : 'Claim My Building'}
719
+ </span>
720
+ </div>
721
+ </button>
722
+ <p className="text-center text-[10px] text-white/30 mt-2">
723
+ Your wallet shapes a unique skyscraper in the city
724
+ </p>
725
+ </div>
726
+ )}
727
+
728
+ {/* On-chain verification overlay */}
729
+ {onChainVerify && (
730
+ <div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/70 backdrop-blur-sm">
731
+ <div className="relative w-[380px] max-w-[90vw] bg-gradient-to-b from-gray-900/95 to-black/95 border border-white/10 rounded-3xl p-6 shadow-2xl overflow-hidden">
732
+ {/* Scanning line animation */}
733
+ <div className="absolute inset-0 overflow-hidden rounded-3xl pointer-events-none">
734
+ <div
735
+ className="absolute left-0 right-0 h-px bg-gradient-to-r from-transparent via-[#E35930] to-transparent"
736
+ style={{ animation: 'scanLine 2s ease-in-out infinite', top: '0%' }}
737
+ />
738
+ </div>
739
+
740
+ {/* Header */}
741
+ <div className="flex items-center gap-3 mb-5">
742
+ <div className={`w-10 h-10 rounded-xl flex items-center justify-center text-lg ${onChainVerify.phase === 'confirmed'
743
+ ? 'bg-emerald-500/20'
744
+ : onChainVerify.phase === 'failed'
745
+ ? 'bg-red-500/20'
746
+ : 'bg-[#E35930]/20'
747
+ }`}>
748
+ {onChainVerify.phase === 'confirmed' ? '✓' : onChainVerify.phase === 'failed' ? '✕' : '⛓'}
749
+ </div>
750
+ <div>
751
+ <h3 className="text-sm font-bold text-white tracking-tight">
752
+ {onChainVerify.phase === 'confirmed'
753
+ ? 'Verified on Solana'
754
+ : onChainVerify.phase === 'failed'
755
+ ? 'Verification Failed'
756
+ : 'Verifying On-Chain...'}
757
+ </h3>
758
+ <p className="text-[10px] text-white/30 font-mono">
759
+ Block #{onChainVerify.blockSlot.toLocaleString()}
760
+ </p>
761
+ </div>
762
+ </div>
763
+
764
+ {/* Transaction hash */}
765
+ <div className="bg-black/40 rounded-xl px-3 py-2 mb-4 border border-white/[0.06]">
766
+ <p className="text-[8px] uppercase tracking-widest text-white/20 mb-1">TX HASH</p>
767
+ <p className="text-[10px] font-mono text-[#E35930]/70 break-all leading-relaxed">
768
+ {onChainVerify.hash.slice(0, 32)}...
769
+ </p>
770
+ </div>
771
+
772
+ {/* Verification steps */}
773
+ <div className="space-y-2.5 mb-4">
774
+ {onChainVerify.steps.map((step, i) => (
775
+ <div key={i} className="flex items-center gap-3">
776
+ <div className={`w-5 h-5 rounded-full flex items-center justify-center shrink-0 transition-all duration-500 ${step.done
777
+ ? 'bg-emerald-500/20 text-emerald-400'
778
+ : onChainVerify.steps[i - 1]?.done || i === 0
779
+ ? 'bg-[#E35930]/20 text-[#E35930]'
780
+ : 'bg-white/5 text-white/15'
781
+ }`}>
782
+ {step.done ? (
783
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
784
+ <path d="M20 6L9 17l-5-5" />
785
+ </svg>
786
+ ) : (onChainVerify.steps[i - 1]?.done || i === 0) && !step.done ? (
787
+ <div className="w-2.5 h-2.5 border-2 border-current border-t-transparent rounded-full animate-spin" />
788
+ ) : (
789
+ <div className="w-1.5 h-1.5 rounded-full bg-current opacity-30" />
790
+ )}
791
+ </div>
792
+ <span className={`text-xs transition-all duration-500 ${step.done ? 'text-white/70' : 'text-white/25'
793
+ }`}>
794
+ {step.label}
795
+ </span>
796
+ {step.done && (
797
+ <span className="ml-auto text-[8px] font-mono text-emerald-400/50">
798
+ {(Math.random() * 0.4 + 0.1).toFixed(3)}s
799
+ </span>
800
+ )}
801
+ </div>
802
+ ))}
803
+ </div>
804
+
805
+ {/* Result bar */}
806
+ {onChainVerify.phase === 'confirmed' && (
807
+ <div className="bg-emerald-500/10 border border-emerald-500/20 rounded-xl px-4 py-3 text-center animate-[fadeSlideUp_0.5s_ease-out]">
808
+ <p className="text-xs font-bold text-emerald-300">Parcel Claimed Successfully</p>
809
+ <p className="text-[9px] text-emerald-400/40 mt-0.5">Your building is being generated on-chain</p>
810
+ </div>
811
+ )}
812
+ {onChainVerify.phase === 'failed' && (
813
+ <div className="bg-red-500/10 border border-red-500/20 rounded-xl px-4 py-3 text-center animate-[fadeSlideUp_0.5s_ease-out]">
814
+ <p className="text-xs font-bold text-red-300">Verification Failed</p>
815
+ <p className="text-[9px] text-red-400/40 mt-0.5">Please try again</p>
816
+ </div>
817
+ )}
818
+
819
+ {/* Progress bar */}
820
+ {(onChainVerify.phase === 'hashing' || onChainVerify.phase === 'verifying') && (
821
+ <div className="h-1 rounded-full bg-white/5 overflow-hidden">
822
+ <div
823
+ className="h-full rounded-full transition-all duration-700 ease-out"
824
+ style={{
825
+ width: `${(onChainVerify.steps.filter(s => s.done).length / onChainVerify.steps.length) * 100}%`,
826
+ background: 'linear-gradient(90deg, #E35930, #8b5cf6)',
827
+ }}
828
+ />
829
+ </div>
830
+ )}
831
+ </div>
832
+ </div>
833
+ )}
834
+
835
+ {/* Car arrival cinematic overlay */}
836
+ {claimArrival && (
837
+ <div className="fixed inset-0 z-50 pointer-events-none">
838
+ {/* Cinematic letterbox bars */}
839
+ <div className="absolute top-0 left-0 right-0 h-16 bg-gradient-to-b from-black/80 to-transparent" />
840
+ <div className="absolute bottom-0 left-0 right-0 h-16 bg-gradient-to-t from-black/80 to-transparent" />
841
+
842
+ {/* Status text */}
843
+ <div className="absolute bottom-20 left-1/2 -translate-x-1/2 text-center">
844
+ <p className={`text-sm font-bold tracking-wider uppercase transition-all duration-1000 ${claimArrival.phase === 'driving'
845
+ ? 'text-[#E35930] animate-pulse'
846
+ : 'text-white animate-[fadeSlideUp_0.5s_ease-out]'
847
+ }`}>
848
+ {claimArrival.phase === 'driving' ? 'Arriving at your building...' : 'Welcome to Solanapolis'}
849
+ </p>
850
+ <p className="font-mono text-xs text-white/30 mt-1">
851
+ {claimArrival.address.slice(0, 6)}...{claimArrival.address.slice(-4)}
852
+ </p>
853
+ </div>
854
+ </div>
855
+ )}
856
+
857
+ {/* Citizen ID Card modal */}
858
+ {showCard && (() => {
859
+ const myAddr = profile?.wallet_address || activeWalletAddress;
860
+ const myWallet = myAddr ? wallets.find((w) => w.address === myAddr) : null;
861
+ return myWallet ? (
862
+ <Suspense fallback={null}>
863
+ <CitizenCardModal
864
+ wallet={myWallet}
865
+ identityName={
866
+ profile?.x_username
867
+ ? `@${profile.x_username}`
868
+ : myWallet.identityName || null
869
+ }
870
+ onClose={() => setShowCard(false)}
871
+ />
872
+ </Suspense>
873
+ ) : null;
874
+ })()}
875
+
876
+ {/* Top center — search */}
877
+ <div className="absolute top-[4.5rem] left-3 right-3 sm:top-5 sm:left-1/2 sm:right-auto sm:-translate-x-1/2 z-10">
878
+ <WalletSearch
879
+ wallets={wallets}
880
+ onSelect={handleSelectWallet}
881
+ onRefetch={fetchWallets}
882
+ />
883
+ </div>
884
+
885
+ {/* Left side — activity feed */}
886
+ <div className="hidden sm:block">
887
+ <ActivityFeed wallets={wallets} onClickAddress={handleClickAddress} />
888
+ </div>
889
+
890
+ {/* Top right — auth + connect wallet */}
891
+ <div className="absolute top-3 right-3 sm:top-5 sm:right-5 z-10 flex flex-col items-end gap-3">
892
+ {phantomConnected || !!profile ? (
893
+ <AuthPanel onClickAddress={handleClickAddress} />
894
+ ) : (
895
+ <button
896
+ onClick={openPhantomModal}
897
+ className="flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-purple-600/80 to-purple-500/80 hover:from-purple-500/90 hover:to-purple-400/90 backdrop-blur-xl border border-purple-400/20 rounded-2xl text-sm text-white font-medium transition-all cursor-pointer shadow-lg shadow-purple-900/30"
898
+ >
899
+ <svg width="16" height="16" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
900
+ <rect width="128" height="128" rx="26" fill="#AB9FF2" />
901
+ <path fillRule="evenodd" clipRule="evenodd" d="M55.6416 82.1477C50.8744 89.4525 42.8862 98.6966 32.2568 98.6966C27.232 98.6966 22.4004 96.628 22.4004 87.6424C22.4004 64.7584 53.6445 29.3335 82.6339 29.3335C99.1257 29.3335 105.697 40.7755 105.697 53.7689C105.697 70.4471 94.8739 89.5171 84.1156 89.5171C80.7013 89.5171 79.0264 87.6424 79.0264 84.6688C79.0264 83.8931 79.1552 83.0527 79.4129 82.1477C75.7409 88.4182 68.6546 94.2361 62.0192 94.2361C57.1877 94.2361 54.7397 91.1979 54.7397 86.9314C54.7397 85.3799 55.0618 83.7638 55.6416 82.1477ZM80.6133 53.3182C80.6133 57.1044 78.3795 58.9975 75.8806 58.9975C73.3438 58.9975 71.1479 57.1044 71.1479 53.3182C71.1479 49.532 73.3438 47.6389 75.8806 47.6389C78.3795 47.6389 80.6133 49.532 80.6133 53.3182ZM94.8102 53.3184C94.8102 57.1046 92.5763 58.9977 90.0775 58.9977C87.5407 58.9977 85.3447 57.1046 85.3447 53.3184C85.3447 49.5323 87.5407 47.6392 90.0775 47.6392C92.5763 47.6392 94.8102 49.5323 94.8102 53.3184Z" fill="#FFFDF8" />
902
+ </svg>
903
+ Connect Wallet
904
+ </button>
905
+ )}
906
+ {/* Wallet / Swap panel: top-right on sm+, hidden here on mobile */}
907
+ <div className="hidden sm:block">
908
+ <WalletPanel wallet={selectedWallet} onClose={handleDeselect} />
909
+ <SwapPanel swap={trackedCar} onClose={handleDeselect} />
910
+ </div>
911
+ </div>
912
+
913
+ {/* Wallet / Swap panel: bottom sheet on mobile */}
914
+ <div className="block sm:hidden fixed bottom-0 left-0 right-0 z-30">
915
+ <WalletPanel wallet={selectedWallet} onClose={handleDeselect} />
916
+ <SwapPanel swap={trackedCar} onClose={handleDeselect} />
917
+ </div>
918
+
919
+ {/* Camera controls help — hidden on mobile and during combat (HUD takes over) */}
920
+ <div className={`hidden sm:block absolute bottom-6 left-5 z-10 bg-black/50 backdrop-blur-xl border border-white/[0.08] rounded-2xl px-4 py-3 text-xs text-white/35 space-y-1.5 transition-opacity ${combatActive || worldFlyMode ? 'opacity-0 pointer-events-none' : 'opacity-100'}`}>
921
+ <p className="text-white/50 font-medium mb-1">Controls</p>
922
+ {flyMode ? (
923
+ <>
924
+ <p><span className="text-white/45">W / ↑</span> Throttle up</p>
925
+ <p><span className="text-white/45">S / ↓</span> Throttle down</p>
926
+ <p><span className="text-white/45">A / ←</span> Bank left</p>
927
+ <p><span className="text-white/45">D / →</span> Bank right</p>
928
+ <p><span className="text-white/45">Q / Space</span> Climb</p>
929
+ <p><span className="text-white/45">E</span> Descend</p>
930
+ <p><span className="text-white/45">Esc</span> Exit flight</p>
931
+ </>
932
+ ) : carMode ? (
933
+ <>
934
+ <p><span className="text-white/45">W / ↑</span> Accelerate</p>
935
+ <p><span className="text-white/45">S / ↓</span> Brake / Reverse</p>
936
+ <p><span className="text-white/45">A / ←</span> Steer left</p>
937
+ <p><span className="text-white/45">D / →</span> Steer right</p>
938
+ <p><span className="text-white/45">Space</span> Brake</p>
939
+ <p><span className="text-white/45">Esc</span> Exit drive</p>
940
+ </>
941
+ ) : (
942
+ <>
943
+ <p><span className="text-white/45">Left-click</span> {selectedWallet ? "Orbit" : "Pan"}</p>
944
+ <p><span className="text-white/45">Right-click</span> {selectedWallet ? "Pan" : "Rotate"}</p>
945
+ <p><span className="text-white/45">Scroll</span> Zoom</p>
946
+ </>
947
+ )}
948
+ <div className="flex gap-1.5 mt-2">
949
+ <button
950
+ onClick={() => {
951
+ initAudio();
952
+ playClick();
953
+ if (carMode) {
954
+ setCarMode(false);
955
+ playerCarPosRef.current = null;
956
+ endCombatSession();
957
+ } else {
958
+ if (flyMode) {
959
+ endCombatSession();
960
+ }
961
+ handleDeselect();
962
+ setWorldFlyMode(false);
963
+ setFlyMode(false);
964
+ planePosRef.current = null;
965
+ setCarMode(true);
966
+ mpManager.startSession("car");
967
+ }
968
+ }}
969
+ className={`flex-1 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors cursor-pointer border ${carMode
970
+ ? "bg-[#E35930]/20 border-[#E35930]/40 text-[#E35930] hover:bg-[#E35930]/30"
971
+ : "bg-white/5 border-white/10 text-white/50 hover:text-white/70 hover:bg-white/10"
972
+ }`}
973
+ >
974
+ {carMode ? "Exit Drive" : "Drive"}
975
+ </button>
976
+ <button
977
+ onClick={() => {
978
+ initAudio();
979
+ playClick();
980
+ if (flyMode) {
981
+ setFlyMode(false);
982
+ planePosRef.current = null;
983
+ endCombatSession();
984
+ } else {
985
+ if (carMode) {
986
+ endCombatSession();
987
+ }
988
+ handleDeselect();
989
+ setWorldFlyMode(false);
990
+ setCarMode(false);
991
+ playerCarPosRef.current = null;
992
+ setFlyMode(true);
993
+ mpManager.startSession("plane");
994
+ }
995
+ }}
996
+ title={flyMode ? 'Exit flight mode' : 'Fly your plane around the city'}
997
+ className={`flex-1 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors cursor-pointer border ${flyMode
998
+ ? "bg-blue-500/20 border-blue-400/40 text-blue-400 hover:bg-blue-500/30"
999
+ : "bg-white/5 border-white/10 text-white/50 hover:text-white/70 hover:bg-white/10"
1000
+ }`}
1001
+ >
1002
+ {flyMode ? "Exit Flight" : "✈ Fly"}
1003
+ </button>
1004
+ <button
1005
+ onClick={() => {
1006
+ initAudio();
1007
+ playClick();
1008
+ if (worldFlyMode) {
1009
+ setWorldFlyMode(false);
1010
+ } else {
1011
+ if (carMode || flyMode) {
1012
+ endCombatSession();
1013
+ }
1014
+ handleDeselect();
1015
+ setWorldFlyMode(true);
1016
+ }
1017
+ }}
1018
+ className={`flex-1 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors cursor-pointer border ${worldFlyMode
1019
+ ? "bg-purple-500/20 border-purple-400/40 text-purple-300 hover:bg-purple-500/30"
1020
+ : "bg-white/5 border-white/10 text-white/50 hover:text-white/70 hover:bg-white/10"
1021
+ }`}
1022
+ >
1023
+ {worldFlyMode ? "Exit World" : "🌍 World"}
1024
+ </button>
1025
+ </div>
1026
+ </div>
1027
+
1028
+ {/* Bottom right — credits (hidden during combat / globe overlays) */}
1029
+ <div className={`hidden sm:flex absolute bottom-20 right-5 z-10 flex-col items-end gap-1.5 transition-opacity ${combatActive || worldFlyMode ? 'opacity-0 pointer-events-none' : 'opacity-100'}`}>
1030
+ <a href="https://www.helius.dev/" target="_blank" rel="noopener noreferrer" className="flex items-center gap-2 text-white/30 hover:text-white/55 text-xs transition-colors">
1031
+ <span>Powered by</span>
1032
+ <img src="/helius-logo.svg" alt="Helius" className="h-4" />
1033
+ </a>
1034
+ <p className="text-[10px] text-white/20 tracking-wide">
1035
+ shoutout <span className="text-[#E35930]/60 font-medium">Nathan</span> 🔥
1036
+ </p>
1037
+ </div>
1038
+
1039
+ {/* Network stats HUD */}
1040
+ <NetworkStats />
1041
+
1042
+ {/* Time control buttons */}
1043
+ <div className="absolute bottom-4 sm:bottom-6 left-1/2 -translate-x-1/2 z-10 flex gap-1 bg-black/50 backdrop-blur-xl border border-white/[0.08] rounded-full px-2.5 py-2">
1044
+ {TIME_PRESETS.map((preset) => (
1045
+ <button
1046
+ key={preset.id}
1047
+ onClick={() => handleTimePreset(preset.id, preset.time)}
1048
+ className={`px-2.5 sm:px-3.5 py-1.5 min-h-[44px] sm:min-h-0 rounded-full text-xs font-medium transition-colors cursor-pointer ${activePreset === preset.id
1049
+ ? "bg-white/15 text-white"
1050
+ : "text-white/40 hover:text-white/70"
1051
+ }`}
1052
+ >
1053
+ {preset.label}
1054
+ </button>
1055
+ ))}
1056
+ <button
1057
+ onClick={handleCycle}
1058
+ className={`px-2.5 sm:px-3.5 py-1.5 min-h-[44px] sm:min-h-0 rounded-full text-xs font-medium transition-colors cursor-pointer ${activePreset === "cycle"
1059
+ ? "bg-white/15 text-white"
1060
+ : "text-white/40 hover:text-white/70"
1061
+ }`}
1062
+ >
1063
+ Cycle
1064
+ </button>
1065
+ </div>
1066
+
1067
+ <Canvas
1068
+ camera={{ position: [200, 150, 200], fov: 50, near: 5, far: 2000 }}
1069
+ onPointerMissed={handleDeselect}
1070
+ >
1071
+ <SceneLighting timeRef={timeRef} autoModeRef={autoModeRef} />
1072
+
1073
+ <CityGrid
1074
+ wallets={wallets}
1075
+ timeRef={timeRef}
1076
+ onSelectWallet={handleSelectWallet}
1077
+ onHoverWindow={setWindowHover}
1078
+ selectedAddress={selectedWallet?.address ?? null}
1079
+ />
1080
+
1081
+ <InstancedCars
1082
+ swapQueueRef={swapQueueRef}
1083
+ wallets={wallets}
1084
+ timeRef={timeRef}
1085
+ onHoverSwap={setWindowHover}
1086
+ onClickCar={handleClickCar}
1087
+ trackedCarSlot={cameraFollowSlot}
1088
+ trackedCarPosRef={trackedCarPosRef}
1089
+ onCarDied={handleCarDied}
1090
+ selectedAddress={selectedWallet?.address ?? null}
1091
+ swapBurstRef={swapBurstRef}
1092
+ />
1093
+
1094
+ {/* Resident planes — first 100 wallets each get a plane circling their building */}
1095
+ <InstancedCityPlanes
1096
+ wallets={wallets}
1097
+ timeRef={timeRef}
1098
+ onHoverPlane={setWindowHover}
1099
+ onClickPlane={handleSelectWallet}
1100
+ positionsRef={planePositionsRef}
1101
+ />
1102
+
1103
+ {/* Resident cars — first 250 wallets each get a car driving the streets */}
1104
+ <InstancedResidentCars
1105
+ wallets={wallets}
1106
+ timeRef={timeRef}
1107
+ onHoverCar={setWindowHover}
1108
+ onClickCar={handleSelectWallet}
1109
+ positionsRef={residentCarPositionsRef}
1110
+ />
1111
+
1112
+ {/* Beach, ocean, seagulls, boats — southern shoreline */}
1113
+ <BeachScene />
1114
+
1115
+ {/* Famous crypto-themed landmarks around the city */}
1116
+ <CityLandmarks />
1117
+
1118
+ <PlayerCar
1119
+ active={carMode}
1120
+ collisionMap={collisionMap}
1121
+ positionRef={playerCarPosRef}
1122
+ headingRef={playerCarHeadingRef}
1123
+ speedRef={carSpeedRef}
1124
+ />
1125
+
1126
+ {/* In-city player plane */}
1127
+ <PlayerPlane
1128
+ active={flyMode}
1129
+ positionRef={planePosRef}
1130
+ headingRef={planeHeadingRef}
1131
+ pitchRef={planePitchRef}
1132
+ speedRef={planeSpeedRef}
1133
+ walletAddress={activeWalletAddress}
1134
+ />
1135
+
1136
+ {selectedWallet && selectedPosition && (
1137
+ <SelectionBeam
1138
+ position={selectedPosition}
1139
+ buildingHeight={getBuildingDimensions(selectedWallet).height}
1140
+ />
1141
+ )}
1142
+
1143
+ {/* New building spotlights */}
1144
+ {newSpotlights.map(s => (
1145
+ <NewBuildingSpotlight
1146
+ key={s.id}
1147
+ position={s.position}
1148
+ buildingHeight={s.height}
1149
+ />
1150
+ ))}
1151
+
1152
+ <SwapParticles swapBurstRef={swapBurstRef} />
1153
+
1154
+ {/* Projectile system — active in car or plane mode */}
1155
+ <ProjectileRenderer
1156
+ manager={projectileManager}
1157
+ planePositionsRef={planePositionsRef}
1158
+ carPositionsRef={residentCarPositionsRef}
1159
+ active={combatActive}
1160
+ playerPosRef={carMode ? playerCarPosRef : planePosRef}
1161
+ playerHeadingRef={carMode ? playerCarHeadingRef : planeHeadingRef}
1162
+ playerPitchRef={planePitchRef}
1163
+ mode={carMode ? "car" : "plane"}
1164
+ />
1165
+
1166
+ <CameraControls
1167
+ targetPosition={selectedPosition}
1168
+ trackingRef={flyMode ? planePosRef : carMode ? playerCarPosRef : trackedCarPosRef}
1169
+ isTracking={flyMode || carMode || cameraFollowSlot !== null}
1170
+ playerCarMode={carMode}
1171
+ flyMode={flyMode}
1172
+ headingRef={flyMode ? planeHeadingRef : playerCarHeadingRef}
1173
+ />
1174
+
1175
+ {/* AI Town NPCs — LLM-powered agents walking the streets */}
1176
+ <AITownNPCs />
1177
+
1178
+ {/* Dubai Marina district — influencers, lambos, explosions */}
1179
+ <DubaiDistrict />
1180
+
1181
+ {/* Real multiplayer players — in-world floating labels/markers */}
1182
+ <RealPlayerTags
1183
+ active={combatActive}
1184
+ remotePlayers={mpManager.remotePlayers}
1185
+ />
1186
+ </Canvas>
1187
+
1188
+ {/* Cesium globe overlay — interactive 3D Earth with OSM Buildings + metadata */}
1189
+ {worldFlyMode && !flyMode && !cesiumFlightMode && (
1190
+ <Suspense fallback={
1191
+ <div className="fixed inset-0 z-50 bg-[#0a0a12] flex items-center justify-center">
1192
+ <div className="text-center">
1193
+ <div className="w-10 h-10 border-2 border-emerald-500/30 border-t-emerald-400 rounded-full animate-spin mx-auto mb-4" />
1194
+ <p className="text-white/60 text-sm">Loading globe…</p>
1195
+ </div>
1196
+ </div>
1197
+ }>
1198
+ <CesiumGlobe
1199
+ walletAddress={activeWalletAddress}
1200
+ onExit={() => setWorldFlyMode(false)}
1201
+ onFlyAt={(lon: number, lat: number, alt: number, heading: number) => {
1202
+ setWorldFlyMode(false);
1203
+ setCesiumFlightMode(true);
1204
+ (window as Window & { __solanapolis_fly_coords?: { lon: number; lat: number; alt: number; heading: number } }).__solanapolis_fly_coords = { lon, lat, alt, heading };
1205
+ }}
1206
+ />
1207
+ </Suspense>
1208
+ )}
1209
+
1210
+ {/* Cesium flight simulator overlay — only from World globe "Fly At" */}
1211
+ {cesiumFlightMode && (
1212
+ <Suspense fallback={
1213
+ <div className="fixed inset-0 z-50 bg-[#0a0a12] flex items-center justify-center">
1214
+ <div className="text-center">
1215
+ <div className="w-10 h-10 border-2 border-[#E35930]/30 border-t-[#E35930] rounded-full animate-spin mx-auto mb-4" />
1216
+ <p className="text-white/60 text-sm">Loading flight simulator...</p>
1217
+ </div>
1218
+ </div>
1219
+ }>
1220
+ <CesiumFlight
1221
+ walletAddress={activeWalletAddress}
1222
+ onExit={() => { setCesiumFlightMode(false); }}
1223
+ />
1224
+ </Suspense>
1225
+ )}
1226
+
1227
+ {/* Game HUD — minimap + score (active in car or plane mode, not Cesium) */}
1228
+ {combatActive && (
1229
+ <GameHUD
1230
+ active={combatActive}
1231
+ mode={carMode ? "car" : "plane"}
1232
+ manager={projectileManager}
1233
+ playerPosRef={carMode ? playerCarPosRef : planePosRef}
1234
+ playerHeadingRef={carMode ? playerCarHeadingRef : planeHeadingRef}
1235
+ planePositionsRef={planePositionsRef}
1236
+ carPositionsRef={residentCarPositionsRef}
1237
+ remotePlayers={mpManager.remotePlayers}
1238
+ buildingPositions={buildingPositions}
1239
+ />
1240
+ )}
1241
+
1242
+ {/* Cockpit HUD — first-person view with Solana data (car + plane) */}
1243
+ <CockpitHUD
1244
+ active={(flyMode && !cesiumFlightMode) || carMode}
1245
+ mode={carMode ? "car" : "plane"}
1246
+ positionRef={carMode ? playerCarPosRef : planePosRef}
1247
+ headingRef={carMode ? playerCarHeadingRef : planeHeadingRef}
1248
+ pitchRef={flyMode ? planePitchRef : undefined}
1249
+ speedRef={carMode ? carSpeedRef : planeSpeedRef}
1250
+ walletAddress={activeWalletAddress}
1251
+ hp={projectileManager.playerHP}
1252
+ maxHP={projectileManager.playerMaxHP}
1253
+ destroyed={projectileManager.destroyed}
1254
+ respawnRemaining={projectileManager.getRespawnRemaining()}
1255
+ />
1256
+
1257
+ {/* Leaderboard panel — visible in combat mode */}
1258
+ <LeaderboardPanel
1259
+ visible={combatActive}
1260
+ currentWallet={activeWalletAddress}
1261
+ currentScore={projectileManager.score}
1262
+ />
1263
+
1264
+ {/* In-game chat — requires wallet + building in the city */}
1265
+ <GameChat
1266
+ walletAddress={activeWalletAddress}
1267
+ displayName={profile?.x_username || null}
1268
+ hasBuilding={!!activeWalletAddress && wallets.some(w => w.address === activeWalletAddress)}
1269
+ />
1270
+
1271
+ {/* Parcel Reward — first 16 wallets get hidden SOL */}
1272
+ <ParcelReward walletAddress={activeWalletAddress} />
1273
+
1274
+ {/* Treasure hunt challenge banner */}
1275
+ <ParcelChallengeBanner
1276
+ walletConnected={walletConnected}
1277
+ walletCount={wallets.length}
1278
+ />
1279
+
1280
+ {/* Treasure Gate — play-to-win overlay before entering the city */}
1281
+ {showTreasureGate && activeWalletAddress && (
1282
+ <TreasureGate
1283
+ walletAddress={activeWalletAddress}
1284
+ onEnterCity={handleTreasureGateComplete}
1285
+ />
1286
+ )}
1287
+ </div>
1288
+ );
1289
+ }