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,128 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useRef, useState, MutableRefObject, useMemo } from "react";
|
|
3
|
+
import { useFrame } from "@react-three/fiber";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
import { createClient } from "@/lib/supabase";
|
|
6
|
+
import { PlaneTransform } from "./PlaneMode";
|
|
7
|
+
|
|
8
|
+
// Reusable objects to avoid per-frame GC pressure
|
|
9
|
+
const _targetPos = new THREE.Vector3();
|
|
10
|
+
const _targetQuat = new THREE.Quaternion();
|
|
11
|
+
const _euler = new THREE.Euler();
|
|
12
|
+
|
|
13
|
+
interface RemotePlane {
|
|
14
|
+
wallet: string;
|
|
15
|
+
x: number; y: number; z: number;
|
|
16
|
+
yaw: number; pitch: number;
|
|
17
|
+
lastSeen: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function walletColor(address: string): string {
|
|
21
|
+
let hash = 0;
|
|
22
|
+
for (let i = 0; i < address.length; i++) hash = ((hash << 5) - hash + address.charCodeAt(i)) | 0;
|
|
23
|
+
const hue = Math.abs(hash) % 360;
|
|
24
|
+
return `hsl(${hue},70%,60%)`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function RemotePlaneMesh({ plane }: { plane: RemotePlane }) {
|
|
28
|
+
const groupRef = useRef<THREE.Group>(null);
|
|
29
|
+
const color = walletColor(plane.wallet);
|
|
30
|
+
const mat = useMemo(() => new THREE.MeshStandardMaterial({ color, metalness: 0.4, roughness: 0.5 }), [color]);
|
|
31
|
+
|
|
32
|
+
useFrame(() => {
|
|
33
|
+
if (!groupRef.current) return;
|
|
34
|
+
_targetPos.set(plane.x, plane.y, plane.z);
|
|
35
|
+
groupRef.current.position.lerp(_targetPos, 0.15);
|
|
36
|
+
_euler.set(plane.pitch, plane.yaw, 0, "YXZ");
|
|
37
|
+
_targetQuat.setFromEuler(_euler);
|
|
38
|
+
groupRef.current.quaternion.slerp(_targetQuat, 0.15);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<group ref={groupRef} position={[plane.x, plane.y, plane.z]}>
|
|
43
|
+
{/* Simplified plane mesh */}
|
|
44
|
+
<mesh material={mat}>
|
|
45
|
+
<boxGeometry args={[0.7, 0.7, 4.5]} />
|
|
46
|
+
</mesh>
|
|
47
|
+
<mesh material={mat}>
|
|
48
|
+
<boxGeometry args={[9, 0.15, 2.0]} />
|
|
49
|
+
</mesh>
|
|
50
|
+
<mesh position={[0, 0, -2.1]} material={mat}>
|
|
51
|
+
<boxGeometry args={[3.5, 0.12, 0.9]} />
|
|
52
|
+
</mesh>
|
|
53
|
+
<mesh position={[0, 0.6, -2.0]} material={mat}>
|
|
54
|
+
<boxGeometry args={[0.12, 1.1, 0.7]} />
|
|
55
|
+
</mesh>
|
|
56
|
+
{/* Colored point light to identify the plane */}
|
|
57
|
+
<pointLight color={color} intensity={3} distance={15} decay={2} position={[0, 2, 0]} />
|
|
58
|
+
</group>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface MultiplayerPlanesProps {
|
|
63
|
+
active: boolean;
|
|
64
|
+
myWallet: string | null;
|
|
65
|
+
positionRef: MutableRefObject<PlaneTransform | null>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default function MultiplayerPlanes({ active, myWallet, positionRef }: MultiplayerPlanesProps) {
|
|
69
|
+
const [remotePlanes, setRemotePlanes] = useState<RemotePlane[]>([]);
|
|
70
|
+
const channelRef = useRef<ReturnType<ReturnType<typeof createClient>["channel"]> | null>(null);
|
|
71
|
+
const supabaseRef = useRef(createClient());
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
const supabase = supabaseRef.current;
|
|
75
|
+
const channel = supabase.channel("heliopolis-planes");
|
|
76
|
+
channelRef.current = channel;
|
|
77
|
+
|
|
78
|
+
channel.on("broadcast", { event: "plane-position" }, ({ payload }: { payload: RemotePlane & { wallet: string } }) => {
|
|
79
|
+
if (payload.wallet === myWallet) return;
|
|
80
|
+
setRemotePlanes(prev => {
|
|
81
|
+
const existing = prev.findIndex(p => p.wallet === payload.wallet);
|
|
82
|
+
const updated = { ...payload, lastSeen: Date.now() };
|
|
83
|
+
if (existing >= 0) {
|
|
84
|
+
const next = [...prev];
|
|
85
|
+
next[existing] = updated;
|
|
86
|
+
return next;
|
|
87
|
+
}
|
|
88
|
+
return [...prev, updated];
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
channel.subscribe();
|
|
93
|
+
|
|
94
|
+
const cleanupInterval = setInterval(() => {
|
|
95
|
+
setRemotePlanes(prev => prev.filter(p => Date.now() - p.lastSeen < 5000));
|
|
96
|
+
}, 3000);
|
|
97
|
+
|
|
98
|
+
return () => {
|
|
99
|
+
clearInterval(cleanupInterval);
|
|
100
|
+
channel.unsubscribe();
|
|
101
|
+
};
|
|
102
|
+
}, [myWallet]);
|
|
103
|
+
|
|
104
|
+
// Broadcast own position every 100ms when active
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (!active || !myWallet) return;
|
|
107
|
+
|
|
108
|
+
const interval = setInterval(() => {
|
|
109
|
+
const pos = positionRef.current;
|
|
110
|
+
if (!pos || !channelRef.current) return;
|
|
111
|
+
channelRef.current.send({
|
|
112
|
+
type: "broadcast",
|
|
113
|
+
event: "plane-position",
|
|
114
|
+
payload: { wallet: myWallet, ...pos, lastSeen: Date.now() },
|
|
115
|
+
});
|
|
116
|
+
}, 100);
|
|
117
|
+
|
|
118
|
+
return () => clearInterval(interval);
|
|
119
|
+
}, [active, myWallet, positionRef]);
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<>
|
|
123
|
+
{remotePlanes.map(plane => (
|
|
124
|
+
<RemotePlaneMesh key={plane.wallet} plane={plane} />
|
|
125
|
+
))}
|
|
126
|
+
</>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
interface Stats {
|
|
6
|
+
slot: number;
|
|
7
|
+
epoch: number;
|
|
8
|
+
epochProgress: string;
|
|
9
|
+
tps: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function NetworkStats() {
|
|
13
|
+
const [stats, setStats] = useState<Stats | null>(null);
|
|
14
|
+
const [prevTps, setPrevTps] = useState(0);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
async function fetchStats() {
|
|
18
|
+
try {
|
|
19
|
+
const res = await fetch("/api/network-stats");
|
|
20
|
+
if (!res.ok) return;
|
|
21
|
+
const data = await res.json();
|
|
22
|
+
setStats((prev) => {
|
|
23
|
+
if (prev) setPrevTps(prev.tps);
|
|
24
|
+
return data;
|
|
25
|
+
});
|
|
26
|
+
} catch {
|
|
27
|
+
// silently fail
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fetchStats();
|
|
32
|
+
const interval = setInterval(fetchStats, 10000); // every 10s
|
|
33
|
+
return () => clearInterval(interval);
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
if (!stats) return null;
|
|
37
|
+
|
|
38
|
+
const tpsDelta = stats.tps - prevTps;
|
|
39
|
+
const tpsColor =
|
|
40
|
+
tpsDelta > 0 ? "text-green-300/70" : tpsDelta < 0 ? "text-red-300/70" : "text-white/50";
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className="absolute bottom-20 sm:bottom-16 right-3 sm:right-40 z-10 bg-black/50 backdrop-blur-xl border border-white/[0.08] rounded-2xl px-3.5 py-2.5 text-xs flex gap-4 items-center">
|
|
44
|
+
{/* TPS */}
|
|
45
|
+
<div className="flex flex-col items-center">
|
|
46
|
+
<span className="text-white/25 text-[9px] uppercase tracking-wider">TPS</span>
|
|
47
|
+
<span className={`font-mono font-semibold text-sm tabular-nums ${tpsColor}`}>
|
|
48
|
+
{stats.tps.toLocaleString()}
|
|
49
|
+
</span>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
{/* Divider */}
|
|
53
|
+
<div className="w-px h-6 bg-white/[0.08]" />
|
|
54
|
+
|
|
55
|
+
{/* Slot */}
|
|
56
|
+
<div className="flex flex-col items-center">
|
|
57
|
+
<span className="text-white/25 text-[9px] uppercase tracking-wider">Slot</span>
|
|
58
|
+
<span className="font-mono text-white/50 text-[11px] tabular-nums">
|
|
59
|
+
{stats.slot.toLocaleString()}
|
|
60
|
+
</span>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
{/* Divider */}
|
|
64
|
+
<div className="w-px h-6 bg-white/[0.08]" />
|
|
65
|
+
|
|
66
|
+
{/* Epoch */}
|
|
67
|
+
<div className="flex flex-col items-center">
|
|
68
|
+
<span className="text-white/25 text-[9px] uppercase tracking-wider">
|
|
69
|
+
Epoch {stats.epoch}
|
|
70
|
+
</span>
|
|
71
|
+
<div className="w-14 h-1.5 bg-white/[0.06] rounded-full overflow-hidden mt-1">
|
|
72
|
+
<div
|
|
73
|
+
className="h-full rounded-full transition-all duration-1000"
|
|
74
|
+
style={{
|
|
75
|
+
width: `${stats.epochProgress}%`,
|
|
76
|
+
background: "linear-gradient(90deg, #6366f1, #E35930)",
|
|
77
|
+
}}
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useRef, useMemo } from "react";
|
|
3
|
+
import { useFrame } from "@react-three/fiber";
|
|
4
|
+
import * as THREE from "three";
|
|
5
|
+
|
|
6
|
+
interface NewBuildingSpotlightProps {
|
|
7
|
+
position: [number, number, number];
|
|
8
|
+
buildingHeight: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Dramatic spotlight + ring for newly placed buildings.
|
|
13
|
+
* Pulses brightly then fades over ~6 seconds.
|
|
14
|
+
*/
|
|
15
|
+
export default function NewBuildingSpotlight({ position, buildingHeight }: NewBuildingSpotlightProps) {
|
|
16
|
+
const groupRef = useRef<THREE.Group>(null);
|
|
17
|
+
const beamRef = useRef<THREE.Mesh>(null);
|
|
18
|
+
const glowRef = useRef<THREE.PointLight>(null);
|
|
19
|
+
const ringRef = useRef<THREE.Mesh>(null);
|
|
20
|
+
const startTime = useRef(Date.now());
|
|
21
|
+
|
|
22
|
+
const beamMat = useMemo(() => new THREE.MeshBasicMaterial({
|
|
23
|
+
color: "#E35930",
|
|
24
|
+
transparent: true,
|
|
25
|
+
opacity: 0.25,
|
|
26
|
+
side: THREE.DoubleSide,
|
|
27
|
+
depthWrite: false,
|
|
28
|
+
blending: THREE.AdditiveBlending,
|
|
29
|
+
}), []);
|
|
30
|
+
|
|
31
|
+
const ringMat = useMemo(() => new THREE.MeshBasicMaterial({
|
|
32
|
+
color: "#E35930",
|
|
33
|
+
transparent: true,
|
|
34
|
+
opacity: 0.4,
|
|
35
|
+
side: THREE.DoubleSide,
|
|
36
|
+
depthWrite: false,
|
|
37
|
+
blending: THREE.AdditiveBlending,
|
|
38
|
+
}), []);
|
|
39
|
+
|
|
40
|
+
useFrame(() => {
|
|
41
|
+
if (!groupRef.current) return;
|
|
42
|
+
const elapsed = (Date.now() - startTime.current) / 1000;
|
|
43
|
+
|
|
44
|
+
// Fade out after 4 seconds
|
|
45
|
+
const fade = elapsed < 4 ? 1 : Math.max(0, 1 - (elapsed - 4) / 2);
|
|
46
|
+
|
|
47
|
+
if (fade <= 0) {
|
|
48
|
+
groupRef.current.visible = false;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
groupRef.current.visible = true;
|
|
53
|
+
|
|
54
|
+
if (beamRef.current) {
|
|
55
|
+
beamMat.opacity = 0.2 * fade * (0.7 + 0.3 * Math.sin(elapsed * 4));
|
|
56
|
+
}
|
|
57
|
+
if (glowRef.current) {
|
|
58
|
+
glowRef.current.intensity = 15 * fade * (0.7 + 0.3 * Math.sin(elapsed * 4));
|
|
59
|
+
}
|
|
60
|
+
if (ringRef.current) {
|
|
61
|
+
const scale = 1 + elapsed * 1.5;
|
|
62
|
+
ringRef.current.scale.setScalar(Math.min(scale, 4));
|
|
63
|
+
ringMat.opacity = 0.5 * fade * Math.max(0, 1 - elapsed * 0.12);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const beamHeight = Math.max(100, buildingHeight + 80);
|
|
68
|
+
const beamY = buildingHeight + beamHeight / 2;
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<group ref={groupRef} position={position}>
|
|
72
|
+
{/* Vertical beam */}
|
|
73
|
+
<mesh ref={beamRef} material={beamMat} position={[0, beamY - position[1], 0]}>
|
|
74
|
+
<cylinderGeometry args={[0.15, 2.0, beamHeight, 8, 1, true]} />
|
|
75
|
+
</mesh>
|
|
76
|
+
|
|
77
|
+
{/* Pulsing ring at building top */}
|
|
78
|
+
<mesh ref={ringRef} material={ringMat} position={[0, buildingHeight - position[1] + 1, 0]} rotation={[Math.PI / 2, 0, 0]}>
|
|
79
|
+
<ringGeometry args={[1.5, 2.5, 24]} />
|
|
80
|
+
</mesh>
|
|
81
|
+
|
|
82
|
+
{/* Point light glow */}
|
|
83
|
+
<pointLight
|
|
84
|
+
ref={glowRef}
|
|
85
|
+
position={[0, buildingHeight - position[1] + 2, 0]}
|
|
86
|
+
color="#E35930"
|
|
87
|
+
intensity={15}
|
|
88
|
+
distance={50}
|
|
89
|
+
decay={2}
|
|
90
|
+
/>
|
|
91
|
+
</group>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
|
|
5
|
+
interface ParcelChallengeBannerProps {
|
|
6
|
+
walletConnected: boolean;
|
|
7
|
+
walletCount: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const TOTAL_REWARD_PARCELS = 16;
|
|
11
|
+
const REWARD_PER_PARCEL = 0.069420;
|
|
12
|
+
const TREASURY_ADDRESS = "76QQqndM87DBQAeAnRnjdMSiVBgBNrm55HMa88MRVc7p";
|
|
13
|
+
|
|
14
|
+
interface TreasuryInfo {
|
|
15
|
+
balanceSOL: number;
|
|
16
|
+
parcelsRemaining: number;
|
|
17
|
+
solUsdValue: number | null;
|
|
18
|
+
solPrice: number | null;
|
|
19
|
+
totalUsdValue: number | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default function ParcelChallengeBanner({ walletConnected, walletCount }: ParcelChallengeBannerProps) {
|
|
23
|
+
const [visible, setVisible] = useState(false);
|
|
24
|
+
const [dismissed, setDismissed] = useState(false);
|
|
25
|
+
const [treasury, setTreasury] = useState<TreasuryInfo | null>(null);
|
|
26
|
+
|
|
27
|
+
const slotsRemaining = Math.max(0, TOTAL_REWARD_PARCELS - walletCount);
|
|
28
|
+
const allClaimed = slotsRemaining === 0;
|
|
29
|
+
|
|
30
|
+
// Fetch live treasury balance
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
async function fetchTreasury() {
|
|
33
|
+
try {
|
|
34
|
+
const res = await fetch("/api/treasury");
|
|
35
|
+
if (res.ok) {
|
|
36
|
+
const data = await res.json();
|
|
37
|
+
setTreasury({
|
|
38
|
+
balanceSOL: data.balanceSOL,
|
|
39
|
+
parcelsRemaining: data.parcelsRemaining,
|
|
40
|
+
solUsdValue: data.solUsdValue ?? null,
|
|
41
|
+
solPrice: data.solPrice ?? null,
|
|
42
|
+
totalUsdValue: data.totalUsdValue ?? null,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
} catch { /* silent */ }
|
|
46
|
+
}
|
|
47
|
+
fetchTreasury();
|
|
48
|
+
const interval = setInterval(fetchTreasury, 30000); // refresh every 30s
|
|
49
|
+
return () => clearInterval(interval);
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const timer = setTimeout(() => {
|
|
54
|
+
if (sessionStorage.getItem("parcel-banner-dismissed")) return;
|
|
55
|
+
setVisible(true);
|
|
56
|
+
}, walletConnected ? 1000 : 3000);
|
|
57
|
+
return () => clearTimeout(timer);
|
|
58
|
+
}, [walletConnected]);
|
|
59
|
+
|
|
60
|
+
if (!visible || dismissed) return null;
|
|
61
|
+
|
|
62
|
+
function handleDismiss() {
|
|
63
|
+
setDismissed(true);
|
|
64
|
+
sessionStorage.setItem("parcel-banner-dismissed", "1");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const treasuryBalance = treasury?.balanceSOL ?? null;
|
|
68
|
+
const fundedParcels = treasury?.parcelsRemaining ?? slotsRemaining;
|
|
69
|
+
const depleted = treasuryBalance !== null && treasuryBalance < REWARD_PER_PARCEL;
|
|
70
|
+
const solPrice = treasury?.solPrice ?? null;
|
|
71
|
+
const treasuryUsd = treasury?.solUsdValue ?? null;
|
|
72
|
+
const parcelUsd = solPrice !== null ? (REWARD_PER_PARCEL * solPrice) : null;
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div className="fixed top-20 left-1/2 -translate-x-1/2 z-30 animate-[fadeSlideDown_0.5s_ease-out] w-[95vw] max-w-lg">
|
|
76
|
+
<div className="relative overflow-hidden bg-gradient-to-r from-amber-900/95 via-yellow-900/90 to-orange-900/95 backdrop-blur-2xl border border-amber-500/30 rounded-2xl shadow-2xl shadow-amber-900/40">
|
|
77
|
+
{/* Animated shine */}
|
|
78
|
+
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
|
79
|
+
<div className="absolute -top-1/2 -left-1/2 w-full h-full bg-gradient-to-br from-white/5 to-transparent rotate-12 animate-[shimmer_3s_ease-in-out_infinite]" />
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
{/* Close */}
|
|
83
|
+
<button
|
|
84
|
+
onClick={handleDismiss}
|
|
85
|
+
className="absolute top-3 right-3 w-7 h-7 flex items-center justify-center rounded-full bg-white/10 hover:bg-white/20 text-white/40 hover:text-white/70 text-sm transition-all cursor-pointer z-10"
|
|
86
|
+
>
|
|
87
|
+
✕
|
|
88
|
+
</button>
|
|
89
|
+
|
|
90
|
+
<div className="px-5 py-4">
|
|
91
|
+
{/* Header */}
|
|
92
|
+
<div className="flex items-start gap-3 mb-3">
|
|
93
|
+
<div className="text-3xl mt-0.5 flex-shrink-0 animate-bounce" style={{ animationDuration: "2s" }}>
|
|
94
|
+
💎
|
|
95
|
+
</div>
|
|
96
|
+
<div>
|
|
97
|
+
<h2 className="text-base font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-amber-200 via-yellow-300 to-amber-200 tracking-tight leading-tight">
|
|
98
|
+
0.069420 SOL TREASURE HUNT
|
|
99
|
+
</h2>
|
|
100
|
+
<p className="text-[11px] text-amber-300/60 mt-0.5">
|
|
101
|
+
Hidden rewards in city parcels — claim yours first!
|
|
102
|
+
</p>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
{/* Stats grid */}
|
|
107
|
+
<div className="grid grid-cols-4 gap-1.5 mb-3">
|
|
108
|
+
<div className="bg-black/30 rounded-xl px-2 py-2 text-center border border-amber-500/10">
|
|
109
|
+
<p className="text-[7px] uppercase tracking-wider text-amber-400/40 mb-0.5">Per Parcel</p>
|
|
110
|
+
<p className="text-xs font-bold text-amber-200 tabular-nums">0.069420</p>
|
|
111
|
+
<p className="text-[7px] text-amber-400/30">
|
|
112
|
+
{parcelUsd !== null ? `≈ $${parcelUsd.toFixed(2)}` : 'SOL'}
|
|
113
|
+
</p>
|
|
114
|
+
</div>
|
|
115
|
+
<div className="bg-black/30 rounded-xl px-2 py-2 text-center border border-amber-500/10">
|
|
116
|
+
<p className="text-[7px] uppercase tracking-wider text-amber-400/40 mb-0.5">Total</p>
|
|
117
|
+
<p className="text-xs font-bold text-amber-200 tabular-nums">{TOTAL_REWARD_PARCELS}</p>
|
|
118
|
+
<p className="text-[7px] text-amber-400/30">parcels</p>
|
|
119
|
+
</div>
|
|
120
|
+
<div className="bg-black/30 rounded-xl px-2 py-2 text-center border border-amber-500/10">
|
|
121
|
+
<p className="text-[7px] uppercase tracking-wider text-amber-400/40 mb-0.5">Can Fund</p>
|
|
122
|
+
<p className={`text-xs font-bold tabular-nums ${depleted ? 'text-red-400' : 'text-emerald-400'}`}>
|
|
123
|
+
{fundedParcels}
|
|
124
|
+
</p>
|
|
125
|
+
<p className="text-[7px] text-amber-400/30">{depleted ? 'empty' : 'left'}</p>
|
|
126
|
+
</div>
|
|
127
|
+
<div className="bg-black/30 rounded-xl px-2 py-2 text-center border border-amber-500/10">
|
|
128
|
+
<p className="text-[7px] uppercase tracking-wider text-amber-400/40 mb-0.5">Treasury</p>
|
|
129
|
+
<p className="text-xs font-bold text-amber-200 tabular-nums">
|
|
130
|
+
{treasuryBalance !== null ? treasuryBalance.toFixed(4) : '...'}
|
|
131
|
+
</p>
|
|
132
|
+
<p className="text-[7px] text-amber-400/30">
|
|
133
|
+
{treasuryUsd !== null ? `≈ $${treasuryUsd.toFixed(2)}` : 'SOL'}
|
|
134
|
+
</p>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
{/* SOL price ticker */}
|
|
139
|
+
{solPrice !== null && (
|
|
140
|
+
<div className="flex items-center justify-center gap-2 mb-2">
|
|
141
|
+
<span className="text-[8px] text-amber-400/30">SOL/USD</span>
|
|
142
|
+
<span className="text-[10px] font-bold text-amber-200/70 tabular-nums">
|
|
143
|
+
${solPrice.toFixed(2)}
|
|
144
|
+
</span>
|
|
145
|
+
<span className="text-[8px] text-amber-400/20">via Helius</span>
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
148
|
+
|
|
149
|
+
{/* Treasury wallet address */}
|
|
150
|
+
<div className="bg-black/20 rounded-lg px-3 py-1.5 mb-3 border border-amber-500/8 flex items-center gap-2">
|
|
151
|
+
<span className="text-[9px] text-amber-400/40 whitespace-nowrap">Treasury:</span>
|
|
152
|
+
<a
|
|
153
|
+
href={`https://solscan.io/account/${TREASURY_ADDRESS}`}
|
|
154
|
+
target="_blank"
|
|
155
|
+
rel="noopener noreferrer"
|
|
156
|
+
className="text-[9px] text-amber-300/50 hover:text-amber-200/80 font-mono truncate transition-colors"
|
|
157
|
+
>
|
|
158
|
+
{TREASURY_ADDRESS}
|
|
159
|
+
</a>
|
|
160
|
+
<span className="text-[9px]">🔗</span>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
{/* How it works */}
|
|
164
|
+
<div className="bg-black/20 rounded-xl px-3 py-2.5 border border-amber-500/8 mb-3">
|
|
165
|
+
<p className="text-[9px] uppercase tracking-wider text-amber-400/40 mb-1.5 font-medium">How It Works</p>
|
|
166
|
+
<div className="space-y-1 text-[11px] text-amber-200/70">
|
|
167
|
+
<p className="flex items-center gap-2">
|
|
168
|
+
<span className="text-sm">1️⃣</span>
|
|
169
|
+
<span>Connect your Solana wallet</span>
|
|
170
|
+
</p>
|
|
171
|
+
<p className="flex items-center gap-2">
|
|
172
|
+
<span className="text-sm">2️⃣</span>
|
|
173
|
+
<span>Your building is generated in the city</span>
|
|
174
|
+
</p>
|
|
175
|
+
<p className="flex items-center gap-2">
|
|
176
|
+
<span className="text-sm">3️⃣</span>
|
|
177
|
+
<span>If you're in the <strong className="text-amber-200">first {TOTAL_REWARD_PARCELS}</strong>, your parcel has hidden SOL!</span>
|
|
178
|
+
</p>
|
|
179
|
+
<p className="flex items-center gap-2">
|
|
180
|
+
<span className="text-sm">4️⃣</span>
|
|
181
|
+
<span>Click <strong className="text-amber-200">"Claim Reward"</strong> to receive 0.069420 SOL</span>
|
|
182
|
+
</p>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
{/* CTA */}
|
|
187
|
+
{!walletConnected ? (
|
|
188
|
+
<div className="text-center">
|
|
189
|
+
<p className="text-xs text-amber-300/80 font-medium animate-pulse">
|
|
190
|
+
⚡ Connect your wallet to check if you qualify! ⚡
|
|
191
|
+
</p>
|
|
192
|
+
</div>
|
|
193
|
+
) : depleted ? (
|
|
194
|
+
<div className="text-center py-1 bg-red-500/10 rounded-xl border border-red-500/10">
|
|
195
|
+
<p className="text-xs text-red-300/70 font-medium">
|
|
196
|
+
🚫 Treasury depleted — all rewards have been claimed!
|
|
197
|
+
</p>
|
|
198
|
+
</div>
|
|
199
|
+
) : allClaimed ? (
|
|
200
|
+
<div className="text-center py-1 bg-red-500/10 rounded-xl border border-red-500/10">
|
|
201
|
+
<p className="text-xs text-red-300/70 font-medium">
|
|
202
|
+
🚫 All {TOTAL_REWARD_PARCELS} parcel slots filled!
|
|
203
|
+
</p>
|
|
204
|
+
</div>
|
|
205
|
+
) : (
|
|
206
|
+
<div className="text-center">
|
|
207
|
+
<p className="text-xs text-emerald-300/80 font-medium">
|
|
208
|
+
✅ {fundedParcels} reward{fundedParcels !== 1 ? 's' : ''} still available — claim your parcel!
|
|
209
|
+
</p>
|
|
210
|
+
</div>
|
|
211
|
+
)}
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
{/* Progress bar */}
|
|
215
|
+
<div className="h-1 bg-black/30">
|
|
216
|
+
<div
|
|
217
|
+
className="h-full transition-all duration-1000"
|
|
218
|
+
style={{
|
|
219
|
+
width: `${treasuryBalance !== null
|
|
220
|
+
? Math.max(0, 100 - (treasuryBalance / (TOTAL_REWARD_PARCELS * REWARD_PER_PARCEL)) * 100)
|
|
221
|
+
: 0}%`,
|
|
222
|
+
background: depleted
|
|
223
|
+
? "linear-gradient(90deg, #ef4444, #f87171)"
|
|
224
|
+
: "linear-gradient(90deg, #f59e0b, #eab308, #f59e0b)",
|
|
225
|
+
}}
|
|
226
|
+
/>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
<style jsx>{`
|
|
231
|
+
@keyframes fadeSlideDown {
|
|
232
|
+
from { opacity: 0; transform: translate(-50%, -20px); }
|
|
233
|
+
to { opacity: 1; transform: translate(-50%, 0); }
|
|
234
|
+
}
|
|
235
|
+
@keyframes shimmer {
|
|
236
|
+
0%, 100% { transform: translateX(-100%) rotate(12deg); }
|
|
237
|
+
50% { transform: translateX(200%) rotate(12deg); }
|
|
238
|
+
}
|
|
239
|
+
`}</style>
|
|
240
|
+
</div>
|
|
241
|
+
);
|
|
242
|
+
}
|