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,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
+ }