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,123 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef, useCallback } from "react";
|
|
4
|
+
import { PlacedWallet } from "@/types/wallet";
|
|
5
|
+
import { getBuildingDimensions, getWalletWorldPosition } from "@/lib/building-math";
|
|
6
|
+
|
|
7
|
+
interface IngestionBannerProps {
|
|
8
|
+
address: string;
|
|
9
|
+
onComplete: (wallet: PlacedWallet, position: [number, number, number]) => void;
|
|
10
|
+
onFailed: () => void;
|
|
11
|
+
onRefetch: () => Promise<PlacedWallet[]>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function IngestionBanner({
|
|
15
|
+
address,
|
|
16
|
+
onComplete,
|
|
17
|
+
onFailed,
|
|
18
|
+
onRefetch,
|
|
19
|
+
}: IngestionBannerProps) {
|
|
20
|
+
const [txnsFetched, setTxnsFetched] = useState(0);
|
|
21
|
+
const [ingestionPhase, setIngestionPhase] = useState<"queued" | "processing">("queued");
|
|
22
|
+
const [status, setStatus] = useState<"ingesting" | "complete" | "failed">("ingesting");
|
|
23
|
+
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
24
|
+
|
|
25
|
+
const stopPolling = useCallback(() => {
|
|
26
|
+
if (pollRef.current) {
|
|
27
|
+
clearInterval(pollRef.current);
|
|
28
|
+
pollRef.current = null;
|
|
29
|
+
}
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
pollRef.current = setInterval(async () => {
|
|
34
|
+
try {
|
|
35
|
+
const res = await fetch(`/api/wallet/${address}`);
|
|
36
|
+
if (!res.ok) return;
|
|
37
|
+
const data = await res.json();
|
|
38
|
+
|
|
39
|
+
// Use txnsFetched from queue for live progress, fall back to txnCount
|
|
40
|
+
setTxnsFetched(data.txnsFetched ?? data.txnCount ?? 0);
|
|
41
|
+
if (data.ingestionStatus === "processing") {
|
|
42
|
+
setIngestionPhase("processing");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (data.ingestionStatus === "complete") {
|
|
46
|
+
stopPolling();
|
|
47
|
+
setStatus("complete");
|
|
48
|
+
|
|
49
|
+
// Retry refetch — position assignment may lag behind completion
|
|
50
|
+
let wallet: PlacedWallet | undefined;
|
|
51
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
52
|
+
const fresh = await onRefetch();
|
|
53
|
+
wallet = fresh.find((w) => w.address === address);
|
|
54
|
+
if (wallet) break;
|
|
55
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (wallet) {
|
|
59
|
+
const dims = getBuildingDimensions(wallet);
|
|
60
|
+
const pos = getWalletWorldPosition(wallet, dims);
|
|
61
|
+
onComplete(wallet, pos);
|
|
62
|
+
} else {
|
|
63
|
+
onFailed();
|
|
64
|
+
}
|
|
65
|
+
} else if (data.ingestionStatus === "failed") {
|
|
66
|
+
stopPolling();
|
|
67
|
+
setStatus("failed");
|
|
68
|
+
setTimeout(onFailed, 2000);
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
// Keep polling on transient errors
|
|
72
|
+
}
|
|
73
|
+
}, 5000);
|
|
74
|
+
|
|
75
|
+
return stopPolling;
|
|
76
|
+
}, [address, stopPolling, onRefetch, onComplete, onFailed]);
|
|
77
|
+
|
|
78
|
+
const shortAddress = `${address.slice(0, 4)}...${address.slice(-4)}`;
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div className="absolute top-32 left-3 right-3 sm:top-[4.5rem] sm:left-1/2 sm:right-auto sm:-translate-x-1/2 z-20">
|
|
82
|
+
<div
|
|
83
|
+
className={`flex items-center justify-center gap-2.5 px-5 py-2.5 rounded-full backdrop-blur-xl border transition-colors text-xs sm:text-sm ${
|
|
84
|
+
status === "complete"
|
|
85
|
+
? "bg-green-900/50 border-green-500/20"
|
|
86
|
+
: status === "failed"
|
|
87
|
+
? "bg-red-900/50 border-red-500/20"
|
|
88
|
+
: "bg-black/50 border-white/[0.08]"
|
|
89
|
+
}`}
|
|
90
|
+
>
|
|
91
|
+
{status === "ingesting" && (
|
|
92
|
+
<span className="w-3 h-3 border-2 border-purple-400/40 border-t-purple-400 rounded-full animate-spin" />
|
|
93
|
+
)}
|
|
94
|
+
{status === "complete" && (
|
|
95
|
+
<svg className="w-3.5 h-3.5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
|
|
96
|
+
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
|
97
|
+
</svg>
|
|
98
|
+
)}
|
|
99
|
+
{status === "failed" && (
|
|
100
|
+
<span className="text-red-400 text-sm font-bold">!</span>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
<span className="text-sm text-white/80">
|
|
104
|
+
{status === "complete" ? (
|
|
105
|
+
<>Building ready for <span className="font-mono text-green-300">{shortAddress}</span></>
|
|
106
|
+
) : status === "failed" ? (
|
|
107
|
+
<>Indexing failed for <span className="font-mono text-red-300">{shortAddress}</span></>
|
|
108
|
+
) : (
|
|
109
|
+
<>
|
|
110
|
+
{ingestionPhase === "queued" ? "In queue" : "Indexing"}{" "}
|
|
111
|
+
<span className="font-mono text-purple-300">{shortAddress}</span>
|
|
112
|
+
{ingestionPhase === "processing" && txnsFetched > 0 && (
|
|
113
|
+
<span className="text-white/40 ml-1.5">
|
|
114
|
+
— {txnsFetched.toLocaleString()} txns
|
|
115
|
+
</span>
|
|
116
|
+
)}
|
|
117
|
+
</>
|
|
118
|
+
)}
|
|
119
|
+
</span>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRef, useEffect, useCallback, useMemo } from "react";
|
|
4
|
+
import { useFrame, ThreeEvent } from "@react-three/fiber";
|
|
5
|
+
import * as THREE from "three";
|
|
6
|
+
import { PlacedWallet } from "@/types/wallet";
|
|
7
|
+
import {
|
|
8
|
+
getBuildingDimensions,
|
|
9
|
+
getBuildingColor,
|
|
10
|
+
getWindowCols,
|
|
11
|
+
getWindowFillRatio,
|
|
12
|
+
getLitRatio,
|
|
13
|
+
getInstanceSeed,
|
|
14
|
+
getWindowRows,
|
|
15
|
+
floors,
|
|
16
|
+
} from "@/lib/building-math";
|
|
17
|
+
import { createBuildingMaterial } from "@/lib/building-shader";
|
|
18
|
+
import { CELL_SIZE, ROAD_WIDTH, BLOCK_SIZE, BLOCK_STRIDE, GRID_WORLD } from "@/lib/city-constants";
|
|
19
|
+
import { WindowHoverInfo } from "./WindowTooltip";
|
|
20
|
+
|
|
21
|
+
interface InstancedBuildingsProps {
|
|
22
|
+
wallets: PlacedWallet[];
|
|
23
|
+
onSelectWallet: (wallet: PlacedWallet, position: [number, number, number]) => void;
|
|
24
|
+
onHoverWindow?: (info: WindowHoverInfo | null) => void;
|
|
25
|
+
timeRef: React.MutableRefObject<number>;
|
|
26
|
+
selectedAddress?: string | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Compute world position for a placed wallet */
|
|
30
|
+
function walletWorldPos(
|
|
31
|
+
w: PlacedWallet,
|
|
32
|
+
dims: { width: number; depth: number; height: number }
|
|
33
|
+
): [number, number, number] {
|
|
34
|
+
const offsetX = -GRID_WORLD / 2 + BLOCK_SIZE / 2 + ROAD_WIDTH / 2;
|
|
35
|
+
const offsetZ = -GRID_WORLD / 2 + BLOCK_SIZE / 2 + ROAD_WIDTH / 2;
|
|
36
|
+
|
|
37
|
+
const blockOriginX = offsetX + w.blockCol * BLOCK_STRIDE - BLOCK_SIZE / 2;
|
|
38
|
+
const blockOriginZ = offsetZ + w.blockRow * BLOCK_STRIDE - BLOCK_SIZE / 2;
|
|
39
|
+
|
|
40
|
+
const localRow = Math.floor(w.localSlot / 4);
|
|
41
|
+
const localCol = w.localSlot % 4;
|
|
42
|
+
|
|
43
|
+
const x = blockOriginX + localCol * CELL_SIZE + dims.width / 2 + 0.5;
|
|
44
|
+
const z = blockOriginZ + localRow * CELL_SIZE + dims.depth / 2 + 0.5;
|
|
45
|
+
const y = dims.height / 2;
|
|
46
|
+
|
|
47
|
+
return [x, y, z];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const _dummy = new THREE.Object3D();
|
|
51
|
+
const _color = new THREE.Color();
|
|
52
|
+
|
|
53
|
+
export default function InstancedBuildings({
|
|
54
|
+
wallets,
|
|
55
|
+
onSelectWallet,
|
|
56
|
+
onHoverWindow,
|
|
57
|
+
timeRef,
|
|
58
|
+
selectedAddress,
|
|
59
|
+
}: InstancedBuildingsProps) {
|
|
60
|
+
const meshRef = useRef<THREE.InstancedMesh>(null);
|
|
61
|
+
const hoveredId = useRef<number | null>(null);
|
|
62
|
+
|
|
63
|
+
// Time uniforms for shader
|
|
64
|
+
const timeUniformRef = useRef<{ value: number }>({ value: 0 });
|
|
65
|
+
const elapsedUniformRef = useRef<{ value: number }>({ value: 0 });
|
|
66
|
+
|
|
67
|
+
// ShaderMaterial with standard PBR + window logic baked in (no onBeforeCompile)
|
|
68
|
+
const material = useMemo(
|
|
69
|
+
() => createBuildingMaterial(timeUniformRef.current, elapsedUniformRef.current),
|
|
70
|
+
[],
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Precompute dimensions and colors
|
|
74
|
+
const buildingData = useMemo(() => {
|
|
75
|
+
return wallets.map((w) => {
|
|
76
|
+
const dims = getBuildingDimensions(w);
|
|
77
|
+
const color = getBuildingColor(dims.height);
|
|
78
|
+
const pos = walletWorldPos(w, dims);
|
|
79
|
+
return { dims, color, pos };
|
|
80
|
+
});
|
|
81
|
+
}, [wallets]);
|
|
82
|
+
|
|
83
|
+
// Set instance matrices, colors, and window attributes after mount
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
const mesh = meshRef.current;
|
|
86
|
+
if (!mesh || wallets.length === 0) return;
|
|
87
|
+
|
|
88
|
+
const count = wallets.length;
|
|
89
|
+
const colors = new Float32Array(count * 3);
|
|
90
|
+
const windowColsArr = new Float32Array(count);
|
|
91
|
+
const fillRatioArr = new Float32Array(count);
|
|
92
|
+
const litRatioArr = new Float32Array(count);
|
|
93
|
+
const seedArr = new Float32Array(count);
|
|
94
|
+
const floorsArr = new Float32Array(count);
|
|
95
|
+
|
|
96
|
+
for (let i = 0; i < count; i++) {
|
|
97
|
+
const w = wallets[i];
|
|
98
|
+
const { dims, color, pos } = buildingData[i];
|
|
99
|
+
|
|
100
|
+
_dummy.position.set(pos[0], pos[1], pos[2]);
|
|
101
|
+
_dummy.scale.set(dims.width, dims.height, dims.depth);
|
|
102
|
+
_dummy.rotation.set(0, 0, 0);
|
|
103
|
+
_dummy.updateMatrix();
|
|
104
|
+
mesh.setMatrixAt(i, _dummy.matrix);
|
|
105
|
+
|
|
106
|
+
_color.set(color);
|
|
107
|
+
colors[i * 3] = _color.r;
|
|
108
|
+
colors[i * 3 + 1] = _color.g;
|
|
109
|
+
colors[i * 3 + 2] = _color.b;
|
|
110
|
+
|
|
111
|
+
// Window attributes
|
|
112
|
+
windowColsArr[i] = getWindowCols(dims.width);
|
|
113
|
+
fillRatioArr[i] = getWindowFillRatio(w.uniqueTokensSwapped ?? 0);
|
|
114
|
+
litRatioArr[i] = getLitRatio(w.latestBlocktime);
|
|
115
|
+
seedArr[i] = getInstanceSeed(w.address);
|
|
116
|
+
floorsArr[i] = getWindowRows(floors(w.txnCount));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
mesh.instanceMatrix.needsUpdate = true;
|
|
120
|
+
|
|
121
|
+
// Building color via custom attribute (bypasses Three.js instancing color)
|
|
122
|
+
mesh.geometry.setAttribute(
|
|
123
|
+
"instanceBuildingColor",
|
|
124
|
+
new THREE.InstancedBufferAttribute(colors, 3)
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// Highlight attribute for hover effect (0.0 = normal, 1.0 = hovered)
|
|
128
|
+
const highlightArr = new Float32Array(count);
|
|
129
|
+
mesh.geometry.setAttribute(
|
|
130
|
+
"instanceHighlight",
|
|
131
|
+
new THREE.InstancedBufferAttribute(highlightArr, 1)
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// Window attributes
|
|
135
|
+
mesh.geometry.setAttribute(
|
|
136
|
+
"instanceWindowCols",
|
|
137
|
+
new THREE.InstancedBufferAttribute(windowColsArr, 1)
|
|
138
|
+
);
|
|
139
|
+
mesh.geometry.setAttribute(
|
|
140
|
+
"instanceFillRatio",
|
|
141
|
+
new THREE.InstancedBufferAttribute(fillRatioArr, 1)
|
|
142
|
+
);
|
|
143
|
+
mesh.geometry.setAttribute(
|
|
144
|
+
"instanceLitRatio",
|
|
145
|
+
new THREE.InstancedBufferAttribute(litRatioArr, 1)
|
|
146
|
+
);
|
|
147
|
+
mesh.geometry.setAttribute(
|
|
148
|
+
"instanceSeed",
|
|
149
|
+
new THREE.InstancedBufferAttribute(seedArr, 1)
|
|
150
|
+
);
|
|
151
|
+
mesh.geometry.setAttribute(
|
|
152
|
+
"instanceFloors",
|
|
153
|
+
new THREE.InstancedBufferAttribute(floorsArr, 1)
|
|
154
|
+
);
|
|
155
|
+
}, [wallets, buildingData]);
|
|
156
|
+
|
|
157
|
+
// Hover effect + time uniform update
|
|
158
|
+
useFrame((state) => {
|
|
159
|
+
timeUniformRef.current.value = timeRef.current;
|
|
160
|
+
elapsedUniformRef.current.value = state.clock.elapsedTime;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const handlePointerMove = useCallback(
|
|
164
|
+
(e: ThreeEvent<PointerEvent>) => {
|
|
165
|
+
e.stopPropagation();
|
|
166
|
+
const id = e.instanceId;
|
|
167
|
+
if (id === undefined) {
|
|
168
|
+
onHoverWindow?.(null);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Update building highlight only when instance changes
|
|
173
|
+
const prev = hoveredId.current;
|
|
174
|
+
if (prev !== id) {
|
|
175
|
+
const mesh = meshRef.current;
|
|
176
|
+
if (mesh) {
|
|
177
|
+
const highlightAttr = mesh.geometry.getAttribute(
|
|
178
|
+
"instanceHighlight"
|
|
179
|
+
) as THREE.InstancedBufferAttribute;
|
|
180
|
+
if (highlightAttr) {
|
|
181
|
+
if (prev !== null) {
|
|
182
|
+
highlightAttr.setX(prev, 0.0);
|
|
183
|
+
}
|
|
184
|
+
highlightAttr.setX(id, 1.0);
|
|
185
|
+
highlightAttr.needsUpdate = true;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
hoveredId.current = id;
|
|
189
|
+
document.body.style.cursor = "pointer";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Tooltip logic
|
|
193
|
+
if (!onHoverWindow) return;
|
|
194
|
+
|
|
195
|
+
const w = wallets[id];
|
|
196
|
+
|
|
197
|
+
// If this building is NOT the selected one, show building-level tooltip
|
|
198
|
+
if (w.address !== selectedAddress) {
|
|
199
|
+
onHoverWindow({
|
|
200
|
+
address: w.address,
|
|
201
|
+
tokenIndex: 0,
|
|
202
|
+
screenX: e.nativeEvent.clientX,
|
|
203
|
+
screenY: e.nativeEvent.clientY,
|
|
204
|
+
mode: "building",
|
|
205
|
+
identityName: w.identityName,
|
|
206
|
+
identityType: w.identityType,
|
|
207
|
+
identityCategory: w.identityCategory,
|
|
208
|
+
});
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Selected building — window-level token hit detection
|
|
213
|
+
const faceNormal = e.face?.normal;
|
|
214
|
+
if (!faceNormal || Math.abs(faceNormal.y) > 0.5) {
|
|
215
|
+
onHoverWindow(null);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const { dims, pos } = buildingData[id];
|
|
220
|
+
|
|
221
|
+
// Convert world hit to local box coords (-0.5 to 0.5)
|
|
222
|
+
const localX = (e.point.x - pos[0]) / dims.width;
|
|
223
|
+
const localY = (e.point.y - pos[1]) / dims.height;
|
|
224
|
+
const localZ = (e.point.z - pos[2]) / dims.depth;
|
|
225
|
+
|
|
226
|
+
// Face UV (matching shader logic)
|
|
227
|
+
let faceU: number, faceV: number;
|
|
228
|
+
if (Math.abs(faceNormal.x) > 0.5) {
|
|
229
|
+
faceU = localZ + 0.5;
|
|
230
|
+
faceV = localY + 0.5;
|
|
231
|
+
} else {
|
|
232
|
+
faceU = localX + 0.5;
|
|
233
|
+
faceV = localY + 0.5;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const cols = getWindowCols(dims.width);
|
|
237
|
+
const rows = getWindowRows(floors(w.txnCount));
|
|
238
|
+
|
|
239
|
+
if (cols < 1 || rows < 1) {
|
|
240
|
+
onHoverWindow(null);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const colIdx = Math.floor(faceU * cols);
|
|
245
|
+
const rowIdx = Math.floor(faceV * rows);
|
|
246
|
+
const cellU = faceU * cols - colIdx;
|
|
247
|
+
const cellV = faceV * rows - rowIdx;
|
|
248
|
+
|
|
249
|
+
if (!(cellU > 0.2 && cellU < 0.8 && cellV > 0.2 && cellV < 0.8)) {
|
|
250
|
+
onHoverWindow(null);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Deterministic token index from window position
|
|
255
|
+
const seed = getInstanceSeed(w.address);
|
|
256
|
+
const tokenIndex = Math.abs(
|
|
257
|
+
(colIdx * 127 + rowIdx * 311 + Math.floor(seed * 10007)) | 0
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
onHoverWindow({
|
|
261
|
+
address: w.address,
|
|
262
|
+
tokenIndex,
|
|
263
|
+
screenX: e.nativeEvent.clientX,
|
|
264
|
+
screenY: e.nativeEvent.clientY,
|
|
265
|
+
mode: "token",
|
|
266
|
+
});
|
|
267
|
+
},
|
|
268
|
+
[wallets, buildingData, onHoverWindow, selectedAddress]
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const handlePointerOut = useCallback(() => {
|
|
272
|
+
const prev = hoveredId.current;
|
|
273
|
+
const mesh = meshRef.current;
|
|
274
|
+
if (prev !== null && mesh) {
|
|
275
|
+
const highlightAttr = mesh.geometry.getAttribute(
|
|
276
|
+
"instanceHighlight"
|
|
277
|
+
) as THREE.InstancedBufferAttribute;
|
|
278
|
+
if (highlightAttr) {
|
|
279
|
+
highlightAttr.setX(prev, 0.0);
|
|
280
|
+
highlightAttr.needsUpdate = true;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
hoveredId.current = null;
|
|
284
|
+
document.body.style.cursor = "default";
|
|
285
|
+
onHoverWindow?.(null);
|
|
286
|
+
}, [onHoverWindow]);
|
|
287
|
+
|
|
288
|
+
const handleClick = useCallback(
|
|
289
|
+
(e: ThreeEvent<MouseEvent>) => {
|
|
290
|
+
e.stopPropagation();
|
|
291
|
+
const id = e.instanceId;
|
|
292
|
+
if (id === undefined || id >= wallets.length) return;
|
|
293
|
+
const wallet = wallets[id];
|
|
294
|
+
const { pos } = buildingData[id];
|
|
295
|
+
onSelectWallet(wallet, pos);
|
|
296
|
+
},
|
|
297
|
+
[wallets, buildingData, onSelectWallet]
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
if (wallets.length === 0) return null;
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
<instancedMesh
|
|
304
|
+
key={wallets.length}
|
|
305
|
+
ref={meshRef}
|
|
306
|
+
args={[undefined!, undefined!, wallets.length]}
|
|
307
|
+
frustumCulled={false}
|
|
308
|
+
onPointerMove={handlePointerMove}
|
|
309
|
+
onPointerOut={handlePointerOut}
|
|
310
|
+
onClick={handleClick}
|
|
311
|
+
>
|
|
312
|
+
<boxGeometry args={[1, 1, 1]} />
|
|
313
|
+
<primitive object={material} attach="material" />
|
|
314
|
+
</instancedMesh>
|
|
315
|
+
);
|
|
316
|
+
}
|