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,190 @@
1
+ "use client";
2
+ import { useRef, useEffect, useMemo, MutableRefObject } from "react";
3
+ import { useFrame, useThree } from "@react-three/fiber";
4
+ import * as THREE from "three";
5
+ import {
6
+ PlaneInput,
7
+ PlaneState,
8
+ createPlaneState,
9
+ updatePlane,
10
+ applyMouseDelta,
11
+ } from "@/lib/plane-physics";
12
+
13
+ export interface PlaneTransform {
14
+ x: number; y: number; z: number;
15
+ yaw: number; pitch: number;
16
+ }
17
+
18
+ interface PlaneModeProps {
19
+ active: boolean;
20
+ positionRef: MutableRefObject<PlaneTransform | null>;
21
+ playerColor?: string;
22
+ }
23
+
24
+ function PlaneMesh({ color }: { color: string }) {
25
+ const mat = useMemo(() => new THREE.MeshStandardMaterial({ color, metalness: 0.5, roughness: 0.4 }), [color]);
26
+ const darkMat = useMemo(() => new THREE.MeshStandardMaterial({ color: "#333", metalness: 0.3, roughness: 0.7 }), []);
27
+ const glassMat = useMemo(() => new THREE.MeshStandardMaterial({ color: "#88ccff", transparent: true, opacity: 0.6, metalness: 0.2, roughness: 0.1 }), []);
28
+
29
+ return (
30
+ <group>
31
+ {/* Fuselage */}
32
+ <mesh material={mat}>
33
+ <boxGeometry args={[0.7, 0.7, 4.5]} />
34
+ </mesh>
35
+ {/* Nose cone */}
36
+ <mesh position={[0, 0, 2.5]} rotation={[Math.PI / 2, 0, 0]} material={mat}>
37
+ <coneGeometry args={[0.35, 1.0, 8]} />
38
+ </mesh>
39
+ {/* Cockpit glass */}
40
+ <mesh position={[0, 0.45, 0.8]} material={glassMat}>
41
+ <boxGeometry args={[0.55, 0.35, 1.0]} />
42
+ </mesh>
43
+ {/* Main wings */}
44
+ <mesh position={[0, -0.1, 0]} material={mat}>
45
+ <boxGeometry args={[9, 0.15, 2.0]} />
46
+ </mesh>
47
+ {/* Wing tips */}
48
+ <mesh position={[4.8, -0.1, -0.1]} rotation={[0, 0.15, 0.2]} material={mat}>
49
+ <boxGeometry args={[0.8, 0.12, 1.0]} />
50
+ </mesh>
51
+ <mesh position={[-4.8, -0.1, -0.1]} rotation={[0, -0.15, -0.2]} material={mat}>
52
+ <boxGeometry args={[0.8, 0.12, 1.0]} />
53
+ </mesh>
54
+ {/* Horizontal tail */}
55
+ <mesh position={[0, 0, -2.1]} material={mat}>
56
+ <boxGeometry args={[3.5, 0.12, 0.9]} />
57
+ </mesh>
58
+ {/* Vertical tail */}
59
+ <mesh position={[0, 0.6, -2.0]} material={mat}>
60
+ <boxGeometry args={[0.12, 1.1, 0.7]} />
61
+ </mesh>
62
+ {/* Engines */}
63
+ <mesh position={[2.2, -0.3, 0.3]} rotation={[Math.PI / 2, 0, 0]} material={darkMat}>
64
+ <cylinderGeometry args={[0.22, 0.25, 1.2, 10]} />
65
+ </mesh>
66
+ <mesh position={[-2.2, -0.3, 0.3]} rotation={[Math.PI / 2, 0, 0]} material={darkMat}>
67
+ <cylinderGeometry args={[0.22, 0.25, 1.2, 10]} />
68
+ </mesh>
69
+ </group>
70
+ );
71
+ }
72
+
73
+ export default function PlaneMode({ active, positionRef, playerColor = "#E35930" }: PlaneModeProps) {
74
+ const { camera } = useThree();
75
+ const groupRef = useRef<THREE.Group>(null);
76
+ const inputRef = useRef<PlaneInput>({ forward: false, backward: false, left: false, right: false, up: false, down: false, boost: false });
77
+ const stateRef = useRef<PlaneState>(createPlaneState(0, 0));
78
+ const pointerLockedRef = useRef(false);
79
+
80
+ // Reset plane state when entering plane mode
81
+ useEffect(() => {
82
+ if (active) {
83
+ stateRef.current = createPlaneState(
84
+ camera.position.x,
85
+ camera.position.z,
86
+ );
87
+ stateRef.current.y = Math.max(camera.position.y, 50);
88
+ } else {
89
+ positionRef.current = null;
90
+ pointerLockedRef.current = false;
91
+ }
92
+ }, [active, camera, positionRef]);
93
+
94
+ // Pointer lock
95
+ useEffect(() => {
96
+ if (!active) return;
97
+
98
+ function onClick() {
99
+ if (!pointerLockedRef.current) {
100
+ document.body.requestPointerLock();
101
+ }
102
+ }
103
+ function onLockChange() {
104
+ pointerLockedRef.current = !!document.pointerLockElement;
105
+ }
106
+ function onMouseMove(e: MouseEvent) {
107
+ if (!pointerLockedRef.current) return;
108
+ applyMouseDelta(stateRef.current, e.movementX, e.movementY);
109
+ }
110
+
111
+ document.addEventListener("click", onClick);
112
+ document.addEventListener("pointerlockchange", onLockChange);
113
+ document.addEventListener("mousemove", onMouseMove);
114
+ return () => {
115
+ document.removeEventListener("click", onClick);
116
+ document.removeEventListener("pointerlockchange", onLockChange);
117
+ document.removeEventListener("mousemove", onMouseMove);
118
+ if (document.pointerLockElement) document.exitPointerLock();
119
+ };
120
+ }, [active]);
121
+
122
+ // Keyboard controls
123
+ useEffect(() => {
124
+ if (!active) return;
125
+ function handleKey(e: KeyboardEvent, down: boolean) {
126
+ const tag = (e.target as HTMLElement)?.tagName;
127
+ if (tag === "INPUT" || tag === "TEXTAREA") return;
128
+ const input = inputRef.current;
129
+ switch (e.key.toLowerCase()) {
130
+ case "w": case "arrowup": input.forward = down; break;
131
+ case "s": case "arrowdown": input.backward = down; break;
132
+ case "a": case "arrowleft": input.left = down; break;
133
+ case "d": case "arrowright": input.right = down; break;
134
+ case "q": input.up = down; break;
135
+ case "e": input.down = down; break;
136
+ case "shift": input.boost = down; break;
137
+ }
138
+ if (e.key.startsWith("Arrow")) e.preventDefault();
139
+ if (e.key === " ") e.preventDefault();
140
+ }
141
+ const onDown = (e: KeyboardEvent) => handleKey(e, true);
142
+ const onUp = (e: KeyboardEvent) => handleKey(e, false);
143
+ window.addEventListener("keydown", onDown);
144
+ window.addEventListener("keyup", onUp);
145
+ return () => {
146
+ window.removeEventListener("keydown", onDown);
147
+ window.removeEventListener("keyup", onUp);
148
+ };
149
+ }, [active]);
150
+
151
+ useFrame((_, delta) => {
152
+ if (!active || !groupRef.current) return;
153
+
154
+ updatePlane(stateRef.current, inputRef.current, Math.min(delta, 0.1));
155
+ const state = stateRef.current;
156
+
157
+ // Update plane mesh
158
+ groupRef.current.position.set(state.x, state.y, state.z);
159
+ groupRef.current.rotation.set(state.pitch, state.yaw, 0, "YXZ");
160
+ groupRef.current.visible = true;
161
+
162
+ // Write position for multiplayer
163
+ positionRef.current = { x: state.x, y: state.y, z: state.z, yaw: state.yaw, pitch: state.pitch };
164
+
165
+ // Chase camera — behind and slightly above
166
+ const behindDist = 14;
167
+ const aboveOffset = 4;
168
+ const cosP = Math.cos(state.pitch);
169
+ const camX = state.x - Math.sin(state.yaw) * cosP * behindDist;
170
+ const camY = state.y + aboveOffset + Math.sin(state.pitch) * behindDist * 0.4;
171
+ const camZ = state.z - Math.cos(state.yaw) * cosP * behindDist;
172
+ camera.position.lerp(new THREE.Vector3(camX, camY, camZ), 0.1);
173
+
174
+ // Look ahead of plane
175
+ const lookX = state.x + Math.sin(state.yaw) * cosP * 8;
176
+ const lookY = state.y - Math.sin(state.pitch) * 8;
177
+ const lookZ = state.z + Math.cos(state.yaw) * cosP * 8;
178
+ camera.lookAt(lookX, lookY, lookZ);
179
+ });
180
+
181
+ useFrame(() => {
182
+ if (!active && groupRef.current) groupRef.current.visible = false;
183
+ });
184
+
185
+ return (
186
+ <group ref={groupRef} visible={false}>
187
+ <PlaneMesh color={playerColor} />
188
+ </group>
189
+ );
190
+ }
@@ -0,0 +1,211 @@
1
+ "use client";
2
+
3
+ import { useRef, useEffect, useMemo, MutableRefObject } from "react";
4
+ import { useFrame } from "@react-three/fiber";
5
+ import * as THREE from "three";
6
+ import { CollisionMap } from "@/lib/collision-map";
7
+ import { CarEngine, initAudio } from "@/lib/sound-engine";
8
+ import {
9
+ CarInput,
10
+ CarState,
11
+ createCarState,
12
+ updateCar,
13
+ CAR_Y,
14
+ } from "@/lib/player-car";
15
+
16
+ interface PlayerCarProps {
17
+ active: boolean;
18
+ collisionMap: CollisionMap | null;
19
+ positionRef: MutableRefObject<[number, number, number] | null>;
20
+ headingRef: MutableRefObject<number>;
21
+ speedRef?: MutableRefObject<number>;
22
+ }
23
+
24
+ export default function PlayerCar({ active, collisionMap, positionRef, headingRef, speedRef }: PlayerCarProps) {
25
+ const groupRef = useRef<THREE.Group>(null);
26
+ const arrowRef = useRef<THREE.Mesh>(null);
27
+ const inputRef = useRef<CarInput>({
28
+ forward: false,
29
+ backward: false,
30
+ left: false,
31
+ right: false,
32
+ brake: false,
33
+ });
34
+ const stateRef = useRef<CarState>(createCarState());
35
+ const engineRef = useRef<CarEngine | null>(null);
36
+
37
+ // Reset car state when entering car mode
38
+ useEffect(() => {
39
+ if (active) {
40
+ stateRef.current = createCarState();
41
+ inputRef.current = {
42
+ forward: false,
43
+ backward: false,
44
+ left: false,
45
+ right: false,
46
+ brake: false,
47
+ };
48
+ } else {
49
+ positionRef.current = null;
50
+ // Stop engine sound
51
+ engineRef.current?.stop();
52
+ engineRef.current = null;
53
+ }
54
+ return () => {
55
+ engineRef.current?.stop();
56
+ engineRef.current = null;
57
+ };
58
+ }, [active, positionRef]);
59
+
60
+ // Keyboard listeners
61
+ useEffect(() => {
62
+ if (!active) return;
63
+
64
+ function handleKey(e: KeyboardEvent, down: boolean) {
65
+ // Skip if typing in an input field
66
+ const tag = (e.target as HTMLElement)?.tagName;
67
+ if (tag === "INPUT" || tag === "TEXTAREA") return;
68
+
69
+ const input = inputRef.current;
70
+ switch (e.key) {
71
+ case "w":
72
+ case "W":
73
+ case "ArrowUp":
74
+ input.forward = down;
75
+ if (e.key.startsWith("Arrow")) e.preventDefault();
76
+ break;
77
+ case "s":
78
+ case "S":
79
+ case "ArrowDown":
80
+ input.backward = down;
81
+ if (e.key.startsWith("Arrow")) e.preventDefault();
82
+ break;
83
+ case "a":
84
+ case "A":
85
+ case "ArrowLeft":
86
+ input.left = down;
87
+ if (e.key.startsWith("Arrow")) e.preventDefault();
88
+ break;
89
+ case "d":
90
+ case "D":
91
+ case "ArrowRight":
92
+ input.right = down;
93
+ if (e.key.startsWith("Arrow")) e.preventDefault();
94
+ break;
95
+ case " ":
96
+ input.brake = down;
97
+ e.preventDefault();
98
+ break;
99
+ }
100
+ }
101
+
102
+ const onDown = (e: KeyboardEvent) => handleKey(e, true);
103
+ const onUp = (e: KeyboardEvent) => handleKey(e, false);
104
+
105
+ window.addEventListener("keydown", onDown);
106
+ window.addEventListener("keyup", onUp);
107
+ return () => {
108
+ window.removeEventListener("keydown", onDown);
109
+ window.removeEventListener("keyup", onUp);
110
+ };
111
+ }, [active]);
112
+
113
+ // Physics + rendering each frame
114
+ useFrame((_, delta) => {
115
+ if (!active || !groupRef.current) return;
116
+
117
+ const state = stateRef.current;
118
+ updateCar(state, inputRef.current, delta, collisionMap);
119
+
120
+ // Start engine on first frame if needed
121
+ if (!engineRef.current) {
122
+ initAudio();
123
+ const eng = new CarEngine();
124
+ eng.start();
125
+ engineRef.current = eng;
126
+ }
127
+ // Update engine throttle based on speed
128
+ const maxSpeed = 25;
129
+ engineRef.current.setThrottle(Math.abs(state.speed) / maxSpeed);
130
+
131
+ // Update mesh transforms
132
+ groupRef.current.position.set(state.x, CAR_Y, state.z);
133
+ groupRef.current.rotation.y = state.heading;
134
+ groupRef.current.visible = true;
135
+
136
+ // Write position + heading for camera tracking
137
+ positionRef.current = [state.x, CAR_Y, state.z];
138
+ headingRef.current = state.heading;
139
+ if (speedRef) speedRef.current = Math.abs(state.speed);
140
+
141
+ // Bobbing arrow
142
+ if (arrowRef.current) {
143
+ arrowRef.current.position.y = 2.0 + Math.sin(Date.now() * 0.003) * 0.2;
144
+ }
145
+ });
146
+
147
+ // Hide when inactive
148
+ useFrame(() => {
149
+ if (!active && groupRef.current) {
150
+ groupRef.current.visible = false;
151
+ }
152
+ });
153
+
154
+ // Materials
155
+ const bodyMat = useMemo(() => new THREE.MeshStandardMaterial({
156
+ color: "#E35930",
157
+ metalness: 0.4,
158
+ roughness: 0.5,
159
+ }), []);
160
+
161
+ const cabinMat = useMemo(() => new THREE.MeshStandardMaterial({
162
+ color: "#b8421f",
163
+ metalness: 0.3,
164
+ roughness: 0.6,
165
+ }), []);
166
+
167
+ const wheelMat = useMemo(() => new THREE.MeshStandardMaterial({
168
+ color: "#333333",
169
+ metalness: 0.2,
170
+ roughness: 0.8,
171
+ }), []);
172
+
173
+ const arrowMat = useMemo(() => new THREE.MeshStandardMaterial({
174
+ color: "#8b5cf6",
175
+ emissive: "#8b5cf6",
176
+ emissiveIntensity: 0.6,
177
+ transparent: true,
178
+ opacity: 0.85,
179
+ }), []);
180
+
181
+ return (
182
+ <group ref={groupRef} visible={false}>
183
+ {/* Body */}
184
+ <mesh material={bodyMat}>
185
+ <boxGeometry args={[0.7, 0.35, 1.5]} />
186
+ </mesh>
187
+
188
+ {/* Cabin */}
189
+ <mesh position={[0, 0.25, -0.1]} material={cabinMat}>
190
+ <boxGeometry args={[0.6, 0.25, 0.65]} />
191
+ </mesh>
192
+
193
+ {/* Wheels */}
194
+ {[
195
+ [-0.35, -0.12, 0.45],
196
+ [0.35, -0.12, 0.45],
197
+ [-0.35, -0.12, -0.45],
198
+ [0.35, -0.12, -0.45],
199
+ ].map((pos, i) => (
200
+ <mesh key={i} position={pos as [number, number, number]} rotation={[0, 0, Math.PI / 2]} material={wheelMat}>
201
+ <cylinderGeometry args={[0.12, 0.12, 0.1, 8]} />
202
+ </mesh>
203
+ ))}
204
+
205
+ {/* Floating arrow marker */}
206
+ <mesh ref={arrowRef} position={[0, 2.0, 0]} rotation={[Math.PI, 0, 0]} material={arrowMat}>
207
+ <coneGeometry args={[0.25, 0.5, 4]} />
208
+ </mesh>
209
+ </group>
210
+ );
211
+ }
@@ -0,0 +1,255 @@
1
+ "use client";
2
+
3
+ import { MutableRefObject, useEffect, useMemo, useRef } from "react";
4
+ import { useFrame, useThree } from "@react-three/fiber";
5
+ import * as THREE from "three";
6
+ import {
7
+ PlaneInput,
8
+ PlaneState,
9
+ createPlaneState,
10
+ updatePlane,
11
+ } from "@/lib/player-plane";
12
+
13
+ interface PlayerPlaneProps {
14
+ active: boolean;
15
+ positionRef: MutableRefObject<[number, number, number] | null>;
16
+ headingRef: MutableRefObject<number>;
17
+ pitchRef?: MutableRefObject<number>;
18
+ speedRef?: MutableRefObject<number>;
19
+ walletAddress?: string | null;
20
+ }
21
+
22
+ export default function PlayerPlane({
23
+ active,
24
+ positionRef,
25
+ headingRef,
26
+ pitchRef,
27
+ speedRef,
28
+ walletAddress,
29
+ }: PlayerPlaneProps) {
30
+ const { camera } = useThree();
31
+ const groupRef = useRef<THREE.Group>(null);
32
+ const labelRef = useRef<THREE.Mesh>(null);
33
+ const spotlightTargetRef = useRef<THREE.Object3D>(new THREE.Object3D());
34
+
35
+ const inputRef = useRef<PlaneInput>({
36
+ forward: false,
37
+ backward: false,
38
+ left: false,
39
+ right: false,
40
+ up: false,
41
+ down: false,
42
+ });
43
+ const stateRef = useRef<PlaneState>(createPlaneState());
44
+
45
+ useEffect(() => {
46
+ if (active) {
47
+ stateRef.current = createPlaneState();
48
+ inputRef.current = {
49
+ forward: false,
50
+ backward: false,
51
+ left: false,
52
+ right: false,
53
+ up: false,
54
+ down: false,
55
+ };
56
+ } else {
57
+ positionRef.current = null;
58
+ if (pitchRef) pitchRef.current = 0;
59
+ }
60
+ }, [active, positionRef, pitchRef]);
61
+
62
+ useEffect(() => {
63
+ if (!active) return;
64
+
65
+ function handleKey(e: KeyboardEvent, down: boolean) {
66
+ const tag = (e.target as HTMLElement)?.tagName;
67
+ if (tag === "INPUT" || tag === "TEXTAREA") return;
68
+
69
+ const input = inputRef.current;
70
+ switch (e.key) {
71
+ case "w":
72
+ case "W":
73
+ case "ArrowUp":
74
+ input.forward = down;
75
+ if (e.key.startsWith("Arrow")) e.preventDefault();
76
+ break;
77
+ case "s":
78
+ case "S":
79
+ case "ArrowDown":
80
+ input.backward = down;
81
+ if (e.key.startsWith("Arrow")) e.preventDefault();
82
+ break;
83
+ case "a":
84
+ case "A":
85
+ case "ArrowLeft":
86
+ input.left = down;
87
+ if (e.key.startsWith("Arrow")) e.preventDefault();
88
+ break;
89
+ case "d":
90
+ case "D":
91
+ case "ArrowRight":
92
+ input.right = down;
93
+ if (e.key.startsWith("Arrow")) e.preventDefault();
94
+ break;
95
+ case "q":
96
+ case "Q":
97
+ case " ":
98
+ input.up = down;
99
+ e.preventDefault();
100
+ break;
101
+ case "e":
102
+ case "E":
103
+ input.down = down;
104
+ break;
105
+ }
106
+ }
107
+
108
+ const onDown = (e: KeyboardEvent) => handleKey(e, true);
109
+ const onUp = (e: KeyboardEvent) => handleKey(e, false);
110
+ window.addEventListener("keydown", onDown);
111
+ window.addEventListener("keyup", onUp);
112
+ return () => {
113
+ window.removeEventListener("keydown", onDown);
114
+ window.removeEventListener("keyup", onUp);
115
+ };
116
+ }, [active]);
117
+
118
+ useFrame((_, delta) => {
119
+ if (!groupRef.current) return;
120
+
121
+ if (!active) {
122
+ groupRef.current.visible = false;
123
+ return;
124
+ }
125
+
126
+ const state = stateRef.current;
127
+ updatePlane(state, inputRef.current, delta);
128
+
129
+ groupRef.current.position.set(state.x, state.y, state.z);
130
+ // YXZ order feels better for aircraft (yaw -> pitch -> roll)
131
+ groupRef.current.rotation.set(state.pitch, state.heading, state.roll, "YXZ");
132
+ groupRef.current.visible = true;
133
+
134
+ positionRef.current = [state.x, state.y, state.z];
135
+ headingRef.current = state.heading;
136
+ if (pitchRef) pitchRef.current = state.pitch;
137
+ if (speedRef) speedRef.current = state.speed;
138
+
139
+ // Keep label readable from any camera angle
140
+ if (labelRef.current) {
141
+ labelRef.current.lookAt(camera.position);
142
+ }
143
+ });
144
+
145
+ const bodyMat = useMemo(
146
+ () =>
147
+ new THREE.MeshStandardMaterial({
148
+ color: "#E35930",
149
+ metalness: 0.6,
150
+ roughness: 0.3,
151
+ }),
152
+ [],
153
+ );
154
+
155
+ const wingMat = useMemo(
156
+ () =>
157
+ new THREE.MeshStandardMaterial({
158
+ color: "#c0c0c0",
159
+ metalness: 0.7,
160
+ roughness: 0.2,
161
+ }),
162
+ [],
163
+ );
164
+
165
+ const glassMat = useMemo(
166
+ () =>
167
+ new THREE.MeshStandardMaterial({
168
+ color: "#88ccff",
169
+ metalness: 0.9,
170
+ roughness: 0.1,
171
+ transparent: true,
172
+ opacity: 0.6,
173
+ }),
174
+ [],
175
+ );
176
+
177
+ const engineMat = useMemo(
178
+ () =>
179
+ new THREE.MeshStandardMaterial({
180
+ color: "#E35930",
181
+ emissive: "#E35930",
182
+ emissiveIntensity: 0.8,
183
+ }),
184
+ [],
185
+ );
186
+
187
+ const trailMat = useMemo(
188
+ () =>
189
+ new THREE.MeshStandardMaterial({
190
+ color: "#ff6600",
191
+ emissive: "#ff4400",
192
+ emissiveIntensity: 2,
193
+ transparent: true,
194
+ opacity: 0.6,
195
+ }),
196
+ [],
197
+ );
198
+
199
+ return (
200
+ <group ref={groupRef} visible={false}>
201
+ <mesh material={bodyMat} rotation={[Math.PI / 2, 0, 0]}>
202
+ <capsuleGeometry args={[0.35, 2.5, 4, 8]} />
203
+ </mesh>
204
+
205
+ <mesh material={glassMat} position={[0, 0.2, 1]}>
206
+ <sphereGeometry args={[0.3, 8, 6, 0, Math.PI * 2, 0, Math.PI / 2]} />
207
+ </mesh>
208
+
209
+ <mesh material={wingMat} position={[0, -0.05, 0]}>
210
+ <boxGeometry args={[5, 0.08, 1]} />
211
+ </mesh>
212
+
213
+ <mesh material={bodyMat} position={[2.5, 0.15, 0]} rotation={[0, 0, 0.3]}>
214
+ <boxGeometry args={[0.3, 0.06, 0.7]} />
215
+ </mesh>
216
+ <mesh material={bodyMat} position={[-2.5, 0.15, 0]} rotation={[0, 0, -0.3]}>
217
+ <boxGeometry args={[0.3, 0.06, 0.7]} />
218
+ </mesh>
219
+
220
+ <mesh material={bodyMat} position={[0, 0.5, -1.6]}>
221
+ <boxGeometry args={[0.06, 0.8, 0.6]} />
222
+ </mesh>
223
+
224
+ <mesh material={wingMat} position={[0, 0.05, -1.5]}>
225
+ <boxGeometry args={[1.8, 0.06, 0.5]} />
226
+ </mesh>
227
+
228
+ <mesh material={engineMat} position={[0, 0, -1.8]}>
229
+ <sphereGeometry args={[0.2, 8, 8]} />
230
+ </mesh>
231
+
232
+ <mesh material={trailMat} position={[0, 0, -2.5]} scale={[0.15, 0.15, 1.5]}>
233
+ <coneGeometry args={[1, 2, 6]} />
234
+ </mesh>
235
+
236
+ <primitive object={spotlightTargetRef.current} position={[0, -50, 4]} />
237
+ <spotLight
238
+ position={[0, -1, 2]}
239
+ angle={0.4}
240
+ penumbra={0.5}
241
+ intensity={3}
242
+ distance={80}
243
+ color="#E35930"
244
+ target={spotlightTargetRef.current}
245
+ />
246
+
247
+ {walletAddress && (
248
+ <mesh ref={labelRef} position={[0, 1.5, 0]}>
249
+ <planeGeometry args={[2, 0.3]} />
250
+ <meshBasicMaterial color="#E35930" transparent opacity={0.5} />
251
+ </mesh>
252
+ )}
253
+ </group>
254
+ );
255
+ }