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