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,253 @@
1
+ import * as THREE from "three";
2
+
3
+ /**
4
+ * Find `#include <name>` in the source (with any leading whitespace) and replace
5
+ * the entire line with the replacement string.
6
+ */
7
+ function replaceInclude(source: string, name: string, replacement: string): string {
8
+ const needle = `#include <${name}>`;
9
+ const idx = source.indexOf(needle);
10
+ if (idx < 0) return source;
11
+ let start = idx;
12
+ while (start > 0 && source[start - 1] !== "\n") start--;
13
+ return source.substring(0, start) + replacement + "\n" + source.substring(idx + needle.length);
14
+ }
15
+
16
+ /**
17
+ * Creates a ShaderMaterial based on Three.js's standard (PBR) shader with
18
+ * procedural window logic baked in. Uses ShaderMaterial directly instead of
19
+ * onBeforeCompile to avoid program cache invalidation issues.
20
+ */
21
+ export function createBuildingMaterial(
22
+ timeUniform: { value: number },
23
+ elapsedUniform?: { value: number },
24
+ ): THREE.ShaderMaterial {
25
+ const std = THREE.ShaderLib.standard;
26
+
27
+ // Start from the standard PBR shader source
28
+ let vertexShader = std.vertexShader;
29
+ let fragmentShader = std.fragmentShader;
30
+
31
+ // --- Vertex modifications ---
32
+ vertexShader = replaceInclude(
33
+ vertexShader,
34
+ "common",
35
+ /* glsl */ `
36
+ attribute vec3 instanceBuildingColor;
37
+ attribute float instanceWindowCols;
38
+ attribute float instanceFillRatio;
39
+ attribute float instanceLitRatio;
40
+ attribute float instanceSeed;
41
+ attribute float instanceFloors;
42
+ attribute float instanceHighlight;
43
+ varying vec3 vBuildingColor;
44
+ varying float vWindowCols, vFillRatio, vLitRatio, vSeed, vFloors;
45
+ varying float vHighlight;
46
+ varying vec3 vModelNormal;
47
+ varying vec2 vFaceUV;
48
+ #include <common>
49
+ `,
50
+ );
51
+
52
+ vertexShader = replaceInclude(
53
+ vertexShader,
54
+ "begin_vertex",
55
+ /* glsl */ `
56
+ #include <begin_vertex>
57
+ vBuildingColor = instanceBuildingColor;
58
+ vHighlight = instanceHighlight;
59
+ vWindowCols = instanceWindowCols;
60
+ vFillRatio = instanceFillRatio;
61
+ vLitRatio = instanceLitRatio;
62
+ vSeed = instanceSeed;
63
+ vFloors = instanceFloors;
64
+ vModelNormal = normal;
65
+ if (abs(normal.x) > 0.5) {
66
+ vFaceUV = vec2(position.z + 0.5, position.y + 0.5);
67
+ } else {
68
+ vFaceUV = vec2(position.x + 0.5, position.y + 0.5);
69
+ }
70
+ `,
71
+ );
72
+
73
+ // --- Fragment modifications ---
74
+ fragmentShader = replaceInclude(
75
+ fragmentShader,
76
+ "common",
77
+ /* glsl */ `
78
+ uniform float uTime;
79
+ uniform float uElapsed;
80
+ varying vec3 vBuildingColor;
81
+ varying float vWindowCols, vFillRatio, vLitRatio, vSeed, vFloors;
82
+ varying float vHighlight;
83
+ varying vec3 vModelNormal;
84
+ varying vec2 vFaceUV;
85
+ #include <common>
86
+ `,
87
+ );
88
+
89
+ fragmentShader = replaceInclude(
90
+ fragmentShader,
91
+ "emissivemap_fragment",
92
+ /* glsl */ `
93
+ #include <emissivemap_fragment>
94
+
95
+ // Time-of-day
96
+ float timeNow = fract(uTime);
97
+
98
+ // Sunset factor: peaks at t=0.50
99
+ float sunsetFactor = 0.0;
100
+ if (timeNow >= 0.42 && timeNow < 0.50) {
101
+ sunsetFactor = (timeNow - 0.42) / 0.08;
102
+ } else if (timeNow >= 0.50 && timeNow < 0.58) {
103
+ sunsetFactor = 1.0 - (timeNow - 0.50) / 0.08;
104
+ }
105
+
106
+ // Night factor (scene darkness)
107
+ float nightFactor = 0.0;
108
+ if (timeNow >= 0.50 && timeNow < 0.58) {
109
+ nightFactor = (timeNow - 0.50) / 0.08;
110
+ } else if (timeNow >= 0.58 && timeNow < 0.92) {
111
+ nightFactor = 1.0;
112
+ } else if (timeNow >= 0.92 && timeNow <= 1.0) {
113
+ nightFactor = 1.0 - (timeNow - 0.92) / 0.08;
114
+ }
115
+
116
+ // Window light factor — starts at sunset (~20%), full at night, ~20% at sunrise, off during day
117
+ float windowFactor = 0.0;
118
+ if (timeNow >= 0.42 && timeNow < 0.50) {
119
+ // During sunset: ramp 0→0.2
120
+ windowFactor = 0.2 * (timeNow - 0.42) / 0.08;
121
+ } else if (timeNow >= 0.50 && timeNow < 0.58) {
122
+ // Sunset→night: ramp 0.2→1.0
123
+ windowFactor = 0.2 + 0.8 * (timeNow - 0.50) / 0.08;
124
+ } else if (timeNow >= 0.58 && timeNow < 0.92) {
125
+ // Full night
126
+ windowFactor = 1.0;
127
+ } else if (timeNow >= 0.92 && timeNow <= 1.0) {
128
+ // Night→sunrise: ramp 1.0→0.2
129
+ windowFactor = 0.2 + 0.8 * (1.0 - (timeNow - 0.92) / 0.08);
130
+ } else if (timeNow >= 0.0 && timeNow < 0.08) {
131
+ // During sunrise: ramp 0.2→0
132
+ windowFactor = 0.2 * (1.0 - timeNow / 0.08);
133
+ }
134
+
135
+ // Base building color
136
+ vec3 baseColor = vBuildingColor;
137
+ vec3 sunsetTint = mix(baseColor, baseColor * vec3(1.2, 0.95, 0.75), sunsetFactor * 0.4);
138
+
139
+ // Night: darken overall output
140
+ float nightDim = mix(1.0, 0.45, nightFactor);
141
+
142
+ // 80% emissive / 20% diffuse, dimmed at night
143
+ diffuseColor.rgb = sunsetTint * 0.2 * nightDim;
144
+ totalEmissiveRadiance += sunsetTint * 0.8 * nightDim;
145
+
146
+ // Hover highlight: subtle additive glow that preserves base color
147
+ totalEmissiveRadiance += vec3(0.02, 0.02, 0.035) * vHighlight;
148
+
149
+ // --- Window logic (side faces only) ---
150
+ if (abs(vModelNormal.y) < 0.5) {
151
+ // Unique face index (0-3) so each side has different patterns
152
+ float faceId = 0.0;
153
+ if (vModelNormal.x > 0.5) faceId = 1.0;
154
+ else if (vModelNormal.x < -0.5) faceId = 2.0;
155
+ else if (vModelNormal.z > 0.5) faceId = 3.0;
156
+
157
+ float cols = vWindowCols;
158
+ float rows = vFloors;
159
+
160
+ if (cols > 0.5 && rows > 0.5) {
161
+ float cellU = fract(vFaceUV.x * cols);
162
+ float cellV = fract(vFaceUV.y * rows);
163
+ bool inWindowSlot = cellU > 0.2 && cellU < 0.8 && cellV > 0.2 && cellV < 0.8;
164
+
165
+ if (inWindowSlot) {
166
+ float colIdx = floor(vFaceUV.x * cols);
167
+ float rowIdx = floor(vFaceUV.y * rows);
168
+
169
+ // Hash 1: does window exist? (faceId makes each side unique)
170
+ float h1 = fract(sin(colIdx * 127.1 + rowIdx * 311.7 + vSeed * 758.5 + faceId * 1731.3) * 43758.5453);
171
+ bool hasWindow = h1 < vFillRatio;
172
+
173
+ if (hasWindow) {
174
+ // Hash 2: per-window threshold for when it turns on/off
175
+ float h2 = fract(sin(colIdx * 53.3 + rowIdx * 419.2 + vSeed * 317.9 + faceId * 2137.7) * 29187.3217);
176
+
177
+ // Scale lit threshold by windowFactor so windows turn on/off gradually
178
+ // h2 is uniform 0–1; window is lit when h2 < effectiveRatio
179
+ // As windowFactor rises 0→1, more windows cross the threshold
180
+ float effectiveLitRatio = vLitRatio * windowFactor;
181
+ bool isLit = h2 < effectiveLitRatio;
182
+
183
+ if (isLit) {
184
+ // Per-window brightness & color variation
185
+ float h3 = fract(sin(colIdx * 173.7 + rowIdx * 239.1 + vSeed * 491.3 + faceId * 863.5) * 15731.4219);
186
+ float h4 = fract(sin(colIdx * 97.3 + rowIdx * 587.1 + vSeed * 163.7 + faceId * 1279.3) * 38147.2917);
187
+ float h5 = fract(sin(colIdx * 211.9 + rowIdx * 349.3 + vSeed * 607.1 + faceId * 1013.7) * 21317.7631);
188
+ float brightness = 0.3 + h3 * 1.0; // 0.3–1.3 wide range
189
+
190
+ // Distinct color categories: warm yellow, cool white, amber, blue-ish
191
+ vec3 windowColor;
192
+ if (h4 < 0.35) {
193
+ windowColor = vec3(0.9, 0.7, 0.28); // warm yellow
194
+ } else if (h4 < 0.55) {
195
+ windowColor = vec3(0.75, 0.72, 0.55); // cool white / fluorescent
196
+ } else if (h4 < 0.8) {
197
+ windowColor = vec3(0.95, 0.55, 0.18); // deep amber
198
+ } else {
199
+ windowColor = vec3(0.5, 0.6, 0.8); // bluish TV glow
200
+ }
201
+ // Extra per-window jitter on top
202
+ windowColor *= 0.85 + h5 * 0.3;
203
+
204
+ // Slow pulsing — each window at its own speed & phase
205
+ float pulseSpeed = 0.12 + h3 * 0.28; // 0.12–0.4 Hz
206
+ float pulsePhase = h5 * 6.2832; // 0–2π offset
207
+ float pulse = 0.4 + 0.6 * sin(uElapsed * pulseSpeed + pulsePhase);
208
+ brightness *= pulse;
209
+
210
+ totalEmissiveRadiance += windowColor * windowFactor * 1.2 * brightness;
211
+ } else if (windowFactor > 0.0) {
212
+ // Dark (unlit) windows at night/twilight
213
+ float h6 = fract(sin(colIdx * 211.9 + rowIdx * 349.3 + vSeed * 607.1 + faceId * 1013.7) * 21317.7631);
214
+ float darkVar = 0.2 + h6 * 0.4; // 0.2–0.6 wider spread
215
+ float darkMix = windowFactor; // fade dark effect in with night
216
+ totalEmissiveRadiance *= mix(1.0, darkVar, darkMix);
217
+ diffuseColor.rgb *= mix(1.0, darkVar, darkMix);
218
+ } else {
219
+ // Daytime window variation — glass tint
220
+ float h7 = fract(sin(colIdx * 131.3 + rowIdx * 277.9 + vSeed * 523.7 + faceId * 947.1) * 18397.5143);
221
+ float dayVar = 0.55 + h7 * 0.4; // 0.55–0.95
222
+ totalEmissiveRadiance *= dayVar;
223
+ diffuseColor.rgb *= dayVar;
224
+ }
225
+ }
226
+ // !hasWindow -> normal wall, no modification
227
+ }
228
+ }
229
+ }
230
+ `,
231
+ );
232
+
233
+ const uniforms = THREE.UniformsUtils.clone(std.uniforms);
234
+ uniforms.uTime = timeUniform; // direct reference, not cloned
235
+ uniforms.uElapsed = elapsedUniform ?? { value: 0 };
236
+ uniforms.roughness.value = 0.85;
237
+ uniforms.metalness.value = 0.05;
238
+
239
+ const material = new THREE.ShaderMaterial({
240
+ uniforms,
241
+ vertexShader,
242
+ fragmentShader,
243
+ lights: true,
244
+ fog: true,
245
+ defines: { STANDARD: "" },
246
+ });
247
+
248
+ // Tell Three.js this behaves like a standard material for rendering purposes
249
+ // (enables correct light uniform binding)
250
+ (material as unknown as Record<string, boolean>).isMeshStandardMaterial = true;
251
+
252
+ return material;
253
+ }
@@ -0,0 +1,244 @@
1
+ import {
2
+ BLOCK_SIZE, BLOCK_STRIDE, OFFSET_X, OFFSET_Z,
3
+ V_ROADS, H_ROADS, PARK_BLOCKS,
4
+ } from "./city-constants";
5
+
6
+ export interface CarPath {
7
+ waypoints: { x: number; z: number }[];
8
+ totalLength: number;
9
+ }
10
+
11
+ const LANE_OFFSET = 1.5; // right-hand traffic offset from road center
12
+
13
+ /** Find the nearest V_ROAD index to a given x position */
14
+ function nearestVRoad(x: number): number {
15
+ let best = 0;
16
+ let bestDist = Infinity;
17
+ for (let i = 0; i < V_ROADS.length; i++) {
18
+ const d = Math.abs(V_ROADS[i] - x);
19
+ if (d < bestDist) { bestDist = d; best = i; }
20
+ }
21
+ return best;
22
+ }
23
+
24
+ /** Find the nearest H_ROAD index to a given z position */
25
+ function nearestHRoad(z: number): number {
26
+ let best = 0;
27
+ let bestDist = Infinity;
28
+ for (let i = 0; i < H_ROADS.length; i++) {
29
+ const d = Math.abs(H_ROADS[i] - z);
30
+ if (d < bestDist) { bestDist = d; best = i; }
31
+ }
32
+ return best;
33
+ }
34
+
35
+ type Direction = "north" | "south" | "east" | "west";
36
+
37
+ function laneAdjust(
38
+ roadX: number,
39
+ roadZ: number,
40
+ dir: Direction,
41
+ ): { x: number; z: number } {
42
+ // Lane offset to the right of travel direction
43
+ switch (dir) {
44
+ case "north": return { x: roadX + LANE_OFFSET, z: roadZ }; // traveling -Z, lane offset +X
45
+ case "south": return { x: roadX - LANE_OFFSET, z: roadZ }; // traveling +Z, lane offset -X
46
+ case "east": return { x: roadX, z: roadZ + LANE_OFFSET }; // traveling +X, lane offset +Z
47
+ case "west": return { x: roadX, z: roadZ - LANE_OFFSET }; // traveling -X, lane offset -Z
48
+ }
49
+ }
50
+
51
+ function randomDirection(rng: () => number, exclude?: Direction): Direction {
52
+ const dirs: Direction[] = ["north", "south", "east", "west"];
53
+ const filtered = exclude ? dirs.filter(d => d !== opposite(exclude)) : dirs;
54
+ return filtered[Math.floor(rng() * filtered.length)];
55
+ }
56
+
57
+ function opposite(dir: Direction): Direction {
58
+ switch (dir) {
59
+ case "north": return "south";
60
+ case "south": return "north";
61
+ case "east": return "west";
62
+ case "west": return "east";
63
+ }
64
+ }
65
+
66
+ function turnOptions(dir: Direction): Direction[] {
67
+ // straight, left, right (no U-turn)
68
+ switch (dir) {
69
+ case "north": return ["north", "west", "east"];
70
+ case "south": return ["south", "east", "west"];
71
+ case "east": return ["east", "north", "south"];
72
+ case "west": return ["west", "south", "north"];
73
+ }
74
+ }
75
+
76
+ function pickTurn(dir: Direction, rng: () => number): Direction {
77
+ const opts = turnOptions(dir);
78
+ const r = rng();
79
+ if (r < 0.5) return opts[0]; // straight 50%
80
+ if (r < 0.75) return opts[1]; // left 25%
81
+ return opts[2]; // right 25%
82
+ }
83
+
84
+ function nextRoadIndex(current: number, dir: "positive" | "negative", max: number): number | null {
85
+ const next = dir === "positive" ? current + 1 : current - 1;
86
+ if (next < 0 || next >= max) return null;
87
+ return next;
88
+ }
89
+
90
+ /** Check if a block at (row, col) is a park */
91
+ function isPark(row: number, col: number): boolean {
92
+ return PARK_BLOCKS.has(`${row},${col}`);
93
+ }
94
+
95
+ function computeLength(waypoints: { x: number; z: number }[]): number {
96
+ let len = 0;
97
+ for (let i = 1; i < waypoints.length; i++) {
98
+ const dx = waypoints[i].x - waypoints[i - 1].x;
99
+ const dz = waypoints[i].z - waypoints[i - 1].z;
100
+ len += Math.sqrt(dx * dx + dz * dz);
101
+ }
102
+ return len;
103
+ }
104
+
105
+ let _rngSeed = 42;
106
+ function defaultRng(): number {
107
+ _rngSeed = (_rngSeed * 16807 + 0) % 2147483647;
108
+ return _rngSeed / 2147483647;
109
+ }
110
+
111
+ /** Generate a path starting near a building at (blockRow, blockCol) */
112
+ export function generatePathFromBuilding(blockRow: number, blockCol: number): CarPath {
113
+ const rng = defaultRng;
114
+
115
+ // Building center
116
+ const bx = OFFSET_X + blockCol * BLOCK_STRIDE;
117
+ const bz = OFFSET_Z + blockRow * BLOCK_STRIDE;
118
+
119
+ // Find nearest roads
120
+ const vi = nearestVRoad(bx);
121
+ const hi = nearestHRoad(bz);
122
+
123
+ // Pick whether to start on the vertical or horizontal road
124
+ const startOnV = rng() < 0.5;
125
+ let roadIdx: number;
126
+ let crossIdx: number;
127
+ let dir: Direction;
128
+
129
+ if (startOnV) {
130
+ roadIdx = vi;
131
+ crossIdx = hi;
132
+ dir = rng() < 0.5 ? "north" : "south";
133
+ } else {
134
+ roadIdx = hi;
135
+ crossIdx = vi;
136
+ dir = rng() < 0.5 ? "east" : "west";
137
+ }
138
+
139
+ return buildPath(roadIdx, crossIdx, startOnV, dir, rng);
140
+ }
141
+
142
+ /** Generate a random path from a random edge */
143
+ export function generateRandomPath(): CarPath {
144
+ const rng = defaultRng;
145
+ const startOnV = rng() < 0.5;
146
+
147
+ if (startOnV) {
148
+ const roadIdx = Math.floor(rng() * V_ROADS.length);
149
+ const crossIdx = rng() < 0.5 ? 0 : H_ROADS.length - 1;
150
+ const dir: Direction = crossIdx === 0 ? "south" : "north";
151
+ return buildPath(roadIdx, crossIdx, true, dir, rng);
152
+ } else {
153
+ const roadIdx = Math.floor(rng() * H_ROADS.length);
154
+ const crossIdx = rng() < 0.5 ? 0 : V_ROADS.length - 1;
155
+ const dir: Direction = crossIdx === 0 ? "east" : "west";
156
+ return buildPath(roadIdx, crossIdx, false, dir, rng);
157
+ }
158
+ }
159
+
160
+ function buildPath(
161
+ roadIdx: number,
162
+ crossIdx: number,
163
+ onVertical: boolean,
164
+ dir: Direction,
165
+ rng: () => number,
166
+ ): CarPath {
167
+ const segments = 6 + Math.floor(rng() * 5); // 6-10 segments
168
+ const waypoints: { x: number; z: number }[] = [];
169
+
170
+ let currentV = onVertical ? roadIdx : crossIdx;
171
+ let currentH = onVertical ? crossIdx : roadIdx;
172
+ let currentDir = dir;
173
+
174
+ // Starting intersection
175
+ const startPos = laneAdjust(V_ROADS[currentV], H_ROADS[currentH], currentDir);
176
+ waypoints.push(startPos);
177
+
178
+ for (let seg = 0; seg < segments; seg++) {
179
+ // Move one block in the current direction
180
+ let nextV = currentV;
181
+ let nextH = currentH;
182
+
183
+ switch (currentDir) {
184
+ case "north": nextH = currentH - 1; break;
185
+ case "south": nextH = currentH + 1; break;
186
+ case "east": nextV = currentV + 1; break;
187
+ case "west": nextV = currentV - 1; break;
188
+ }
189
+
190
+ // Bounds check
191
+ if (nextV < 0 || nextV >= V_ROADS.length || nextH < 0 || nextH >= H_ROADS.length) {
192
+ break;
193
+ }
194
+
195
+ // Add waypoint at next intersection
196
+ const pos = laneAdjust(V_ROADS[nextV], H_ROADS[nextH], currentDir);
197
+ waypoints.push(pos);
198
+
199
+ currentV = nextV;
200
+ currentH = nextH;
201
+
202
+ // Decide turn at this intersection
203
+ currentDir = pickTurn(currentDir, rng);
204
+
205
+ // If turning, add a second waypoint at the same intersection but with new lane offset
206
+ if (seg < segments - 1) {
207
+ const turnPos = laneAdjust(V_ROADS[currentV], H_ROADS[currentH], currentDir);
208
+ // Only add if it's different (i.e., we turned)
209
+ if (turnPos.x !== pos.x || turnPos.z !== pos.z) {
210
+ waypoints.push(turnPos);
211
+ }
212
+ }
213
+ }
214
+
215
+ // Need at least 2 waypoints
216
+ if (waypoints.length < 2) {
217
+ // Fallback: just go one block
218
+ const fallbackDir = dir;
219
+ let fv = onVertical ? roadIdx : crossIdx;
220
+ let fh = onVertical ? crossIdx : roadIdx;
221
+ const w1 = laneAdjust(V_ROADS[fv], H_ROADS[fh], fallbackDir);
222
+ switch (fallbackDir) {
223
+ case "north": fh--; break;
224
+ case "south": fh++; break;
225
+ case "east": fv++; break;
226
+ case "west": fv--; break;
227
+ }
228
+ fv = Math.max(0, Math.min(V_ROADS.length - 1, fv));
229
+ fh = Math.max(0, Math.min(H_ROADS.length - 1, fh));
230
+ const w2 = laneAdjust(V_ROADS[fv], H_ROADS[fh], fallbackDir);
231
+ return { waypoints: [w1, w2], totalLength: computeLength([w1, w2]) };
232
+ }
233
+
234
+ return { waypoints, totalLength: computeLength(waypoints) };
235
+ }
236
+
237
+ /** Pre-generate a pool of random paths for fallback use */
238
+ export function generatePathPool(count: number): CarPath[] {
239
+ const pool: CarPath[] = [];
240
+ for (let i = 0; i < count; i++) {
241
+ pool.push(generateRandomPath());
242
+ }
243
+ return pool;
244
+ }
@@ -0,0 +1,182 @@
1
+ import * as THREE from "three";
2
+ import { CarPath } from "./car-paths";
3
+
4
+ const MAX_CARS = 60;
5
+
6
+ // Car color palette (8 colors)
7
+ const CAR_COLORS = [
8
+ new THREE.Color("#e74c3c"), // red
9
+ new THREE.Color("#3498db"), // blue
10
+ new THREE.Color("#2ecc71"), // green
11
+ new THREE.Color("#f1c40f"), // yellow
12
+ new THREE.Color("#e67e22"), // orange
13
+ new THREE.Color("#9b59b6"), // purple
14
+ new THREE.Color("#1abc9c"), // teal
15
+ new THREE.Color("#ecf0f1"), // white
16
+ ];
17
+
18
+ export interface ActiveCar {
19
+ slotIndex: number;
20
+ path: CarPath;
21
+ progress: number; // distance traveled in world units
22
+ speed: number; // 15-25 world units/sec
23
+ phase: "fadein" | "driving" | "fadeout" | "dead";
24
+ phaseTimer: number;
25
+ color: THREE.Color;
26
+ // Swap metadata for tooltip
27
+ walletAddress: string;
28
+ signature: string;
29
+ tokenIn: string | null;
30
+ tokenOut: string | null;
31
+ amountSol: number | null;
32
+ }
33
+
34
+ export interface CarPosition {
35
+ x: number;
36
+ y: number;
37
+ z: number;
38
+ rotY: number;
39
+ scale: number;
40
+ }
41
+
42
+ let _colorIdx = 0;
43
+
44
+ export class CarSystem {
45
+ cars: (ActiveCar | null)[];
46
+ private freeSlots: number[];
47
+
48
+ constructor() {
49
+ this.cars = new Array(MAX_CARS).fill(null);
50
+ this.freeSlots = Array.from({ length: MAX_CARS }, (_, i) => i);
51
+ }
52
+
53
+ get maxCars(): number {
54
+ return MAX_CARS;
55
+ }
56
+
57
+ spawnCar(
58
+ path: CarPath,
59
+ walletAddress: string,
60
+ signature: string,
61
+ tokenIn: string | null,
62
+ tokenOut: string | null,
63
+ amountSol: number | null,
64
+ ): boolean {
65
+ if (this.freeSlots.length === 0) return false;
66
+ const slot = this.freeSlots.pop()!;
67
+
68
+ const color = CAR_COLORS[_colorIdx % CAR_COLORS.length];
69
+ _colorIdx++;
70
+
71
+ this.cars[slot] = {
72
+ slotIndex: slot,
73
+ path,
74
+ progress: 0,
75
+ speed: 8 + Math.random() * 6, // 8-14
76
+ phase: "fadein",
77
+ phaseTimer: 0,
78
+ color,
79
+ walletAddress,
80
+ signature,
81
+ tokenIn,
82
+ tokenOut,
83
+ amountSol,
84
+ };
85
+ return true;
86
+ }
87
+
88
+ update(delta: number): void {
89
+ for (let i = 0; i < MAX_CARS; i++) {
90
+ const car = this.cars[i];
91
+ if (!car) continue;
92
+
93
+ car.phaseTimer += delta;
94
+
95
+ switch (car.phase) {
96
+ case "fadein":
97
+ if (car.phaseTimer >= 0.5) {
98
+ car.phase = "driving";
99
+ car.phaseTimer = 0;
100
+ }
101
+ car.progress += car.speed * delta;
102
+ break;
103
+
104
+ case "driving":
105
+ car.progress += car.speed * delta;
106
+ if (car.progress >= car.path.totalLength) {
107
+ car.phase = "fadeout";
108
+ car.phaseTimer = 0;
109
+ }
110
+ break;
111
+
112
+ case "fadeout":
113
+ if (car.phaseTimer >= 0.5) {
114
+ car.phase = "dead";
115
+ }
116
+ break;
117
+
118
+ case "dead":
119
+ this.cars[i] = null;
120
+ this.freeSlots.push(i);
121
+ break;
122
+ }
123
+ }
124
+ }
125
+
126
+ /** Get world position, rotation and scale for a car at its current progress */
127
+ getCarPosition(car: ActiveCar): CarPosition {
128
+ const { waypoints, totalLength } = car.path;
129
+ const clampedProgress = Math.min(car.progress, totalLength);
130
+
131
+ // Find segment
132
+ let accumulated = 0;
133
+ let segIdx = 0;
134
+ for (let i = 1; i < waypoints.length; i++) {
135
+ const dx = waypoints[i].x - waypoints[i - 1].x;
136
+ const dz = waypoints[i].z - waypoints[i - 1].z;
137
+ const segLen = Math.sqrt(dx * dx + dz * dz);
138
+ if (accumulated + segLen >= clampedProgress) {
139
+ segIdx = i - 1;
140
+ break;
141
+ }
142
+ accumulated += segLen;
143
+ segIdx = i - 1;
144
+ }
145
+
146
+ const wp0 = waypoints[segIdx];
147
+ const wp1 = waypoints[Math.min(segIdx + 1, waypoints.length - 1)];
148
+ const dx = wp1.x - wp0.x;
149
+ const dz = wp1.z - wp0.z;
150
+ const segLen = Math.sqrt(dx * dx + dz * dz);
151
+ const t = segLen > 0 ? (clampedProgress - accumulated) / segLen : 0;
152
+
153
+ const x = wp0.x + dx * t;
154
+ const z = wp0.z + dz * t;
155
+ // Road surface at Y=0.02, car body center above
156
+ const y = 0.02 + 0.12 + 0.175; // wheel radius + half body height
157
+
158
+ // Rotation: face travel direction
159
+ let rotY = 0;
160
+ if (Math.abs(dx) > 0.01 || Math.abs(dz) > 0.01) {
161
+ rotY = Math.atan2(dx, dz);
162
+ }
163
+
164
+ // Scale for fade
165
+ let scale = 1;
166
+ if (car.phase === "fadein") {
167
+ const ft = Math.min(car.phaseTimer / 0.5, 1);
168
+ scale = 1 - (1 - ft) * (1 - ft); // ease-out
169
+ } else if (car.phase === "fadeout") {
170
+ const ft = Math.min(car.phaseTimer / 0.5, 1);
171
+ scale = 1 - ft * ft; // ease-in
172
+ } else if (car.phase === "dead") {
173
+ scale = 0;
174
+ }
175
+
176
+ return { x, y, z, rotY, scale };
177
+ }
178
+
179
+ getCarSwapInfo(slotIndex: number): ActiveCar | null {
180
+ return this.cars[slotIndex] ?? null;
181
+ }
182
+ }
@@ -0,0 +1,29 @@
1
+ // Shared grid constants — single source of truth for the city layout.
2
+ // Imported by CityGrid, InstancedBuildings, InstancedHouses, building-math, car-paths, etc.
3
+
4
+ export const BLOCKS_PER_ROW = 26;
5
+ export const CELL_SIZE = 4;
6
+ export const ROAD_WIDTH = 6;
7
+ export const SIDEWALK_WIDTH = 0.6;
8
+ export const SLOTS_PER_BLOCK = 16; // 4x4 cells per block
9
+
10
+ export const BLOCK_SIZE = 4 * CELL_SIZE; // 16
11
+ export const BLOCK_STRIDE = BLOCK_SIZE + ROAD_WIDTH; // 22
12
+ export const GRID_WORLD = BLOCKS_PER_ROW * BLOCK_STRIDE; // 572
13
+
14
+ export const OFFSET_X = -GRID_WORLD / 2 + BLOCK_SIZE / 2 + ROAD_WIDTH / 2;
15
+ export const OFFSET_Z = OFFSET_X;
16
+
17
+ // Precomputed road center positions (27 vertical, 27 horizontal)
18
+ export const V_ROADS: number[] = Array.from({ length: 27 }, (_, i) =>
19
+ OFFSET_X - BLOCK_SIZE / 2 - ROAD_WIDTH / 2 + i * BLOCK_STRIDE
20
+ );
21
+ export const H_ROADS: number[] = Array.from({ length: 27 }, (_, i) =>
22
+ OFFSET_Z - BLOCK_SIZE / 2 - ROAD_WIDTH / 2 + i * BLOCK_STRIDE
23
+ );
24
+
25
+ // Hardcoded park blocks — matches DB seed
26
+ export const PARK_BLOCKS = new Set([
27
+ "10,13", "11,13", "13,11", "12,3", "13,3",
28
+ "8,18", "20,12", "4,10", "18,21", "22,6",
29
+ ]);