solanapolis 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/README.md +518 -0
  2. package/bin/solanapolis.js +197 -0
  3. package/convex/_generated/api.d.ts +175 -0
  4. package/convex/_generated/api.js +23 -0
  5. package/convex/_generated/dataModel.d.ts +60 -0
  6. package/convex/_generated/server.d.ts +143 -0
  7. package/convex/_generated/server.js +93 -0
  8. package/convex/agent/conversation.ts +352 -0
  9. package/convex/agent/embeddingsCache.ts +110 -0
  10. package/convex/agent/memory.ts +450 -0
  11. package/convex/agent/schema.ts +53 -0
  12. package/convex/aiChat.ts +54 -0
  13. package/convex/aiTown/agent.ts +382 -0
  14. package/convex/aiTown/agentDescription.ts +27 -0
  15. package/convex/aiTown/agentInputs.ts +155 -0
  16. package/convex/aiTown/agentOperations.ts +178 -0
  17. package/convex/aiTown/conversation.ts +395 -0
  18. package/convex/aiTown/conversationMembership.ts +38 -0
  19. package/convex/aiTown/game.ts +371 -0
  20. package/convex/aiTown/ids.ts +32 -0
  21. package/convex/aiTown/inputHandler.ts +9 -0
  22. package/convex/aiTown/inputs.ts +25 -0
  23. package/convex/aiTown/insertInput.ts +20 -0
  24. package/convex/aiTown/location.ts +32 -0
  25. package/convex/aiTown/main.ts +154 -0
  26. package/convex/aiTown/movement.ts +189 -0
  27. package/convex/aiTown/player.ts +310 -0
  28. package/convex/aiTown/playerDescription.ts +35 -0
  29. package/convex/aiTown/schema.ts +79 -0
  30. package/convex/aiTown/world.ts +65 -0
  31. package/convex/aiTown/worldMap.ts +74 -0
  32. package/convex/chat.ts +79 -0
  33. package/convex/constants.ts +78 -0
  34. package/convex/convex.config.ts +6 -0
  35. package/convex/crons.ts +89 -0
  36. package/convex/engine/abstractGame.ts +199 -0
  37. package/convex/engine/historicalObject.ts +355 -0
  38. package/convex/engine/schema.ts +56 -0
  39. package/convex/http.ts +36 -0
  40. package/convex/init.ts +110 -0
  41. package/convex/messages.ts +53 -0
  42. package/convex/npcCarAgents.ts +415 -0
  43. package/convex/schema.ts +61 -0
  44. package/convex/streaming.ts +23 -0
  45. package/convex/testing.ts +202 -0
  46. package/convex/tsconfig.json +18 -0
  47. package/convex/util/FastIntegerCompression.ts +221 -0
  48. package/convex/util/assertNever.ts +4 -0
  49. package/convex/util/asyncMap.ts +20 -0
  50. package/convex/util/compression.ts +71 -0
  51. package/convex/util/geometry.ts +132 -0
  52. package/convex/util/isSimpleObject.ts +11 -0
  53. package/convex/util/llm.ts +724 -0
  54. package/convex/util/minheap.ts +38 -0
  55. package/convex/util/object.ts +22 -0
  56. package/convex/util/sleep.ts +3 -0
  57. package/convex/util/types.ts +33 -0
  58. package/convex/util/xxhash.ts +228 -0
  59. package/convex/world.ts +257 -0
  60. package/data/animations/campfire.json +45 -0
  61. package/data/animations/gentlesparkle.json +37 -0
  62. package/data/animations/gentlesplash.json +61 -0
  63. package/data/animations/gentlewaterfall.json +61 -0
  64. package/data/animations/windmill.json +78 -0
  65. package/data/characters.ts +121 -0
  66. package/data/convertMap.js +74 -0
  67. package/data/gentle.js +330 -0
  68. package/data/spritesheets/f1.ts +75 -0
  69. package/data/spritesheets/f2.ts +75 -0
  70. package/data/spritesheets/f3.ts +75 -0
  71. package/data/spritesheets/f4.ts +75 -0
  72. package/data/spritesheets/f5.ts +75 -0
  73. package/data/spritesheets/f6.ts +75 -0
  74. package/data/spritesheets/f7.ts +75 -0
  75. package/data/spritesheets/f8.ts +75 -0
  76. package/data/spritesheets/p1.ts +59 -0
  77. package/data/spritesheets/p2.ts +59 -0
  78. package/data/spritesheets/p3.ts +59 -0
  79. package/data/spritesheets/player.ts +59 -0
  80. package/data/spritesheets/types.ts +26 -0
  81. package/eslint.config.mjs +37 -0
  82. package/next.config.ts +7 -0
  83. package/package.json +85 -0
  84. package/postcss.config.mjs +7 -0
  85. package/public/file.svg +1 -0
  86. package/public/globe.svg +1 -0
  87. package/public/helius-icon.svg +84 -0
  88. package/public/helius-logo.svg +85 -0
  89. package/public/next.svg +1 -0
  90. package/public/plane.glb +0 -0
  91. package/public/vercel.svg +1 -0
  92. package/public/window.svg +1 -0
  93. package/scripts/clear-city.ts +74 -0
  94. package/scripts/seed-wallets.ts +185 -0
  95. package/scripts/setup-webhook.ts +73 -0
  96. package/src/app/api/auth/callback/route.ts +6 -0
  97. package/src/app/api/auth/link-wallet/route.ts +6 -0
  98. package/src/app/api/auth/phantom/route.ts +6 -0
  99. package/src/app/api/broadcast-position/route.ts +59 -0
  100. package/src/app/api/leaderboard/route.ts +85 -0
  101. package/src/app/api/network-stats/route.ts +86 -0
  102. package/src/app/api/parcel-reward/route.ts +181 -0
  103. package/src/app/api/queue-status/route.ts +30 -0
  104. package/src/app/api/snapshots/route.ts +37 -0
  105. package/src/app/api/transactions/enhanced/route.ts +57 -0
  106. package/src/app/api/treasury/route.ts +83 -0
  107. package/src/app/api/wallet/[address]/balances/route.ts +124 -0
  108. package/src/app/api/wallet/[address]/identity/route.ts +32 -0
  109. package/src/app/api/wallet/[address]/route.ts +216 -0
  110. package/src/app/api/wallet/[address]/traded-tokens/route.ts +41 -0
  111. package/src/app/api/wallets/route.ts +68 -0
  112. package/src/app/api/webhooks/helius/route.ts +76 -0
  113. package/src/app/auth/callback/page.tsx +29 -0
  114. package/src/app/favicon.ico +0 -0
  115. package/src/app/globals.css +39 -0
  116. package/src/app/layout.tsx +43 -0
  117. package/src/app/page.tsx +16 -0
  118. package/src/components/AITownNPCs.tsx +206 -0
  119. package/src/components/ActivityFeed.tsx +189 -0
  120. package/src/components/AuthPanel.tsx +163 -0
  121. package/src/components/BeachScene.tsx +280 -0
  122. package/src/components/Building.tsx +138 -0
  123. package/src/components/CesiumFlight.tsx +1768 -0
  124. package/src/components/CesiumGlobe.tsx +616 -0
  125. package/src/components/CitizenCard.tsx +442 -0
  126. package/src/components/CitizenCardModal.tsx +153 -0
  127. package/src/components/CityGrid.tsx +313 -0
  128. package/src/components/CityLandmarks.tsx +427 -0
  129. package/src/components/CityScene.tsx +1289 -0
  130. package/src/components/CitySlotsBadge.tsx +68 -0
  131. package/src/components/CockpitHUD.tsx +460 -0
  132. package/src/components/ConvexWrapper.tsx +19 -0
  133. package/src/components/DubaiDistrict.tsx +630 -0
  134. package/src/components/FlightMiniMap.tsx +133 -0
  135. package/src/components/GameChat.tsx +383 -0
  136. package/src/components/GameHUD.tsx +393 -0
  137. package/src/components/Ground.tsx +14 -0
  138. package/src/components/HowItWorksModal.tsx +251 -0
  139. package/src/components/IngestionBanner.tsx +123 -0
  140. package/src/components/InstancedBuildings.tsx +316 -0
  141. package/src/components/InstancedCars.tsx +504 -0
  142. package/src/components/InstancedCityPlanes.tsx +259 -0
  143. package/src/components/InstancedHouses.tsx +246 -0
  144. package/src/components/InstancedLampPosts.tsx +201 -0
  145. package/src/components/InstancedResidentCars.tsx +357 -0
  146. package/src/components/InstancedRoadDashes.tsx +42 -0
  147. package/src/components/InstancedSkyscrapers.tsx +434 -0
  148. package/src/components/InstancedTrees.tsx +67 -0
  149. package/src/components/LeaderboardPanel.tsx +136 -0
  150. package/src/components/MultiplayerPlanes.tsx +128 -0
  151. package/src/components/NetworkStats.tsx +83 -0
  152. package/src/components/NewBuildingSpotlight.tsx +93 -0
  153. package/src/components/ParcelChallengeBanner.tsx +242 -0
  154. package/src/components/ParcelReward.tsx +191 -0
  155. package/src/components/Park.tsx +42 -0
  156. package/src/components/PhantomWrapper.tsx +22 -0
  157. package/src/components/PixelStreamViewer.tsx +335 -0
  158. package/src/components/PlaneMode.tsx +190 -0
  159. package/src/components/PlayerCar.tsx +211 -0
  160. package/src/components/PlayerPlane.tsx +255 -0
  161. package/src/components/ProjectileRenderer.tsx +249 -0
  162. package/src/components/QueueStatusBanner.tsx +86 -0
  163. package/src/components/RealPlayerTags.tsx +82 -0
  164. package/src/components/SceneLighting.tsx +382 -0
  165. package/src/components/SelectionBeam.tsx +59 -0
  166. package/src/components/SwapPanel.tsx +104 -0
  167. package/src/components/SwapParticles.tsx +237 -0
  168. package/src/components/TreasureGate.tsx +505 -0
  169. package/src/components/WalletPanel.tsx +421 -0
  170. package/src/components/WalletSearch.tsx +244 -0
  171. package/src/components/WelcomeOverlay.tsx +135 -0
  172. package/src/components/WindowTooltip.tsx +498 -0
  173. package/src/context/AuthContext.tsx +230 -0
  174. package/src/lib/bot-detection.ts +125 -0
  175. package/src/lib/building-math.ts +136 -0
  176. package/src/lib/building-shader.ts +253 -0
  177. package/src/lib/car-paths.ts +244 -0
  178. package/src/lib/car-system.ts +182 -0
  179. package/src/lib/city-constants.ts +29 -0
  180. package/src/lib/city-slots.ts +35 -0
  181. package/src/lib/city-zoning.ts +64 -0
  182. package/src/lib/collision-map.ts +147 -0
  183. package/src/lib/day-night.ts +252 -0
  184. package/src/lib/export-card.ts +28 -0
  185. package/src/lib/helius-webhook.ts +90 -0
  186. package/src/lib/helius.ts +74 -0
  187. package/src/lib/house-shader.ts +119 -0
  188. package/src/lib/mock-data.ts +56 -0
  189. package/src/lib/multiplayer-manager.ts +329 -0
  190. package/src/lib/plane-physics.ts +66 -0
  191. package/src/lib/player-car.ts +147 -0
  192. package/src/lib/player-plane.ts +200 -0
  193. package/src/lib/projectile-system.ts +272 -0
  194. package/src/lib/skyscraper-types.ts +52 -0
  195. package/src/lib/sound-engine.ts +464 -0
  196. package/src/lib/supabase-admin.ts +9 -0
  197. package/src/lib/supabase.ts +8 -0
  198. package/src/lib/swap-events.ts +70 -0
  199. package/src/middleware.ts +37 -0
  200. package/src/types/phantom.d.ts +16 -0
  201. package/src/types/wallet.ts +20 -0
  202. package/tsconfig.json +34 -0
@@ -0,0 +1,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
+ }