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,280 @@
1
+ "use client";
2
+
3
+ import { useRef, useMemo, useEffect } from "react";
4
+ import { useFrame } from "@react-three/fiber";
5
+ import * as THREE from "three";
6
+ import { OceanAmbient, maybePlaySeagull, initAudio } from "@/lib/sound-engine";
7
+
8
+ // ─── Constants ────────────────────────────────────────────────
9
+
10
+ const OCEAN_SIZE = 600;
11
+ const OCEAN_OFFSET = 180; // how far from city centre the beach starts
12
+ const BEACH_DEPTH = 40; // sandy strip width
13
+ const WAVE_SEGMENTS = 128;
14
+ const SEAGULL_COUNT = 18;
15
+ const BOAT_COUNT = 50;
16
+
17
+ // ─── Helpers ──────────────────────────────────────────────────
18
+
19
+ function seededRandom(seed: number): number {
20
+ const x = Math.sin(seed * 127.1 + 311.7) * 43758.5453;
21
+ return x - Math.floor(x);
22
+ }
23
+
24
+ // ─── Ocean surface (animated waves via vertex shader) ─────────
25
+
26
+ function Ocean() {
27
+ const meshRef = useRef<THREE.Mesh>(null);
28
+ const geoRef = useRef<THREE.PlaneGeometry>(null);
29
+
30
+ useFrame(({ clock }) => {
31
+ if (!geoRef.current) return;
32
+ const t = clock.getElapsedTime();
33
+ const pos = geoRef.current.attributes.position;
34
+ for (let i = 0; i < pos.count; i++) {
35
+ const x = pos.getX(i);
36
+ const z = pos.getZ(i);
37
+ const y =
38
+ Math.sin(x * 0.08 + t * 0.6) * 0.8 +
39
+ Math.sin(z * 0.12 + t * 0.8) * 0.5 +
40
+ Math.sin((x + z) * 0.05 + t * 0.3) * 1.2;
41
+ pos.setY(i, y);
42
+ }
43
+ pos.needsUpdate = true;
44
+ geoRef.current.computeVertexNormals();
45
+ });
46
+
47
+ return (
48
+ <mesh
49
+ ref={meshRef}
50
+ rotation={[-Math.PI / 2, 0, 0]}
51
+ position={[0, -0.5, -(OCEAN_OFFSET + OCEAN_SIZE / 2)]}
52
+ receiveShadow
53
+ >
54
+ <planeGeometry ref={geoRef} args={[OCEAN_SIZE * 2, OCEAN_SIZE, WAVE_SEGMENTS, WAVE_SEGMENTS]} />
55
+ <meshStandardMaterial
56
+ color="#0c4a6e"
57
+ transparent
58
+ opacity={0.85}
59
+ metalness={0.3}
60
+ roughness={0.4}
61
+ side={THREE.DoubleSide}
62
+ />
63
+ </mesh>
64
+ );
65
+ }
66
+
67
+ // ─── Sandy beach strip ────────────────────────────────────────
68
+
69
+ function Beach() {
70
+ return (
71
+ <mesh
72
+ rotation={[-Math.PI / 2, 0, 0]}
73
+ position={[0, -0.08, -(OCEAN_OFFSET - BEACH_DEPTH / 2)]}
74
+ receiveShadow
75
+ >
76
+ <planeGeometry args={[OCEAN_SIZE * 2, BEACH_DEPTH]} />
77
+ <meshStandardMaterial color="#e8c97a" roughness={0.95} metalness={0} />
78
+ </mesh>
79
+ );
80
+ }
81
+
82
+ // ─── Foam line (white strip at water's edge) ──────────────────
83
+
84
+ function Foam() {
85
+ const ref = useRef<THREE.Mesh>(null);
86
+
87
+ useFrame(({ clock }) => {
88
+ if (!ref.current) return;
89
+ const t = clock.getElapsedTime();
90
+ ref.current.position.z = -(OCEAN_OFFSET) + Math.sin(t * 0.5) * 1.5;
91
+ (ref.current.material as THREE.MeshStandardMaterial).opacity =
92
+ 0.3 + Math.sin(t * 0.8) * 0.15;
93
+ });
94
+
95
+ return (
96
+ <mesh ref={ref} rotation={[-Math.PI / 2, 0, 0]} position={[0, 0.01, -OCEAN_OFFSET]}>
97
+ <planeGeometry args={[OCEAN_SIZE * 2, 3]} />
98
+ <meshStandardMaterial color="#ffffff" transparent opacity={0.4} roughness={1} />
99
+ </mesh>
100
+ );
101
+ }
102
+
103
+ // ─── Seagulls (simple animated "V" shapes) ────────────────────
104
+
105
+ function Seagulls() {
106
+ const groupRef = useRef<THREE.Group>(null);
107
+
108
+ const birds = useMemo(() => {
109
+ return Array.from({ length: SEAGULL_COUNT }, (_, i) => ({
110
+ id: i,
111
+ cx: (seededRandom(i * 3) - 0.5) * 400,
112
+ cy: 25 + seededRandom(i * 3 + 1) * 30,
113
+ cz: -(OCEAN_OFFSET + seededRandom(i * 3 + 2) * OCEAN_SIZE * 0.4),
114
+ radius: 15 + seededRandom(i * 7) * 30,
115
+ speed: 0.15 + seededRandom(i * 11) * 0.25,
116
+ phaseOffset: seededRandom(i * 13) * Math.PI * 2,
117
+ flapSpeed: 3 + seededRandom(i * 17) * 3,
118
+ }));
119
+ }, []);
120
+
121
+ useFrame(({ clock }) => {
122
+ if (!groupRef.current) return;
123
+ const t = clock.getElapsedTime();
124
+ groupRef.current.children.forEach((group, i) => {
125
+ const b = birds[i];
126
+ const angle = t * b.speed + b.phaseOffset;
127
+ group.position.set(
128
+ b.cx + Math.cos(angle) * b.radius,
129
+ b.cy + Math.sin(t * 0.5 + b.phaseOffset) * 2,
130
+ b.cz + Math.sin(angle) * b.radius * 0.5,
131
+ );
132
+ group.rotation.y = angle + Math.PI / 2;
133
+
134
+ // Wing flap
135
+ const flap = Math.sin(t * b.flapSpeed + b.phaseOffset) * 0.4;
136
+ const leftWing = group.children[0] as THREE.Mesh;
137
+ const rightWing = group.children[1] as THREE.Mesh;
138
+ if (leftWing) leftWing.rotation.z = flap;
139
+ if (rightWing) rightWing.rotation.z = -flap;
140
+ });
141
+ });
142
+
143
+ return (
144
+ <group ref={groupRef}>
145
+ {birds.map((b) => (
146
+ <group key={b.id}>
147
+ {/* Left wing */}
148
+ <mesh position={[-0.4, 0, 0]}>
149
+ <boxGeometry args={[0.8, 0.05, 0.15]} />
150
+ <meshStandardMaterial color="#e8e8e8" />
151
+ </mesh>
152
+ {/* Right wing */}
153
+ <mesh position={[0.4, 0, 0]}>
154
+ <boxGeometry args={[0.8, 0.05, 0.15]} />
155
+ <meshStandardMaterial color="#e8e8e8" />
156
+ </mesh>
157
+ {/* Body */}
158
+ <mesh>
159
+ <boxGeometry args={[0.12, 0.08, 0.5]} />
160
+ <meshStandardMaterial color="#d4d4d4" />
161
+ </mesh>
162
+ </group>
163
+ ))}
164
+ </group>
165
+ );
166
+ }
167
+
168
+ // ─── Boats (50 boats floating on the ocean) ───────────────────
169
+
170
+ interface BoatData {
171
+ id: number;
172
+ x: number;
173
+ z: number;
174
+ heading: number;
175
+ speed: number;
176
+ size: number;
177
+ hue: number;
178
+ }
179
+
180
+ function Boats() {
181
+ const groupRef = useRef<THREE.Group>(null);
182
+
183
+ const boats = useMemo<BoatData[]>(() => {
184
+ return Array.from({ length: BOAT_COUNT }, (_, i) => ({
185
+ id: i,
186
+ x: (seededRandom(i * 31) - 0.5) * OCEAN_SIZE * 1.5,
187
+ z: -(OCEAN_OFFSET + 20 + seededRandom(i * 37) * OCEAN_SIZE * 0.6),
188
+ heading: seededRandom(i * 41) * Math.PI * 2,
189
+ speed: 0.5 + seededRandom(i * 43) * 2,
190
+ size: 0.7 + seededRandom(i * 47) * 0.6,
191
+ hue: Math.floor(seededRandom(i * 53) * 360),
192
+ }));
193
+ }, []);
194
+
195
+ useFrame(({ clock }) => {
196
+ if (!groupRef.current) return;
197
+ const t = clock.getElapsedTime();
198
+ groupRef.current.children.forEach((boat, i) => {
199
+ const b = boats[i];
200
+ // Gentle cruising motion
201
+ const dx = Math.sin(b.heading) * b.speed * 0.3;
202
+ const dz = Math.cos(b.heading) * b.speed * 0.3;
203
+ boat.position.set(
204
+ b.x + dx * t * 0.1 + Math.sin(t * 0.2 + i) * 3,
205
+ Math.sin(t * 0.8 + i * 0.7) * 0.6,
206
+ b.z + dz * t * 0.1 + Math.cos(t * 0.15 + i) * 3,
207
+ );
208
+ // Rock on waves
209
+ boat.rotation.x = Math.sin(t * 0.7 + i * 0.5) * 0.06;
210
+ boat.rotation.z = Math.sin(t * 0.5 + i * 0.3) * 0.04;
211
+ boat.rotation.y = b.heading + Math.sin(t * 0.1 + i) * 0.05;
212
+ });
213
+ });
214
+
215
+ return (
216
+ <group ref={groupRef}>
217
+ {boats.map((b) => {
218
+ const color = `hsl(${b.hue}, 60%, 50%)`;
219
+ const s = b.size;
220
+ return (
221
+ <group key={b.id} position={[b.x, 0, b.z]}>
222
+ {/* Hull */}
223
+ <mesh>
224
+ <boxGeometry args={[2 * s, 0.6 * s, 5 * s]} />
225
+ <meshStandardMaterial color={color} roughness={0.6} />
226
+ </mesh>
227
+ {/* Cabin */}
228
+ <mesh position={[0, 0.5 * s, -0.3 * s]}>
229
+ <boxGeometry args={[1.2 * s, 0.7 * s, 1.5 * s]} />
230
+ <meshStandardMaterial color="#f0f0f0" roughness={0.5} />
231
+ </mesh>
232
+ {/* Mast / pole */}
233
+ <mesh position={[0, 1.2 * s, 0.5 * s]}>
234
+ <boxGeometry args={[0.08 * s, 1.5 * s, 0.08 * s]} />
235
+ <meshStandardMaterial color="#8B7355" roughness={0.8} />
236
+ </mesh>
237
+ </group>
238
+ );
239
+ })}
240
+ </group>
241
+ );
242
+ }
243
+
244
+ // ─── Main Beach Scene Export ──────────────────────────────────
245
+
246
+ export default function BeachScene() {
247
+ const oceanSoundRef = useRef<OceanAmbient | null>(null);
248
+ const startedRef = useRef(false);
249
+
250
+ // Start ocean ambient on first user interaction
251
+ useFrame(() => {
252
+ if (!startedRef.current) {
253
+ try {
254
+ initAudio();
255
+ const oa = new OceanAmbient();
256
+ oa.start();
257
+ oceanSoundRef.current = oa;
258
+ startedRef.current = true;
259
+ } catch { /* AudioContext not ready yet */ }
260
+ }
261
+ // Random seagull calls
262
+ maybePlaySeagull();
263
+ });
264
+
265
+ useEffect(() => {
266
+ return () => {
267
+ oceanSoundRef.current?.stop();
268
+ };
269
+ }, []);
270
+
271
+ return (
272
+ <group>
273
+ <Beach />
274
+ <Foam />
275
+ <Ocean />
276
+ <Seagulls />
277
+ <Boats />
278
+ </group>
279
+ );
280
+ }
@@ -0,0 +1,138 @@
1
+ "use client";
2
+
3
+ import { useRef, useState, useMemo } from "react";
4
+ import { useFrame } from "@react-three/fiber";
5
+ import { Mesh, Color, AdditiveBlending } from "three";
6
+ import { WalletBuilding } from "@/types/wallet";
7
+ import { getBuildingDimensions, getBuildingColor } from "@/lib/building-math";
8
+
9
+ interface BuildingProps {
10
+ wallet: WalletBuilding;
11
+ position: [number, number, number];
12
+ selected?: boolean;
13
+ onSelect: (wallet: WalletBuilding, position: [number, number, number]) => void;
14
+ }
15
+
16
+ const BEAM_HEIGHT = 60;
17
+
18
+ /**
19
+ * Selection effect — only mounted when a building is selected.
20
+ * Isolates the useFrame callback so unselected buildings have zero per-frame cost.
21
+ */
22
+ function SelectionEffect({
23
+ position,
24
+ height,
25
+ width,
26
+ depth,
27
+ color,
28
+ }: {
29
+ position: [number, number, number];
30
+ height: number;
31
+ width: number;
32
+ depth: number;
33
+ color: Color;
34
+ }) {
35
+ const glowRef = useRef<Mesh>(null);
36
+ const beamRef = useRef<Mesh>(null);
37
+ const buildingMatRef = useRef<import("three").MeshStandardMaterial>(null);
38
+
39
+ useFrame(({ clock }) => {
40
+ const t = clock.getElapsedTime();
41
+ const pulse = 0.4 + Math.sin(t * 2.5) * 0.2;
42
+
43
+ if (buildingMatRef.current) {
44
+ buildingMatRef.current.emissiveIntensity = pulse;
45
+ }
46
+ if (glowRef.current) {
47
+ glowRef.current.scale.set(1 + pulse * 0.15, 1, 1 + pulse * 0.15);
48
+ const gMat = glowRef.current.material as import("three").MeshBasicMaterial;
49
+ gMat.opacity = 0.12 + Math.sin(t * 2.5) * 0.06;
50
+ }
51
+ if (beamRef.current) {
52
+ const bMat = beamRef.current.material as import("three").MeshBasicMaterial;
53
+ bMat.opacity = 0.15 + Math.sin(t * 3) * 0.07;
54
+ }
55
+ });
56
+
57
+ return (
58
+ <>
59
+ {/* Invisible overlay mesh to drive emissive on the building —
60
+ we attach the material ref here and sync it in useFrame.
61
+ Actually, let's just render the glow shell and beam. */}
62
+ <mesh ref={glowRef} position={position}>
63
+ <boxGeometry args={[width + 0.4, height + 0.2, depth + 0.4]} />
64
+ <meshBasicMaterial
65
+ color={color}
66
+ transparent
67
+ opacity={0.15}
68
+ blending={AdditiveBlending}
69
+ depthWrite={false}
70
+ />
71
+ </mesh>
72
+
73
+ <mesh
74
+ ref={beamRef}
75
+ position={[position[0], position[1] - height / 2 + height + BEAM_HEIGHT / 2, position[2]]}
76
+ >
77
+ <cylinderGeometry args={[0.08, Math.max(width, depth) * 0.4, BEAM_HEIGHT, 8, 1, true]} />
78
+ <meshBasicMaterial
79
+ color={color}
80
+ transparent
81
+ opacity={0.18}
82
+ blending={AdditiveBlending}
83
+ depthWrite={false}
84
+ side={2}
85
+ />
86
+ </mesh>
87
+ </>
88
+ );
89
+ }
90
+
91
+ export default function Building({ wallet, position, selected, onSelect }: BuildingProps) {
92
+ const [hovered, setHovered] = useState(false);
93
+
94
+ const { height, width, depth } = getBuildingDimensions(wallet);
95
+ const color = getBuildingColor(height);
96
+ const colorObj = useMemo(() => new Color(color), [color]);
97
+
98
+ const y = position[1] + height / 2;
99
+
100
+ return (
101
+ <group>
102
+ <mesh
103
+ position={[position[0], y, position[2]]}
104
+ onPointerOver={(e) => {
105
+ e.stopPropagation();
106
+ setHovered(true);
107
+ document.body.style.cursor = "pointer";
108
+ }}
109
+ onPointerOut={() => {
110
+ setHovered(false);
111
+ document.body.style.cursor = "default";
112
+ }}
113
+ onClick={(e) => {
114
+ e.stopPropagation();
115
+ onSelect(wallet, [position[0], y, position[2]]);
116
+ }}
117
+ >
118
+ <boxGeometry args={[width, height, depth]} />
119
+ <meshStandardMaterial
120
+ color={hovered || selected ? "#ffffff" : color}
121
+ emissive={hovered ? color : selected ? colorObj : "#000000"}
122
+ emissiveIntensity={hovered ? 0.3 : selected ? 0.5 : 0}
123
+ />
124
+ </mesh>
125
+
126
+ {/* Only mounted when selected — no per-frame cost for unselected buildings */}
127
+ {selected && (
128
+ <SelectionEffect
129
+ position={[position[0], y, position[2]]}
130
+ height={height}
131
+ width={width}
132
+ depth={depth}
133
+ color={colorObj}
134
+ />
135
+ )}
136
+ </group>
137
+ );
138
+ }