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,259 @@
1
+ "use client";
2
+
3
+ import { useRef, useEffect, useMemo, useCallback } from "react";
4
+ import { useFrame, ThreeEvent } from "@react-three/fiber";
5
+ import * as THREE from "three";
6
+ import { PlacedWallet } from "@/types/wallet";
7
+ import { getWalletWorldPosition, getBuildingDimensions, getInstanceSeed } from "@/lib/building-math";
8
+ import { MAX_PLANES } from "@/lib/city-slots";
9
+ import { WindowHoverInfo } from "./WindowTooltip";
10
+
11
+ interface InstancedCityPlanesProps {
12
+ wallets: PlacedWallet[];
13
+ timeRef: React.MutableRefObject<number>;
14
+ onHoverPlane?: (info: WindowHoverInfo | null) => void;
15
+ onClickPlane?: (wallet: PlacedWallet, position: [number, number, number]) => void;
16
+ positionsRef?: React.MutableRefObject<Array<{ x: number; y: number; z: number }>>;
17
+ }
18
+
19
+ // Each plane has a unique orbit
20
+ interface PlaneOrbit {
21
+ cx: number; // orbit center X (building X)
22
+ cz: number; // orbit center Z (building Z)
23
+ radius: number; // orbit radius
24
+ altitude: number; // flight altitude
25
+ speed: number; // angular speed (rad/s)
26
+ phase: number; // starting phase
27
+ bankScale: number; // visual roll scale
28
+ }
29
+
30
+ const _dummy = new THREE.Object3D();
31
+ const _color = new THREE.Color();
32
+
33
+ export default function InstancedCityPlanes({
34
+ wallets,
35
+ timeRef,
36
+ onHoverPlane,
37
+ onClickPlane,
38
+ positionsRef,
39
+ }: InstancedCityPlanesProps) {
40
+ const count = Math.min(wallets.length, MAX_PLANES);
41
+ const bodyRef = useRef<THREE.InstancedMesh>(null);
42
+ const wingRef = useRef<THREE.InstancedMesh>(null);
43
+ const tailRef = useRef<THREE.InstancedMesh>(null);
44
+ const hoveredIdx = useRef<number | null>(null);
45
+ const elapsedRef = useRef(0);
46
+
47
+ // Compute orbits — each plane circles above its wallet's building
48
+ const orbits: PlaneOrbit[] = useMemo(() => {
49
+ const result: PlaneOrbit[] = [];
50
+ for (let i = 0; i < count; i++) {
51
+ const w = wallets[i];
52
+ const dims = getBuildingDimensions(w);
53
+ const pos = getWalletWorldPosition(w, dims);
54
+ const seed = getInstanceSeed(w.address);
55
+ const seed2 = getInstanceSeed(w.address + "orbit");
56
+
57
+ result.push({
58
+ cx: pos[0],
59
+ cz: pos[2],
60
+ radius: 8 + seed * 18, // 8–26 units orbit radius
61
+ altitude: 30 + seed2 * 50 + dims.height, // above building
62
+ speed: 0.3 + seed * 0.5, // 0.3–0.8 rad/s
63
+ phase: seed2 * Math.PI * 2, // random start angle
64
+ bankScale: 0.15 + seed * 0.15, // visual bank intensity
65
+ });
66
+ }
67
+ return result;
68
+ }, [wallets, count]);
69
+
70
+ // Materials
71
+ const bodyMat = useMemo(() => new THREE.MeshStandardMaterial({
72
+ color: "#E35930",
73
+ metalness: 0.6,
74
+ roughness: 0.3,
75
+ }), []);
76
+
77
+ const wingMat = useMemo(() => new THREE.MeshStandardMaterial({
78
+ color: "#c8c8c8",
79
+ metalness: 0.7,
80
+ roughness: 0.2,
81
+ }), []);
82
+
83
+ const tailMat = useMemo(() => new THREE.MeshStandardMaterial({
84
+ color: "#E35930",
85
+ metalness: 0.5,
86
+ roughness: 0.35,
87
+ }), []);
88
+
89
+ // Instance colors — tint each plane slightly based on wallet seed
90
+ useEffect(() => {
91
+ if (!bodyRef.current || count === 0) return;
92
+ const bodyMesh = bodyRef.current;
93
+ const wingMesh = wingRef.current;
94
+ const tailMesh = tailRef.current;
95
+
96
+ for (let i = 0; i < count; i++) {
97
+ const seed = getInstanceSeed(wallets[i].address);
98
+ // Warm color palette: orange → coral → amber
99
+ const hue = 0.03 + seed * 0.06; // 0.03–0.09
100
+ const sat = 0.75 + seed * 0.2;
101
+ const light = 0.45 + seed * 0.15;
102
+ _color.setHSL(hue, sat, light);
103
+ bodyMesh.setColorAt(i, _color);
104
+ tailMesh?.setColorAt(i, _color);
105
+
106
+ // Wings stay silver/gray
107
+ _color.setHSL(0, 0, 0.7 + seed * 0.15);
108
+ wingMesh?.setColorAt(i, _color);
109
+ }
110
+
111
+ if (bodyMesh.instanceColor) bodyMesh.instanceColor.needsUpdate = true;
112
+ if (wingMesh?.instanceColor) wingMesh.instanceColor.needsUpdate = true;
113
+ if (tailMesh?.instanceColor) tailMesh.instanceColor.needsUpdate = true;
114
+ }, [wallets, count]);
115
+
116
+ // Animate all planes
117
+ useFrame((_, delta) => {
118
+ if (count === 0) return;
119
+ const body = bodyRef.current;
120
+ const wing = wingRef.current;
121
+ const tail = tailRef.current;
122
+ if (!body || !wing || !tail) return;
123
+
124
+ elapsedRef.current += delta;
125
+ const t = elapsedRef.current;
126
+
127
+ // Prepare positions output for HUD/projectiles
128
+ const posOut = positionsRef ? [] as Array<{ x: number; y: number; z: number }> : null;
129
+
130
+ for (let i = 0; i < count; i++) {
131
+ const orb = orbits[i];
132
+ const angle = orb.phase + t * orb.speed;
133
+
134
+ // Position along circular orbit
135
+ const x = orb.cx + Math.cos(angle) * orb.radius;
136
+ const z = orb.cz + Math.sin(angle) * orb.radius;
137
+ // Gentle altitude oscillation
138
+ const y = orb.altitude + Math.sin(t * 0.5 + orb.phase) * 2;
139
+
140
+ if (posOut) posOut.push({ x, y, z });
141
+
142
+ // Heading — tangent to circle (perpendicular to radius)
143
+ const heading = angle + Math.PI / 2;
144
+ // Bank into the turn
145
+ const bank = -orb.bankScale;
146
+ // Gentle pitch oscillation
147
+ const pitch = Math.sin(t * 0.8 + orb.phase) * 0.05;
148
+
149
+ // Fuselage body (capsule-like box)
150
+ _dummy.position.set(x, y, z);
151
+ _dummy.rotation.set(pitch, heading, bank, "YXZ");
152
+ _dummy.scale.set(0.6, 0.5, 2.8);
153
+ _dummy.updateMatrix();
154
+ body.setMatrixAt(i, _dummy.matrix);
155
+
156
+ // Wings (wide flat box)
157
+ _dummy.scale.set(5.0, 0.07, 1.0);
158
+ _dummy.updateMatrix();
159
+ wing.setMatrixAt(i, _dummy.matrix);
160
+
161
+ // Tail fin (vertical)
162
+ _dummy.position.set(
163
+ x - Math.sin(heading) * 1.2,
164
+ y + 0.5,
165
+ z - Math.cos(heading) * 1.2,
166
+ );
167
+ _dummy.scale.set(0.06, 0.8, 0.5);
168
+ _dummy.updateMatrix();
169
+ tail.setMatrixAt(i, _dummy.matrix);
170
+ }
171
+
172
+ body.instanceMatrix.needsUpdate = true;
173
+ wing.instanceMatrix.needsUpdate = true;
174
+ tail.instanceMatrix.needsUpdate = true;
175
+
176
+ if (positionsRef && posOut) positionsRef.current = posOut;
177
+ });
178
+
179
+ // Pointer handlers
180
+ const handlePointerMove = useCallback((e: ThreeEvent<PointerEvent>) => {
181
+ e.stopPropagation();
182
+ const id = e.instanceId;
183
+ if (id === undefined || id >= count) { onHoverPlane?.(null); return; }
184
+
185
+ if (hoveredIdx.current !== id) {
186
+ hoveredIdx.current = id;
187
+ document.body.style.cursor = "pointer";
188
+ }
189
+
190
+ const w = wallets[id];
191
+ onHoverPlane?.({
192
+ address: w.address,
193
+ tokenIndex: 0,
194
+ screenX: e.nativeEvent.clientX,
195
+ screenY: e.nativeEvent.clientY,
196
+ mode: "building",
197
+ identityName: w.identityName,
198
+ identityType: w.identityType,
199
+ identityCategory: w.identityCategory,
200
+ });
201
+ }, [wallets, count, onHoverPlane]);
202
+
203
+ const handlePointerOut = useCallback(() => {
204
+ hoveredIdx.current = null;
205
+ document.body.style.cursor = "default";
206
+ onHoverPlane?.(null);
207
+ }, [onHoverPlane]);
208
+
209
+ const handleClick = useCallback((e: ThreeEvent<MouseEvent>) => {
210
+ e.stopPropagation();
211
+ const id = e.instanceId;
212
+ if (id === undefined || id >= count) return;
213
+ const w = wallets[id];
214
+ const dims = getBuildingDimensions(w);
215
+ const pos = getWalletWorldPosition(w, dims);
216
+ onClickPlane?.(w, pos);
217
+ }, [wallets, count, onClickPlane]);
218
+
219
+ if (count === 0) return null;
220
+
221
+ return (
222
+ <group>
223
+ {/* Fuselage bodies */}
224
+ <instancedMesh
225
+ ref={bodyRef}
226
+ args={[undefined!, undefined!, count]}
227
+ frustumCulled={false}
228
+ onPointerMove={handlePointerMove}
229
+ onPointerOut={handlePointerOut}
230
+ onClick={handleClick}
231
+ >
232
+ <capsuleGeometry args={[0.3, 1.8, 4, 8]} />
233
+ <primitive object={bodyMat} attach="material" />
234
+ </instancedMesh>
235
+
236
+ {/* Wings */}
237
+ <instancedMesh
238
+ ref={wingRef}
239
+ args={[undefined!, undefined!, count]}
240
+ frustumCulled={false}
241
+ raycast={() => { }} // no raycasting on wings - body handles it
242
+ >
243
+ <boxGeometry args={[1, 1, 1]} />
244
+ <primitive object={wingMat} attach="material" />
245
+ </instancedMesh>
246
+
247
+ {/* Tail fins */}
248
+ <instancedMesh
249
+ ref={tailRef}
250
+ args={[undefined!, undefined!, count]}
251
+ frustumCulled={false}
252
+ raycast={() => { }} // no raycasting
253
+ >
254
+ <boxGeometry args={[1, 1, 1]} />
255
+ <primitive object={tailMat} attach="material" />
256
+ </instancedMesh>
257
+ </group>
258
+ );
259
+ }
@@ -0,0 +1,246 @@
1
+ "use client";
2
+
3
+ import { useMemo, useRef } from "react";
4
+ import { useFrame } from "@react-three/fiber";
5
+ import * as THREE from "three";
6
+ import { PlacedWallet } from "@/types/wallet";
7
+ import { createHouseMaterial } from "@/lib/house-shader";
8
+ import {
9
+ CELL_SIZE, BLOCK_SIZE, BLOCK_STRIDE, BLOCKS_PER_ROW,
10
+ OFFSET_X, OFFSET_Z, SLOTS_PER_BLOCK, PARK_BLOCKS,
11
+ } from "@/lib/city-constants";
12
+
13
+ // Wall color palette — warm earthy tones
14
+ const WALL_COLORS = [
15
+ "#d4c4a8", // cream
16
+ "#c9b99a", // warm tan
17
+ "#b8a88a", // sand
18
+ "#d6cbb8", // pale beige
19
+ "#c2b8a3", // khaki
20
+ "#bfc8c0", // sage grey
21
+ "#c4b4a0", // wheat
22
+ "#d0c0a0", // light gold
23
+ "#baa892", // dusty brown
24
+ "#c8c0b0", // stone
25
+ ].map((c) => new THREE.Color(c));
26
+
27
+ // Roof color palette
28
+ const ROOF_COLORS = [
29
+ "#8b4433", // terracotta
30
+ "#6b3a2a", // dark brick
31
+ "#5a4a3a", // warm slate
32
+ "#7a5a3a", // cedar brown
33
+ "#6a4a2a", // chocolate
34
+ "#4a5a4a", // moss
35
+ "#8a6a4a", // sienna
36
+ "#5a3a2a", // espresso
37
+ ].map((c) => new THREE.Color(c));
38
+
39
+ function makeRng(seed: number) {
40
+ let s = Math.abs(seed) || 1;
41
+ return () => {
42
+ s = (s * 16807 + 0) % 2147483647;
43
+ return s / 2147483647;
44
+ };
45
+ }
46
+
47
+ export interface HouseInstance {
48
+ x: number;
49
+ z: number;
50
+ w: number;
51
+ h: number;
52
+ d: number;
53
+ roofH: number;
54
+ rotY: number;
55
+ wallColor: THREE.Color;
56
+ roofColor: THREE.Color;
57
+ hasChimney: boolean;
58
+ }
59
+
60
+ export function computeHouseSlots(occupiedSlots: Set<string>): HouseInstance[] {
61
+ const houses: HouseInstance[] = [];
62
+
63
+ for (let row = 0; row < BLOCKS_PER_ROW; row++) {
64
+ for (let col = 0; col < BLOCKS_PER_ROW; col++) {
65
+ if (PARK_BLOCKS.has(`${row},${col}`)) continue;
66
+
67
+ const rng = makeRng(row * 1009 + col * 2003 + 7);
68
+
69
+ for (let slot = 0; slot < SLOTS_PER_BLOCK; slot++) {
70
+ // Always consume RNG for every slot for stability
71
+ const shouldPlace = rng() < 0.15;
72
+ const jitterX = (rng() - 0.5) * 0.6;
73
+ const jitterZ = (rng() - 0.5) * 0.6;
74
+ const rotY = Math.floor(rng() * 4) * (Math.PI / 2);
75
+
76
+ // Independent axis variation
77
+ const w = 0.8 + rng() * 0.9; // 0.8–1.7
78
+ const d = 0.8 + rng() * 0.9; // 0.8–1.7
79
+ const h = 0.6 + rng() * 0.8; // 0.6–1.4
80
+ const roofH = 0.3 + rng() * 0.5; // 0.3–0.8
81
+
82
+ const wallIdx = Math.floor(rng() * WALL_COLORS.length);
83
+ const roofIdx = Math.floor(rng() * ROOF_COLORS.length);
84
+ const hasChimney = rng() < 0.4;
85
+
86
+ if (!shouldPlace) continue;
87
+ if (occupiedSlots.has(`${row},${col},${slot}`)) continue;
88
+
89
+ const localRow = Math.floor(slot / 4);
90
+ const localCol = slot % 4;
91
+
92
+ const blockOriginX = OFFSET_X + col * BLOCK_STRIDE - BLOCK_SIZE / 2;
93
+ const blockOriginZ = OFFSET_Z + row * BLOCK_STRIDE - BLOCK_SIZE / 2;
94
+
95
+ const x = blockOriginX + localCol * CELL_SIZE + CELL_SIZE / 2 + jitterX;
96
+ const z = blockOriginZ + localRow * CELL_SIZE + CELL_SIZE / 2 + jitterZ;
97
+
98
+ houses.push({
99
+ x, z, w, h, d, roofH, rotY,
100
+ wallColor: WALL_COLORS[wallIdx],
101
+ roofColor: ROOF_COLORS[roofIdx],
102
+ hasChimney,
103
+ });
104
+ }
105
+ }
106
+ }
107
+
108
+ return houses;
109
+ }
110
+
111
+ interface InstancedHousesProps {
112
+ wallets: PlacedWallet[];
113
+ timeRef: React.MutableRefObject<number>;
114
+ }
115
+
116
+ const _dummy = new THREE.Object3D();
117
+ const _col = new THREE.Color();
118
+
119
+ export default function InstancedHouses({ wallets, timeRef }: InstancedHousesProps) {
120
+ const timeUniformRef = useRef<{ value: number }>({ value: 0 });
121
+
122
+ const occupiedSlots = useMemo(() => {
123
+ const set = new Set<string>();
124
+ for (const w of wallets) {
125
+ set.add(`${w.blockRow},${w.blockCol},${w.localSlot}`);
126
+ }
127
+ return set;
128
+ }, [wallets]);
129
+
130
+ const houses = useMemo(() => computeHouseSlots(occupiedSlots), [occupiedSlots]);
131
+ const chimneyHouses = useMemo(() => houses.filter((h) => h.hasChimney), [houses]);
132
+
133
+ // Pre-build geometries with color attributes baked in
134
+ const bodyGeo = useMemo(() => {
135
+ const geo = new THREE.BoxGeometry(1, 1, 1);
136
+ return geo;
137
+ }, []);
138
+
139
+ const roofGeo = useMemo(() => {
140
+ // radius = sqrt(2)/2 so the 4 corners of the pyramid base land exactly
141
+ // at (±0.5, ±0.5) when rotated 45° — matching the unit box edges
142
+ const geo = new THREE.ConeGeometry(Math.SQRT2 / 2, 1, 4);
143
+ // Rotate 45° around Y so pyramid edges align with box edges
144
+ geo.rotateY(Math.PI / 4);
145
+ return geo;
146
+ }, []);
147
+
148
+ const wallMat = useMemo(() => createHouseMaterial(timeUniformRef.current, 0.9, 1.0), []);
149
+ const roofMat = useMemo(() => createHouseMaterial(timeUniformRef.current, 0.8, 0.0), []);
150
+ const chimMat = useMemo(() => createHouseMaterial(timeUniformRef.current, 0.85, 1.0), []);
151
+
152
+ useFrame(() => {
153
+ timeUniformRef.current.value = timeRef.current;
154
+ });
155
+
156
+ // Build meshes imperatively so colors exist before first render
157
+ const bodyMesh = useMemo(() => {
158
+ if (houses.length === 0) return null;
159
+ const mesh = new THREE.InstancedMesh(bodyGeo, wallMat, houses.length);
160
+ const colors = new Float32Array(houses.length * 3);
161
+
162
+ for (let i = 0; i < houses.length; i++) {
163
+ const h = houses[i];
164
+ _dummy.position.set(h.x, h.h / 2, h.z);
165
+ _dummy.rotation.set(0, h.rotY, 0);
166
+ _dummy.scale.set(h.w, h.h, h.d);
167
+ _dummy.updateMatrix();
168
+ mesh.setMatrixAt(i, _dummy.matrix);
169
+
170
+ colors[i * 3] = h.wallColor.r;
171
+ colors[i * 3 + 1] = h.wallColor.g;
172
+ colors[i * 3 + 2] = h.wallColor.b;
173
+ }
174
+
175
+ mesh.instanceColor = new THREE.InstancedBufferAttribute(colors, 3);
176
+ mesh.instanceMatrix.needsUpdate = true;
177
+ mesh.frustumCulled = false;
178
+ return mesh;
179
+ }, [houses, bodyGeo, wallMat]);
180
+
181
+ const roofMesh = useMemo(() => {
182
+ if (houses.length === 0) return null;
183
+ const mesh = new THREE.InstancedMesh(roofGeo, roofMat, houses.length);
184
+ const colors = new Float32Array(houses.length * 3);
185
+
186
+ for (let i = 0; i < houses.length; i++) {
187
+ const h = houses[i];
188
+ _dummy.position.set(h.x, h.h + h.roofH / 2, h.z);
189
+ _dummy.rotation.set(0, h.rotY, 0);
190
+ _dummy.scale.set(h.w, h.roofH, h.d);
191
+ _dummy.updateMatrix();
192
+ mesh.setMatrixAt(i, _dummy.matrix);
193
+
194
+ colors[i * 3] = h.roofColor.r;
195
+ colors[i * 3 + 1] = h.roofColor.g;
196
+ colors[i * 3 + 2] = h.roofColor.b;
197
+ }
198
+
199
+ mesh.instanceColor = new THREE.InstancedBufferAttribute(colors, 3);
200
+ mesh.instanceMatrix.needsUpdate = true;
201
+ mesh.frustumCulled = false;
202
+ return mesh;
203
+ }, [houses, roofGeo, roofMat]);
204
+
205
+ const chimneyMesh = useMemo(() => {
206
+ if (chimneyHouses.length === 0) return null;
207
+ const geo = new THREE.BoxGeometry(1, 1, 1);
208
+ const mesh = new THREE.InstancedMesh(geo, chimMat, chimneyHouses.length);
209
+ const colors = new Float32Array(chimneyHouses.length * 3);
210
+
211
+ for (let i = 0; i < chimneyHouses.length; i++) {
212
+ const h = chimneyHouses[i];
213
+ const chimW = 0.12;
214
+ const chimH = h.roofH * 0.9;
215
+
216
+ const ox = Math.cos(h.rotY) * h.w * 0.25 - Math.sin(h.rotY) * h.d * 0.15;
217
+ const oz = Math.sin(h.rotY) * h.w * 0.25 + Math.cos(h.rotY) * h.d * 0.15;
218
+
219
+ _dummy.position.set(h.x + ox, h.h + chimH / 2 + h.roofH * 0.3, h.z + oz);
220
+ _dummy.rotation.set(0, 0, 0);
221
+ _dummy.scale.set(chimW, chimH, chimW);
222
+ _dummy.updateMatrix();
223
+ mesh.setMatrixAt(i, _dummy.matrix);
224
+
225
+ _col.set(h.roofColor).multiplyScalar(0.7);
226
+ colors[i * 3] = _col.r;
227
+ colors[i * 3 + 1] = _col.g;
228
+ colors[i * 3 + 2] = _col.b;
229
+ }
230
+
231
+ mesh.instanceColor = new THREE.InstancedBufferAttribute(colors, 3);
232
+ mesh.instanceMatrix.needsUpdate = true;
233
+ mesh.frustumCulled = false;
234
+ return mesh;
235
+ }, [chimneyHouses, chimMat]);
236
+
237
+ if (houses.length === 0) return null;
238
+
239
+ return (
240
+ <>
241
+ {bodyMesh && <primitive object={bodyMesh} />}
242
+ {roofMesh && <primitive object={roofMesh} />}
243
+ {chimneyMesh && <primitive object={chimneyMesh} />}
244
+ </>
245
+ );
246
+ }
@@ -0,0 +1,201 @@
1
+ "use client";
2
+
3
+ import { useRef, useEffect, useMemo } from "react";
4
+ import { useFrame } from "@react-three/fiber";
5
+ import * as THREE from "three";
6
+ import { lampIntensity } from "@/lib/day-night";
7
+
8
+ interface LampDatum {
9
+ pos: [number, number, number];
10
+ rotY: number;
11
+ }
12
+
13
+ interface InstancedLampPostsProps {
14
+ lamps: LampDatum[];
15
+ timeRef: React.MutableRefObject<number>;
16
+ }
17
+
18
+ const POLE_HEIGHT = 3.2;
19
+ const ARM_LENGTH = 0.7;
20
+ const DECAL_RADIUS = 4.5;
21
+
22
+ const decalVertexShader = /* glsl */ `
23
+ varying vec2 vUv;
24
+ void main() {
25
+ vUv = uv;
26
+ gl_Position = projectionMatrix * modelViewMatrix * instanceMatrix * vec4(position, 1.0);
27
+ }
28
+ `;
29
+
30
+ const decalFragmentShader = /* glsl */ `
31
+ uniform float uIntensity;
32
+ uniform vec3 uColor;
33
+ varying vec2 vUv;
34
+ void main() {
35
+ vec2 center = vUv - 0.5;
36
+ float dist = length(center) * 2.0;
37
+ float falloff = 1.0 - smoothstep(0.0, 1.0, dist);
38
+ falloff *= falloff;
39
+ gl_FragColor = vec4(uColor, falloff * uIntensity * 0.3);
40
+ }
41
+ `;
42
+
43
+ export default function InstancedLampPosts({ lamps, timeRef }: InstancedLampPostsProps) {
44
+ const poleRef = useRef<THREE.InstancedMesh>(null);
45
+ const armRef = useRef<THREE.InstancedMesh>(null);
46
+ const housingRef = useRef<THREE.InstancedMesh>(null);
47
+ const bulbRef = useRef<THREE.InstancedMesh>(null);
48
+ const bulbMatRef = useRef<THREE.MeshStandardMaterial>(null);
49
+ const decalRef = useRef<THREE.InstancedMesh>(null);
50
+ const dummy = useMemo(() => new THREE.Object3D(), []);
51
+
52
+ const decalMaterialRef = useRef<THREE.ShaderMaterial | null>(null);
53
+ if (!decalMaterialRef.current) {
54
+ decalMaterialRef.current = new THREE.ShaderMaterial({
55
+ vertexShader: decalVertexShader,
56
+ fragmentShader: decalFragmentShader,
57
+ transparent: true,
58
+ depthWrite: false,
59
+ blending: THREE.AdditiveBlending,
60
+ polygonOffset: true,
61
+ polygonOffsetFactor: -1,
62
+ polygonOffsetUnits: -1,
63
+ uniforms: {
64
+ uIntensity: { value: 0 },
65
+ uColor: { value: new THREE.Color("#ffcc66") },
66
+ },
67
+ });
68
+ }
69
+ const decalMaterial = decalMaterialRef.current;
70
+
71
+ // Precompute arm-tip world positions for every lamp (avoids trig each frame)
72
+ const tipPositions = useMemo(() => {
73
+ return lamps.map(({ pos, rotY }) => {
74
+ const dx = Math.cos(rotY) * ARM_LENGTH;
75
+ const dz = -Math.sin(rotY) * ARM_LENGTH;
76
+ return new THREE.Vector3(pos[0] + dx, pos[1] + POLE_HEIGHT - 0.25, pos[2] + dz);
77
+ });
78
+ }, [lamps]);
79
+
80
+ useEffect(() => {
81
+ return () => {
82
+ decalMaterial.dispose();
83
+ };
84
+ }, [decalMaterial]);
85
+
86
+ // Set instance matrices
87
+ useEffect(() => {
88
+ const pole = poleRef.current;
89
+ const arm = armRef.current;
90
+ const housing = housingRef.current;
91
+ const bulb = bulbRef.current;
92
+ const decal = decalRef.current;
93
+ if (!pole || !arm || !housing || !bulb || !decal) return;
94
+
95
+ for (let i = 0; i < lamps.length; i++) {
96
+ const { pos, rotY } = lamps[i];
97
+ const [x, y, z] = pos;
98
+ const cosR = Math.cos(rotY);
99
+ const sinR = -Math.sin(rotY);
100
+
101
+ // Pole
102
+ dummy.position.set(x, y + POLE_HEIGHT / 2, z);
103
+ dummy.rotation.set(0, 0, 0);
104
+ dummy.scale.set(1, 1, 1);
105
+ dummy.updateMatrix();
106
+ pole.setMatrixAt(i, dummy.matrix);
107
+
108
+ // Arm
109
+ dummy.position.set(
110
+ x + cosR * ARM_LENGTH / 2,
111
+ y + POLE_HEIGHT - 0.05,
112
+ z + sinR * ARM_LENGTH / 2,
113
+ );
114
+ dummy.rotation.set(0, rotY, Math.PI / 2);
115
+ dummy.updateMatrix();
116
+ arm.setMatrixAt(i, dummy.matrix);
117
+
118
+ // Housing
119
+ dummy.position.set(
120
+ x + cosR * ARM_LENGTH,
121
+ y + POLE_HEIGHT - 0.15,
122
+ z + sinR * ARM_LENGTH,
123
+ );
124
+ dummy.rotation.set(0, rotY, 0);
125
+ dummy.updateMatrix();
126
+ housing.setMatrixAt(i, dummy.matrix);
127
+
128
+ // Bulb
129
+ dummy.position.set(
130
+ x + cosR * ARM_LENGTH,
131
+ y + POLE_HEIGHT - 0.22,
132
+ z + sinR * ARM_LENGTH,
133
+ );
134
+ dummy.rotation.set(0, 0, 0);
135
+ dummy.updateMatrix();
136
+ bulb.setMatrixAt(i, dummy.matrix);
137
+
138
+ // Decal — flat circle on the ground beneath the lamp tip
139
+ const tip = tipPositions[i];
140
+ dummy.position.set(tip.x, 0.01, tip.z);
141
+ dummy.rotation.set(-Math.PI / 2, 0, 0);
142
+ dummy.scale.set(DECAL_RADIUS, DECAL_RADIUS, 1);
143
+ dummy.updateMatrix();
144
+ decal.setMatrixAt(i, dummy.matrix);
145
+ }
146
+
147
+ pole.instanceMatrix.needsUpdate = true;
148
+ arm.instanceMatrix.needsUpdate = true;
149
+ housing.instanceMatrix.needsUpdate = true;
150
+ bulb.instanceMatrix.needsUpdate = true;
151
+ decal.instanceMatrix.needsUpdate = true;
152
+ }, [lamps, dummy, tipPositions]);
153
+
154
+ useFrame(() => {
155
+ const intensity = lampIntensity(timeRef.current);
156
+
157
+ // Update bulb emissive
158
+ if (bulbMatRef.current) {
159
+ bulbMatRef.current.emissiveIntensity = intensity * 2.5;
160
+ bulbMatRef.current.opacity = 0.3 + intensity * 0.7;
161
+ }
162
+
163
+ // Update decal intensity
164
+ decalMaterialRef.current!.uniforms.uIntensity.value = intensity;
165
+ });
166
+
167
+ if (lamps.length === 0) return null;
168
+
169
+ return (
170
+ <>
171
+ <instancedMesh ref={poleRef} args={[undefined!, undefined!, lamps.length]} frustumCulled={false}>
172
+ <cylinderGeometry args={[0.04, 0.06, POLE_HEIGHT, 6]} />
173
+ <meshStandardMaterial color="#555566" metalness={0.6} roughness={0.4} />
174
+ </instancedMesh>
175
+ <instancedMesh ref={armRef} args={[undefined!, undefined!, lamps.length]} frustumCulled={false}>
176
+ <cylinderGeometry args={[0.03, 0.03, ARM_LENGTH, 4]} />
177
+ <meshStandardMaterial color="#555566" metalness={0.6} roughness={0.4} />
178
+ </instancedMesh>
179
+ <instancedMesh ref={housingRef} args={[undefined!, undefined!, lamps.length]} frustumCulled={false}>
180
+ <boxGeometry args={[0.22, 0.12, 0.16]} />
181
+ <meshStandardMaterial color="#444455" metalness={0.5} roughness={0.3} />
182
+ </instancedMesh>
183
+ <instancedMesh ref={bulbRef} args={[undefined!, undefined!, lamps.length]} frustumCulled={false}>
184
+ <sphereGeometry args={[0.1, 6, 6]} />
185
+ <meshStandardMaterial
186
+ ref={bulbMatRef}
187
+ color="#ffcc66"
188
+ emissive="#ffcc66"
189
+ emissiveIntensity={0}
190
+ transparent
191
+ opacity={0.3}
192
+ />
193
+ </instancedMesh>
194
+
195
+ {/* Instanced ground decals — one additive-blended circle per lamp */}
196
+ <instancedMesh ref={decalRef} args={[undefined!, undefined!, lamps.length]} frustumCulled={false} renderOrder={-1} material={decalMaterial}>
197
+ <circleGeometry args={[1, 32]} />
198
+ </instancedMesh>
199
+ </>
200
+ );
201
+ }