solanapolis 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +518 -0
- package/bin/solanapolis.js +197 -0
- package/convex/_generated/api.d.ts +175 -0
- package/convex/_generated/api.js +23 -0
- package/convex/_generated/dataModel.d.ts +60 -0
- package/convex/_generated/server.d.ts +143 -0
- package/convex/_generated/server.js +93 -0
- package/convex/agent/conversation.ts +352 -0
- package/convex/agent/embeddingsCache.ts +110 -0
- package/convex/agent/memory.ts +450 -0
- package/convex/agent/schema.ts +53 -0
- package/convex/aiChat.ts +54 -0
- package/convex/aiTown/agent.ts +382 -0
- package/convex/aiTown/agentDescription.ts +27 -0
- package/convex/aiTown/agentInputs.ts +155 -0
- package/convex/aiTown/agentOperations.ts +178 -0
- package/convex/aiTown/conversation.ts +395 -0
- package/convex/aiTown/conversationMembership.ts +38 -0
- package/convex/aiTown/game.ts +371 -0
- package/convex/aiTown/ids.ts +32 -0
- package/convex/aiTown/inputHandler.ts +9 -0
- package/convex/aiTown/inputs.ts +25 -0
- package/convex/aiTown/insertInput.ts +20 -0
- package/convex/aiTown/location.ts +32 -0
- package/convex/aiTown/main.ts +154 -0
- package/convex/aiTown/movement.ts +189 -0
- package/convex/aiTown/player.ts +310 -0
- package/convex/aiTown/playerDescription.ts +35 -0
- package/convex/aiTown/schema.ts +79 -0
- package/convex/aiTown/world.ts +65 -0
- package/convex/aiTown/worldMap.ts +74 -0
- package/convex/chat.ts +79 -0
- package/convex/constants.ts +78 -0
- package/convex/convex.config.ts +6 -0
- package/convex/crons.ts +89 -0
- package/convex/engine/abstractGame.ts +199 -0
- package/convex/engine/historicalObject.ts +355 -0
- package/convex/engine/schema.ts +56 -0
- package/convex/http.ts +36 -0
- package/convex/init.ts +110 -0
- package/convex/messages.ts +53 -0
- package/convex/npcCarAgents.ts +415 -0
- package/convex/schema.ts +61 -0
- package/convex/streaming.ts +23 -0
- package/convex/testing.ts +202 -0
- package/convex/tsconfig.json +18 -0
- package/convex/util/FastIntegerCompression.ts +221 -0
- package/convex/util/assertNever.ts +4 -0
- package/convex/util/asyncMap.ts +20 -0
- package/convex/util/compression.ts +71 -0
- package/convex/util/geometry.ts +132 -0
- package/convex/util/isSimpleObject.ts +11 -0
- package/convex/util/llm.ts +724 -0
- package/convex/util/minheap.ts +38 -0
- package/convex/util/object.ts +22 -0
- package/convex/util/sleep.ts +3 -0
- package/convex/util/types.ts +33 -0
- package/convex/util/xxhash.ts +228 -0
- package/convex/world.ts +257 -0
- package/data/animations/campfire.json +45 -0
- package/data/animations/gentlesparkle.json +37 -0
- package/data/animations/gentlesplash.json +61 -0
- package/data/animations/gentlewaterfall.json +61 -0
- package/data/animations/windmill.json +78 -0
- package/data/characters.ts +121 -0
- package/data/convertMap.js +74 -0
- package/data/gentle.js +330 -0
- package/data/spritesheets/f1.ts +75 -0
- package/data/spritesheets/f2.ts +75 -0
- package/data/spritesheets/f3.ts +75 -0
- package/data/spritesheets/f4.ts +75 -0
- package/data/spritesheets/f5.ts +75 -0
- package/data/spritesheets/f6.ts +75 -0
- package/data/spritesheets/f7.ts +75 -0
- package/data/spritesheets/f8.ts +75 -0
- package/data/spritesheets/p1.ts +59 -0
- package/data/spritesheets/p2.ts +59 -0
- package/data/spritesheets/p3.ts +59 -0
- package/data/spritesheets/player.ts +59 -0
- package/data/spritesheets/types.ts +26 -0
- package/eslint.config.mjs +37 -0
- package/next.config.ts +7 -0
- package/package.json +85 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/helius-icon.svg +84 -0
- package/public/helius-logo.svg +85 -0
- package/public/next.svg +1 -0
- package/public/plane.glb +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/scripts/clear-city.ts +74 -0
- package/scripts/seed-wallets.ts +185 -0
- package/scripts/setup-webhook.ts +73 -0
- package/src/app/api/auth/callback/route.ts +6 -0
- package/src/app/api/auth/link-wallet/route.ts +6 -0
- package/src/app/api/auth/phantom/route.ts +6 -0
- package/src/app/api/broadcast-position/route.ts +59 -0
- package/src/app/api/leaderboard/route.ts +85 -0
- package/src/app/api/network-stats/route.ts +86 -0
- package/src/app/api/parcel-reward/route.ts +181 -0
- package/src/app/api/queue-status/route.ts +30 -0
- package/src/app/api/snapshots/route.ts +37 -0
- package/src/app/api/transactions/enhanced/route.ts +57 -0
- package/src/app/api/treasury/route.ts +83 -0
- package/src/app/api/wallet/[address]/balances/route.ts +124 -0
- package/src/app/api/wallet/[address]/identity/route.ts +32 -0
- package/src/app/api/wallet/[address]/route.ts +216 -0
- package/src/app/api/wallet/[address]/traded-tokens/route.ts +41 -0
- package/src/app/api/wallets/route.ts +68 -0
- package/src/app/api/webhooks/helius/route.ts +76 -0
- package/src/app/auth/callback/page.tsx +29 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +39 -0
- package/src/app/layout.tsx +43 -0
- package/src/app/page.tsx +16 -0
- package/src/components/AITownNPCs.tsx +206 -0
- package/src/components/ActivityFeed.tsx +189 -0
- package/src/components/AuthPanel.tsx +163 -0
- package/src/components/BeachScene.tsx +280 -0
- package/src/components/Building.tsx +138 -0
- package/src/components/CesiumFlight.tsx +1768 -0
- package/src/components/CesiumGlobe.tsx +616 -0
- package/src/components/CitizenCard.tsx +442 -0
- package/src/components/CitizenCardModal.tsx +153 -0
- package/src/components/CityGrid.tsx +313 -0
- package/src/components/CityLandmarks.tsx +427 -0
- package/src/components/CityScene.tsx +1289 -0
- package/src/components/CitySlotsBadge.tsx +68 -0
- package/src/components/CockpitHUD.tsx +460 -0
- package/src/components/ConvexWrapper.tsx +19 -0
- package/src/components/DubaiDistrict.tsx +630 -0
- package/src/components/FlightMiniMap.tsx +133 -0
- package/src/components/GameChat.tsx +383 -0
- package/src/components/GameHUD.tsx +393 -0
- package/src/components/Ground.tsx +14 -0
- package/src/components/HowItWorksModal.tsx +251 -0
- package/src/components/IngestionBanner.tsx +123 -0
- package/src/components/InstancedBuildings.tsx +316 -0
- package/src/components/InstancedCars.tsx +504 -0
- package/src/components/InstancedCityPlanes.tsx +259 -0
- package/src/components/InstancedHouses.tsx +246 -0
- package/src/components/InstancedLampPosts.tsx +201 -0
- package/src/components/InstancedResidentCars.tsx +357 -0
- package/src/components/InstancedRoadDashes.tsx +42 -0
- package/src/components/InstancedSkyscrapers.tsx +434 -0
- package/src/components/InstancedTrees.tsx +67 -0
- package/src/components/LeaderboardPanel.tsx +136 -0
- package/src/components/MultiplayerPlanes.tsx +128 -0
- package/src/components/NetworkStats.tsx +83 -0
- package/src/components/NewBuildingSpotlight.tsx +93 -0
- package/src/components/ParcelChallengeBanner.tsx +242 -0
- package/src/components/ParcelReward.tsx +191 -0
- package/src/components/Park.tsx +42 -0
- package/src/components/PhantomWrapper.tsx +22 -0
- package/src/components/PixelStreamViewer.tsx +335 -0
- package/src/components/PlaneMode.tsx +190 -0
- package/src/components/PlayerCar.tsx +211 -0
- package/src/components/PlayerPlane.tsx +255 -0
- package/src/components/ProjectileRenderer.tsx +249 -0
- package/src/components/QueueStatusBanner.tsx +86 -0
- package/src/components/RealPlayerTags.tsx +82 -0
- package/src/components/SceneLighting.tsx +382 -0
- package/src/components/SelectionBeam.tsx +59 -0
- package/src/components/SwapPanel.tsx +104 -0
- package/src/components/SwapParticles.tsx +237 -0
- package/src/components/TreasureGate.tsx +505 -0
- package/src/components/WalletPanel.tsx +421 -0
- package/src/components/WalletSearch.tsx +244 -0
- package/src/components/WelcomeOverlay.tsx +135 -0
- package/src/components/WindowTooltip.tsx +498 -0
- package/src/context/AuthContext.tsx +230 -0
- package/src/lib/bot-detection.ts +125 -0
- package/src/lib/building-math.ts +136 -0
- package/src/lib/building-shader.ts +253 -0
- package/src/lib/car-paths.ts +244 -0
- package/src/lib/car-system.ts +182 -0
- package/src/lib/city-constants.ts +29 -0
- package/src/lib/city-slots.ts +35 -0
- package/src/lib/city-zoning.ts +64 -0
- package/src/lib/collision-map.ts +147 -0
- package/src/lib/day-night.ts +252 -0
- package/src/lib/export-card.ts +28 -0
- package/src/lib/helius-webhook.ts +90 -0
- package/src/lib/helius.ts +74 -0
- package/src/lib/house-shader.ts +119 -0
- package/src/lib/mock-data.ts +56 -0
- package/src/lib/multiplayer-manager.ts +329 -0
- package/src/lib/plane-physics.ts +66 -0
- package/src/lib/player-car.ts +147 -0
- package/src/lib/player-plane.ts +200 -0
- package/src/lib/projectile-system.ts +272 -0
- package/src/lib/skyscraper-types.ts +52 -0
- package/src/lib/sound-engine.ts +464 -0
- package/src/lib/supabase-admin.ts +9 -0
- package/src/lib/supabase.ts +8 -0
- package/src/lib/swap-events.ts +70 -0
- package/src/middleware.ts +37 -0
- package/src/types/phantom.d.ts +16 -0
- package/src/types/wallet.ts +20 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,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
|
+
]);
|