latterworks commited on
Commit
f3f7dfb
·
verified ·
1 Parent(s): 85bb5d2

Chat overlay, tactical map bot/player distinction, red lead dot, evasion AI, 7v6 bots, less turbulence, skirt depthWrite fix

Browse files
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
- {pilots.filter(pilot => pilot.damage.fuselage > 0).map(pilot => {
390
- const point = project(pilot.x, pilot.z);
391
- const color = pilot.team === 1 ? "#fb7185" : "#38bdf8";
392
- return (
393
- <g key={pilot.id} transform={`translate(${point.x} ${point.y}) rotate(${pilot.yaw * 180 / Math.PI})`}>
394
- <path
395
- d="M0 -13L8 10L0 6L-8 10Z"
396
- fill={pilot.id === "player" ? "#facc15" : color}
397
- stroke="#020617"
398
- strokeWidth="2"
399
- />
400
- </g>
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-3 text-[9px] text-slate-300">
439
- <div className="flex items-center gap-3"><span className="text-amber-300">▲</span> Your aircraft</div>
440
- <div className="flex items-center gap-3"><span className="text-rose-400">▲</span> Red team</div>
441
- <div className="flex items-center gap-3"><span className="text-sky-400">▲</span> Blue team</div>
 
 
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-amber-400 rounded-full"
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
- const aimDot = bearingAngle.dot(localForward);
116
-
117
- // Keep speed high enough to pursue across large map distances.
118
- // Only bleed throttle when very close and already lined up.
119
- if (dist > 1200) {
120
- bot.throttle = Math.min(1.0, bot.throttle + dt * 0.5);
121
- } else if (aimDot < 0.85) {
122
- bot.throttle = Math.max(0.65, bot.throttle - dt * 0.3);
 
 
 
 
 
123
  } else {
124
- bot.throttle = Math.min(1.0, bot.throttle + dt * 0.4);
125
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
 
127
- if (aimDot > 0.93 && dist < 1200) {
128
- handleWeaponFiring(bot, true, false, dt);
 
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 = 5;
294
- let teammateCount = 4;
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 - 450) / 200, 0, 1) * 0.28;
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;