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,68 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { SlotCounts } from "@/lib/city-slots";
|
|
4
|
+
|
|
5
|
+
interface CitySlotsBadgeProps {
|
|
6
|
+
counts: SlotCounts;
|
|
7
|
+
loading?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function CitySlotsBadge({ counts, loading }: CitySlotsBadgeProps) {
|
|
11
|
+
if (loading) {
|
|
12
|
+
return (
|
|
13
|
+
<div className="flex items-center gap-2 mt-1">
|
|
14
|
+
<span className="text-sm text-white/40">Loading...</span>
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="flex flex-col gap-1.5 mt-1.5">
|
|
21
|
+
{/* Total wallets */}
|
|
22
|
+
<div className="flex items-center gap-2">
|
|
23
|
+
<div className="flex items-center gap-1.5">
|
|
24
|
+
<div className="w-2 h-2 rounded-full bg-[#E35930]" />
|
|
25
|
+
<span className="text-[11px] text-white/50 font-medium">
|
|
26
|
+
{counts.totalWallets.toLocaleString()} / 1,000
|
|
27
|
+
</span>
|
|
28
|
+
<span className="text-[9px] text-white/25">wallets</span>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
{/* Slots bar */}
|
|
33
|
+
<div className="flex gap-1 items-center">
|
|
34
|
+
{/* Planes */}
|
|
35
|
+
<div className="flex items-center gap-1" title={`${counts.planesUsed} / 100 planes claimed`}>
|
|
36
|
+
<span className="text-[10px]">✈</span>
|
|
37
|
+
<div className="w-12 h-1.5 bg-white/[0.06] rounded-full overflow-hidden">
|
|
38
|
+
<div
|
|
39
|
+
className="h-full rounded-full transition-all duration-700"
|
|
40
|
+
style={{
|
|
41
|
+
width: `${Math.min(counts.planesUsed / 100 * 100, 100)}%`,
|
|
42
|
+
background: "linear-gradient(90deg, #E35930, #f59e0b)",
|
|
43
|
+
}}
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
<span className="text-[9px] text-white/30 tabular-nums">{counts.planesUsed}</span>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div className="w-px h-3 bg-white/[0.08]" />
|
|
50
|
+
|
|
51
|
+
{/* Cars */}
|
|
52
|
+
<div className="flex items-center gap-1" title={`${counts.carsUsed} / 250 cars claimed`}>
|
|
53
|
+
<span className="text-[10px]">🚗</span>
|
|
54
|
+
<div className="w-12 h-1.5 bg-white/[0.06] rounded-full overflow-hidden">
|
|
55
|
+
<div
|
|
56
|
+
className="h-full rounded-full transition-all duration-700"
|
|
57
|
+
style={{
|
|
58
|
+
width: `${Math.min(counts.carsUsed / 250 * 100, 100)}%`,
|
|
59
|
+
background: "linear-gradient(90deg, #6366f1, #8b5cf6)",
|
|
60
|
+
}}
|
|
61
|
+
/>
|
|
62
|
+
</div>
|
|
63
|
+
<span className="text-[9px] text-white/30 tabular-nums">{counts.carsUsed}</span>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef, MutableRefObject } from "react";
|
|
4
|
+
|
|
5
|
+
interface CockpitHUDProps {
|
|
6
|
+
active: boolean;
|
|
7
|
+
mode?: "car" | "plane";
|
|
8
|
+
positionRef: MutableRefObject<[number, number, number] | null>;
|
|
9
|
+
headingRef: MutableRefObject<number>;
|
|
10
|
+
pitchRef?: MutableRefObject<number>;
|
|
11
|
+
speedRef?: MutableRefObject<number>;
|
|
12
|
+
walletAddress: string | null;
|
|
13
|
+
hp?: number;
|
|
14
|
+
maxHP?: number;
|
|
15
|
+
destroyed?: boolean;
|
|
16
|
+
respawnRemaining?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface SolanaData {
|
|
20
|
+
solPrice: number | null;
|
|
21
|
+
tps: number | null;
|
|
22
|
+
blockHeight: number | null;
|
|
23
|
+
epoch: number | null;
|
|
24
|
+
walletBalance: number | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Convert radians to degrees, 0-360 range */
|
|
28
|
+
function toDeg(rad: number): number {
|
|
29
|
+
return ((rad * 180) / Math.PI + 360) % 360;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default function CockpitHUD({
|
|
33
|
+
active,
|
|
34
|
+
mode = "plane",
|
|
35
|
+
positionRef,
|
|
36
|
+
headingRef,
|
|
37
|
+
pitchRef,
|
|
38
|
+
speedRef,
|
|
39
|
+
walletAddress,
|
|
40
|
+
hp = 100,
|
|
41
|
+
maxHP = 100,
|
|
42
|
+
destroyed = false,
|
|
43
|
+
respawnRemaining = 0,
|
|
44
|
+
}: CockpitHUDProps) {
|
|
45
|
+
const [flightData, setFlightData] = useState({
|
|
46
|
+
speed: 0,
|
|
47
|
+
altitude: 0,
|
|
48
|
+
heading: 0,
|
|
49
|
+
pitch: 0,
|
|
50
|
+
x: 0,
|
|
51
|
+
z: 0,
|
|
52
|
+
});
|
|
53
|
+
const [solana, setSolana] = useState<SolanaData>({
|
|
54
|
+
solPrice: null,
|
|
55
|
+
tps: null,
|
|
56
|
+
blockHeight: null,
|
|
57
|
+
epoch: null,
|
|
58
|
+
walletBalance: null,
|
|
59
|
+
});
|
|
60
|
+
const animRef = useRef<number>(0);
|
|
61
|
+
const tickRef = useRef(0);
|
|
62
|
+
|
|
63
|
+
// Fetch Solana network data
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (!active) return;
|
|
66
|
+
|
|
67
|
+
async function fetchSolanaData() {
|
|
68
|
+
try {
|
|
69
|
+
// Fetch treasury for SOL price
|
|
70
|
+
const [treasuryRes, networkRes] = await Promise.allSettled([
|
|
71
|
+
fetch("/api/treasury"),
|
|
72
|
+
fetch("/api/network-stats"),
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
const updates: Partial<SolanaData> = {};
|
|
76
|
+
|
|
77
|
+
if (treasuryRes.status === "fulfilled" && treasuryRes.value.ok) {
|
|
78
|
+
const data = await treasuryRes.value.json();
|
|
79
|
+
updates.solPrice = data.solPrice ?? null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (networkRes.status === "fulfilled" && networkRes.value.ok) {
|
|
83
|
+
const data = await networkRes.value.json();
|
|
84
|
+
updates.tps = data.tps ?? null;
|
|
85
|
+
updates.blockHeight = data.blockHeight ?? null;
|
|
86
|
+
updates.epoch = data.epoch ?? null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Get wallet balance if connected
|
|
90
|
+
if (walletAddress) {
|
|
91
|
+
try {
|
|
92
|
+
const balRes = await fetch(`/api/wallet/${walletAddress}/balances`);
|
|
93
|
+
if (balRes.ok) {
|
|
94
|
+
const balData = await balRes.json();
|
|
95
|
+
updates.walletBalance = balData.nativeBalance ?? balData.sol ?? null;
|
|
96
|
+
}
|
|
97
|
+
} catch { /* ignore */ }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
setSolana(prev => ({ ...prev, ...updates }));
|
|
101
|
+
} catch { /* silent */ }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
fetchSolanaData();
|
|
105
|
+
const interval = setInterval(fetchSolanaData, 15000);
|
|
106
|
+
return () => clearInterval(interval);
|
|
107
|
+
}, [active, walletAddress]);
|
|
108
|
+
|
|
109
|
+
// Update flight data at 30fps
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (!active) return;
|
|
112
|
+
|
|
113
|
+
function update() {
|
|
114
|
+
tickRef.current++;
|
|
115
|
+
const pos = positionRef.current;
|
|
116
|
+
if (pos) {
|
|
117
|
+
setFlightData({
|
|
118
|
+
speed: speedRef ? Math.round(speedRef.current) : Math.round(pos[1] > 0 ? 30 : 0),
|
|
119
|
+
altitude: Math.round(pos[1]),
|
|
120
|
+
heading: Math.round(toDeg(headingRef.current)),
|
|
121
|
+
pitch: pitchRef ? Math.round(toDeg(pitchRef.current)) : 0,
|
|
122
|
+
x: Math.round(pos[0]),
|
|
123
|
+
z: Math.round(pos[2]),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
animRef.current = requestAnimationFrame(update);
|
|
127
|
+
}
|
|
128
|
+
animRef.current = requestAnimationFrame(update);
|
|
129
|
+
return () => cancelAnimationFrame(animRef.current);
|
|
130
|
+
}, [active, positionRef, headingRef, pitchRef]);
|
|
131
|
+
|
|
132
|
+
if (!active) return null;
|
|
133
|
+
|
|
134
|
+
const { heading, altitude, pitch } = flightData;
|
|
135
|
+
const speed = Math.abs(flightData.speed);
|
|
136
|
+
const hpPercent = maxHP > 0 ? (hp / maxHP) * 100 : 0;
|
|
137
|
+
const hpColor = hpPercent > 60 ? "#22c55e" : hpPercent > 30 ? "#f59e0b" : "#ef4444";
|
|
138
|
+
const respawnMins = Math.ceil(respawnRemaining / 60000);
|
|
139
|
+
const respawnSecs = Math.ceil((respawnRemaining % 60000) / 1000);
|
|
140
|
+
const isCar = mode === "car";
|
|
141
|
+
|
|
142
|
+
// Destroyed overlay
|
|
143
|
+
if (destroyed) {
|
|
144
|
+
return (
|
|
145
|
+
<div className="fixed inset-0 z-[60] flex items-center justify-center pointer-events-none">
|
|
146
|
+
<div className="absolute inset-0 bg-red-950/40" />
|
|
147
|
+
<div className="absolute inset-0" style={{
|
|
148
|
+
backgroundImage: "radial-gradient(ellipse at center, transparent 40%, rgba(200,0,0,0.15) 100%)",
|
|
149
|
+
}} />
|
|
150
|
+
<div className="relative text-center">
|
|
151
|
+
<div className="text-6xl mb-4 animate-pulse">💥</div>
|
|
152
|
+
<h2 className="text-3xl font-black text-red-400 mb-2 tracking-wider uppercase">
|
|
153
|
+
{isCar ? "Car Destroyed" : "Plane Down"}
|
|
154
|
+
</h2>
|
|
155
|
+
<p className="text-sm text-white/40 mb-6">You were taken out in combat</p>
|
|
156
|
+
{respawnRemaining > 0 ? (
|
|
157
|
+
<div className="bg-black/70 backdrop-blur-md border border-red-500/20 rounded-2xl px-8 py-4 inline-block">
|
|
158
|
+
<p className="text-[9px] uppercase tracking-[0.2em] text-red-400/50 mb-1">Respawn In</p>
|
|
159
|
+
<p className="text-4xl font-mono font-black text-red-400 tabular-nums">
|
|
160
|
+
{respawnMins}:{String(respawnSecs % 60).padStart(2, "0")}
|
|
161
|
+
</p>
|
|
162
|
+
<p className="text-[10px] text-white/20 mt-1">Press ESC to exit</p>
|
|
163
|
+
</div>
|
|
164
|
+
) : (
|
|
165
|
+
<div className="bg-emerald-500/10 border border-emerald-500/20 rounded-2xl px-8 py-4 inline-block pointer-events-auto">
|
|
166
|
+
<p className="text-lg font-bold text-emerald-400">Ready to Respawn!</p>
|
|
167
|
+
<p className="text-[10px] text-emerald-400/40 mt-0.5">Re-enter {isCar ? "Drive" : "Flight"} mode</p>
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<>
|
|
177
|
+
{/* ─── Cockpit Frame ─── */}
|
|
178
|
+
<div className="fixed inset-0 z-[45] pointer-events-none">
|
|
179
|
+
{/* Top cockpit bar — mimics windshield frame */}
|
|
180
|
+
<div
|
|
181
|
+
className="absolute top-0 left-0 right-0 h-8"
|
|
182
|
+
style={{
|
|
183
|
+
background: "linear-gradient(180deg, rgba(20,20,25,0.95) 0%, rgba(20,20,25,0.6) 60%, transparent 100%)",
|
|
184
|
+
}}
|
|
185
|
+
/>
|
|
186
|
+
{/* Bottom cockpit dashboard */}
|
|
187
|
+
<div
|
|
188
|
+
className="absolute bottom-0 left-0 right-0 h-40 sm:h-48"
|
|
189
|
+
style={{
|
|
190
|
+
background: "linear-gradient(0deg, rgba(15,15,20,0.98) 0%, rgba(15,15,20,0.85) 40%, rgba(15,15,20,0.4) 70%, transparent 100%)",
|
|
191
|
+
}}
|
|
192
|
+
/>
|
|
193
|
+
{/* Left pillar */}
|
|
194
|
+
<div
|
|
195
|
+
className="absolute top-0 bottom-0 left-0 w-12 sm:w-20"
|
|
196
|
+
style={{
|
|
197
|
+
background: "linear-gradient(90deg, rgba(15,15,20,0.9) 0%, rgba(15,15,20,0.3) 70%, transparent 100%)",
|
|
198
|
+
}}
|
|
199
|
+
/>
|
|
200
|
+
{/* Right pillar */}
|
|
201
|
+
<div
|
|
202
|
+
className="absolute top-0 bottom-0 right-0 w-12 sm:w-20"
|
|
203
|
+
style={{
|
|
204
|
+
background: "linear-gradient(-90deg, rgba(15,15,20,0.9) 0%, rgba(15,15,20,0.3) 70%, transparent 100%)",
|
|
205
|
+
}}
|
|
206
|
+
/>
|
|
207
|
+
|
|
208
|
+
{/* Subtle HUD scan lines */}
|
|
209
|
+
<div
|
|
210
|
+
className="absolute inset-0 opacity-[0.03]"
|
|
211
|
+
style={{
|
|
212
|
+
backgroundImage: "repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(255,255,255,0.1) 2px, rgba(255,255,255,0.1) 4px)",
|
|
213
|
+
}}
|
|
214
|
+
/>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
{/* ─── HP Bar (top left) ─── */}
|
|
218
|
+
<div className="fixed top-14 left-4 sm:left-6 z-[46] pointer-events-none">
|
|
219
|
+
<div className="bg-black/60 backdrop-blur-md border border-white/[0.06] rounded-xl px-3 py-2 min-w-[120px]">
|
|
220
|
+
<div className="flex items-center justify-between mb-1">
|
|
221
|
+
<span className="text-[8px] uppercase tracking-[0.15em] text-white/30 font-bold">{isCar ? "🚗 HULL" : "✈ HULL"}</span>
|
|
222
|
+
<span className="text-[10px] font-mono font-bold tabular-nums" style={{ color: hpColor }}>
|
|
223
|
+
{hp}/{maxHP}
|
|
224
|
+
</span>
|
|
225
|
+
</div>
|
|
226
|
+
<div className="h-2 rounded-full bg-white/[0.06] overflow-hidden">
|
|
227
|
+
<div
|
|
228
|
+
className="h-full rounded-full transition-all duration-300 ease-out"
|
|
229
|
+
style={{
|
|
230
|
+
width: `${hpPercent}%`,
|
|
231
|
+
backgroundColor: hpColor,
|
|
232
|
+
boxShadow: `0 0 8px ${hpColor}40`,
|
|
233
|
+
}}
|
|
234
|
+
/>
|
|
235
|
+
</div>
|
|
236
|
+
{hpPercent <= 30 && (
|
|
237
|
+
<p className="text-[8px] text-red-400/60 mt-1 animate-pulse font-bold uppercase text-center">! Critical Damage !</p>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
{/* ─── Primary Flight Display (Center Bottom) ─── */}
|
|
243
|
+
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-[46] pointer-events-none">
|
|
244
|
+
<div className="flex items-end gap-4 sm:gap-6">
|
|
245
|
+
{/* Speed tape */}
|
|
246
|
+
<div className="flex flex-col items-center">
|
|
247
|
+
<div className="bg-black/70 backdrop-blur-md border border-[#E35930]/20 rounded-xl px-3 py-2 min-w-[70px]">
|
|
248
|
+
<p className="text-[7px] uppercase tracking-[0.2em] text-[#E35930]/50 text-center mb-0.5">SPD</p>
|
|
249
|
+
<p className="text-xl font-mono font-bold text-[#E35930] text-center tabular-nums">{speed}</p>
|
|
250
|
+
<p className="text-[8px] text-white/20 text-center">km/h</p>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
{/* Artificial horizon / Attitude indicator */}
|
|
255
|
+
<div className="relative">
|
|
256
|
+
<div className="bg-black/70 backdrop-blur-md border border-white/10 rounded-2xl overflow-hidden" style={{ width: 160, height: 100 }}>
|
|
257
|
+
{/* Sky/ground split based on pitch */}
|
|
258
|
+
<div
|
|
259
|
+
className="absolute inset-0 transition-transform duration-200"
|
|
260
|
+
style={{
|
|
261
|
+
transform: `translateY(${Math.min(30, Math.max(-30, pitch * 0.8))}px)`,
|
|
262
|
+
}}
|
|
263
|
+
>
|
|
264
|
+
<div className="absolute top-0 left-0 right-0 h-1/2" style={{ background: "linear-gradient(180deg, #1a3a5c, #2d5a87)" }} />
|
|
265
|
+
<div className="absolute bottom-0 left-0 right-0 h-1/2" style={{ background: "linear-gradient(0deg, #3d2a1a, #5c4a2a)" }} />
|
|
266
|
+
{/* Horizon line */}
|
|
267
|
+
<div className="absolute left-0 right-0 top-1/2 -translate-y-px h-px bg-white/60" />
|
|
268
|
+
{/* Pitch ladder marks */}
|
|
269
|
+
{[-20, -10, 10, 20].map(deg => (
|
|
270
|
+
<div
|
|
271
|
+
key={deg}
|
|
272
|
+
className="absolute left-1/4 right-1/4 h-px bg-white/20"
|
|
273
|
+
style={{ top: `${50 - deg * 1.5}%` }}
|
|
274
|
+
>
|
|
275
|
+
<span className="absolute -top-2 -left-4 text-[7px] text-white/30 font-mono">{Math.abs(deg)}</span>
|
|
276
|
+
</div>
|
|
277
|
+
))}
|
|
278
|
+
</div>
|
|
279
|
+
{/* Fixed aircraft symbol */}
|
|
280
|
+
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-10">
|
|
281
|
+
<div className="w-8 h-px bg-[#E35930]" />
|
|
282
|
+
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-px h-2 bg-[#E35930]" />
|
|
283
|
+
<div className="absolute top-0 -left-4 w-3 h-px bg-[#E35930]" />
|
|
284
|
+
<div className="absolute top-0 right-[-12px] w-3 h-px bg-[#E35930]" />
|
|
285
|
+
</div>
|
|
286
|
+
{/* Heading */}
|
|
287
|
+
<div className="absolute bottom-1 left-1/2 -translate-x-1/2 bg-black/60 rounded px-2 py-0.5">
|
|
288
|
+
<span className="text-[10px] font-mono font-bold text-emerald-400 tabular-nums">{heading}°</span>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
{/* Altitude tape */}
|
|
294
|
+
<div className="flex flex-col items-center">
|
|
295
|
+
<div className="bg-black/70 backdrop-blur-md border border-emerald-500/20 rounded-xl px-3 py-2 min-w-[70px]">
|
|
296
|
+
<p className="text-[7px] uppercase tracking-[0.2em] text-emerald-400/50 text-center mb-0.5">ALT</p>
|
|
297
|
+
<p className="text-xl font-mono font-bold text-emerald-400 text-center tabular-nums">{altitude}</p>
|
|
298
|
+
<p className="text-[8px] text-white/20 text-center">m</p>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
{/* ─── Solana Data Panel (Left side) ─── */}
|
|
305
|
+
<div className="fixed bottom-28 left-4 sm:left-6 z-[46] pointer-events-none">
|
|
306
|
+
<div className="bg-black/60 backdrop-blur-md border border-purple-500/15 rounded-2xl px-3.5 py-3 min-w-[140px]">
|
|
307
|
+
<div className="flex items-center gap-2 mb-2.5">
|
|
308
|
+
<div className="w-1.5 h-1.5 rounded-full bg-purple-400 animate-pulse" />
|
|
309
|
+
<span className="text-[8px] uppercase tracking-[0.2em] text-purple-300/60 font-bold">Solana Network</span>
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
<div className="space-y-2">
|
|
313
|
+
{/* SOL Price */}
|
|
314
|
+
<div className="flex items-center justify-between gap-4">
|
|
315
|
+
<span className="text-[9px] text-white/30">SOL/USD</span>
|
|
316
|
+
<span className="text-xs font-mono font-bold text-white/80 tabular-nums">
|
|
317
|
+
{solana.solPrice !== null ? `$${solana.solPrice.toFixed(2)}` : "—"}
|
|
318
|
+
</span>
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
{/* TPS */}
|
|
322
|
+
<div className="flex items-center justify-between gap-4">
|
|
323
|
+
<span className="text-[9px] text-white/30">TPS</span>
|
|
324
|
+
<span className="text-xs font-mono font-bold text-emerald-400/80 tabular-nums">
|
|
325
|
+
{solana.tps !== null ? solana.tps.toLocaleString() : "—"}
|
|
326
|
+
</span>
|
|
327
|
+
</div>
|
|
328
|
+
|
|
329
|
+
{/* Block Height */}
|
|
330
|
+
<div className="flex items-center justify-between gap-4">
|
|
331
|
+
<span className="text-[9px] text-white/30">Block</span>
|
|
332
|
+
<span className="text-[10px] font-mono text-white/50 tabular-nums">
|
|
333
|
+
{solana.blockHeight !== null ? `#${solana.blockHeight.toLocaleString()}` : "—"}
|
|
334
|
+
</span>
|
|
335
|
+
</div>
|
|
336
|
+
|
|
337
|
+
{/* Epoch */}
|
|
338
|
+
<div className="flex items-center justify-between gap-4">
|
|
339
|
+
<span className="text-[9px] text-white/30">Epoch</span>
|
|
340
|
+
<span className="text-[10px] font-mono text-white/50 tabular-nums">
|
|
341
|
+
{solana.epoch !== null ? solana.epoch : "—"}
|
|
342
|
+
</span>
|
|
343
|
+
</div>
|
|
344
|
+
|
|
345
|
+
{/* Wallet balance */}
|
|
346
|
+
{walletAddress && (
|
|
347
|
+
<>
|
|
348
|
+
<div className="h-px bg-white/[0.06] my-1" />
|
|
349
|
+
<div className="flex items-center justify-between gap-4">
|
|
350
|
+
<span className="text-[9px] text-white/30">Wallet</span>
|
|
351
|
+
<span className="text-xs font-mono font-bold text-amber-300/80 tabular-nums">
|
|
352
|
+
{solana.walletBalance !== null
|
|
353
|
+
? `${solana.walletBalance.toFixed(4)} SOL`
|
|
354
|
+
: "—"}
|
|
355
|
+
</span>
|
|
356
|
+
</div>
|
|
357
|
+
</>
|
|
358
|
+
)}
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
|
|
363
|
+
{/* ─── Compass Rose (Top center) ─── */}
|
|
364
|
+
<div className="fixed top-12 left-1/2 -translate-x-1/2 z-[46] pointer-events-none">
|
|
365
|
+
<div className="bg-black/50 backdrop-blur-md border border-white/[0.08] rounded-xl px-4 py-1.5 flex items-center gap-1">
|
|
366
|
+
{/* Compass tape */}
|
|
367
|
+
{(() => {
|
|
368
|
+
const marks = [];
|
|
369
|
+
const dirs: Record<number, string> = { 0: "N", 45: "NE", 90: "E", 135: "SE", 180: "S", 225: "SW", 270: "W", 315: "NW" };
|
|
370
|
+
for (let offset = -40; offset <= 40; offset += 10) {
|
|
371
|
+
const deg = ((heading + offset) % 360 + 360) % 360;
|
|
372
|
+
const roundDeg = Math.round(deg / 10) * 10;
|
|
373
|
+
const label = dirs[roundDeg];
|
|
374
|
+
const isCenter = offset === 0;
|
|
375
|
+
marks.push(
|
|
376
|
+
<div
|
|
377
|
+
key={offset}
|
|
378
|
+
className="flex flex-col items-center"
|
|
379
|
+
style={{ minWidth: 20, opacity: isCenter ? 1 : 0.3 + (1 - Math.abs(offset) / 40) * 0.3 }}
|
|
380
|
+
>
|
|
381
|
+
{label ? (
|
|
382
|
+
<span className={`text-[9px] font-bold ${isCenter ? "text-[#E35930]" : "text-white/40"}`}>
|
|
383
|
+
{label}
|
|
384
|
+
</span>
|
|
385
|
+
) : (
|
|
386
|
+
<span className="text-[8px] font-mono text-white/20">{roundDeg}</span>
|
|
387
|
+
)}
|
|
388
|
+
<div className={`w-px h-1.5 mt-0.5 ${isCenter ? "bg-[#E35930]" : "bg-white/15"}`} />
|
|
389
|
+
</div>
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
return marks;
|
|
393
|
+
})()}
|
|
394
|
+
</div>
|
|
395
|
+
{/* Center marker triangle */}
|
|
396
|
+
<div className="absolute -bottom-1 left-1/2 -translate-x-1/2 w-0 h-0 border-l-[4px] border-r-[4px] border-t-[4px] border-l-transparent border-r-transparent border-t-[#E35930]" />
|
|
397
|
+
</div>
|
|
398
|
+
|
|
399
|
+
{/* ─── Position info (Right side) ─── */}
|
|
400
|
+
<div className="fixed bottom-28 right-4 sm:right-6 z-[46] pointer-events-none">
|
|
401
|
+
<div className="bg-black/60 backdrop-blur-md border border-white/[0.06] rounded-2xl px-3.5 py-3 min-w-[120px]">
|
|
402
|
+
<div className="flex items-center gap-2 mb-2.5">
|
|
403
|
+
<div className="w-1.5 h-1.5 rounded-full bg-[#E35930]" />
|
|
404
|
+
<span className="text-[8px] uppercase tracking-[0.2em] text-[#E35930]/60 font-bold">Position</span>
|
|
405
|
+
</div>
|
|
406
|
+
<div className="space-y-1.5">
|
|
407
|
+
<div className="flex items-center justify-between gap-4">
|
|
408
|
+
<span className="text-[9px] text-white/30">X</span>
|
|
409
|
+
<span className="text-[10px] font-mono text-white/50 tabular-nums">{flightData.x}</span>
|
|
410
|
+
</div>
|
|
411
|
+
<div className="flex items-center justify-between gap-4">
|
|
412
|
+
<span className="text-[9px] text-white/30">Z</span>
|
|
413
|
+
<span className="text-[10px] font-mono text-white/50 tabular-nums">{flightData.z}</span>
|
|
414
|
+
</div>
|
|
415
|
+
<div className="flex items-center justify-between gap-4">
|
|
416
|
+
<span className="text-[9px] text-white/30">HDG</span>
|
|
417
|
+
<span className="text-[10px] font-mono text-emerald-400/60 tabular-nums">{heading}°</span>
|
|
418
|
+
</div>
|
|
419
|
+
{walletAddress && (
|
|
420
|
+
<>
|
|
421
|
+
<div className="h-px bg-white/[0.06] my-1" />
|
|
422
|
+
<div className="text-[8px] text-white/20 font-mono truncate max-w-[110px]">
|
|
423
|
+
{walletAddress.slice(0, 6)}...{walletAddress.slice(-4)}
|
|
424
|
+
</div>
|
|
425
|
+
</>
|
|
426
|
+
)}
|
|
427
|
+
</div>
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
430
|
+
|
|
431
|
+
{/* ─── Center crosshair ─── */}
|
|
432
|
+
<div className="fixed inset-0 z-[45] pointer-events-none flex items-center justify-center">
|
|
433
|
+
<div className="relative" style={{ width: 60, height: 60 }}>
|
|
434
|
+
{/* Outer ring */}
|
|
435
|
+
<div className="absolute inset-0 border border-white/10 rounded-full" />
|
|
436
|
+
{/* Cross lines */}
|
|
437
|
+
<div className="absolute top-1/2 left-0 w-[18px] h-px bg-[#E35930]/50" />
|
|
438
|
+
<div className="absolute top-1/2 right-0 w-[18px] h-px bg-[#E35930]/50" />
|
|
439
|
+
<div className="absolute left-1/2 top-0 w-px h-[18px] bg-[#E35930]/50" />
|
|
440
|
+
<div className="absolute left-1/2 bottom-0 w-px h-[18px] bg-[#E35930]/50" />
|
|
441
|
+
{/* Center dot */}
|
|
442
|
+
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-1.5 h-1.5 rounded-full bg-[#E35930]" />
|
|
443
|
+
{/* Corner brackets */}
|
|
444
|
+
<div className="absolute top-2 left-2 w-3 h-3 border-t border-l border-[#E35930]/30 rounded-tl" />
|
|
445
|
+
<div className="absolute top-2 right-2 w-3 h-3 border-t border-r border-[#E35930]/30 rounded-tr" />
|
|
446
|
+
<div className="absolute bottom-2 left-2 w-3 h-3 border-b border-l border-[#E35930]/30 rounded-bl" />
|
|
447
|
+
<div className="absolute bottom-2 right-2 w-3 h-3 border-b border-r border-[#E35930]/30 rounded-br" />
|
|
448
|
+
</div>
|
|
449
|
+
</div>
|
|
450
|
+
|
|
451
|
+
{/* ─── Cockpit HUD style overlays ─── */}
|
|
452
|
+
<style jsx>{`
|
|
453
|
+
@keyframes hudGlow {
|
|
454
|
+
0%, 100% { opacity: 0.6; }
|
|
455
|
+
50% { opacity: 1; }
|
|
456
|
+
}
|
|
457
|
+
`}</style>
|
|
458
|
+
</>
|
|
459
|
+
);
|
|
460
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ConvexProvider, ConvexReactClient } from "convex/react";
|
|
4
|
+
import { ReactNode, useMemo } from "react";
|
|
5
|
+
|
|
6
|
+
export default function ConvexWrapper({ children }: { children: ReactNode }) {
|
|
7
|
+
const client = useMemo(() => {
|
|
8
|
+
const url = process.env.NEXT_PUBLIC_CONVEX_URL;
|
|
9
|
+
if (!url) {
|
|
10
|
+
console.warn("[Convex] NEXT_PUBLIC_CONVEX_URL not set");
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
return new ConvexReactClient(url);
|
|
14
|
+
}, []);
|
|
15
|
+
|
|
16
|
+
if (!client) return <>{children}</>;
|
|
17
|
+
|
|
18
|
+
return <ConvexProvider client={client}>{children}</ConvexProvider>;
|
|
19
|
+
}
|