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