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