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,393 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRef, useEffect, MutableRefObject, useCallback } from "react";
|
|
4
|
+
import { ProjectileManager } from "@/lib/projectile-system";
|
|
5
|
+
import { GRID_WORLD } from "@/lib/city-constants";
|
|
6
|
+
import type { RemotePlayer } from "@/lib/multiplayer-manager";
|
|
7
|
+
|
|
8
|
+
interface GameHUDProps {
|
|
9
|
+
active: boolean;
|
|
10
|
+
mode: "car" | "plane";
|
|
11
|
+
manager: ProjectileManager;
|
|
12
|
+
playerPosRef: MutableRefObject<[number, number, number] | null>;
|
|
13
|
+
playerHeadingRef: MutableRefObject<number>;
|
|
14
|
+
planePositionsRef: MutableRefObject<Array<{ x: number; y: number; z: number }>>;
|
|
15
|
+
carPositionsRef: MutableRefObject<Array<{ x: number; y: number; z: number }>>;
|
|
16
|
+
/** Real multiplayer players currently active in combat */
|
|
17
|
+
remotePlayers: Map<string, RemotePlayer>;
|
|
18
|
+
/** Building center positions for minimap dots */
|
|
19
|
+
buildingPositions: Array<{ x: number; z: number }>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const MAP_SIZE = 200; // pixels
|
|
23
|
+
const MAP_PADDING = 16;
|
|
24
|
+
const WORLD_HALF = GRID_WORLD / 2 + 30; // slight margin
|
|
25
|
+
const SCALE = MAP_SIZE / (WORLD_HALF * 2);
|
|
26
|
+
|
|
27
|
+
/** Convert world coords to minimap pixel coords */
|
|
28
|
+
function toMapXY(wx: number, wz: number): [number, number] {
|
|
29
|
+
const mx = (wx + WORLD_HALF) * SCALE;
|
|
30
|
+
const my = (wz + WORLD_HALF) * SCALE;
|
|
31
|
+
return [mx, my];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default function GameHUD({
|
|
35
|
+
active,
|
|
36
|
+
mode,
|
|
37
|
+
manager,
|
|
38
|
+
playerPosRef,
|
|
39
|
+
playerHeadingRef,
|
|
40
|
+
planePositionsRef,
|
|
41
|
+
carPositionsRef,
|
|
42
|
+
remotePlayers,
|
|
43
|
+
buildingPositions,
|
|
44
|
+
}: GameHUDProps) {
|
|
45
|
+
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
46
|
+
const animRef = useRef<number>(0);
|
|
47
|
+
const scoreDisplayRef = useRef(0);
|
|
48
|
+
|
|
49
|
+
const draw = useCallback(() => {
|
|
50
|
+
const canvas = canvasRef.current;
|
|
51
|
+
if (!canvas) return;
|
|
52
|
+
const ctx = canvas.getContext("2d");
|
|
53
|
+
if (!ctx) return;
|
|
54
|
+
|
|
55
|
+
const size = MAP_SIZE * 2; // retina
|
|
56
|
+
canvas.width = size;
|
|
57
|
+
canvas.height = size;
|
|
58
|
+
ctx.scale(2, 2);
|
|
59
|
+
const now = performance.now();
|
|
60
|
+
|
|
61
|
+
// Background
|
|
62
|
+
ctx.fillStyle = "rgba(10, 10, 18, 0.85)";
|
|
63
|
+
ctx.fillRect(0, 0, MAP_SIZE, MAP_SIZE);
|
|
64
|
+
|
|
65
|
+
// Grid lines
|
|
66
|
+
ctx.strokeStyle = "rgba(255,255,255,0.04)";
|
|
67
|
+
ctx.lineWidth = 0.5;
|
|
68
|
+
for (let i = 0; i <= 10; i++) {
|
|
69
|
+
const p = (i / 10) * MAP_SIZE;
|
|
70
|
+
ctx.beginPath(); ctx.moveTo(p, 0); ctx.lineTo(p, MAP_SIZE); ctx.stroke();
|
|
71
|
+
ctx.beginPath(); ctx.moveTo(0, p); ctx.lineTo(MAP_SIZE, p); ctx.stroke();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Buildings — tiny gray dots
|
|
75
|
+
ctx.fillStyle = "rgba(255,255,255,0.12)";
|
|
76
|
+
for (const b of buildingPositions) {
|
|
77
|
+
const [mx, my] = toMapXY(b.x, b.z);
|
|
78
|
+
ctx.fillRect(mx - 0.5, my - 0.5, 1, 1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Resident cars — blue dots
|
|
82
|
+
ctx.fillStyle = "#6366f1";
|
|
83
|
+
for (const c of carPositionsRef.current) {
|
|
84
|
+
const [mx, my] = toMapXY(c.x, c.z);
|
|
85
|
+
ctx.beginPath();
|
|
86
|
+
ctx.arc(mx, my, 1.5, 0, Math.PI * 2);
|
|
87
|
+
ctx.fill();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Resident planes — orange dots
|
|
91
|
+
ctx.fillStyle = "#f59e0b";
|
|
92
|
+
for (const p of planePositionsRef.current) {
|
|
93
|
+
const [mx, my] = toMapXY(p.x, p.z);
|
|
94
|
+
ctx.beginPath();
|
|
95
|
+
ctx.arc(mx, my, 2, 0, Math.PI * 2);
|
|
96
|
+
ctx.fill();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Real humans (multiplayer) — bright pulsing markers with heading
|
|
100
|
+
let humanCars = 0;
|
|
101
|
+
let humanPlanes = 0;
|
|
102
|
+
for (const remote of remotePlayers.values()) {
|
|
103
|
+
if (remote.mode === "spectating") continue;
|
|
104
|
+
|
|
105
|
+
const [mx, my] = toMapXY(remote.x, remote.z);
|
|
106
|
+
if (mx < -6 || my < -6 || mx > MAP_SIZE + 6 || my > MAP_SIZE + 6) continue;
|
|
107
|
+
|
|
108
|
+
const isCar = remote.mode === "car";
|
|
109
|
+
if (isCar) humanCars += 1;
|
|
110
|
+
else humanPlanes += 1;
|
|
111
|
+
|
|
112
|
+
// Pulse ring to draw attention to human players
|
|
113
|
+
const pulsePhase = ((now + remote.lastSeen) % 1400) / 1400;
|
|
114
|
+
const pulseR = (isCar ? 4 : 4.8) + pulsePhase * 3;
|
|
115
|
+
const pulseAlpha = 0.55 * (1 - pulsePhase);
|
|
116
|
+
ctx.strokeStyle = isCar
|
|
117
|
+
? `rgba(56, 189, 248, ${pulseAlpha})`
|
|
118
|
+
: `rgba(236, 72, 153, ${pulseAlpha})`;
|
|
119
|
+
ctx.lineWidth = 1;
|
|
120
|
+
ctx.beginPath();
|
|
121
|
+
ctx.arc(mx, my, pulseR, 0, Math.PI * 2);
|
|
122
|
+
ctx.stroke();
|
|
123
|
+
|
|
124
|
+
// Direction tick
|
|
125
|
+
const hx = mx + Math.sin(remote.heading) * 5.5;
|
|
126
|
+
const hy = my - Math.cos(remote.heading) * 5.5;
|
|
127
|
+
ctx.strokeStyle = isCar ? "rgba(56, 189, 248, 0.9)" : "rgba(236, 72, 153, 0.9)";
|
|
128
|
+
ctx.beginPath();
|
|
129
|
+
ctx.moveTo(mx, my);
|
|
130
|
+
ctx.lineTo(hx, hy);
|
|
131
|
+
ctx.stroke();
|
|
132
|
+
|
|
133
|
+
if (isCar) {
|
|
134
|
+
// Diamond = human driver
|
|
135
|
+
ctx.fillStyle = "#38bdf8";
|
|
136
|
+
ctx.beginPath();
|
|
137
|
+
ctx.moveTo(mx, my - 3.5);
|
|
138
|
+
ctx.lineTo(mx + 3.5, my);
|
|
139
|
+
ctx.lineTo(mx, my + 3.5);
|
|
140
|
+
ctx.lineTo(mx - 3.5, my);
|
|
141
|
+
ctx.closePath();
|
|
142
|
+
ctx.fill();
|
|
143
|
+
} else {
|
|
144
|
+
// Triangle = human pilot
|
|
145
|
+
ctx.save();
|
|
146
|
+
ctx.translate(mx, my);
|
|
147
|
+
ctx.rotate(remote.heading);
|
|
148
|
+
ctx.fillStyle = "#ec4899";
|
|
149
|
+
ctx.beginPath();
|
|
150
|
+
ctx.moveTo(0, -4.5);
|
|
151
|
+
ctx.lineTo(-3.2, 3.2);
|
|
152
|
+
ctx.lineTo(3.2, 3.2);
|
|
153
|
+
ctx.closePath();
|
|
154
|
+
ctx.fill();
|
|
155
|
+
ctx.restore();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// White stroke for readability on dark/light minimap areas
|
|
159
|
+
ctx.strokeStyle = "rgba(255, 255, 255, 0.8)";
|
|
160
|
+
ctx.lineWidth = 0.8;
|
|
161
|
+
ctx.beginPath();
|
|
162
|
+
ctx.arc(mx, my, 3.8, 0, Math.PI * 2);
|
|
163
|
+
ctx.stroke();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Human count label
|
|
167
|
+
const totalHumans = humanCars + humanPlanes;
|
|
168
|
+
if (totalHumans > 0) {
|
|
169
|
+
ctx.fillStyle = "rgba(255, 255, 255, 0.9)";
|
|
170
|
+
ctx.font = "600 9px monospace";
|
|
171
|
+
ctx.textAlign = "left";
|
|
172
|
+
ctx.fillText(`HUMANS ${totalHumans}`, 6, 11);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Active projectiles — red dots
|
|
176
|
+
ctx.fillStyle = "#ff4400";
|
|
177
|
+
for (const p of manager.getAlive()) {
|
|
178
|
+
const [mx, my] = toMapXY(p.x, p.z);
|
|
179
|
+
ctx.beginPath();
|
|
180
|
+
ctx.arc(mx, my, 1.5, 0, Math.PI * 2);
|
|
181
|
+
ctx.fill();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Hit locations — expanding rings
|
|
185
|
+
ctx.strokeStyle = "#ff4400";
|
|
186
|
+
ctx.lineWidth = 1;
|
|
187
|
+
for (const h of manager.hits) {
|
|
188
|
+
const age = (now - h.timestamp) / 1000;
|
|
189
|
+
if (age > 2) continue;
|
|
190
|
+
const [mx, my] = toMapXY(h.x, h.z);
|
|
191
|
+
const radius = 3 + age * 15;
|
|
192
|
+
const alpha = 1 - age / 2;
|
|
193
|
+
ctx.globalAlpha = alpha;
|
|
194
|
+
ctx.beginPath();
|
|
195
|
+
ctx.arc(mx, my, radius, 0, Math.PI * 2);
|
|
196
|
+
ctx.stroke();
|
|
197
|
+
}
|
|
198
|
+
ctx.globalAlpha = 1;
|
|
199
|
+
|
|
200
|
+
// Player position — large arrow
|
|
201
|
+
const pos = playerPosRef.current;
|
|
202
|
+
if (pos) {
|
|
203
|
+
const [px, py] = toMapXY(pos[0], pos[2]);
|
|
204
|
+
const heading = playerHeadingRef.current;
|
|
205
|
+
|
|
206
|
+
ctx.save();
|
|
207
|
+
ctx.translate(px, py);
|
|
208
|
+
ctx.rotate(heading);
|
|
209
|
+
|
|
210
|
+
// FOV cone
|
|
211
|
+
ctx.fillStyle = "rgba(227, 89, 48, 0.12)";
|
|
212
|
+
ctx.beginPath();
|
|
213
|
+
ctx.moveTo(0, 0);
|
|
214
|
+
const coneLen = 40;
|
|
215
|
+
const coneAngle = 0.4;
|
|
216
|
+
ctx.lineTo(Math.sin(-coneAngle) * coneLen, -Math.cos(-coneAngle) * coneLen);
|
|
217
|
+
ctx.lineTo(0, -coneLen * 0.8);
|
|
218
|
+
ctx.lineTo(Math.sin(coneAngle) * coneLen, -Math.cos(coneAngle) * coneLen);
|
|
219
|
+
ctx.closePath();
|
|
220
|
+
ctx.fill();
|
|
221
|
+
|
|
222
|
+
// Player triangle
|
|
223
|
+
ctx.fillStyle = "#E35930";
|
|
224
|
+
ctx.beginPath();
|
|
225
|
+
ctx.moveTo(0, -6);
|
|
226
|
+
ctx.lineTo(-4, 4);
|
|
227
|
+
ctx.lineTo(4, 4);
|
|
228
|
+
ctx.closePath();
|
|
229
|
+
ctx.fill();
|
|
230
|
+
|
|
231
|
+
// White outline
|
|
232
|
+
ctx.strokeStyle = "rgba(255,255,255,0.5)";
|
|
233
|
+
ctx.lineWidth = 0.8;
|
|
234
|
+
ctx.stroke();
|
|
235
|
+
|
|
236
|
+
ctx.restore();
|
|
237
|
+
|
|
238
|
+
// Pulsing ring around player
|
|
239
|
+
const pulsePhase = (now % 2000) / 2000;
|
|
240
|
+
const pulseR = 6 + pulsePhase * 8;
|
|
241
|
+
const pulseAlpha = 1 - pulsePhase;
|
|
242
|
+
ctx.strokeStyle = `rgba(227, 89, 48, ${pulseAlpha * 0.4})`;
|
|
243
|
+
ctx.lineWidth = 1;
|
|
244
|
+
ctx.beginPath();
|
|
245
|
+
ctx.arc(px, py, pulseR, 0, Math.PI * 2);
|
|
246
|
+
ctx.stroke();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Border
|
|
250
|
+
ctx.strokeStyle = "rgba(255,255,255,0.08)";
|
|
251
|
+
ctx.lineWidth = 1;
|
|
252
|
+
ctx.strokeRect(0.5, 0.5, MAP_SIZE - 1, MAP_SIZE - 1);
|
|
253
|
+
|
|
254
|
+
// Smooth score counter
|
|
255
|
+
scoreDisplayRef.current += (manager.score - scoreDisplayRef.current) * 0.1;
|
|
256
|
+
|
|
257
|
+
animRef.current = requestAnimationFrame(draw);
|
|
258
|
+
}, [manager, playerPosRef, playerHeadingRef, planePositionsRef, carPositionsRef, remotePlayers, buildingPositions]);
|
|
259
|
+
|
|
260
|
+
useEffect(() => {
|
|
261
|
+
if (!active) return;
|
|
262
|
+
animRef.current = requestAnimationFrame(draw);
|
|
263
|
+
return () => cancelAnimationFrame(animRef.current);
|
|
264
|
+
}, [active, draw]);
|
|
265
|
+
|
|
266
|
+
if (!active) return null;
|
|
267
|
+
|
|
268
|
+
const score = Math.round(scoreDisplayRef.current);
|
|
269
|
+
const alive = manager.getAlive().length;
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<>
|
|
273
|
+
{/* Minimap — top right, below auth buttons */}
|
|
274
|
+
<div
|
|
275
|
+
className="fixed z-50"
|
|
276
|
+
style={{
|
|
277
|
+
top: 60,
|
|
278
|
+
right: MAP_PADDING,
|
|
279
|
+
width: MAP_SIZE,
|
|
280
|
+
height: MAP_SIZE,
|
|
281
|
+
borderRadius: 12,
|
|
282
|
+
overflow: "hidden",
|
|
283
|
+
border: "1px solid rgba(255,255,255,0.08)",
|
|
284
|
+
boxShadow: "0 8px 32px rgba(0,0,0,0.5)",
|
|
285
|
+
}}
|
|
286
|
+
>
|
|
287
|
+
<canvas
|
|
288
|
+
ref={canvasRef}
|
|
289
|
+
style={{ width: MAP_SIZE, height: MAP_SIZE }}
|
|
290
|
+
/>
|
|
291
|
+
|
|
292
|
+
{/* Map legend */}
|
|
293
|
+
<div className="absolute bottom-1.5 left-2 flex gap-2 items-center">
|
|
294
|
+
<div className="flex items-center gap-1">
|
|
295
|
+
<div className="w-1.5 h-1.5 rounded-full bg-[#f59e0b]" />
|
|
296
|
+
<span className="text-[8px] text-white/30">Planes</span>
|
|
297
|
+
</div>
|
|
298
|
+
<div className="flex items-center gap-1">
|
|
299
|
+
<div className="w-1.5 h-1.5 rounded-full bg-[#6366f1]" />
|
|
300
|
+
<span className="text-[8px] text-white/30">Cars</span>
|
|
301
|
+
</div>
|
|
302
|
+
<div className="flex items-center gap-1">
|
|
303
|
+
<div className="w-1.5 h-1.5 rotate-45 bg-sky-400" />
|
|
304
|
+
<span className="text-[8px] text-sky-300/80">Human car</span>
|
|
305
|
+
</div>
|
|
306
|
+
<div className="flex items-center gap-1">
|
|
307
|
+
<div className="w-0 h-0 border-l-[3px] border-r-[3px] border-b-[5px] border-l-transparent border-r-transparent border-b-pink-400" />
|
|
308
|
+
<span className="text-[8px] text-pink-300/80">Human plane</span>
|
|
309
|
+
</div>
|
|
310
|
+
<div className="flex items-center gap-1">
|
|
311
|
+
<div className="w-1.5 h-1.5 rounded-full bg-[#E35930]" />
|
|
312
|
+
<span className="text-[8px] text-white/30">You</span>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
{/* Score + Ammo HUD — top center */}
|
|
318
|
+
<div className="fixed top-4 left-1/2 -translate-x-1/2 z-50 flex items-center gap-4 bg-black/60 backdrop-blur-xl border border-white/[0.08] rounded-2xl px-5 py-2.5">
|
|
319
|
+
{/* Crosshair icon */}
|
|
320
|
+
<div className="flex flex-col items-center">
|
|
321
|
+
<span className="text-[9px] text-white/25 uppercase tracking-wider">Mode</span>
|
|
322
|
+
<span className="text-sm font-bold text-[#E35930]">
|
|
323
|
+
{mode === "plane" ? "✈ AIR" : "🚗 GROUND"}
|
|
324
|
+
</span>
|
|
325
|
+
</div>
|
|
326
|
+
|
|
327
|
+
<div className="w-px h-8 bg-white/[0.08]" />
|
|
328
|
+
|
|
329
|
+
{/* Score */}
|
|
330
|
+
<div className="flex flex-col items-center">
|
|
331
|
+
<span className="text-[9px] text-white/25 uppercase tracking-wider">Score</span>
|
|
332
|
+
<span className="text-lg font-bold text-white tabular-nums" style={{ fontVariantNumeric: "tabular-nums" }}>
|
|
333
|
+
{score.toLocaleString()}
|
|
334
|
+
</span>
|
|
335
|
+
</div>
|
|
336
|
+
|
|
337
|
+
<div className="w-px h-8 bg-white/[0.08]" />
|
|
338
|
+
|
|
339
|
+
{/* Hits */}
|
|
340
|
+
<div className="flex flex-col items-center">
|
|
341
|
+
<span className="text-[9px] text-white/25 uppercase tracking-wider">Hits</span>
|
|
342
|
+
<span className="text-sm font-semibold text-[#22c55e] tabular-nums">
|
|
343
|
+
{manager.hits.length}
|
|
344
|
+
</span>
|
|
345
|
+
</div>
|
|
346
|
+
|
|
347
|
+
<div className="w-px h-8 bg-white/[0.08]" />
|
|
348
|
+
|
|
349
|
+
{/* Active projectiles */}
|
|
350
|
+
<div className="flex flex-col items-center">
|
|
351
|
+
<span className="text-[9px] text-white/25 uppercase tracking-wider">Active</span>
|
|
352
|
+
<span className="text-sm font-semibold text-[#f59e0b] tabular-nums">
|
|
353
|
+
{alive}
|
|
354
|
+
</span>
|
|
355
|
+
</div>
|
|
356
|
+
|
|
357
|
+
<div className="w-px h-8 bg-white/[0.08]" />
|
|
358
|
+
|
|
359
|
+
{/* Fire key hint */}
|
|
360
|
+
<div className="flex flex-col items-center">
|
|
361
|
+
<span className="text-[9px] text-white/25 uppercase tracking-wider">Fire</span>
|
|
362
|
+
<span className="text-[11px] text-white/50 font-medium">F / Click</span>
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
|
|
366
|
+
{/* Crosshair — center of screen */}
|
|
367
|
+
<div className="fixed inset-0 z-40 pointer-events-none flex items-center justify-center">
|
|
368
|
+
<div className="relative w-8 h-8">
|
|
369
|
+
{/* Cross lines */}
|
|
370
|
+
<div className="absolute top-1/2 left-0 w-full h-px bg-white/20" />
|
|
371
|
+
<div className="absolute left-1/2 top-0 h-full w-px bg-white/20" />
|
|
372
|
+
{/* Center dot */}
|
|
373
|
+
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-1 h-1 rounded-full bg-[#E35930]" />
|
|
374
|
+
{/* Corner brackets */}
|
|
375
|
+
<div className="absolute top-0 left-0 w-2 h-2 border-t border-l border-white/25" />
|
|
376
|
+
<div className="absolute top-0 right-0 w-2 h-2 border-t border-r border-white/25" />
|
|
377
|
+
<div className="absolute bottom-0 left-0 w-2 h-2 border-b border-l border-white/25" />
|
|
378
|
+
<div className="absolute bottom-0 right-0 w-2 h-2 border-b border-r border-white/25" />
|
|
379
|
+
</div>
|
|
380
|
+
</div>
|
|
381
|
+
|
|
382
|
+
{/* Hit markers — flash on screen when you hit something */}
|
|
383
|
+
{manager.hits.filter(h => performance.now() - h.timestamp < 500).map((h, i) => (
|
|
384
|
+
<div
|
|
385
|
+
key={`hit-${h.timestamp}-${i}`}
|
|
386
|
+
className="fixed inset-0 z-40 pointer-events-none flex items-center justify-center"
|
|
387
|
+
>
|
|
388
|
+
<div className="text-[#ff4400] text-3xl font-bold animate-ping">✕</div>
|
|
389
|
+
</div>
|
|
390
|
+
))}
|
|
391
|
+
</>
|
|
392
|
+
);
|
|
393
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
interface GroundProps {
|
|
4
|
+
size: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export default function Ground({ size }: GroundProps) {
|
|
8
|
+
return (
|
|
9
|
+
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.1, 0]} receiveShadow>
|
|
10
|
+
<planeGeometry args={[size, size]} />
|
|
11
|
+
<meshStandardMaterial color="#4a4a65" />
|
|
12
|
+
</mesh>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
|
|
6
|
+
const HELIUS_DOCS = "https://docs.helius.dev";
|
|
7
|
+
|
|
8
|
+
const DATA_SOURCES = [
|
|
9
|
+
{
|
|
10
|
+
category: "Buildings",
|
|
11
|
+
items: [
|
|
12
|
+
{
|
|
13
|
+
visual: "Building height",
|
|
14
|
+
dataPoint: "Transaction count",
|
|
15
|
+
api: "Enhanced Transactions",
|
|
16
|
+
endpoint: "getTransactionsForAddress",
|
|
17
|
+
doc: `${HELIUS_DOCS}/api-reference/enhanced-transactions`,
|
|
18
|
+
detail: "Log-scaled txn count mapped to 1-150 floors",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
visual: "Building width",
|
|
22
|
+
dataPoint: "Volume traded (SOL)",
|
|
23
|
+
api: "Enhanced Transactions",
|
|
24
|
+
endpoint: "getTransactionsForAddress",
|
|
25
|
+
doc: `${HELIUS_DOCS}/api-reference/enhanced-transactions`,
|
|
26
|
+
detail: "Square root-scaled relative to highest volume wallet",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
visual: "Lit windows",
|
|
30
|
+
dataPoint: "Unique tokens swapped & recency",
|
|
31
|
+
api: "Enhanced Transactions",
|
|
32
|
+
endpoint: "getTransactionsForAddress",
|
|
33
|
+
doc: `${HELIUS_DOCS}/api-reference/enhanced-transactions`,
|
|
34
|
+
detail: "Fill ratio from token diversity, brightness from last activity",
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
category: "Wallet Info",
|
|
40
|
+
items: [
|
|
41
|
+
{
|
|
42
|
+
visual: "Wallet age",
|
|
43
|
+
dataPoint: "First funding timestamp",
|
|
44
|
+
api: "Wallet API",
|
|
45
|
+
endpoint: "getWalletFundedBy",
|
|
46
|
+
doc: `${HELIUS_DOCS}/api-reference/wallet-api`,
|
|
47
|
+
detail: "Days since first SOL transfer to this wallet",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
visual: "Wallet identity",
|
|
51
|
+
dataPoint: "Known entity labels",
|
|
52
|
+
api: "Wallet API",
|
|
53
|
+
endpoint: "getWalletIdentity",
|
|
54
|
+
doc: `${HELIUS_DOCS}/api-reference/wallet-api`,
|
|
55
|
+
detail: "Recognizes exchanges, protocols, and notable wallets",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
visual: "Token balances & prices",
|
|
59
|
+
dataPoint: "Fungible asset holdings",
|
|
60
|
+
api: "DAS API",
|
|
61
|
+
endpoint: "getAssetsByOwner",
|
|
62
|
+
doc: `${HELIUS_DOCS}/api-reference/digital-asset-standard`,
|
|
63
|
+
detail: "Live balances with metadata, images, and price info",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
visual: "Token metadata",
|
|
67
|
+
dataPoint: "Name, symbol, image per mint",
|
|
68
|
+
api: "DAS API",
|
|
69
|
+
endpoint: "getAssetsByOwner",
|
|
70
|
+
doc: `${HELIUS_DOCS}/api-reference/digital-asset-standard`,
|
|
71
|
+
detail: "Token names, symbols, and images returned alongside balances",
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
category: "Live Activity",
|
|
77
|
+
items: [
|
|
78
|
+
{
|
|
79
|
+
visual: "Cars on roads",
|
|
80
|
+
dataPoint: "Real-time swap events",
|
|
81
|
+
api: "Webhooks",
|
|
82
|
+
endpoint: "Enhanced Webhooks",
|
|
83
|
+
doc: `${HELIUS_DOCS}/api-reference/webhooks`,
|
|
84
|
+
detail: "SWAP events pushed to the server, cars spawn near the swapping wallet's building",
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
] as const;
|
|
89
|
+
|
|
90
|
+
const INFRA = [
|
|
91
|
+
{ label: "RPC", value: "Helius Mainnet RPC", doc: `${HELIUS_DOCS}/api-reference/rpc` },
|
|
92
|
+
{ label: "Database", value: "Supabase (PostgreSQL)", doc: null },
|
|
93
|
+
{ label: "Realtime", value: "Supabase Realtime", doc: null },
|
|
94
|
+
{ label: "Frontend", value: "React Three Fiber + Next.js", doc: null },
|
|
95
|
+
] as const;
|
|
96
|
+
|
|
97
|
+
export default function HowItWorksModal() {
|
|
98
|
+
const [open, setOpen] = useState(false);
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<>
|
|
102
|
+
<button
|
|
103
|
+
onClick={() => setOpen(true)}
|
|
104
|
+
className="w-6 h-6 flex items-center justify-center rounded-lg text-white/30 hover:text-white/60 hover:bg-white/[0.06] transition-colors cursor-pointer"
|
|
105
|
+
aria-label="How it works"
|
|
106
|
+
>
|
|
107
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
108
|
+
<circle cx="8" cy="8" r="6.5" />
|
|
109
|
+
<path d="M6.5 6.5a1.5 1.5 0 1 1 1.5 1.5v1" />
|
|
110
|
+
<circle cx="8" cy="11.5" r="0.5" fill="currentColor" stroke="none" />
|
|
111
|
+
</svg>
|
|
112
|
+
</button>
|
|
113
|
+
|
|
114
|
+
{open && createPortal(
|
|
115
|
+
<div
|
|
116
|
+
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
|
117
|
+
onClick={() => setOpen(false)}
|
|
118
|
+
>
|
|
119
|
+
<div
|
|
120
|
+
className="relative w-full max-w-lg mx-4 max-h-[80vh] overflow-y-auto bg-[#0e0e1a]/95 backdrop-blur-xl border border-white/[0.08] rounded-2xl p-6"
|
|
121
|
+
onClick={(e) => e.stopPropagation()}
|
|
122
|
+
>
|
|
123
|
+
{/* Header */}
|
|
124
|
+
<div className="flex items-center justify-between mb-5">
|
|
125
|
+
<div>
|
|
126
|
+
<h2 className="text-base font-semibold text-white">How It Works</h2>
|
|
127
|
+
<p className="text-xs text-white/35 mt-0.5">Where the data comes from</p>
|
|
128
|
+
</div>
|
|
129
|
+
<button
|
|
130
|
+
onClick={() => setOpen(false)}
|
|
131
|
+
className="w-7 h-7 flex items-center justify-center rounded-lg text-white/30 hover:text-white hover:bg-white/[0.06] transition-colors cursor-pointer"
|
|
132
|
+
>
|
|
133
|
+
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
134
|
+
<path d="M1 1l8 8M9 1l-8 8" />
|
|
135
|
+
</svg>
|
|
136
|
+
</button>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
{/* Install Helius */}
|
|
140
|
+
<div className="mb-5">
|
|
141
|
+
<h3 className="text-xs font-semibold text-white/40 uppercase tracking-wider mb-2.5">
|
|
142
|
+
Install Helius
|
|
143
|
+
</h3>
|
|
144
|
+
<div className="space-y-2">
|
|
145
|
+
{[
|
|
146
|
+
{ label: "CLI", command: "npm install -g helius-cli" },
|
|
147
|
+
{ label: "MCP", command: "claude mcp add helius npx helius-mcp@latest" },
|
|
148
|
+
{ label: "Marketplace", command: "/plugin marketplace add helius-labs/core-ai" },
|
|
149
|
+
{ label: "Plugin", command: "/plugin install helius@helius-labs" },
|
|
150
|
+
].map((item) => (
|
|
151
|
+
<div
|
|
152
|
+
key={item.label}
|
|
153
|
+
className="bg-white/[0.03] border border-white/[0.06] rounded-xl px-3.5 py-3"
|
|
154
|
+
>
|
|
155
|
+
<div className="text-xs text-white/35 mb-1">{item.label}</div>
|
|
156
|
+
<code className="text-xs text-orange-300/80 font-mono">{item.command}</code>
|
|
157
|
+
</div>
|
|
158
|
+
))}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
{/* Data sources */}
|
|
163
|
+
<div className="space-y-5">
|
|
164
|
+
{DATA_SOURCES.map((group) => (
|
|
165
|
+
<div key={group.category}>
|
|
166
|
+
<h3 className="text-xs font-semibold text-white/40 uppercase tracking-wider mb-2.5">
|
|
167
|
+
{group.category}
|
|
168
|
+
</h3>
|
|
169
|
+
<div className="space-y-2">
|
|
170
|
+
{group.items.map((item) => (
|
|
171
|
+
<div
|
|
172
|
+
key={item.visual}
|
|
173
|
+
className="bg-white/[0.03] border border-white/[0.06] rounded-xl px-3.5 py-3"
|
|
174
|
+
>
|
|
175
|
+
<div className="flex items-start justify-between gap-3">
|
|
176
|
+
<div className="min-w-0">
|
|
177
|
+
<div className="text-sm text-white/80">{item.visual}</div>
|
|
178
|
+
<div className="text-xs text-white/35 mt-0.5">{item.detail}</div>
|
|
179
|
+
</div>
|
|
180
|
+
<a
|
|
181
|
+
href={item.doc}
|
|
182
|
+
target="_blank"
|
|
183
|
+
rel="noopener noreferrer"
|
|
184
|
+
className="shrink-0 px-2 py-1 bg-orange-500/10 border border-orange-500/15 rounded-lg text-xs text-orange-300/80 hover:text-orange-200 hover:bg-orange-500/15 transition-colors"
|
|
185
|
+
>
|
|
186
|
+
{item.endpoint}
|
|
187
|
+
</a>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
))}
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
))}
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
{/* Infrastructure */}
|
|
197
|
+
<div className="mt-5 pt-4 border-t border-white/[0.06]">
|
|
198
|
+
<h3 className="text-xs font-semibold text-white/40 uppercase tracking-wider mb-2.5">
|
|
199
|
+
Infrastructure
|
|
200
|
+
</h3>
|
|
201
|
+
<div className="grid grid-cols-2 gap-2">
|
|
202
|
+
{INFRA.map((item) => (
|
|
203
|
+
<div
|
|
204
|
+
key={item.label}
|
|
205
|
+
className="bg-white/[0.03] border border-white/[0.06] rounded-xl px-3 py-2.5"
|
|
206
|
+
>
|
|
207
|
+
<div className="text-xs text-white/35">{item.label}</div>
|
|
208
|
+
{item.doc ? (
|
|
209
|
+
<a
|
|
210
|
+
href={item.doc}
|
|
211
|
+
target="_blank"
|
|
212
|
+
rel="noopener noreferrer"
|
|
213
|
+
className="text-sm text-white/70 hover:text-white transition-colors"
|
|
214
|
+
>
|
|
215
|
+
{item.value}
|
|
216
|
+
</a>
|
|
217
|
+
) : (
|
|
218
|
+
<div className="text-sm text-white/70">{item.value}</div>
|
|
219
|
+
)}
|
|
220
|
+
</div>
|
|
221
|
+
))}
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
{/* Footer */}
|
|
226
|
+
<div className="mt-5 pt-4 border-t border-white/[0.06] flex items-center justify-between">
|
|
227
|
+
<a
|
|
228
|
+
href="https://www.helius.dev/"
|
|
229
|
+
target="_blank"
|
|
230
|
+
rel="noopener noreferrer"
|
|
231
|
+
className="flex items-center gap-2 text-xs text-white/30 hover:text-white/50 transition-colors"
|
|
232
|
+
>
|
|
233
|
+
<img src="/helius-icon.svg" alt="Helius" className="w-4 h-4" />
|
|
234
|
+
<span>Powered by Helius</span>
|
|
235
|
+
</a>
|
|
236
|
+
<a
|
|
237
|
+
href={HELIUS_DOCS}
|
|
238
|
+
target="_blank"
|
|
239
|
+
rel="noopener noreferrer"
|
|
240
|
+
className="text-xs text-blue-400/60 hover:text-blue-400 transition-colors"
|
|
241
|
+
>
|
|
242
|
+
Full API Docs →
|
|
243
|
+
</a>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
</div>,
|
|
247
|
+
document.body,
|
|
248
|
+
)}
|
|
249
|
+
</>
|
|
250
|
+
);
|
|
251
|
+
}
|