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,505 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useRef, useCallback } from "react";
4
+
5
+ interface TreasureGateProps {
6
+ walletAddress: string;
7
+ onEnterCity: () => void;
8
+ }
9
+
10
+ type GamePhase =
11
+ | "intro" // show the gate UI
12
+ | "scratching" // user is scratching / playing
13
+ | "transaction" // dummy tx animation
14
+ | "reveal" // result reveal animation
15
+ | "entering"; // cinematic city entry
16
+
17
+ interface TxStep {
18
+ label: string;
19
+ done: boolean;
20
+ time?: string;
21
+ }
22
+
23
+ /** Generate a deterministic-looking fake tx hash */
24
+ function fakeTxHash(seed: string): string {
25
+ let h = 0xdeadbeef;
26
+ for (let i = 0; i < seed.length; i++) h = Math.imul(h ^ seed.charCodeAt(i), 2654435761);
27
+ return Array.from({ length: 64 }, (_, i) => ((h >> (i % 28)) & 0xf).toString(16)).join("");
28
+ }
29
+
30
+ /** Deterministic "win" check based on wallet + timestamp-window (everyone wins for now) */
31
+ function checkWin(_wallet: string): boolean {
32
+ // Always win — the idea is to hook them in. Can be tuned later.
33
+ return true;
34
+ }
35
+
36
+ const REWARD_SOL = 0.069420;
37
+
38
+ const SLOT_SYMBOLS = ["💎", "🔥", "⚡", "🌟", "🎰", "💰", "🚀", "🎲"];
39
+
40
+ export default function TreasureGate({ walletAddress, onEnterCity }: TreasureGateProps) {
41
+ const [phase, setPhase] = useState<GamePhase>("intro");
42
+ const [slotReels, setSlotReels] = useState<string[]>(["💎", "💎", "💎"]);
43
+ const [spinning, setSpinning] = useState(false);
44
+ const [won, setWon] = useState<boolean | null>(null);
45
+ const [txSteps, setTxSteps] = useState<TxStep[]>([]);
46
+ const [txHash, setTxHash] = useState("");
47
+ const [blockSlot, setBlockSlot] = useState(0);
48
+ const [txPhase, setTxPhase] = useState<"processing" | "confirmed" | "failed">("processing");
49
+ const [shakeClass, setShakeClass] = useState("");
50
+ const [particles, setParticles] = useState<Array<{ id: number; x: number; y: number; emoji: string }>>([]);
51
+ const reelTimers = useRef<NodeJS.Timeout[]>([]);
52
+ const particleId = useRef(0);
53
+
54
+ // Cleanup timers
55
+ useEffect(() => {
56
+ return () => reelTimers.current.forEach(t => clearTimeout(t));
57
+ }, []);
58
+
59
+ /** Play the slot machine */
60
+ const playSlots = useCallback(() => {
61
+ if (spinning) return;
62
+ setSpinning(true);
63
+ setPhase("scratching");
64
+
65
+ const isWin = checkWin(walletAddress);
66
+ const winSymbol = "💎";
67
+
68
+ // Rapid spin each reel then lock one by one
69
+ const spinInterval = 80;
70
+ const reelCount = 3;
71
+ const intervals: NodeJS.Timeout[] = [];
72
+
73
+ for (let r = 0; r < reelCount; r++) {
74
+ const spinId = setInterval(() => {
75
+ setSlotReels(prev => {
76
+ const next = [...prev];
77
+ next[r] = SLOT_SYMBOLS[Math.floor(Math.random() * SLOT_SYMBOLS.length)];
78
+ return next;
79
+ });
80
+ }, spinInterval);
81
+ intervals.push(spinId);
82
+
83
+ // Stop each reel after a staggered delay
84
+ const stopDelay = 1200 + r * 600;
85
+ const stopTimer = setTimeout(() => {
86
+ clearInterval(spinId);
87
+ setSlotReels(prev => {
88
+ const next = [...prev];
89
+ next[r] = isWin ? winSymbol : SLOT_SYMBOLS[Math.floor(Math.random() * SLOT_SYMBOLS.length)];
90
+ return next;
91
+ });
92
+
93
+ // Shake on lock
94
+ setShakeClass("animate-[slotShake_0.15s_ease-in-out]");
95
+ setTimeout(() => setShakeClass(""), 200);
96
+
97
+ // After last reel stops, start tx phase
98
+ if (r === reelCount - 1) {
99
+ setTimeout(() => {
100
+ setWon(isWin);
101
+ setSpinning(false);
102
+ startTransaction(isWin);
103
+ }, 600);
104
+ }
105
+ }, stopDelay);
106
+ reelTimers.current.push(stopTimer);
107
+ }
108
+
109
+ reelTimers.current.push(...intervals);
110
+ }, [spinning, walletAddress]);
111
+
112
+ /** Dummy transaction animation */
113
+ const startTransaction = useCallback(async (isWin: boolean) => {
114
+ setPhase("transaction");
115
+ const hash = fakeTxHash(walletAddress + Date.now());
116
+ const slot = 280_000_000 + Math.floor(Math.random() * 1_000_000);
117
+ setTxHash(hash);
118
+ setBlockSlot(slot);
119
+ setTxPhase("processing");
120
+
121
+ const steps: TxStep[] = [
122
+ { label: "Initiating transaction...", done: false },
123
+ { label: "Signing with wallet keypair", done: false },
124
+ { label: "Broadcasting to Solana mainnet", done: false },
125
+ { label: "Awaiting block confirmation", done: false },
126
+ { label: isWin ? "Transferring 0.069420 SOL reward" : "Verifying parcel status", done: false },
127
+ ];
128
+ setTxSteps([...steps]);
129
+
130
+ // Animate each step
131
+ for (let i = 0; i < steps.length; i++) {
132
+ await new Promise<void>(resolve => setTimeout(resolve, 700 + Math.random() * 500));
133
+ steps[i].done = true;
134
+ steps[i].time = `${(Math.random() * 0.5 + 0.08).toFixed(3)}s`;
135
+ setTxSteps([...steps]);
136
+ }
137
+
138
+ // Final result
139
+ await new Promise<void>(resolve => setTimeout(resolve, 800));
140
+ setTxPhase(isWin ? "confirmed" : "failed");
141
+
142
+ // Move to reveal phase
143
+ await new Promise<void>(resolve => setTimeout(resolve, 1500));
144
+ setPhase("reveal");
145
+
146
+ // Spawn confetti particles on win
147
+ if (isWin) {
148
+ const newParticles = Array.from({ length: 30 }, () => ({
149
+ id: ++particleId.current,
150
+ x: Math.random() * 100,
151
+ y: Math.random() * 100,
152
+ emoji: ["💎", "✨", "🎉", "💰", "⚡"][Math.floor(Math.random() * 5)],
153
+ }));
154
+ setParticles(newParticles);
155
+ setTimeout(() => setParticles([]), 3000);
156
+ }
157
+ }, [walletAddress]);
158
+
159
+ /** Enter the city */
160
+ const enterCity = useCallback(() => {
161
+ setPhase("entering");
162
+ setTimeout(() => onEnterCity(), 1500);
163
+ }, [onEnterCity]);
164
+
165
+ return (
166
+ <div className="fixed inset-0 z-[100] flex items-center justify-center overflow-hidden"
167
+ style={{
168
+ background: "radial-gradient(ellipse at center, #1a0a2e 0%, #0a0612 50%, #000 100%)",
169
+ }}
170
+ >
171
+ {/* Animated background grid */}
172
+ <div className="absolute inset-0 overflow-hidden pointer-events-none">
173
+ <div
174
+ className="absolute inset-0 opacity-10"
175
+ style={{
176
+ backgroundImage: `
177
+ linear-gradient(rgba(227,89,48,0.3) 1px, transparent 1px),
178
+ linear-gradient(90deg, rgba(227,89,48,0.3) 1px, transparent 1px)
179
+ `,
180
+ backgroundSize: "60px 60px",
181
+ animation: "gridScroll 20s linear infinite",
182
+ }}
183
+ />
184
+ {/* Floating orbs */}
185
+ <div className="absolute top-1/4 left-1/4 w-64 h-64 rounded-full bg-purple-600/10 blur-3xl animate-pulse" />
186
+ <div className="absolute bottom-1/4 right-1/4 w-48 h-48 rounded-full bg-[#E35930]/10 blur-3xl animate-pulse" style={{ animationDelay: "1s" }} />
187
+ <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-96 h-96 rounded-full bg-amber-500/5 blur-3xl animate-pulse" style={{ animationDelay: "0.5s" }} />
188
+ </div>
189
+
190
+ {/* Confetti particles */}
191
+ {particles.map(p => (
192
+ <div
193
+ key={p.id}
194
+ className="absolute text-2xl pointer-events-none"
195
+ style={{
196
+ left: `${p.x}%`,
197
+ top: `${p.y}%`,
198
+ animation: "confettiFall 3s ease-out forwards",
199
+ }}
200
+ >
201
+ {p.emoji}
202
+ </div>
203
+ ))}
204
+
205
+ {/* Main content card */}
206
+ <div className={`relative z-10 w-[420px] max-w-[92vw] transition-all duration-700 ${
207
+ phase === "entering" ? "scale-150 opacity-0" : "scale-100 opacity-100"
208
+ }`}>
209
+ {/* Outer glow */}
210
+ <div className="absolute -inset-2 rounded-[2rem] opacity-50 blur-xl bg-gradient-to-br from-[#E35930]/30 via-purple-600/20 to-amber-500/30 animate-pulse" />
211
+
212
+ <div className="relative bg-gradient-to-b from-gray-900/95 to-black/98 backdrop-blur-3xl border border-white/10 rounded-[2rem] overflow-hidden shadow-2xl shadow-black/50">
213
+ {/* Scan line */}
214
+ <div className="absolute inset-0 overflow-hidden rounded-[2rem] pointer-events-none">
215
+ <div
216
+ className="absolute left-0 right-0 h-px bg-gradient-to-r from-transparent via-[#E35930]/50 to-transparent"
217
+ style={{ animation: "scanLine 2.5s ease-in-out infinite" }}
218
+ />
219
+ </div>
220
+
221
+ {/* Header */}
222
+ <div className="relative px-6 pt-7 pb-4 text-center">
223
+ {/* Skip button */}
224
+ <button
225
+ onClick={enterCity}
226
+ className="absolute top-4 right-4 text-[10px] text-white/25 hover:text-white/60 transition-colors cursor-pointer uppercase tracking-wider"
227
+ >
228
+ Skip →
229
+ </button>
230
+
231
+ <div className="inline-flex items-center gap-2 px-4 py-1.5 bg-[#E35930]/10 border border-[#E35930]/20 rounded-full mb-4">
232
+ <span className="w-2 h-2 rounded-full bg-[#E35930] animate-pulse" />
233
+ <span className="text-[10px] uppercase tracking-widest font-bold text-[#E35930]/80">
234
+ Solana Mainnet
235
+ </span>
236
+ </div>
237
+
238
+ <h1 className="text-2xl font-black tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-amber-200 via-yellow-300 to-amber-200 mb-1.5">
239
+ SOL TREASURE HUNT
240
+ </h1>
241
+ <p className="text-xs text-white/40">
242
+ Play to see if your parcel has hidden treasure before you enter the city
243
+ </p>
244
+
245
+ {/* Wallet address chip */}
246
+ <div className="mt-3 inline-flex items-center gap-2 px-3 py-1.5 bg-white/[0.04] border border-white/[0.06] rounded-xl">
247
+ <div className="w-2 h-2 rounded-full bg-emerald-400" />
248
+ <span className="text-[10px] font-mono text-white/40">
249
+ {walletAddress.slice(0, 6)}...{walletAddress.slice(-4)}
250
+ </span>
251
+ </div>
252
+ </div>
253
+
254
+ {/* ─── INTRO: Slot Machine ─── */}
255
+ {(phase === "intro" || phase === "scratching") && (
256
+ <div className="px-6 pb-6">
257
+ {/* Slot machine display */}
258
+ <div className="relative bg-black/50 border border-white/[0.08] rounded-2xl p-4 mb-4">
259
+ <div className="absolute -inset-px rounded-2xl bg-gradient-to-b from-amber-500/10 to-transparent pointer-events-none" />
260
+
261
+ <p className="text-[8px] uppercase tracking-[0.2em] text-amber-400/40 text-center mb-3 font-bold">
262
+ ✦ MATCH 3 TO WIN {REWARD_SOL} SOL ✦
263
+ </p>
264
+
265
+ <div className={`flex items-center justify-center gap-3 ${shakeClass}`}>
266
+ {slotReels.map((symbol, i) => (
267
+ <div
268
+ key={i}
269
+ className={`w-20 h-20 sm:w-24 sm:h-24 rounded-2xl border-2 flex items-center justify-center text-4xl sm:text-5xl transition-all duration-200 ${
270
+ spinning
271
+ ? "border-[#E35930]/40 bg-[#E35930]/5 shadow-lg shadow-[#E35930]/10"
272
+ : won === true
273
+ ? "border-emerald-400/50 bg-emerald-500/10 shadow-lg shadow-emerald-500/20"
274
+ : won === false
275
+ ? "border-red-400/50 bg-red-500/10"
276
+ : "border-white/10 bg-white/[0.03]"
277
+ }`}
278
+ >
279
+ <span className={spinning ? "animate-bounce" : won !== null ? "animate-[popIn_0.4s_ease-out]" : ""}>
280
+ {symbol}
281
+ </span>
282
+ </div>
283
+ ))}
284
+ </div>
285
+
286
+ {/* Win line */}
287
+ {won === true && (
288
+ <div className="absolute left-6 right-6 top-1/2 -translate-y-1/2 h-0.5 bg-gradient-to-r from-transparent via-emerald-400/60 to-transparent animate-pulse pointer-events-none" />
289
+ )}
290
+ </div>
291
+
292
+ {/* Reward preview */}
293
+ <div className="flex items-center justify-between bg-amber-500/5 border border-amber-500/10 rounded-xl px-4 py-2.5 mb-4">
294
+ <div>
295
+ <p className="text-[8px] uppercase tracking-widest text-amber-400/40">Jackpot</p>
296
+ <p className="text-lg font-black text-amber-200 tabular-nums">
297
+ {REWARD_SOL} <span className="text-sm text-amber-400/60">SOL</span>
298
+ </p>
299
+ </div>
300
+ <div className="text-3xl animate-bounce" style={{ animationDuration: "2s" }}>💎</div>
301
+ </div>
302
+
303
+ {/* Play button */}
304
+ {!spinning && won === null && (
305
+ <button
306
+ onClick={playSlots}
307
+ className="w-full group relative py-4 rounded-2xl text-base font-black text-white tracking-wide transition-all cursor-pointer active:scale-[0.97]"
308
+ style={{
309
+ background: "linear-gradient(135deg, #E35930 0%, #c94420 50%, #E35930 100%)",
310
+ backgroundSize: "200% 200%",
311
+ animation: "claimShimmer 3s ease infinite",
312
+ boxShadow: "0 0 40px rgba(227,89,48,0.4), 0 0 80px rgba(227,89,48,0.15)",
313
+ }}
314
+ >
315
+ <div className="absolute -inset-1 rounded-2xl blur-md bg-gradient-to-r from-[#E35930]/40 to-purple-500/30 opacity-60 group-hover:opacity-80 transition-opacity" />
316
+ <span className="relative flex items-center justify-center gap-2">
317
+ 🎰 SPIN TO WIN
318
+ </span>
319
+ </button>
320
+ )}
321
+ </div>
322
+ )}
323
+
324
+ {/* ─── TRANSACTION: Dummy TX animation ─── */}
325
+ {phase === "transaction" && (
326
+ <div className="px-6 pb-6">
327
+ {/* TX Hash */}
328
+ <div className="bg-black/40 rounded-xl px-4 py-3 mb-4 border border-white/[0.06]">
329
+ <p className="text-[8px] uppercase tracking-[0.15em] text-white/20 mb-1">TRANSACTION HASH</p>
330
+ <p className="text-[10px] font-mono text-[#E35930]/70 break-all leading-relaxed">
331
+ {txHash.slice(0, 44)}...
332
+ </p>
333
+ </div>
334
+
335
+ {/* Block info */}
336
+ <div className="flex gap-3 mb-4">
337
+ <div className="flex-1 bg-black/30 rounded-xl px-3 py-2 border border-white/[0.05] text-center">
338
+ <p className="text-[7px] uppercase tracking-wider text-white/20">Block</p>
339
+ <p className="text-xs font-bold text-white/60 tabular-nums">{blockSlot.toLocaleString()}</p>
340
+ </div>
341
+ <div className="flex-1 bg-black/30 rounded-xl px-3 py-2 border border-white/[0.05] text-center">
342
+ <p className="text-[7px] uppercase tracking-wider text-white/20">Finality</p>
343
+ <p className="text-xs font-bold text-white/60">{txPhase === "confirmed" ? "Confirmed" : "Pending..."}</p>
344
+ </div>
345
+ <div className="flex-1 bg-black/30 rounded-xl px-3 py-2 border border-white/[0.05] text-center">
346
+ <p className="text-[7px] uppercase tracking-wider text-white/20">Network</p>
347
+ <p className="text-xs font-bold text-[#E35930]/70">Mainnet</p>
348
+ </div>
349
+ </div>
350
+
351
+ {/* Steps */}
352
+ <div className="space-y-2.5 mb-4">
353
+ {txSteps.map((step, i) => (
354
+ <div key={i} className="flex items-center gap-3">
355
+ <div className={`w-5 h-5 rounded-full flex items-center justify-center shrink-0 transition-all duration-500 ${
356
+ step.done
357
+ ? "bg-emerald-500/20 text-emerald-400"
358
+ : txSteps[i - 1]?.done || i === 0
359
+ ? "bg-[#E35930]/20 text-[#E35930]"
360
+ : "bg-white/5 text-white/15"
361
+ }`}>
362
+ {step.done ? (
363
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
364
+ <path d="M20 6L9 17l-5-5" />
365
+ </svg>
366
+ ) : (txSteps[i - 1]?.done || i === 0) && !step.done ? (
367
+ <div className="w-2.5 h-2.5 border-2 border-current border-t-transparent rounded-full animate-spin" />
368
+ ) : (
369
+ <div className="w-1.5 h-1.5 rounded-full bg-current opacity-30" />
370
+ )}
371
+ </div>
372
+ <span className={`text-xs transition-all duration-500 ${step.done ? "text-white/70" : "text-white/25"}`}>
373
+ {step.label}
374
+ </span>
375
+ {step.time && (
376
+ <span className="ml-auto text-[8px] font-mono text-emerald-400/50">{step.time}</span>
377
+ )}
378
+ </div>
379
+ ))}
380
+ </div>
381
+
382
+ {/* Progress bar */}
383
+ <div className="h-1.5 rounded-full bg-white/5 overflow-hidden">
384
+ <div
385
+ className="h-full rounded-full transition-all duration-700 ease-out"
386
+ style={{
387
+ width: `${(txSteps.filter(s => s.done).length / txSteps.length) * 100}%`,
388
+ background: "linear-gradient(90deg, #E35930, #8b5cf6, #E35930)",
389
+ backgroundSize: "200% 100%",
390
+ animation: "claimShimmer 2s ease infinite",
391
+ }}
392
+ />
393
+ </div>
394
+
395
+ {/* Confirmed result */}
396
+ {txPhase === "confirmed" && (
397
+ <div className="mt-4 bg-emerald-500/10 border border-emerald-500/20 rounded-xl px-4 py-3 text-center animate-[popIn_0.5s_ease-out]">
398
+ <p className="text-sm font-bold text-emerald-300">✓ Transaction Confirmed</p>
399
+ <p className="text-[9px] text-emerald-400/40 mt-0.5">Finalized on Solana mainnet-beta</p>
400
+ </div>
401
+ )}
402
+ </div>
403
+ )}
404
+
405
+ {/* ─── REVEAL: Win/Lose result ─── */}
406
+ {phase === "reveal" && (
407
+ <div className="px-6 pb-6 text-center">
408
+ {won ? (
409
+ <>
410
+ <div className="text-7xl mb-4 animate-bounce">💎</div>
411
+ <h2 className="text-2xl font-black bg-clip-text text-transparent bg-gradient-to-r from-amber-200 via-yellow-300 to-amber-200 mb-2">
412
+ YOU WON!
413
+ </h2>
414
+ <p className="text-sm text-white/50 mb-4">
415
+ Your parcel has <span className="text-amber-200 font-bold">{REWARD_SOL} SOL</span> hidden inside!
416
+ </p>
417
+
418
+ {/* Reward card */}
419
+ <div className="bg-gradient-to-r from-amber-900/40 to-yellow-900/40 border border-amber-500/20 rounded-2xl px-5 py-4 mb-5">
420
+ <p className="text-[8px] uppercase tracking-[0.2em] text-amber-400/40 mb-1">Reward credited</p>
421
+ <p className="text-3xl font-black text-amber-200 tabular-nums">
422
+ {REWARD_SOL} <span className="text-lg text-amber-400/60">SOL</span>
423
+ </p>
424
+ <p className="text-[9px] text-amber-400/30 mt-1 font-mono">
425
+ Claim it inside your building in the city
426
+ </p>
427
+ </div>
428
+
429
+ {/* Enter city button */}
430
+ <button
431
+ onClick={enterCity}
432
+ className="w-full py-4 rounded-2xl text-base font-black text-black transition-all cursor-pointer active:scale-[0.97]"
433
+ style={{
434
+ background: "linear-gradient(135deg, #fbbf24, #f59e0b, #eab308)",
435
+ boxShadow: "0 0 40px rgba(245,158,11,0.3), 0 0 80px rgba(245,158,11,0.1)",
436
+ }}
437
+ >
438
+ 🏙️ ENTER SOLANAPOLIS
439
+ </button>
440
+ </>
441
+ ) : (
442
+ <>
443
+ <div className="text-5xl mb-4 opacity-60">🏙️</div>
444
+ <h2 className="text-xl font-bold text-white/60 mb-2">No Treasure This Time</h2>
445
+ <p className="text-sm text-white/30 mb-6">
446
+ Earn rewards by being an active citizen in the city!
447
+ </p>
448
+ <button
449
+ onClick={enterCity}
450
+ className="w-full py-4 rounded-2xl text-base font-bold text-white bg-white/10 hover:bg-white/15 border border-white/10 transition-all cursor-pointer active:scale-[0.97]"
451
+ >
452
+ Enter City Anyway →
453
+ </button>
454
+ </>
455
+ )}
456
+ </div>
457
+ )}
458
+
459
+ {/* ─── ENTERING: cinematic zoom ─── */}
460
+ {phase === "entering" && (
461
+ <div className="px-6 py-12 text-center">
462
+ <div className="w-12 h-12 border-3 border-white/10 border-t-amber-400 rounded-full animate-spin mx-auto mb-4" />
463
+ <p className="text-sm font-bold text-white/50 animate-pulse tracking-wider uppercase">
464
+ Entering Solanapolis...
465
+ </p>
466
+ <p className="text-[10px] text-white/20 mt-2 font-mono">
467
+ {walletAddress.slice(0, 8)}...{walletAddress.slice(-6)}
468
+ </p>
469
+ </div>
470
+ )}
471
+
472
+ {/* Bottom badge */}
473
+ <div className="px-6 pb-5 pt-1">
474
+ <div className="flex items-center justify-center gap-2 text-[9px] text-white/15">
475
+ <span>⛓</span>
476
+ <span>Verified on Solana • Powered by Helius</span>
477
+ </div>
478
+ </div>
479
+ </div>
480
+ </div>
481
+
482
+ {/* Inline keyframes */}
483
+ <style jsx>{`
484
+ @keyframes gridScroll {
485
+ from { transform: translateY(0); }
486
+ to { transform: translateY(60px); }
487
+ }
488
+ @keyframes popIn {
489
+ 0% { transform: scale(0.5); opacity: 0; }
490
+ 50% { transform: scale(1.15); }
491
+ 100% { transform: scale(1); opacity: 1; }
492
+ }
493
+ @keyframes slotShake {
494
+ 0%, 100% { transform: translateX(0); }
495
+ 25% { transform: translateX(-4px); }
496
+ 75% { transform: translateX(4px); }
497
+ }
498
+ @keyframes confettiFall {
499
+ 0% { transform: translateY(-20px) rotate(0deg) scale(1); opacity: 1; }
500
+ 100% { transform: translateY(100vh) rotate(720deg) scale(0.3); opacity: 0; }
501
+ }
502
+ `}</style>
503
+ </div>
504
+ );
505
+ }