Spaces:
Sleeping
Sleeping
Chat overlay, tactical map bot/player distinction, red lead dot, evasion AI, 7v6 bots, less turbulence, skirt depthWrite fix
Browse files- src/App.tsx +29 -1
- src/components/GameHUD.tsx +126 -19
- src/game/botAISystem.ts +36 -24
- src/game/gameEngine.ts +2 -2
- src/game/worldRenderer.ts +2 -2
src/App.tsx
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
| 21 |
} from "./types";
|
| 22 |
import { MainMenu } from "./components/MainMenu";
|
| 23 |
import { PilotRegistration } from "./components/PilotRegistration";
|
| 24 |
-
import { GameHUD } from "./components/GameHUD";
|
| 25 |
import { GameEngine } from "./game/gameEngine";
|
| 26 |
import { WorldRenderer } from "./game/worldRenderer";
|
| 27 |
import { InputManager } from "./game/inputManager";
|
|
@@ -135,6 +135,9 @@ export default function App() {
|
|
| 135 |
key: 0
|
| 136 |
});
|
| 137 |
|
|
|
|
|
|
|
|
|
|
| 138 |
const [isPaused, setIsPaused] = useState(false);
|
| 139 |
const isPausedRef = useRef(false);
|
| 140 |
const [showTacticalMap, setShowTacticalMap] = useState(false);
|
|
@@ -374,6 +377,7 @@ export default function App() {
|
|
| 374 |
const wsUrl = `${protocol}${window.location.host}/multiplayer`;
|
| 375 |
try {
|
| 376 |
socket = new WebSocket(wsUrl);
|
|
|
|
| 377 |
socket.onerror = (err) => {
|
| 378 |
console.warn("Multiplayer matchmaking offline or connectivity error. Operating in offline/local capability.", err);
|
| 379 |
};
|
|
@@ -594,6 +598,13 @@ export default function App() {
|
|
| 594 |
});
|
| 595 |
}
|
| 596 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 597 |
} catch (err) {
|
| 598 |
console.error("Multiplayer message parse/apply error:", err);
|
| 599 |
}
|
|
@@ -1144,6 +1155,23 @@ export default function App() {
|
|
| 1144 |
mapId={activeEngine.selectedMapId}
|
| 1145 |
showTacticalMap={showTacticalMap}
|
| 1146 |
onCloseTacticalMap={() => setShowTacticalMap(false)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1147 |
/>
|
| 1148 |
</div>
|
| 1149 |
)}
|
|
|
|
| 21 |
} from "./types";
|
| 22 |
import { MainMenu } from "./components/MainMenu";
|
| 23 |
import { PilotRegistration } from "./components/PilotRegistration";
|
| 24 |
+
import { GameHUD, ChatMessage } from "./components/GameHUD";
|
| 25 |
import { GameEngine } from "./game/gameEngine";
|
| 26 |
import { WorldRenderer } from "./game/worldRenderer";
|
| 27 |
import { InputManager } from "./game/inputManager";
|
|
|
|
| 135 |
key: 0
|
| 136 |
});
|
| 137 |
|
| 138 |
+
const [chatMessages, setChatMessages] = useState<ChatMessage[]>([]);
|
| 139 |
+
const multiplayerSocketRef = useRef<WebSocket | null>(null);
|
| 140 |
+
|
| 141 |
const [isPaused, setIsPaused] = useState(false);
|
| 142 |
const isPausedRef = useRef(false);
|
| 143 |
const [showTacticalMap, setShowTacticalMap] = useState(false);
|
|
|
|
| 377 |
const wsUrl = `${protocol}${window.location.host}/multiplayer`;
|
| 378 |
try {
|
| 379 |
socket = new WebSocket(wsUrl);
|
| 380 |
+
multiplayerSocketRef.current = socket;
|
| 381 |
socket.onerror = (err) => {
|
| 382 |
console.warn("Multiplayer matchmaking offline or connectivity error. Operating in offline/local capability.", err);
|
| 383 |
};
|
|
|
|
| 598 |
});
|
| 599 |
}
|
| 600 |
}
|
| 601 |
+
if (msg.type === "chat_broadcast") {
|
| 602 |
+
setChatMessages(prev => [...prev.slice(-49), {
|
| 603 |
+
sender: msg.senderName,
|
| 604 |
+
text: msg.text,
|
| 605 |
+
ts: Date.now()
|
| 606 |
+
}]);
|
| 607 |
+
}
|
| 608 |
} catch (err) {
|
| 609 |
console.error("Multiplayer message parse/apply error:", err);
|
| 610 |
}
|
|
|
|
| 1155 |
mapId={activeEngine.selectedMapId}
|
| 1156 |
showTacticalMap={showTacticalMap}
|
| 1157 |
onCloseTacticalMap={() => setShowTacticalMap(false)}
|
| 1158 |
+
chatMessages={chatMessages}
|
| 1159 |
+
onSendChat={(text) => {
|
| 1160 |
+
const sock = multiplayerSocketRef.current;
|
| 1161 |
+
if (sock && sock.readyState === WebSocket.OPEN) {
|
| 1162 |
+
sock.send(JSON.stringify({
|
| 1163 |
+
type: "chat",
|
| 1164 |
+
senderName: progression.nickname || "PILOT",
|
| 1165 |
+
text
|
| 1166 |
+
}));
|
| 1167 |
+
} else {
|
| 1168 |
+
setChatMessages(prev => [...prev.slice(-49), {
|
| 1169 |
+
sender: progression.nickname || "PILOT",
|
| 1170 |
+
text,
|
| 1171 |
+
ts: Date.now()
|
| 1172 |
+
}]);
|
| 1173 |
+
}
|
| 1174 |
+
}}
|
| 1175 |
/>
|
| 1176 |
</div>
|
| 1177 |
)}
|
src/components/GameHUD.tsx
CHANGED
|
@@ -18,6 +18,12 @@ import {
|
|
| 18 |
import { Crosshair, MapPin, Zap } from "lucide-react";
|
| 19 |
import { MAP_REGISTRY } from "../game/content/maps/registry";
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
interface HUDProps {
|
| 22 |
playerPilot: Pilot | undefined;
|
| 23 |
pilots: Pilot[];
|
|
@@ -41,6 +47,8 @@ interface HUDProps {
|
|
| 41 |
mapId: string;
|
| 42 |
showTacticalMap: boolean;
|
| 43 |
onCloseTacticalMap: () => void;
|
|
|
|
|
|
|
| 44 |
}
|
| 45 |
|
| 46 |
function clamp(n: number, min: number, max: number) {
|
|
@@ -386,20 +394,51 @@ const TacticalMapOverlay: React.FC<{
|
|
| 386 |
);
|
| 387 |
})}
|
| 388 |
|
| 389 |
-
{
|
| 390 |
-
const
|
| 391 |
-
const
|
| 392 |
-
return (
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
</svg>
|
| 404 |
</div>
|
| 405 |
|
|
@@ -435,10 +474,12 @@ const TacticalMapOverlay: React.FC<{
|
|
| 435 |
<div className="mt-6 text-[8px] font-black tracking-[0.2em] text-slate-500 uppercase">
|
| 436 |
Legend
|
| 437 |
</div>
|
| 438 |
-
<div className="mt-3 space-y-
|
| 439 |
-
<div className="flex items-center gap-3"><span className="text-amber-300">▲</span>
|
| 440 |
-
<div className="flex items-center gap-3"><span className="text-rose-400">▲</span> Red
|
| 441 |
-
<div className="flex items-center gap-3"><span className="text-sky-400">▲</span> Blue
|
|
|
|
|
|
|
| 442 |
<div className="flex items-center gap-3"><span className="text-emerald-300">◇</span> Ground objective</div>
|
| 443 |
</div>
|
| 444 |
</aside>
|
|
@@ -471,7 +512,7 @@ const LeadProjector: React.FC = () => {
|
|
| 471 |
style={{ boxShadow: "0 0 0 1px #000, inset 0 0 0 1px #000" }}
|
| 472 |
>
|
| 473 |
<div
|
| 474 |
-
className="w-1.5 h-1.5 bg-
|
| 475 |
style={{ boxShadow: "0 0 0 1px #000" }}
|
| 476 |
/>
|
| 477 |
|
|
@@ -644,6 +685,8 @@ export const GameHUD: React.FC<HUDProps> = ({
|
|
| 644 |
mapId,
|
| 645 |
showTacticalMap,
|
| 646 |
onCloseTacticalMap,
|
|
|
|
|
|
|
| 647 |
}) => {
|
| 648 |
if (!playerPilot) return null;
|
| 649 |
|
|
@@ -1040,6 +1083,70 @@ export const GameHUD: React.FC<HUDProps> = ({
|
|
| 1040 |
</div>
|
| 1041 |
)}
|
| 1042 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1043 |
</div>
|
| 1044 |
);
|
| 1045 |
};
|
|
|
|
| 18 |
import { Crosshair, MapPin, Zap } from "lucide-react";
|
| 19 |
import { MAP_REGISTRY } from "../game/content/maps/registry";
|
| 20 |
|
| 21 |
+
export interface ChatMessage {
|
| 22 |
+
sender: string;
|
| 23 |
+
text: string;
|
| 24 |
+
ts: number;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
interface HUDProps {
|
| 28 |
playerPilot: Pilot | undefined;
|
| 29 |
pilots: Pilot[];
|
|
|
|
| 47 |
mapId: string;
|
| 48 |
showTacticalMap: boolean;
|
| 49 |
onCloseTacticalMap: () => void;
|
| 50 |
+
chatMessages?: ChatMessage[];
|
| 51 |
+
onSendChat?: (text: string) => void;
|
| 52 |
}
|
| 53 |
|
| 54 |
function clamp(n: number, min: number, max: number) {
|
|
|
|
| 394 |
);
|
| 395 |
})}
|
| 396 |
|
| 397 |
+
{(() => {
|
| 398 |
+
const playerPilotOnMap = pilots.find(p => p.id === "player");
|
| 399 |
+
const myTeam = playerPilotOnMap?.team ?? 1;
|
| 400 |
+
return pilots.filter(p => p.damage.fuselage > 0).map(pilot => {
|
| 401 |
+
const pt = project(pilot.x, pilot.z);
|
| 402 |
+
const isMe = pilot.id === "player";
|
| 403 |
+
const allied = pilot.team === myTeam;
|
| 404 |
+
const teamColor = pilot.team === 1 ? "#fb7185" : "#38bdf8";
|
| 405 |
+
const deg = pilot.yaw * 180 / Math.PI;
|
| 406 |
+
|
| 407 |
+
if (pilot.isBot) {
|
| 408 |
+
// Bots: hollow circle with small cross
|
| 409 |
+
const botColor = allied ? teamColor : teamColor;
|
| 410 |
+
return (
|
| 411 |
+
<g key={pilot.id} transform={`translate(${pt.x} ${pt.y})`}>
|
| 412 |
+
<circle r="7" fill="none" stroke={botColor} strokeWidth="1.5" strokeOpacity="0.6" strokeDasharray="4 2" />
|
| 413 |
+
<line x1="-4" y1="0" x2="4" y2="0" stroke={botColor} strokeWidth="1" strokeOpacity="0.6" />
|
| 414 |
+
<line x1="0" y1="-4" x2="0" y2="4" stroke={botColor} strokeWidth="1" strokeOpacity="0.6" />
|
| 415 |
+
</g>
|
| 416 |
+
);
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
// Real players: solid arrow
|
| 420 |
+
const fill = isMe ? "#facc15" : teamColor;
|
| 421 |
+
return (
|
| 422 |
+
<g key={pilot.id} transform={`translate(${pt.x} ${pt.y}) rotate(${deg})`}>
|
| 423 |
+
<path d="M0 -14L9 11L0 7L-9 11Z" fill={fill} stroke="#020617" strokeWidth="2" />
|
| 424 |
+
{/* Allied player name — not shown for enemies (CoD convention) */}
|
| 425 |
+
{allied && !isMe && (
|
| 426 |
+
<text
|
| 427 |
+
x="0" y="-20"
|
| 428 |
+
textAnchor="middle"
|
| 429 |
+
fill={teamColor}
|
| 430 |
+
fontSize="9"
|
| 431 |
+
fontWeight="700"
|
| 432 |
+
fontFamily="monospace"
|
| 433 |
+
transform={`rotate(${-deg})`}
|
| 434 |
+
>
|
| 435 |
+
{pilot.name}
|
| 436 |
+
</text>
|
| 437 |
+
)}
|
| 438 |
+
</g>
|
| 439 |
+
);
|
| 440 |
+
});
|
| 441 |
+
})()}
|
| 442 |
</svg>
|
| 443 |
</div>
|
| 444 |
|
|
|
|
| 474 |
<div className="mt-6 text-[8px] font-black tracking-[0.2em] text-slate-500 uppercase">
|
| 475 |
Legend
|
| 476 |
</div>
|
| 477 |
+
<div className="mt-3 space-y-2.5 text-[9px] text-slate-300">
|
| 478 |
+
<div className="flex items-center gap-3"><span className="text-amber-300">▲</span> You</div>
|
| 479 |
+
<div className="flex items-center gap-3"><span className="text-rose-400">▲</span> Red player</div>
|
| 480 |
+
<div className="flex items-center gap-3"><span className="text-sky-400">▲</span> Blue player</div>
|
| 481 |
+
<div className="flex items-center gap-3"><span className="text-rose-400 opacity-60">⊕</span> Red bot</div>
|
| 482 |
+
<div className="flex items-center gap-3"><span className="text-sky-400 opacity-60">⊕</span> Blue bot</div>
|
| 483 |
<div className="flex items-center gap-3"><span className="text-emerald-300">◇</span> Ground objective</div>
|
| 484 |
</div>
|
| 485 |
</aside>
|
|
|
|
| 512 |
style={{ boxShadow: "0 0 0 1px #000, inset 0 0 0 1px #000" }}
|
| 513 |
>
|
| 514 |
<div
|
| 515 |
+
className="w-1.5 h-1.5 bg-red-500 rounded-full"
|
| 516 |
style={{ boxShadow: "0 0 0 1px #000" }}
|
| 517 |
/>
|
| 518 |
|
|
|
|
| 685 |
mapId,
|
| 686 |
showTacticalMap,
|
| 687 |
onCloseTacticalMap,
|
| 688 |
+
chatMessages = [],
|
| 689 |
+
onSendChat,
|
| 690 |
}) => {
|
| 691 |
if (!playerPilot) return null;
|
| 692 |
|
|
|
|
| 1083 |
</div>
|
| 1084 |
)}
|
| 1085 |
|
| 1086 |
+
{/* In-game chat overlay */}
|
| 1087 |
+
{onSendChat && (
|
| 1088 |
+
<ChatOverlay messages={chatMessages} onSend={onSendChat} />
|
| 1089 |
+
)}
|
| 1090 |
+
|
| 1091 |
+
</div>
|
| 1092 |
+
);
|
| 1093 |
+
};
|
| 1094 |
+
|
| 1095 |
+
const ChatOverlay: React.FC<{ messages: ChatMessage[]; onSend: (t: string) => void }> = ({ messages, onSend }) => {
|
| 1096 |
+
const [input, setInput] = React.useState("");
|
| 1097 |
+
const [open, setOpen] = React.useState(false);
|
| 1098 |
+
const inputRef = React.useRef<HTMLInputElement>(null);
|
| 1099 |
+
const now = Date.now();
|
| 1100 |
+
const recent = messages.filter(m => now - m.ts < 10000).slice(-5);
|
| 1101 |
+
|
| 1102 |
+
const submit = () => {
|
| 1103 |
+
const text = input.trim();
|
| 1104 |
+
if (text) { onSend(text); setInput(""); }
|
| 1105 |
+
setOpen(false);
|
| 1106 |
+
};
|
| 1107 |
+
|
| 1108 |
+
React.useEffect(() => {
|
| 1109 |
+
const onKey = (e: KeyboardEvent) => {
|
| 1110 |
+
if (e.code === "KeyT" && !(e.target instanceof HTMLInputElement)) {
|
| 1111 |
+
e.preventDefault();
|
| 1112 |
+
setOpen(true);
|
| 1113 |
+
setTimeout(() => inputRef.current?.focus(), 0);
|
| 1114 |
+
}
|
| 1115 |
+
};
|
| 1116 |
+
window.addEventListener("keydown", onKey);
|
| 1117 |
+
return () => window.removeEventListener("keydown", onKey);
|
| 1118 |
+
}, []);
|
| 1119 |
+
|
| 1120 |
+
return (
|
| 1121 |
+
<div className="absolute left-3 bottom-28 z-50 w-72 flex flex-col gap-1 pointer-events-none">
|
| 1122 |
+
<div className="flex flex-col gap-0.5">
|
| 1123 |
+
{recent.map((m, i) => (
|
| 1124 |
+
<div key={i} className="text-[9px] font-mono bg-black/45 rounded px-1.5 py-0.5 leading-snug">
|
| 1125 |
+
<span className="text-amber-400 font-black">{m.sender}</span>
|
| 1126 |
+
<span className="text-slate-200 ml-1">{m.text}</span>
|
| 1127 |
+
</div>
|
| 1128 |
+
))}
|
| 1129 |
+
</div>
|
| 1130 |
+
{open && (
|
| 1131 |
+
<div className="pointer-events-auto flex items-center gap-1 bg-black/70 border border-slate-700 rounded px-2 py-1 mt-0.5">
|
| 1132 |
+
<input
|
| 1133 |
+
ref={inputRef}
|
| 1134 |
+
value={input}
|
| 1135 |
+
onChange={e => setInput(e.target.value)}
|
| 1136 |
+
onKeyDown={e => {
|
| 1137 |
+
if (e.key === "Enter") submit();
|
| 1138 |
+
if (e.key === "Escape") { setOpen(false); setInput(""); }
|
| 1139 |
+
}}
|
| 1140 |
+
maxLength={120}
|
| 1141 |
+
placeholder="Press Enter to send..."
|
| 1142 |
+
className="flex-1 bg-transparent text-[9px] text-white outline-none font-mono placeholder-slate-500"
|
| 1143 |
+
/>
|
| 1144 |
+
<span className="text-[7px] text-slate-500 font-mono">ESC</span>
|
| 1145 |
+
</div>
|
| 1146 |
+
)}
|
| 1147 |
+
{!open && recent.length === 0 && (
|
| 1148 |
+
<div className="text-[6.5px] text-slate-600 font-mono">[T] CHAT</div>
|
| 1149 |
+
)}
|
| 1150 |
</div>
|
| 1151 |
);
|
| 1152 |
};
|
src/game/botAISystem.ts
CHANGED
|
@@ -99,33 +99,45 @@ export class BotAISystem {
|
|
| 99 |
const tPos = new Vector3(target.x, target.y, target.z);
|
| 100 |
const tVel = new Vector3(target.vx, target.vy, target.vz);
|
| 101 |
const dist = botPos.distanceTo(tPos);
|
| 102 |
-
|
| 103 |
-
const bulletSpeed = 800;
|
| 104 |
-
const timeToTgt = dist / bulletSpeed;
|
| 105 |
-
const leadTargetPos = tPos.clone().addScaledVector(tVel, timeToTgt);
|
| 106 |
-
|
| 107 |
-
// Enforce a terrain floor — bots must not dive into the ground chasing.
|
| 108 |
-
const safeLeadY = Math.max(leadTargetPos.y, 180);
|
| 109 |
-
bot.aiState.destinationX = leadTargetPos.x;
|
| 110 |
-
bot.aiState.destinationY = bot.y < 150 ? Math.max(safeLeadY, bot.y + 200) : safeLeadY;
|
| 111 |
-
bot.aiState.destinationZ = leadTargetPos.z;
|
| 112 |
-
|
| 113 |
-
const bearingAngle = leadTargetPos.clone().sub(botPos).normalize();
|
| 114 |
const localForward = getForwardVector(bot);
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
} else {
|
| 124 |
-
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
|
| 127 |
-
|
| 128 |
-
|
|
|
|
| 129 |
}
|
| 130 |
} else {
|
| 131 |
bot.aiState.behavior = "patrol";
|
|
|
|
| 99 |
const tPos = new Vector3(target.x, target.y, target.z);
|
| 100 |
const tVel = new Vector3(target.vx, target.vy, target.vz);
|
| 101 |
const dist = botPos.distanceTo(tPos);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
const localForward = getForwardVector(bot);
|
| 103 |
+
|
| 104 |
+
// Check if the target is behind the bot (being pursued) — trigger evasion break.
|
| 105 |
+
const toTarget = tPos.clone().sub(botPos).normalize();
|
| 106 |
+
const targetOnTail = toTarget.dot(localForward) < -0.5 && dist < 800;
|
| 107 |
+
|
| 108 |
+
if (targetOnTail) {
|
| 109 |
+
// Break turn: juke perpendicular + climb to escape the firing cone.
|
| 110 |
+
const t = Date.now() / 1000;
|
| 111 |
+
const juke = Math.sin(t * 3.7) * 600;
|
| 112 |
+
bot.aiState.destinationX = bot.x + localForward.z * juke;
|
| 113 |
+
bot.aiState.destinationY = Math.max(250, bot.y + 400);
|
| 114 |
+
bot.aiState.destinationZ = bot.z - localForward.x * juke;
|
| 115 |
+
bot.throttle = Math.min(1.0, bot.throttle + dt * 0.8);
|
| 116 |
} else {
|
| 117 |
+
const bulletSpeed = 820;
|
| 118 |
+
const timeToTgt = dist / bulletSpeed;
|
| 119 |
+
const leadTargetPos = tPos.clone().addScaledVector(tVel, timeToTgt);
|
| 120 |
+
|
| 121 |
+
// Terrain floor — bots must not dive into the ground chasing.
|
| 122 |
+
const safeLeadY = Math.max(leadTargetPos.y, 180);
|
| 123 |
+
bot.aiState.destinationX = leadTargetPos.x;
|
| 124 |
+
bot.aiState.destinationY = bot.y < 150 ? Math.max(safeLeadY, bot.y + 200) : safeLeadY;
|
| 125 |
+
bot.aiState.destinationZ = leadTargetPos.z;
|
| 126 |
+
|
| 127 |
+
const bearingAngle = leadTargetPos.clone().sub(botPos).normalize();
|
| 128 |
+
const aimDot = bearingAngle.dot(localForward);
|
| 129 |
+
|
| 130 |
+
if (dist > 1200) {
|
| 131 |
+
bot.throttle = Math.min(1.0, bot.throttle + dt * 0.5);
|
| 132 |
+
} else if (aimDot < 0.85) {
|
| 133 |
+
bot.throttle = Math.max(0.65, bot.throttle - dt * 0.3);
|
| 134 |
+
} else {
|
| 135 |
+
bot.throttle = Math.min(1.0, bot.throttle + dt * 0.4);
|
| 136 |
+
}
|
| 137 |
|
| 138 |
+
if (aimDot > 0.93 && dist < 1200) {
|
| 139 |
+
handleWeaponFiring(bot, true, false, dt);
|
| 140 |
+
}
|
| 141 |
}
|
| 142 |
} else {
|
| 143 |
bot.aiState.behavior = "patrol";
|
src/game/gameEngine.ts
CHANGED
|
@@ -290,8 +290,8 @@ export class GameEngine {
|
|
| 290 |
this.pilots.push(playerPilot);
|
| 291 |
this.playerPilotId = "player";
|
| 292 |
|
| 293 |
-
let adversaryCount =
|
| 294 |
-
let teammateCount =
|
| 295 |
|
| 296 |
if (this.matchMode === MatchMode.DuelArena) {
|
| 297 |
adversaryCount = 1;
|
|
|
|
| 290 |
this.pilots.push(playerPilot);
|
| 291 |
this.playerPilotId = "player";
|
| 292 |
|
| 293 |
+
let adversaryCount = 7;
|
| 294 |
+
let teammateCount = 6;
|
| 295 |
|
| 296 |
if (this.matchMode === MatchMode.DuelArena) {
|
| 297 |
adversaryCount = 1;
|
src/game/worldRenderer.ts
CHANGED
|
@@ -306,7 +306,7 @@ export class WorldRenderer {
|
|
| 306 |
{
|
| 307 |
const skirtSize = world.radius * 12;
|
| 308 |
const fogColor = new THREE.Color(this.mapDef.atmosphere.fogColor);
|
| 309 |
-
const skirtMat = new THREE.MeshBasicMaterial({ color: fogColor, fog: false });
|
| 310 |
const skirtMesh = new THREE.Mesh(
|
| 311 |
new THREE.PlaneGeometry(skirtSize, skirtSize),
|
| 312 |
skirtMat
|
|
@@ -1076,7 +1076,7 @@ export class WorldRenderer {
|
|
| 1076 |
// Kicks in above 450 km/h and peaks near VNE.
|
| 1077 |
this.cameraShakeTime += dt;
|
| 1078 |
const shakeKmph = new THREE.Vector3(playerPilot.vx, playerPilot.vy, playerPilot.vz).length() * 3.6;
|
| 1079 |
-
const shakeStrength = THREE.MathUtils.clamp((shakeKmph -
|
| 1080 |
if (shakeStrength > 0) {
|
| 1081 |
const t = this.cameraShakeTime;
|
| 1082 |
this.camera.position.x += Math.sin(t * 18.7) * Math.sin(t * 6.3) * shakeStrength;
|
|
|
|
| 306 |
{
|
| 307 |
const skirtSize = world.radius * 12;
|
| 308 |
const fogColor = new THREE.Color(this.mapDef.atmosphere.fogColor);
|
| 309 |
+
const skirtMat = new THREE.MeshBasicMaterial({ color: fogColor, fog: false, depthWrite: false });
|
| 310 |
const skirtMesh = new THREE.Mesh(
|
| 311 |
new THREE.PlaneGeometry(skirtSize, skirtSize),
|
| 312 |
skirtMat
|
|
|
|
| 1076 |
// Kicks in above 450 km/h and peaks near VNE.
|
| 1077 |
this.cameraShakeTime += dt;
|
| 1078 |
const shakeKmph = new THREE.Vector3(playerPilot.vx, playerPilot.vy, playerPilot.vz).length() * 3.6;
|
| 1079 |
+
const shakeStrength = THREE.MathUtils.clamp((shakeKmph - 500) / 250, 0, 1) * 0.10;
|
| 1080 |
if (shakeStrength > 0) {
|
| 1081 |
const t = this.cameraShakeTime;
|
| 1082 |
this.camera.position.x += Math.sin(t * 18.7) * Math.sin(t * 6.3) * shakeStrength;
|