cutechicken commited on
Commit
7b1c9a2
·
verified ·
1 Parent(s): 43b163b

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1590 -817
index.html CHANGED
@@ -1,879 +1,1652 @@
1
- <!DOCTYPE html>
2
- <html lang="ko">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>JET FIGHT SIMULATER - FPS Mode</title>
7
- <style>
8
- body {
9
- margin: 0;
10
- overflow: hidden;
11
- background: #000;
12
- font-family: 'Courier New', monospace;
13
- }
14
-
15
- #loading {
16
- position: fixed;
17
- top: 50%;
18
- left: 50%;
19
- transform: translate(-50%, -50%);
20
- background: rgba(0,0,0,0.8);
21
- padding: 20px;
22
- border-radius: 10px;
23
- z-index: 2000;
24
- text-align: center;
25
- }
26
-
27
- .loading-spinner {
28
- width: 50px;
29
- height: 50px;
30
- border: 5px solid #0f0;
31
- border-top: 5px solid transparent;
32
- border-radius: 50%;
33
- animation: spin 1s linear infinite;
34
- margin: 0 auto 20px;
35
- }
36
-
37
- @keyframes spin {
38
- 0% { transform: rotate(0deg); }
39
- 100% { transform: rotate(360deg); }
40
- }
41
-
42
- .loading-text {
43
- color: #0f0;
44
- font-size: 24px;
45
- text-align: center;
46
- }
47
-
48
- #gameContainer {
49
- position: relative;
50
- width: 100vw;
51
- height: 100vh;
52
- cursor: none;
53
- }
54
-
55
- /* HUD 전체 컨테이너 */
56
- #hudContainer {
57
- position: fixed;
58
- top: 0;
59
- left: 0;
60
- width: 100%;
61
- height: 100%;
62
- pointer-events: none;
63
- z-index: 1000;
64
- }
65
-
66
- /* 중앙 HUD */
67
- #hudCrosshair {
68
- position: fixed;
69
- top: 50%;
70
- left: 50%;
71
- transform: translate(-50%, -50%);
72
- width: 400px;
73
- height: 400px;
74
- }
75
-
76
- /* 조준 원 */
77
- .hud-aiming-circle {
78
- position: absolute;
79
- top: 50%;
80
- left: 50%;
81
- transform: translate(-50%, -50%);
82
- width: 150px;
83
- height: 150px;
84
- border: 2px solid rgba(0, 255, 0, 0.5);
85
- border-radius: 50%;
86
- }
87
-
88
- /* 중앙점 */
89
- .hud-center-dot {
90
- position: absolute;
91
- top: 50%;
92
- left: 50%;
93
- transform: translate(-50%, -50%);
94
- width: 4px;
95
- height: 4px;
96
- background: #00ff00;
97
- border-radius: 50%;
98
- }
99
-
100
- /* 수평선 */
101
- .hud-horizon-line {
102
- position: absolute;
103
- top: 50%;
104
- left: 20%;
105
- right: 20%;
106
- height: 1px;
107
- background: rgba(0, 255, 0, 0.4);
108
- }
109
-
110
- /* 피치 래더 컨테이너 */
111
- .hud-pitch-ladder {
112
- position: absolute;
113
- top: 50%;
114
- left: 50%;
115
- transform: translate(-50%, -50%);
116
- width: 200px;
117
- height: 400px;
118
- }
119
 
120
- .pitch-line {
121
- position: absolute;
122
- width: 100%;
123
- display: flex;
124
- align-items: center;
125
- justify-content: center;
126
- }
127
-
128
- .pitch-line-bar {
129
- width: 60px;
130
- height: 1px;
131
- background: rgba(0, 255, 0, 0.4);
132
- }
 
 
 
 
 
 
133
 
134
- .pitch-line-text {
135
- position: absolute;
136
- color: rgba(0, 255, 0, 0.6);
137
- font-size: 10px;
138
- left: -25px;
139
- }
140
-
141
- /* 중앙 HUD 정보 표시 - 새로운 스타일 */
142
- .hud-central-info {
143
- position: absolute;
144
- color: #00ff00;
145
- font-size: 11px;
146
- font-family: 'Courier New', monospace;
147
- text-shadow: 0 0 3px rgba(0, 0, 0, 0.8);
148
- background: rgba(0, 0, 0, 0.3);
149
- padding: 2px 5px;
150
- border-radius: 2px;
151
- }
152
-
153
- /* 중앙 HUD 내 속도 표시 */
154
- #hudSpeedCentral {
155
- left: -120px;
156
- top: 50%;
157
- transform: translateY(-50%);
158
- }
159
-
160
- /* 중앙 HUD 내 고도 표시 */
161
- #hudAltitudeCentral {
162
- right: -120px;
163
- top: 50%;
164
- transform: translateY(-50%);
165
- }
166
-
167
- /* 중앙 HUD 내 헤딩 표시 */
168
- #hudHeadingCentral {
169
- top: -30px;
170
- left: 50%;
171
- transform: translateX(-50%);
172
- }
173
-
174
- /* 중앙 HUD 내 G-Force 표시 */
175
- #hudGForceCentral {
176
- bottom: -30px;
177
- left: 50%;
178
- transform: translateX(-50%);
179
- }
180
-
181
- /* 중앙 HUD 내 스로틀 표시 */
182
- #hudThrottleCentral {
183
- left: -120px;
184
- top: calc(50% + 25px);
185
- }
186
-
187
- /* 타겟 마커 */
188
- .target-marker {
189
- position: absolute;
190
- width: 30px;
191
- height: 30px;
192
- border: 2px solid transparent;
193
- transform: translate(-50%, -50%);
194
- }
195
-
196
- .target-marker.in-crosshair {
197
- border: 2px solid #ffff00;
198
- animation: target-pulse 0.5s infinite;
199
- }
200
-
201
- .target-marker.locked {
202
- border: 2px solid #ff0000;
203
- box-shadow: 0 0 10px #ff0000;
204
- }
205
-
206
- .target-marker .target-box {
207
- position: absolute;
208
- top: -5px;
209
- left: -5px;
210
- right: -5px;
211
- bottom: -5px;
212
- border: 1px solid currentColor;
213
- }
214
-
215
- @keyframes target-pulse {
216
- 0%, 100% { opacity: 1; }
217
- 50% { opacity: 0.5; }
218
- }
219
-
220
- .target-info {
221
- position: absolute;
222
- top: 100%;
223
- left: 50%;
224
- transform: translateX(-50%);
225
- color: #00ff00;
226
- font-size: 10px;
227
- white-space: nowrap;
228
- margin-top: 5px;
229
- }
230
-
231
- #healthBar {
232
- position: absolute;
233
- bottom: 20px;
234
- left: 20px;
235
- width: 200px;
236
- height: 20px;
237
- background: rgba(0,20,0,0.7);
238
- border: 2px solid #0f0;
239
- z-index: 1001;
240
- border-radius: 10px;
241
- overflow: hidden;
242
- }
243
-
244
- #health {
245
- width: 100%;
246
- height: 100%;
247
- background: linear-gradient(90deg, #0f0, #00ff00);
248
- transition: width 0.3s;
249
- }
250
-
251
- #gameTitle {
252
- position: absolute;
253
- top: 60px;
254
- left: 50%;
255
- transform: translateX(-50%);
256
- color: #0f0;
257
- background: rgba(0,20,0,0.7);
258
- padding: 10px 20px;
259
- font-size: 20px;
260
- z-index: 1001;
261
- border: 1px solid #0f0;
262
- border-radius: 5px;
263
- text-transform: uppercase;
264
- letter-spacing: 2px;
265
- }
266
-
267
- #ammoDisplay {
268
- position: absolute;
269
- bottom: 20px;
270
- right: 20px;
271
- color: #0f0;
272
- background: rgba(0,20,0,0.7);
273
- padding: 10px;
274
- font-size: 20px;
275
- z-index: 1001;
276
- border: 1px solid #0f0;
277
- border-radius: 5px;
278
- }
279
-
280
- #radar {
281
- position: absolute;
282
- bottom: 60px;
283
- left: 20px;
284
- width: 200px;
285
- height: 200px;
286
- background: rgba(30, 30, 30, 0.9); /* 더 어두운 회색 배경 */
287
- border: 2px solid #0f0;
288
- border-radius: 50%;
289
- z-index: 1001;
290
- overflow: hidden;
291
- }
292
-
293
- /* RWR 디스플레이 스타일 */
294
- .rwr-display {
295
- position: relative;
296
- width: 100%;
297
- height: 100%;
298
- }
299
-
300
- /* RWR 중앙 항공기 심볼 */
301
- .rwr-center {
302
- position: absolute;
303
- top: 50%;
304
- left: 50%;
305
- transform: translate(-50%, -50%);
306
- width: 15px; /* 더 작게 */
307
- height: 15px; /* 더 작게 */
308
- z-index: 10;
309
- }
310
-
311
- .rwr-aircraft-symbol {
312
- width: 100%;
313
- height: 100%;
314
- position: relative;
315
- display: flex;
316
- align-items: center;
317
- justify-content: center;
318
- }
319
-
320
- .rwr-aircraft-symbol img {
321
- width: 100%;
322
- height: 100%;
323
- object-fit: contain;
324
- filter: drop-shadow(0 0 2px #00ff00); /* 초록색 빛나는 효과 */
325
- }
326
-
327
- /* RWR 거리 링 */
328
- .rwr-range-ring {
329
- position: absolute;
330
- border: 1px solid rgba(0, 255, 0, 0.4);
331
- border-radius: 50%;
332
- top: 50%;
333
- left: 50%;
334
- transform: translate(-50%, -50%);
335
- }
336
 
337
- .rwr-ring-inner {
338
- width: 60px;
339
- height: 60px;
 
 
 
 
 
 
 
 
340
  }
341
 
342
- .rwr-ring-middle {
343
- width: 120px;
344
- height: 120px;
345
- }
346
-
347
- .rwr-ring-outer {
348
- width: 180px;
349
- height: 180px;
350
- }
351
-
352
- /* RWR 중앙에 작은 원 추가 */
353
- .rwr-center-dot {
354
- position: absolute;
355
- top: calc(50% + 4px); /* 4px 아래로 이동 */
356
- left: 50%;
357
- transform: translate(-50%, -50%);
358
- width: 4px;
359
- height: 4px;
360
- background: #00ff00;
361
- border-radius: 50%;
362
- z-index: 11;
363
- }
364
 
365
- /* RWR 방향 표시 */
366
- .rwr-direction-marks {
367
- position: absolute;
368
- width: 100%;
369
- height: 100%;
370
- }
371
-
372
- .rwr-direction-text {
373
- position: absolute;
374
- color: #0f0;
375
- font-size: 12px;
376
- font-weight: bold;
377
- transform: translate(-50%, -50%);
378
- text-shadow: 0 0 2px #0f0;
379
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
 
381
- .rwr-north {
382
- top: 15px;
383
- left: 50%;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  }
385
 
386
- .rwr-east {
387
- top: 50%;
388
- right: 15px;
389
- left: auto;
390
- transform: translateY(-50%);
391
  }
 
 
 
 
 
 
 
 
 
 
 
 
392
 
393
- .rwr-south {
394
- bottom: 15px;
395
- left: 50%;
396
- top: auto;
397
- transform: translateX(-50%);
398
  }
399
 
400
- .rwr-west {
401
- top: 50%;
402
- left: 15px;
 
 
 
 
 
 
403
  }
404
 
405
- /* RWR 위협 표시 */
406
- .rwr-threat {
407
- position: absolute;
408
- width: 15px;
409
- height: 15px;
410
- transform: translate(-50%, -50%);
411
- text-align: center;
412
- font-size: 10px;
413
- font-weight: bold;
414
- z-index: 5;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
  }
416
 
417
- /* 위협 레벨별 색상 */
418
- .rwr-threat.level-low {
419
- color: #ffff00; /* 노란색 */
420
- text-shadow: 0 0 3px #ffff00;
421
- }
422
 
423
- .rwr-threat.level-medium {
424
- color: #ff8800; /* 주황색 */
425
- text-shadow: 0 0 3px #ff8800;
 
 
426
  }
427
 
428
- .rwr-threat.level-high {
429
- color: #ff0000; /* 빨간색 */
430
- text-shadow: 0 0 3px #ff0000;
 
 
431
  }
432
 
433
- .rwr-threat.missile-lock {
434
- animation: rwr-flash 0.3s infinite;
 
 
435
  }
436
 
437
- @keyframes rwr-flash {
438
- 0%, 50% { opacity: 1; }
439
- 51%, 100% { opacity: 0.3; }
 
440
  }
441
-
442
- #mission {
443
- position: absolute;
444
- top: 10px;
445
- left: 10px;
446
- color: #0f0;
447
- background: rgba(0,20,0,0.7);
448
- padding: 10px;
449
- font-size: 16px;
450
- z-index: 1001;
451
- border: 1px solid #0f0;
452
- border-radius: 5px;
453
- }
454
-
455
- #radarLine {
456
- position: absolute;
457
- top: 50%;
458
- left: 50%;
459
- width: 50%;
460
- height: 2px;
461
- background: #0f0;
462
- transform-origin: left center;
463
- animation: radar-sweep 4s infinite linear;
464
- }
465
-
466
- .enemy-dot {
467
- position: absolute;
468
- width: 6px;
469
- height: 6px;
470
- background: #ff0000;
471
- border-radius: 50%;
472
- transform: translate(-50%, -50%);
473
- }
474
-
475
- @keyframes radar-sweep {
476
- from {
477
- transform: rotate(0deg);
478
- }
479
- to {
480
- transform: rotate(360deg);
481
- }
482
- }
483
-
484
- #gameStats {
485
- position: absolute;
486
- top: 10px;
487
- right: 20px;
488
- color: #0f0;
489
- background: rgba(0,20,0,0.7);
490
- padding: 10px;
491
- font-size: 16px;
492
- z-index: 1001;
493
- border: 1px solid #0f0;
494
- border-radius: 5px;
495
- text-align: right;
496
- }
497
-
498
- .start-screen {
499
- position: fixed;
500
- top: 0;
501
- left: 0;
502
- width: 100%;
503
- height: 100%;
504
- background: rgba(0,0,0,0.8);
505
- display: none;
506
- justify-content: center;
507
- align-items: center;
508
- flex-direction: column;
509
- z-index: 2000;
510
- }
511
-
512
- .start-button {
513
- padding: 15px 30px;
514
- font-size: 24px;
515
- background: #0f0;
516
- color: #000;
517
- border: none;
518
- border-radius: 5px;
519
- cursor: pointer;
520
- margin-top: 20px;
521
- transition: transform 0.2s;
522
- }
523
-
524
- .start-button:hover {
525
- transform: scale(1.1);
526
- }
527
-
528
- /* 기존 개별 HUD 정보들은 숨김 */
529
- #hudSpeed, #hudAltitude, #hudHeading, #hudPitch, #hudRoll, #hudTurnRate {
530
- display: none;
531
- }
532
- </style>
533
- </head>
534
- <body>
535
- <!-- 로딩 화면 -->
536
- <div id="loading">
537
- <div class="loading-spinner"></div>
538
- <div class="loading-text">Loading Fighter Resources...</div>
539
- <div style="color: #0f0; font-size: 16px; margin-top: 10px;">
540
- <p>Loading Aircraft Models...</p>
541
- <p>Loading Audio Assets...</p>
542
- <p>Preparing Game Environment...</p>
543
- </div>
544
- </div>
545
-
546
- <!-- 게임 시작 화면 -->
547
- <div class="start-screen" id="startScreen">
548
- <h1 style="color: #0f0; font-size: 48px; margin-bottom: 20px;">JET FIGHT SIMULATER</h1>
549
- <button class="start-button" onclick="startGame()">Start Game</button>
550
- <div style="color: #0f0; margin-top: 20px; text-align: center;">
551
- <p>Controls:</p>
552
- <p>W/S - Throttle Control</p>
553
- <p>A/D - Rudder Control</p>
554
- <p>Mouse - Aircraft Control</p>
555
- <p>Left Click - Fire</p>
556
- <p>F - Escape Stall</p>
557
- </div>
558
- </div>
559
-
560
- <!-- 게임 화면 -->
561
- <div id="gameContainer">
562
- <!-- 게임 타이틀 (게임 시작 시 숨겨짐) -->
563
- <div id="gameTitle" style="display: none;">JET FIGHT SIMULATER</div>
564
- <div id="mission">MISSION: DESTROY ENEMY JET</div>
565
- <div id="gameStats">
566
- <div id="score">Score: 0</div>
567
- <div id="time">Time: 180s</div>
568
- </div>
569
-
570
- <!-- HUD 컨테이너 -->
571
- <div id="hudContainer">
572
- <!-- 중앙 HUD -->
573
- <div id="hudCrosshair">
574
- <div class="hud-aiming-circle"></div>
575
- <div class="hud-center-dot"></div>
576
- <div class="hud-horizon-line"></div>
577
-
578
- <!-- 중앙 HUD 정보 표시 -->
579
- <div class="hud-central-info" id="hudSpeedCentral">SPD: 0 KT</div>
580
- <div class="hud-central-info" id="hudAltitudeCentral">ALT: 0 M</div>
581
- <div class="hud-central-info" id="hudHeadingCentral">HDG: 000°</div>
582
- <div class="hud-central-info" id="hudGForceCentral">G: 1.0</div>
583
- <div class="hud-central-info" id="hudThrottleCentral">THR: 60%</div>
584
 
585
- <!-- 피치 래더 -->
586
- <div class="hud-pitch-ladder" id="pitchLadder">
587
- <!-- 10도당 20픽셀 간격 -->
588
- <div class="pitch-line" style="top: calc(50% - 80px);">
589
- <div class="pitch-line-bar"></div>
590
- <span class="pitch-line-text">40</span>
591
- </div>
592
- <div class="pitch-line" style="top: calc(50% - 60px);">
593
- <div class="pitch-line-bar"></div>
594
- <span class="pitch-line-text">30</span>
595
- </div>
596
- <div class="pitch-line" style="top: calc(50% - 40px);">
597
- <div class="pitch-line-bar"></div>
598
- <span class="pitch-line-text">20</span>
599
- </div>
600
- <div class="pitch-line" style="top: calc(50% - 20px);">
601
- <div class="pitch-line-bar"></div>
602
- <span class="pitch-line-text">10</span>
603
- </div>
604
- <!-- 0도 라인 - 정확히 중앙 -->
605
- <div class="pitch-line" style="top: 50%;">
606
- <div class="pitch-line-bar" style="width: 100px; height: 2px; background: rgba(0, 255, 0, 0.8);"></div>
607
- <span class="pitch-line-text" style="color: rgba(0, 255, 0, 0.8); font-weight: bold;">0</span>
608
- </div>
609
- <div class="pitch-line" style="top: calc(50% + 20px);">
610
- <div class="pitch-line-bar"></div>
611
- <span class="pitch-line-text">-10</span>
612
- </div>
613
- <div class="pitch-line" style="top: calc(50% + 40px);">
614
- <div class="pitch-line-bar"></div>
615
- <span class="pitch-line-text">-20</span>
616
- </div>
617
- <div class="pitch-line" style="top: calc(50% + 60px);">
618
- <div class="pitch-line-bar"></div>
619
- <span class="pitch-line-text">-30</span>
620
- </div>
621
- <div class="pitch-line" style="top: calc(50% + 80px);">
622
- <div class="pitch-line-bar"></div>
623
- <span class="pitch-line-text">-40</span>
624
- </div>
625
- </div>
626
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
627
 
628
- <!-- 기존 비행 정보 (숨김) -->
629
- <div class="hud-info" id="hudSpeed">SPD: 0 KT</div>
630
- <div class="hud-info" id="hudAltitude">ALT: 0 M</div>
631
- <div class="hud-info" id="hudHeading">HDG: 000°</div>
632
- <div class="hud-info" id="hudPitch">PITCH: 0°</div>
633
- <div class="hud-info" id="hudRoll">ROLL: 0°</div>
634
- <div class="hud-info" id="hudTurnRate">TURN: 0°/s</div>
635
-
636
- <!-- 타겟 마커 레이어 -->
637
- <div id="targetMarkers"></div>
638
- </div>
639
-
640
- <!-- 체력바 -->
641
- <div id="healthBar">
642
- <div id="health"></div>
643
- </div>
644
-
645
- <!-- 탄약 표시 -->
646
- <div id="ammoDisplay">AMMO: 300</div>
647
-
648
- <!-- 레이더 (RWR) -->
649
- <div id="radar">
650
- <div class="rwr-display">
651
- <!-- RWR 거리 링 -->
652
- <div class="rwr-range-ring rwr-ring-inner"></div>
653
- <div class="rwr-range-ring rwr-ring-middle"></div>
654
- <div class="rwr-range-ring rwr-ring-outer"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
655
 
656
- <!-- 방향 표시 -->
657
- <div class="rwr-direction-marks">
658
- <div class="rwr-direction-text rwr-north">N</div>
659
- <div class="rwr-direction-text rwr-east">E</div>
660
- <div class="rwr-direction-text rwr-south">S</div>
661
- <div class="rwr-direction-text rwr-west">W</div>
662
- </div>
663
 
664
- <!-- 중앙 항공기 심볼 -->
665
- <div class="rwr-center">
666
- <div class="rwr-aircraft-symbol" id="rwrSymbol">
667
- <!-- 이미지는 JavaScript로 로드 -->
668
- </div>
669
- </div>
670
 
671
- <!-- 중앙 점 -->
672
- <div class="rwr-center-dot"></div>
 
 
 
 
 
673
 
674
- <!-- 위협 표시 영역 -->
675
- <div id="rwrThreats"></div>
676
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
677
 
678
- <!-- 기존 레이더 요소 (숨김) -->
679
- <div id="radarLine" style="display: none;"></div>
680
- </div>
681
- </div>
 
 
 
 
682
 
683
- <script type="importmap">
684
- {
685
- "imports": {
686
- "three": "https://unpkg.com/[email protected]/build/three.module.js",
687
- "three/addons/": "https://unpkg.com/three@0.157.0/examples/jsm/"
 
 
 
 
688
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
689
  }
690
- </script>
691
-
692
- <!-- 피치 래더 및 HUD 업데이트 스크립트 -->
693
- <script>
694
- // 전역 변수
695
- let gameStarted = false;
696
-
697
- // 게임 준비 완료 시 HUD 업데이트 시작
698
- window.addEventListener('gameReady', function() {
699
- console.log('Game ready - starting HUD updates');
700
-
701
- // RWR 심볼 이미지 로드
702
- const rwrSymbol = document.getElementById('rwrSymbol');
703
- if (rwrSymbol) {
704
- // 방법 1: img 태그로 시도
705
- const img = new Image();
706
- img.src = 'effects/symbol.png';
707
- img.style.width = '100%';
708
- img.style.height = '100%';
709
- img.style.objectFit = 'contain';
710
- img.style.filter = 'drop-shadow(0 0 2px #00ff00)'; // 초록색으로 변경
 
 
 
711
 
712
- img.onload = function() {
713
- console.log('RWR symbol image loaded successfully');
714
- rwrSymbol.innerHTML = '';
715
- rwrSymbol.appendChild(img);
 
 
 
 
 
 
716
  };
717
 
718
- img.onerror = function() {
719
- console.log('RWR symbol image failed to load, using fallback');
720
- // 이미지 로드 실패시 작은 초록색 점으로 대체
721
- rwrSymbol.innerHTML = `
722
- <div style="width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;">
723
- <div style="width: 8px; height: 8px; background: #00ff00; border-radius: 50%; box-shadow: 0 0 4px #00ff00;"></div>
724
- </div>
725
- `;
726
- };
 
 
 
 
 
727
  }
 
 
 
 
 
 
 
 
728
 
729
- // 피치 래더 초기 위치 강제 설정
730
- setTimeout(function() {
731
- const pitchLadder = document.getElementById('pitchLadder');
732
- if (pitchLadder) {
733
- // CSS로 직접 위�� 조정
734
- pitchLadder.style.position = 'absolute';
735
- pitchLadder.style.top = 'calc(50% - 200px)'; // 200px 위로 올림
736
- pitchLadder.style.left = '50%';
737
- pitchLadder.style.transform = 'translate(-50%, 0)';
738
- console.log('Pitch ladder initial position set');
739
- }
740
- }, 100);
741
 
742
- // 중앙 HUD 정보 업데이트를 위한 인터벌
743
- setInterval(function() {
744
- if (window.gameInstance && window.gameInstance.fighter && window.gameInstance.fighter.isLoaded) {
745
- const fighter = window.gameInstance.fighter;
746
-
747
- // 비행 정보 계산
748
- const speedKnots = Math.round(fighter.speed * 1.94384);
749
- const altitudeMeters = Math.round(fighter.altitude);
750
- const throttlePercent = Math.round(fighter.throttle * 100);
751
- const gForce = fighter.gForce.toFixed(1);
752
- const headingDegrees = Math.round(((fighter.rotation.y * (180 / Math.PI)) + 360) % 360);
753
-
754
- // 중앙 HUD 정보 업데이트
755
- const hudSpeedCentral = document.getElementById('hudSpeedCentral');
756
- const hudAltitudeCentral = document.getElementById('hudAltitudeCentral');
757
- const hudHeadingCentral = document.getElementById('hudHeadingCentral');
758
- const hudGForceCentral = document.getElementById('hudGForceCentral');
759
- const hudThrottleCentral = document.getElementById('hudThrottleCentral');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
760
 
761
- if (hudSpeedCentral) hudSpeedCentral.textContent = `SPD: ${speedKnots} KT`;
762
- if (hudAltitudeCentral) hudAltitudeCentral.textContent = `ALT: ${altitudeMeters} M`;
763
- if (hudHeadingCentral) hudHeadingCentral.textContent = `HDG: ${String(headingDegrees).padStart(3, '0')}°`;
764
- if (hudGForceCentral) {
765
- hudGForceCentral.textContent = `G: ${gForce}`;
766
- // G-Force에 따른 색상 변경
767
- if (fighter.overG) {
768
- hudGForceCentral.style.color = '#ff0000';
769
- } else if (parseFloat(gForce) > 7) {
770
- hudGForceCentral.style.color = '#ffff00';
771
- } else {
772
- hudGForceCentral.style.color = '#00ff00';
773
- }
774
  }
775
- if (hudThrottleCentral) hudThrottleCentral.textContent = `THR: ${throttlePercent}%`;
776
 
777
- // 피치 래더 동적 업데이트 - 기본 위치에서 피치 각도만큼 이동
778
- const pitchLadder = document.getElementById('pitchLadder');
779
- if (pitchLadder) {
780
- const pitchDegrees = fighter.rotation.x * (180 / Math.PI);
781
- // 10도당 20픽셀, 음수로 반대 방향
782
- const pitchOffset = -200 + (-pitchDegrees * 2);
783
- pitchLadder.style.top = `calc(50% + ${pitchOffset}px)`;
784
- }
785
 
786
- // HUD 크로스헤어 회전 (롤 각도에 따라)
787
- const hudCrosshair = document.getElementById('hudCrosshair');
788
- if (hudCrosshair && fighter.rotation) {
789
- const rollDegrees = fighter.rotation.z * (180 / Math.PI);
790
- hudCrosshair.style.transform = `translate(-50%, -50%) rotate(${-rollDegrees}deg)`;
791
- }
792
- // RWR 업데이트
793
- updateRWR(fighter);
794
  }
795
- }, 16); // 약 60fps
796
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
797
 
798
- // RWR 업데이트 함수
799
- function updateRWR(fighter) {
800
- const rwrThreats = document.getElementById('rwrThreats');
801
- if (!rwrThreats || !window.gameInstance) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
802
 
803
- // 기존 위협 표시 제거
804
- rwrThreats.innerHTML = '';
805
 
806
- // 항공기 표시
807
- if (window.gameInstance.enemies) {
808
- window.gameInstance.enemies.forEach((enemy, index) => {
809
- if (!enemy.mesh || !enemy.isLoaded) return;
810
-
811
- const distance = fighter.position.distanceTo(enemy.position);
812
-
813
- // 10km 이내의 적만 RWR에 표시
814
- if (distance <= 10000) {
815
- // 상대 위치 계산
816
- const relativePos = enemy.position.clone().sub(fighter.position);
817
-
818
- // 전투기의 heading을 고려한 각도 계산
819
- const angle = Math.atan2(relativePos.x, relativePos.z) - fighter.rotation.y;
820
-
821
- // RWR 상의 위치 계산 (최대 반경 90px)
822
- const maxRadius = 90;
823
- const relativeDistance = Math.min(distance / 10000, 1) * maxRadius;
824
-
825
- const x = Math.sin(angle) * relativeDistance + 100; // 중앙이 100px
826
- const y = -Math.cos(angle) * relativeDistance + 100;
827
-
828
- // 위협 심볼 생성
829
- const threat = document.createElement('div');
830
- threat.className = 'rwr-threat';
831
-
832
- // 거리에 따른 위협 레벨 (적 항공기만 표시)
833
- if (distance < 2000) {
834
- threat.classList.add('level-high');
835
- threat.classList.add('missile-lock');
836
- threat.textContent = '◆';
837
- } else if (distance < 5000) {
838
- threat.classList.add('level-medium');
839
- threat.textContent = '◆';
840
- } else {
841
- threat.classList.add('level-low');
842
- threat.textContent = '◆';
843
- }
844
-
845
- threat.style.left = `${x}px`;
846
- threat.style.top = `${y}px`;
847
-
848
- rwrThreats.appendChild(threat);
 
 
 
 
 
 
 
 
 
 
 
 
849
  }
850
- });
 
851
  }
852
  }
853
 
854
- // startGame 함수 정의
855
- window.startGame = function() {
856
- if (!window.gameInstance || !window.gameInstance.isLoaded || !window.gameInstance.isBGMReady) {
857
- console.log('게임이 아직 준비되지 않았습니다...');
858
- return;
 
 
859
  }
860
 
861
- gameStarted = true;
862
- document.getElementById('startScreen').style.display = 'none';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
863
 
864
- // 게임 타이틀 숨기기
865
- const gameTitle = document.getElementById('gameTitle');
866
- if (gameTitle) {
867
- gameTitle.style.display = 'none';
 
 
 
 
 
 
 
 
 
 
868
  }
 
 
 
 
 
 
869
 
870
- document.body.requestPointerLock();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
871
 
872
- window.gameInstance.startBGM();
873
- window.gameInstance.startGame();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
874
  }
875
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
876
 
877
- <script src="game.js" type="module"></script>
878
- </body>
879
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as THREE from 'three';
2
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
+ // 게임 상태 추적 변수
5
+ let gameCanStart = false;
6
+ let gameStarted = false;
7
+
8
+ // 게임 상수
9
+ const GAME_CONSTANTS = {
10
+ MISSION_DURATION: 180,
11
+ MAP_SIZE: 15000,
12
+ MAX_ALTITUDE: 15000,
13
+ MIN_ALTITUDE: 0,
14
+ MAX_SPEED: 800,
15
+ STALL_SPEED: 300, // 300kt 이하에서 스톨 위험
16
+ GRAVITY: 9.8,
17
+ MOUSE_SENSITIVITY: 0.001,
18
+ MAX_G_FORCE: 12.0,
19
+ ENEMY_COUNT: 4,
20
+ MISSILE_COUNT: 6,
21
+ AMMO_COUNT: 300
22
+ };
23
 
24
+ // 전투기 클래스
25
+ class Fighter {
26
+ constructor() {
27
+ this.mesh = null;
28
+ this.isLoaded = false;
29
+
30
+ // 물리 속성
31
+ this.position = new THREE.Vector3(0, 2000, 0);
32
+ this.velocity = new THREE.Vector3(0, 0, 350); // 초기 속도 350kt
33
+ this.acceleration = new THREE.Vector3(0, 0, 0);
34
+ this.rotation = new THREE.Euler(0, 0, 0);
35
+
36
+ // 비행 제어
37
+ this.throttle = 0.6; // 초기 스로틀 60%
38
+ this.speed = 350; // 초기 속도 350kt
39
+ this.altitude = 2000;
40
+ this.gForce = 1.0;
41
+ this.health = 100;
42
+
43
+ // 조종 입력 시스템
44
+ this.pitchInput = 0;
45
+ this.rollInput = 0;
46
+ this.yawInput = 0;
47
+
48
+ // 마우스 누적 입력
49
+ this.mousePitch = 0;
50
+ this.mouseRoll = 0;
51
+
52
+ // 부드러운 회전을 위한 목표값
53
+ this.targetPitch = 0;
54
+ this.targetRoll = 0;
55
+ this.targetYaw = 0;
56
+
57
+ // 무기
58
+ this.missiles = GAME_CONSTANTS.MISSILE_COUNT;
59
+ this.ammo = GAME_CONSTANTS.AMMO_COUNT;
60
+ this.bullets = [];
61
+ this.lastShootTime = 0;
62
+
63
+ // 스톨 탈출을 위한 F키 상태
64
+ this.escapeKeyPressed = false;
65
+ this.stallEscapeProgress = 0; // 스톨 탈출 진행도 (0~2초)
66
+
67
+ // 카메라 설정
68
+ this.cameraDistance = 250;
69
+ this.cameraHeight = 30;
70
+ this.cameraLag = 0.06;
71
+
72
+ // 경고 시스템
73
+ this.altitudeWarning = false;
74
+ this.stallWarning = false;
75
+ this.warningBlinkTimer = 0;
76
+ this.warningBlinkState = false;
77
+
78
+ // Over-G 시스템
79
+ this.overG = false;
80
+ this.maxGForce = 9.0;
81
+ this.overGTimer = 0; // Over-G 지속 시간
82
+
83
+ // 경고음 시스템
84
+ this.warningAudios = {
85
+ altitude: null,
86
+ pullup: null,
87
+ overg: null,
88
+ stall: null,
89
+ normal: null // 엔진 소리 추가
90
+ };
91
+ this.initializeWarningAudios();
92
+ }
93
+
94
+ initializeWarningAudios() {
95
+ try {
96
+ this.warningAudios.altitude = new Audio('sounds/altitude.ogg');
97
+ this.warningAudios.altitude.volume = 0.75;
98
+
99
+ this.warningAudios.pullup = new Audio('sounds/pullup.ogg');
100
+ this.warningAudios.pullup.volume = 0.9;
101
+
102
+ this.warningAudios.overg = new Audio('sounds/overg.ogg');
103
+ this.warningAudios.overg.volume = 0.75;
104
+
105
+ this.warningAudios.stall = new Audio('sounds/alert.ogg');
106
+ this.warningAudios.stall.volume = 0.75;
107
+
108
+ // 엔진 소리 설정
109
+ this.warningAudios.normal = new Audio('sounds/normal.ogg');
110
+ this.warningAudios.normal.volume = 0.5;
111
+ this.warningAudios.normal.loop = true; // 엔진 소리는 계속 반복
112
+
113
+ // 경고음에만 ended 이벤트 리스너 추가 (엔진 소리 제외)
114
+ Object.keys(this.warningAudios).forEach(key => {
115
+ if (key !== 'normal' && this.warningAudios[key]) {
116
+ this.warningAudios[key].addEventListener('ended', () => {
117
+ this.updateWarningAudios();
118
+ });
119
+ }
120
+ });
121
+ } catch (e) {
122
+ console.log('Warning audio initialization failed:', e);
123
+ }
124
+ }
125
+
126
+ startEngineSound() {
127
+ // 엔진 소리 시작
128
+ if (this.warningAudios.normal) {
129
+ this.warningAudios.normal.play().catch(e => {
130
+ console.log('Engine sound failed to start:', e);
131
+ });
132
+ }
133
+ }
134
+
135
+ updateWarningAudios() {
136
+ let currentWarning = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
 
138
+ if (this.altitude < 250) {
139
+ currentWarning = 'pullup';
140
+ }
141
+ else if (this.altitude < 500) {
142
+ currentWarning = 'altitude';
143
+ }
144
+ else if (this.overG) {
145
+ currentWarning = 'overg';
146
+ }
147
+ else if (this.stallWarning) {
148
+ currentWarning = 'stall';
149
  }
150
 
151
+ // 경고음만 관리 (엔진 소리는 제외)
152
+ Object.keys(this.warningAudios).forEach(key => {
153
+ if (key !== 'normal' && key !== currentWarning && this.warningAudios[key] && !this.warningAudios[key].paused) {
154
+ this.warningAudios[key].pause();
155
+ this.warningAudios[key].currentTime = 0;
156
+ }
157
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
+ if (currentWarning && this.warningAudios[currentWarning]) {
160
+ if (this.warningAudios[currentWarning].paused) {
161
+ this.warningAudios[currentWarning].play().catch(e => {});
162
+ }
 
 
 
 
 
 
 
 
 
 
163
  }
164
+ }
165
+
166
+ stopAllWarningAudios() {
167
+ // 모든 오디오 정지 (엔진 소리 포함)
168
+ Object.values(this.warningAudios).forEach(audio => {
169
+ if (audio && !audio.paused) {
170
+ audio.pause();
171
+ audio.currentTime = 0;
172
+ }
173
+ });
174
+ }
175
+
176
+ async initialize(scene, loader) {
177
+ try {
178
+ const result = await loader.loadAsync('models/f-15.glb');
179
+ this.mesh = result.scene;
180
+ this.mesh.position.copy(this.position);
181
+ this.mesh.scale.set(2, 2, 2);
182
+ this.mesh.rotation.y = Math.PI / 4;
183
+
184
+ this.mesh.traverse((child) => {
185
+ if (child.isMesh) {
186
+ child.castShadow = true;
187
+ child.receiveShadow = true;
188
+ }
189
+ });
190
+
191
+ scene.add(this.mesh);
192
+ this.isLoaded = true;
193
+ console.log('F-15 전투기 로딩 완료');
194
+ } catch (error) {
195
+ console.error('F-15 모델 로딩 실패:', error);
196
+ this.createFallbackModel(scene);
197
+ }
198
+ }
199
+
200
+ createFallbackModel(scene) {
201
+ const group = new THREE.Group();
202
+
203
+ const fuselageGeometry = new THREE.CylinderGeometry(0.8, 1.5, 12, 8);
204
+ const fuselageMaterial = new THREE.MeshLambertMaterial({ color: 0x606060 });
205
+ const fuselage = new THREE.Mesh(fuselageGeometry, fuselageMaterial);
206
+ fuselage.rotation.x = Math.PI / 2;
207
+ group.add(fuselage);
208
+
209
+ const wingGeometry = new THREE.BoxGeometry(16, 0.3, 4);
210
+ const wingMaterial = new THREE.MeshLambertMaterial({ color: 0x404040 });
211
+ const wings = new THREE.Mesh(wingGeometry, wingMaterial);
212
+ wings.position.z = -1;
213
+ group.add(wings);
214
+
215
+ const tailGeometry = new THREE.BoxGeometry(0.3, 4, 3);
216
+ const tailMaterial = new THREE.MeshLambertMaterial({ color: 0x404040 });
217
+ const tail = new THREE.Mesh(tailGeometry, tailMaterial);
218
+ tail.position.z = -5;
219
+ tail.position.y = 1.5;
220
+ group.add(tail);
221
+
222
+ const horizontalTailGeometry = new THREE.BoxGeometry(6, 0.2, 2);
223
+ const horizontalTail = new THREE.Mesh(horizontalTailGeometry, tailMaterial);
224
+ horizontalTail.position.z = -5;
225
+ horizontalTail.position.y = 0.5;
226
+ group.add(horizontalTail);
227
+
228
+ this.mesh = group;
229
+ this.mesh.position.copy(this.position);
230
+ this.mesh.scale.set(2, 2, 2);
231
+ scene.add(this.mesh);
232
+ this.isLoaded = true;
233
+
234
+ console.log('Fallback 전투기 모델 생성 완료');
235
+ }
236
+
237
+ updateMouseInput(deltaX, deltaY) {
238
+ const sensitivity = GAME_CONSTANTS.MOUSE_SENSITIVITY * 1.0;
239
+
240
+ // 마우스 Y축: 피치(기수 상하)
241
+ this.targetPitch -= deltaY * sensitivity;
242
+
243
+ // 마우스 X축: 요(Yaw) - 몸체를 좌우로 회전
244
+ this.targetYaw += deltaX * sensitivity * 0.8; // 요 회전 감도 조정
245
 
246
+ // 요 회전에 따른 자동 롤 (뱅크 턴)
247
+ // 좌우로 회전할 때 자연스럽게 날개가 기울어짐
248
+ const yawRate = deltaX * sensitivity * 0.8;
249
+ this.targetRoll = -yawRate * 15; // 요 회전량에 비례하여 롤 발생
250
+
251
+ // 각도 제한
252
+ const maxPitchAngle = Math.PI / 3; // 60도
253
+ const maxRollAngle = Math.PI * 0.5; // 90도로 제한 (자동 롤)
254
+
255
+ this.targetPitch = Math.max(-maxPitchAngle, Math.min(maxPitchAngle, this.targetPitch));
256
+
257
+ // 자동 롤은 제한된 범위 내에서만 작동
258
+ if (Math.abs(this.targetRoll) < maxRollAngle) {
259
+ // 롤 각도 제한 적용
260
+ this.targetRoll = Math.max(-maxRollAngle, Math.min(maxRollAngle, this.targetRoll));
261
+ }
262
+ }
263
+
264
+ updateControls(keys, deltaTime) {
265
+ // W/S: 스로틀만 제어 (가속/감속)
266
+ if (keys.w) {
267
+ this.throttle = Math.min(1.0, this.throttle + deltaTime * 0.5); // 천천히 가속
268
+ }
269
+ if (keys.s) {
270
+ this.throttle = Math.max(0.1, this.throttle - deltaTime * 0.5); // 천천히 감속
271
  }
272
 
273
+ // A/D: 보조 요 제어 (러더) - 선택적 사용
274
+ if (keys.a) {
275
+ this.targetYaw -= deltaTime * 0.4; // 감소된 러더 효과
 
 
276
  }
277
+ if (keys.d) {
278
+ this.targetYaw += deltaTime * 0.4; // 감소된 러더 효과
279
+ }
280
+ }
281
+
282
+ updatePhysics(deltaTime) {
283
+ if (!this.mesh) return;
284
+
285
+ // 부드러운 회전 보간
286
+ const rotationSpeed = deltaTime * 2.0;
287
+ this.rotation.x = THREE.MathUtils.lerp(this.rotation.x, this.targetPitch, rotationSpeed);
288
+ this.rotation.y = THREE.MathUtils.lerp(this.rotation.y, this.targetYaw, rotationSpeed);
289
 
290
+ // 롤 자동 복귀 시스템
291
+ if (Math.abs(this.targetYaw - this.rotation.y) < 0.05) {
292
+ // 요 회전이 거의 없을 때 롤을 0으로 복귀
293
+ this.targetRoll *= 0.95;
 
294
  }
295
 
296
+ this.rotation.z = THREE.MathUtils.lerp(this.rotation.z, this.targetRoll, rotationSpeed * 1.5); // 롤은 더 빠르게 반응
297
+
298
+ // 워썬더 스타일: 요 회전이 주도적, 롤은 보조적
299
+ // 롤에 따른 추가 요 회전은 제거하거나 최소화
300
+ let bankTurnRate = 0;
301
+ if (Math.abs(this.rotation.z) > 0.3) { // 롤이 충분히 클 때만
302
+ const bankAngle = this.rotation.z;
303
+ bankTurnRate = Math.sin(bankAngle) * deltaTime * 0.1; // 매우 작은 선회율
304
+ this.targetYaw += bankTurnRate;
305
  }
306
 
307
+ // 현실적인 속도 계산
308
+ const minSpeed = 0; // 최소 속도 0kt
309
+ const maxSpeed = 600; // 최대 속도 600kt
310
+ let targetSpeed = minSpeed + (maxSpeed - minSpeed) * this.throttle;
311
+
312
+ // 피치 각도에 따른 속도 변화
313
+ const pitchAngle = this.rotation.x;
314
+ const pitchDegrees = Math.abs(pitchAngle) * (180 / Math.PI);
315
+
316
+ // 기수가 위를 향하고 있을 경우 빠른 속도 감소
317
+ if (pitchAngle < -0.1 && !this.stallWarning) { // 스톨이 아닐 때만 상승으로 인한 감속
318
+ const climbFactor = Math.abs(pitchAngle) / (Math.PI / 2); // 90도 기준
319
+ if (pitchDegrees > 30) { // 30도 이상일 때 급격한 감속
320
+ targetSpeed *= Math.max(0, 1 - climbFactor * 1.5); // 최대 150% 감속 (0kt까지)
321
+ } else {
322
+ targetSpeed *= (1 - climbFactor * 0.3); // 정상적인 감속
323
+ }
324
+ } else if (pitchAngle > 0.1) { // 기수가 아래로 (하강) - 스톨 상태에서도 적용
325
+ const diveFactor = pitchAngle / (Math.PI / 3);
326
+ targetSpeed *= (1 + diveFactor * 0.4); // 하강 시 가속 증가 (0.2 -> 0.4)
327
+ }
328
+
329
+ // G-Force 계산 개선
330
+ const turnRate = Math.abs(bankTurnRate) * 100;
331
+ const pitchRate = Math.abs(this.rotation.x - this.targetPitch) * 10;
332
+
333
+ // 고도에 따른 G-Force 증가 배율 계산
334
+ const altitudeInKm = this.position.y / 1000; // 미터를 킬로미터로 변환
335
+ const altitudeMultiplier = 1 + (altitudeInKm * 0.2); // 1km당 20% 증가
336
+
337
+ // 스로틀에 따른 G-Force 증가 배율 계산
338
+ // THR 50% 이하: 0배, THR 75%: 0.5배, THR 100%: 1.0배
339
+ let throttleGMultiplier = 0;
340
+ if (this.throttle > 0.5) {
341
+ // 0.5 ~ 1.0 범위를 0 ~ 1.0으로 매핑
342
+ throttleGMultiplier = (this.throttle - 0.5) * 2.0;
343
  }
344
 
345
+ // 비정상적인 자세에 의한 G-Force 추가
346
+ let abnormalG = 0;
 
 
 
347
 
348
+ // 뒤집힌 상태 (롤이 90도 이상)
349
+ const isInverted = Math.abs(this.rotation.z) > Math.PI / 2;
350
+ if (isInverted) {
351
+ const baseG = 3.0 + Math.abs(Math.abs(this.rotation.z) - Math.PI / 2) * 2;
352
+ abnormalG += baseG * altitudeMultiplier * (1 + throttleGMultiplier); // 스로틀 배율 추가
353
  }
354
 
355
+ // 피치 각도가 ±40도 이상일 때 추가 G-Force
356
+ if (pitchDegrees >= 40) {
357
+ // 40도 이상일 급격한 G-Force 증가
358
+ const extremePitchG = (pitchDegrees - 40) * 0.15; // 40도 초과분당 0.15G 추가
359
+ abnormalG += extremePitchG * altitudeMultiplier * (1 + throttleGMultiplier);
360
  }
361
 
362
+ // 기수가 계속 위를 향하고 있는 경우 (피치가 -30도 이하)
363
+ if (pitchAngle < -Math.PI / 6) {
364
+ const baseG = 2.0 + Math.abs(pitchAngle + Math.PI / 6) * 3;
365
+ abnormalG += baseG * altitudeMultiplier * (1 + throttleGMultiplier); // 스로틀 배율 추가
366
  }
367
 
368
+ // 기수가 과도하게 아래를 향하고 있는 경우 (피치가 60도 이상)
369
+ if (pitchAngle > Math.PI / 3) {
370
+ const baseG = 2.0 + Math.abs(pitchAngle - Math.PI / 3) * 3;
371
+ abnormalG += baseG * altitudeMultiplier * (1 + throttleGMultiplier); // 스로틀 배율 추가
372
  }
373
+
374
+ // 급격한 기동에 의한 G-Force
375
+ const maneuverG = (turnRate + pitchRate + (Math.abs(this.rotation.z) * 3)) * (1 + throttleGMultiplier * 0.5);
376
+
377
+ // 총 G-Force 계산
378
+ this.gForce = 1.0 + maneuverG + abnormalG;
379
+
380
+ // G-Force 회복 조건 수정
381
+ // 1. Over-G 상태가 아닌 경우에만 회복
382
+ // 2. 피치가 ±10도 이내일 때만 회복
383
+ // 3. 스톨 상태가 아닐 때만 회복
384
+ const isPitchNeutral = Math.abs(pitchDegrees) <= 10;
385
+
386
+ if (!this.overG && isPitchNeutral && !isInverted && !this.stallWarning) {
387
+ // 스로틀이 높을수록 G-Force가 천천히 감소
388
+ const recoveryRate = 2.0 - throttleGMultiplier * 1.5; // THR 100%일 때 0.5, THR 50%일 때 2.0
389
+ this.gForce = THREE.MathUtils.lerp(this.gForce, 1.0 + maneuverG, deltaTime * recoveryRate);
390
+ } else if (this.overG) {
391
+ // Over-G 상태에서는 피치가 0도 근처(±10도)가 되고 스톨이 회복될 때까지 회복하지 않음
392
+ if (!isPitchNeutral || this.stallWarning) {
393
+ // 피치가 중립이 아니거나 스톨 상태면 G-Force 유지 또는 증가만 가능
394
+ this.gForce = Math.max(this.gForce, 1.0 + maneuverG + abnormalG);
395
+ } else {
396
+ // 피치가 중립이고 스톨이 아닐 때만 매우 천천히 회복
397
+ const overGRecoveryRate = 0.3 - throttleGMultiplier * 0.2; // Over-G 상태에서는 더 느린 회복
398
+ this.gForce = THREE.MathUtils.lerp(this.gForce, 1.0 + maneuverG, deltaTime * overGRecoveryRate);
399
+ }
400
+ }
401
+
402
+ // 스톨 상태에서는 Over-G가 감소하지 않도록 추가 처리
403
+ if (this.stallWarning && this.overG) {
404
+ // 스톨 중에는 G-Force를 현재 값 이상으로 유지
405
+ this.gForce = Math.max(this.gForce, this.maxGForce);
406
+ }
407
+
408
+ this.overG = this.gForce > this.maxGForce;
409
+
410
+ // Over-G 타이머 업데이트
411
+ if (this.overG) {
412
+ this.overGTimer += deltaTime;
413
+
414
+ // 1.5초 이상 Over-G 상태일 경우
415
+ if (this.overGTimer > 1.5) {
416
+ // 속도 급격히 감소 (0kt까지)
417
+ targetSpeed *= Math.max(0, 1 - (this.overGTimer - 1.5) * 0.5);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
 
419
+ // 시야 흐림 효과��� UI에서 처리
420
+ }
421
+ } else {
422
+ this.overGTimer = 0; // Over-G 상태가 아니면 타이머 리셋
423
+ }
424
+
425
+ // 스톨 경고: 300kt 이하에서 스톨 위험
426
+ const speedKnots = this.speed * 1.94384; // m/s to knots
427
+ const wasStalling = this.stallWarning;
428
+
429
+ // 스톨 진입 조건
430
+ if (!this.stallWarning && speedKnots < GAME_CONSTANTS.STALL_SPEED) {
431
+ this.stallWarning = true;
432
+ this.stallEscapeProgress = 0; // 스톨 진입 시 진행도 초기화
433
+ }
434
+
435
+ // 스톨 탈출 진행도 업데이트
436
+ if (this.stallWarning && this.escapeKeyPressed && pitchAngle > 0.2 && speedKnots > GAME_CONSTANTS.STALL_SPEED + 50) {
437
+ // F키를 누르고 있고 조건이 맞으면 진행도 증가
438
+ this.stallEscapeProgress += deltaTime;
439
+
440
+ // 2초 이상 F키를 누르고 있으면 스톨 탈출
441
+ if (this.stallEscapeProgress >= 2.0) {
442
+ this.stallWarning = false;
443
+ this.stallEscapeProgress = 0;
444
+ }
445
+ } else if (this.stallWarning && !this.escapeKeyPressed) {
446
+ // F키를 놓으면 진행도 감소
447
+ this.stallEscapeProgress = Math.max(0, this.stallEscapeProgress - deltaTime * 2);
448
+ }
449
+
450
+ // 속도 변화 적용
451
+ if (this.stallWarning) {
452
+ // 스톨 상태에서의 속도 변화
453
+ if (pitchAngle > 0.1) { // 기수가 아래를 향할 때
454
+ // 다이빙으로 인한 속도 증가
455
+ const diveSpeedGain = Math.min(pitchAngle * 300, 200); // 최대 200m/s 증가
456
+ this.speed = Math.min(maxSpeed, this.speed + diveSpeedGain * deltaTime);
457
+ } else {
458
+ // 기수가 위를 향하거나 수평일 때는 속도 감소
459
+ this.speed = Math.max(0, this.speed - deltaTime * 100);
460
+ }
461
+ } else {
462
+ // 정상 비행 시 속도 변화
463
+ this.speed = THREE.MathUtils.lerp(this.speed, targetSpeed, deltaTime * 0.5);
464
+ }
465
+
466
+ // 스톨 상태에서의 물리 효과
467
+ if (this.stallWarning) {
468
+ // 바닥으로 추락하며 가속도가 빠르게 붙음
469
+ this.targetPitch = Math.min(Math.PI / 3, this.targetPitch + deltaTime * 2.0); // 기수가 빠르게 아래로
470
+
471
+ // 조종 불능 상태
472
+ this.rotation.x += (Math.random() - 0.5) * deltaTime * 0.5;
473
+ this.rotation.z += (Math.random() - 0.5) * deltaTime * 0.5;
474
+
475
+ // 중력에 의한 가속
476
+ const gravityAcceleration = GAME_CONSTANTS.GRAVITY * deltaTime * 3.0; // 3배 중력
477
+ this.velocity.y -= gravityAcceleration;
478
+ }
479
+
480
+ // 속도 벡터 계산
481
+ const noseDirection = new THREE.Vector3(0, 0, 1);
482
+ noseDirection.applyEuler(this.rotation);
483
+
484
+ if (!this.stallWarning) {
485
+ // 정상 비행 시
486
+ this.velocity = noseDirection.multiplyScalar(this.speed);
487
+ } else {
488
+ // 스톨 시에는 중력이 주도적이지만 다이빙 속도도 반영
489
+ this.velocity.x = noseDirection.x * this.speed * 0.5; // 전방 속도 증가
490
+ this.velocity.z = noseDirection.z * this.speed * 0.5;
491
+ // y 속도는 위에서 중력으로 처리됨
492
+ }
493
+
494
+ // 정상 비행 시 중력 효과
495
+ if (!this.stallWarning) {
496
+ const gravityEffect = GAME_CONSTANTS.GRAVITY * deltaTime * 0.15;
497
+ this.velocity.y -= gravityEffect;
498
+
499
+ // 양력 효과 (속도에 비례)
500
+ const liftFactor = (this.speed / maxSpeed) * 0.8;
501
+ const lift = gravityEffect * liftFactor;
502
+ this.velocity.y += lift;
503
+ }
504
+
505
+ // 위치 업데이트
506
+ this.position.add(this.velocity.clone().multiplyScalar(deltaTime));
507
+
508
+ // 지면 충돌
509
+ if (this.position.y <= GAME_CONSTANTS.MIN_ALTITUDE) {
510
+ this.position.y = GAME_CONSTANTS.MIN_ALTITUDE;
511
+ this.health = 0;
512
+ return;
513
+ }
514
+
515
+ // 최대 고도 제한
516
+ if (this.position.y > GAME_CONSTANTS.MAX_ALTITUDE) {
517
+ this.position.y = GAME_CONSTANTS.MAX_ALTITUDE;
518
+ this.altitudeWarning = true;
519
+ if (this.velocity.y > 0) this.velocity.y = 0;
520
+ } else {
521
+ this.altitudeWarning = false;
522
+ }
523
+
524
+ // 맵 경계 처리
525
+ const mapLimit = GAME_CONSTANTS.MAP_SIZE / 2;
526
+ if (this.position.x > mapLimit) this.position.x = -mapLimit;
527
+ if (this.position.x < -mapLimit) this.position.x = mapLimit;
528
+ if (this.position.z > mapLimit) this.position.z = -mapLimit;
529
+ if (this.position.z < -mapLimit) this.position.z = mapLimit;
530
+
531
+ // 메시 위치 및 회전 업데이트
532
+ this.mesh.position.copy(this.position);
533
+ this.mesh.rotation.x = this.rotation.x;
534
+ this.mesh.rotation.y = this.rotation.y + 3 * Math.PI / 2;
535
+ this.mesh.rotation.z = this.rotation.z;
536
+
537
+ // 경고 깜빡임 타이머
538
+ this.warningBlinkTimer += deltaTime;
539
+ if (this.warningBlinkTimer >= 1.0) {
540
+ this.warningBlinkTimer = 0;
541
+ this.warningBlinkState = !this.warningBlinkState;
542
+ }
543
+
544
+ // 고도 계산
545
+ this.altitude = this.position.y;
546
+
547
+ // 경고음 업데이트 (엔진 소리는 계속 유지)
548
+ this.updateWarningAudios();
549
+
550
+ // 엔진 소리 볼륨을 스로틀에 연동
551
+ if (this.warningAudios.normal && !this.warningAudios.normal.paused) {
552
+ this.warningAudios.normal.volume = 0.3 + this.throttle * 0.4; // 0.3~0.7
553
+ }
554
+ }
555
+
556
+ shoot(scene) {
557
+ const currentTime = Date.now();
558
+ if (currentTime - this.lastShootTime < 100 || this.ammo <= 0) return;
559
+
560
+ this.lastShootTime = currentTime;
561
+ this.ammo--;
562
+
563
+ const bulletGeometry = new THREE.SphereGeometry(0.2);
564
+ const bulletMaterial = new THREE.MeshBasicMaterial({
565
+ color: 0xffff00,
566
+ emissive: 0xffff00,
567
+ emissiveIntensity: 0.7
568
+ });
569
+ const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
570
+
571
+ const muzzleOffset = new THREE.Vector3(0, 0, 8);
572
+ muzzleOffset.applyEuler(this.rotation);
573
+ bullet.position.copy(this.position).add(muzzleOffset);
574
+
575
+ const bulletSpeed = 1000;
576
+ const direction = new THREE.Vector3(0, 0, 1);
577
+ direction.applyEuler(this.rotation);
578
+ bullet.velocity = direction.multiplyScalar(bulletSpeed).add(this.velocity);
579
+
580
+ scene.add(bullet);
581
+ this.bullets.push(bullet);
582
+
583
+ try {
584
+ const audio = new Audio('sounds/gunfire.ogg');
585
+ if (audio) {
586
+ audio.volume = 0.3;
587
+ audio.play().catch(e => console.log('Gunfire sound failed to play'));
588
+ }
589
+ } catch (e) {}
590
+ }
591
+
592
+ updateBullets(scene, deltaTime) {
593
+ for (let i = this.bullets.length - 1; i >= 0; i--) {
594
+ const bullet = this.bullets[i];
595
+ bullet.position.add(bullet.velocity.clone().multiplyScalar(deltaTime));
596
+
597
+ if (bullet.position.distanceTo(this.position) > 8000 ||
598
+ bullet.position.y < 0 ||
599
+ bullet.position.y > GAME_CONSTANTS.MAX_ALTITUDE + 500) {
600
+ scene.remove(bullet);
601
+ this.bullets.splice(i, 1);
602
+ }
603
+ }
604
+ }
605
+
606
+ takeDamage(damage) {
607
+ this.health -= damage;
608
+ return this.health <= 0;
609
+ }
610
+
611
+ getCameraPosition() {
612
+ const backward = new THREE.Vector3(0, 0, -1);
613
+ const up = new THREE.Vector3(0, 1, 0);
614
+
615
+ backward.applyEuler(this.rotation);
616
+ up.applyEuler(this.rotation);
617
+
618
+ const cameraPosition = this.position.clone()
619
+ .add(backward.multiplyScalar(this.cameraDistance))
620
+ .add(up.multiplyScalar(this.cameraHeight));
621
+
622
+ return cameraPosition;
623
+ }
624
+
625
+ getCameraTarget() {
626
+ return this.position.clone();
627
+ }
628
+ }
629
+
630
+ // 적 전투기 클래스
631
+ class EnemyFighter {
632
+ constructor(scene, position) {
633
+ this.mesh = null;
634
+ this.isLoaded = false;
635
+ this.scene = scene;
636
+ this.position = position.clone();
637
+ this.velocity = new THREE.Vector3(0, 0, 120);
638
+ this.rotation = new THREE.Euler(0, 0, 0);
639
+ this.health = 100;
640
+ this.speed = 120;
641
+ this.bullets = [];
642
+ this.lastShootTime = 0;
643
+
644
+ this.aiState = 'patrol';
645
+ this.targetPosition = position.clone();
646
+ this.patrolCenter = position.clone();
647
+ this.patrolRadius = 2000;
648
+ this.lastStateChange = 0;
649
+ }
650
+
651
+ async initialize(loader) {
652
+ try {
653
+ const result = await loader.loadAsync('models/mig-29.glb');
654
+ this.mesh = result.scene;
655
+ this.mesh.position.copy(this.position);
656
+ this.mesh.scale.set(1.5, 1.5, 1.5);
657
+ this.mesh.rotation.y = 3 * Math.PI / 2;
658
+
659
+ this.mesh.traverse((child) => {
660
+ if (child.isMesh) {
661
+ child.castShadow = true;
662
+ child.receiveShadow = true;
663
+ }
664
+ });
665
+
666
+ this.scene.add(this.mesh);
667
+ this.isLoaded = true;
668
+ console.log('MiG-29 적기 로딩 완료');
669
+ } catch (error) {
670
+ console.error('MiG-29 모델 로딩 실패:', error);
671
+ this.createFallbackModel();
672
+ }
673
+ }
674
+
675
+ createFallbackModel() {
676
+ const group = new THREE.Group();
677
+
678
+ const fuselageGeometry = new THREE.CylinderGeometry(0.6, 1.0, 8, 8);
679
+ const fuselageMaterial = new THREE.MeshLambertMaterial({ color: 0x800000 });
680
+ const fuselage = new THREE.Mesh(fuselageGeometry, fuselageMaterial);
681
+ fuselage.rotation.x = -Math.PI / 2;
682
+ group.add(fuselage);
683
+
684
+ const wingGeometry = new THREE.BoxGeometry(12, 0.3, 3);
685
+ const wingMaterial = new THREE.MeshLambertMaterial({ color: 0x600000 });
686
+ const wings = new THREE.Mesh(wingGeometry, wingMaterial);
687
+ wings.position.z = -0.5;
688
+ group.add(wings);
689
+
690
+ this.mesh = group;
691
+ this.mesh.position.copy(this.position);
692
+ this.mesh.scale.set(1.5, 1.5, 1.5);
693
+ this.scene.add(this.mesh);
694
+ this.isLoaded = true;
695
+
696
+ console.log('Fallback 적기 모델 생성 완료');
697
+ }
698
+
699
+ update(playerPosition, deltaTime) {
700
+ if (!this.mesh || !this.isLoaded) return;
701
+
702
+ const currentTime = Date.now();
703
+ const distanceToPlayer = this.position.distanceTo(playerPosition);
704
+
705
+ if (distanceToPlayer < 5000) {
706
+ const direction = new THREE.Vector3()
707
+ .subVectors(playerPosition, this.position)
708
+ .normalize();
709
+
710
+ this.velocity = direction.multiplyScalar(this.speed);
711
+ this.rotation.y = Math.atan2(direction.x, direction.z);
712
+
713
+ if (distanceToPlayer < 2000 && currentTime - this.lastShootTime > 1500) {
714
+ this.shoot();
715
+ }
716
+ } else {
717
+ if (this.position.distanceTo(this.targetPosition) < 300) {
718
+ const angle = Math.random() * Math.PI * 2;
719
+ this.targetPosition = this.patrolCenter.clone().add(
720
+ new THREE.Vector3(
721
+ Math.cos(angle) * this.patrolRadius,
722
+ (Math.random() - 0.5) * 1000,
723
+ Math.sin(angle) * this.patrolRadius
724
+ )
725
+ );
726
+ }
727
+
728
+ const direction = new THREE.Vector3()
729
+ .subVectors(this.targetPosition, this.position)
730
+ .normalize();
731
+
732
+ this.velocity = direction.multiplyScalar(this.speed * 0.7);
733
+ this.rotation.y = Math.atan2(direction.x, direction.z);
734
+ }
735
+
736
+ this.position.add(this.velocity.clone().multiplyScalar(deltaTime));
737
+
738
+ if (this.position.y < GAME_CONSTANTS.MIN_ALTITUDE) {
739
+ this.position.y = GAME_CONSTANTS.MIN_ALTITUDE;
740
+ }
741
+ if (this.position.y > GAME_CONSTANTS.MAX_ALTITUDE) {
742
+ this.position.y = GAME_CONSTANTS.MAX_ALTITUDE;
743
+ }
744
+
745
+ const mapLimit = GAME_CONSTANTS.MAP_SIZE / 2;
746
+ if (this.position.x > mapLimit) this.position.x = -mapLimit;
747
+ if (this.position.x < -mapLimit) this.position.x = mapLimit;
748
+ if (this.position.z > mapLimit) this.position.z = -mapLimit;
749
+ if (this.position.z < -mapLimit) this.position.z = mapLimit;
750
+
751
+ this.mesh.position.copy(this.position);
752
+ this.mesh.rotation.x = this.rotation.x;
753
+ this.mesh.rotation.y = this.rotation.y + 3 * Math.PI / 2;
754
+ this.mesh.rotation.z = this.rotation.z;
755
+
756
+ this.updateBullets(deltaTime);
757
+ }
758
+
759
+ shoot() {
760
+ this.lastShootTime = Date.now();
761
+
762
+ const bulletGeometry = new THREE.SphereGeometry(0.15);
763
+ const bulletMaterial = new THREE.MeshBasicMaterial({
764
+ color: 0xff0000,
765
+ emissive: 0xff0000,
766
+ emissiveIntensity: 0.5
767
+ });
768
+ const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
769
+
770
+ const muzzleOffset = new THREE.Vector3(0, 0, 6);
771
+ muzzleOffset.applyEuler(this.rotation);
772
+ bullet.position.copy(this.position).add(muzzleOffset);
773
+
774
+ const direction = new THREE.Vector3(0, 0, 1);
775
+ direction.applyEuler(this.rotation);
776
+ bullet.velocity = direction.multiplyScalar(800);
777
+
778
+ this.scene.add(bullet);
779
+ this.bullets.push(bullet);
780
+ }
781
+
782
+ updateBullets(deltaTime) {
783
+ for (let i = this.bullets.length - 1; i >= 0; i--) {
784
+ const bullet = this.bullets[i];
785
+ bullet.position.add(bullet.velocity.clone().multiplyScalar(deltaTime));
786
+
787
+ if (bullet.position.distanceTo(this.position) > 5000 ||
788
+ bullet.position.y < 0) {
789
+ this.scene.remove(bullet);
790
+ this.bullets.splice(i, 1);
791
+ }
792
+ }
793
+ }
794
+
795
+ takeDamage(damage) {
796
+ this.health -= damage;
797
+ return this.health <= 0;
798
+ }
799
+
800
+ destroy() {
801
+ if (this.mesh) {
802
+ this.scene.remove(this.mesh);
803
+ this.bullets.forEach(bullet => this.scene.remove(bullet));
804
+ this.bullets = [];
805
+ this.isLoaded = false;
806
+ }
807
+ }
808
+ }
809
+
810
+ // 메인 게임 클래스
811
+ class Game {
812
+ constructor() {
813
+ this.scene = new THREE.Scene();
814
+ this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 50000);
815
+ this.renderer = new THREE.WebGLRenderer({ antialias: true });
816
+ this.renderer.setSize(window.innerWidth, window.innerHeight);
817
+ this.renderer.shadowMap.enabled = true;
818
+ this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
819
+ this.renderer.setClearColor(0x87CEEB);
820
+ this.renderer.setPixelRatio(window.devicePixelRatio);
821
+
822
+ document.getElementById('gameContainer').appendChild(this.renderer.domElement);
823
+
824
+ this.loader = new GLTFLoader();
825
+ this.fighter = new Fighter();
826
+ this.enemies = [];
827
+ this.isLoaded = false;
828
+ this.isBGMReady = false;
829
+ this.isGameOver = false;
830
+ this.gameTime = GAME_CONSTANTS.MISSION_DURATION;
831
+ this.score = 0;
832
+ this.lastTime = performance.now();
833
+ this.gameTimer = null;
834
+ this.animationFrameId = null;
835
+
836
+ this.bgm = null;
837
+ this.bgmPlaying = false;
838
+
839
+ this.keys = { w: false, a: false, s: false, d: false, f: false };
840
+ this.isStarted = false;
841
+
842
+ this.setupScene();
843
+ this.setupEventListeners();
844
+ this.preloadGame();
845
+ }
846
+
847
+ async preloadGame() {
848
+ try {
849
+ console.log('게임 리소스 사전 로딩 중...');
850
+
851
+ await this.fighter.initialize(this.scene, this.loader);
852
+
853
+ if (!this.fighter.isLoaded) {
854
+ throw new Error('전투기 로딩 실패');
855
+ }
856
+
857
+ await this.preloadEnemies();
858
+
859
+ this.isLoaded = true;
860
+ console.log('게임 리소스 로딩 완료');
861
 
862
+ await this.preloadBGM();
863
+
864
+ this.showStartScreen();
865
+
866
+ this.animate();
867
+
868
+ } catch (error) {
869
+ console.error('게임 사전 로딩 실패:', error);
870
+ document.getElementById('loading').innerHTML =
871
+ '<div class="loading-text" style="color: red;">로딩 실패. 페이지를 새로고침해주세요.</div>';
872
+ }
873
+ }
874
+
875
+ showStartScreen() {
876
+ if (this.isLoaded && this.isBGMReady) {
877
+ const loadingElement = document.getElementById('loading');
878
+ if (loadingElement) {
879
+ loadingElement.style.display = 'none';
880
+ }
881
+
882
+ const startScreen = document.getElementById('startScreen');
883
+ if (startScreen) {
884
+ startScreen.style.display = 'flex';
885
+ }
886
+
887
+ window.dispatchEvent(new Event('gameReady'));
888
+
889
+ console.log('모든 리소스 준비 완료 - Start Game 버튼 표시');
890
+ }
891
+ }
892
+
893
+ async preloadBGM() {
894
+ console.log('BGM 사전 로딩...');
895
+
896
+ return new Promise((resolve) => {
897
+ try {
898
+ this.bgm = new Audio('sounds/main.ogg');
899
+ this.bgm.volume = 0.25;
900
+ this.bgm.loop = true;
901
+
902
+ this.bgm.addEventListener('canplaythrough', () => {
903
+ console.log('BGM 재생 준비 완료');
904
+ this.isBGMReady = true;
905
+ resolve();
906
+ });
907
 
908
+ this.bgm.addEventListener('error', (e) => {
909
+ console.log('BGM 에러:', e);
910
+ this.isBGMReady = true;
911
+ resolve();
912
+ });
 
 
913
 
914
+ this.bgm.load();
 
 
 
 
 
915
 
916
+ setTimeout(() => {
917
+ if (!this.isBGMReady) {
918
+ console.log('BGM 로딩 타임아웃 - 게임 진행');
919
+ this.isBGMReady = true;
920
+ resolve();
921
+ }
922
+ }, 3000);
923
 
924
+ } catch (error) {
925
+ console.log('BGM 사전 로딩 실패:', error);
926
+ this.isBGMReady = true;
927
+ resolve();
928
+ }
929
+ });
930
+ }
931
+
932
+ async preloadEnemies() {
933
+ for (let i = 0; i < GAME_CONSTANTS.ENEMY_COUNT; i++) {
934
+ const angle = (i / GAME_CONSTANTS.ENEMY_COUNT) * Math.PI * 2;
935
+ const distance = 6000 + Math.random() * 3000;
936
+
937
+ const position = new THREE.Vector3(
938
+ Math.cos(angle) * distance,
939
+ 2500 + Math.random() * 2000,
940
+ Math.sin(angle) * distance
941
+ );
942
+
943
+ const enemy = new EnemyFighter(this.scene, position);
944
+ await enemy.initialize(this.loader);
945
+ this.enemies.push(enemy);
946
+ }
947
+ }
948
+
949
+ setupScene() {
950
+ this.scene.background = new THREE.Color(0x87CEEB);
951
+ this.scene.fog = new THREE.Fog(0x87CEEB, 1000, 30000);
952
+
953
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
954
+ this.scene.add(ambientLight);
955
+
956
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
957
+ directionalLight.position.set(8000, 10000, 8000);
958
+ directionalLight.castShadow = true;
959
+ directionalLight.shadow.mapSize.width = 2048;
960
+ directionalLight.shadow.mapSize.height = 2048;
961
+ directionalLight.shadow.camera.near = 0.5;
962
+ directionalLight.shadow.camera.far = 20000;
963
+ directionalLight.shadow.camera.left = -10000;
964
+ directionalLight.shadow.camera.right = 10000;
965
+ directionalLight.shadow.camera.top = 10000;
966
+ directionalLight.shadow.camera.bottom = -10000;
967
+ this.scene.add(directionalLight);
968
+
969
+ const groundGeometry = new THREE.PlaneGeometry(GAME_CONSTANTS.MAP_SIZE, GAME_CONSTANTS.MAP_SIZE);
970
+ const groundMaterial = new THREE.MeshLambertMaterial({
971
+ color: 0x8FBC8F,
972
+ transparent: true,
973
+ opacity: 0.8
974
+ });
975
+ const ground = new THREE.Mesh(groundGeometry, groundMaterial);
976
+ ground.rotation.x = -Math.PI / 2;
977
+ ground.receiveShadow = true;
978
+ this.scene.add(ground);
979
+
980
+ this.addClouds();
981
+ }
982
+
983
+ addClouds() {
984
+ const cloudGeometry = new THREE.SphereGeometry(100, 8, 6);
985
+ const cloudMaterial = new THREE.MeshLambertMaterial({
986
+ color: 0xffffff,
987
+ transparent: true,
988
+ opacity: 0.5
989
+ });
990
+
991
+ for (let i = 0; i < 100; i++) {
992
+ const cloud = new THREE.Mesh(cloudGeometry, cloudMaterial);
993
+ cloud.position.set(
994
+ (Math.random() - 0.5) * GAME_CONSTANTS.MAP_SIZE,
995
+ Math.random() * 4000 + 1000,
996
+ (Math.random() - 0.5) * GAME_CONSTANTS.MAP_SIZE
997
+ );
998
+ cloud.scale.set(
999
+ Math.random() * 3 + 1,
1000
+ Math.random() * 2 + 0.5,
1001
+ Math.random() * 3 + 1
1002
+ );
1003
+ this.scene.add(cloud);
1004
+ }
1005
+ }
1006
+
1007
+ setupEventListeners() {
1008
+ document.addEventListener('keydown', (event) => {
1009
+ if (this.isGameOver || !gameStarted) return;
1010
 
1011
+ switch(event.code) {
1012
+ case 'KeyW': this.keys.w = true; break;
1013
+ case 'KeyA': this.keys.a = true; break;
1014
+ case 'KeyS': this.keys.s = true; break;
1015
+ case 'KeyD': this.keys.d = true; break;
1016
+ case 'KeyF': this.keys.f = true; break;
1017
+ }
1018
+ });
1019
 
1020
+ document.addEventListener('keyup', (event) => {
1021
+ if (this.isGameOver || !gameStarted) return;
1022
+
1023
+ switch(event.code) {
1024
+ case 'KeyW': this.keys.w = false; break;
1025
+ case 'KeyA': this.keys.a = false; break;
1026
+ case 'KeyS': this.keys.s = false; break;
1027
+ case 'KeyD': this.keys.d = false; break;
1028
+ case 'KeyF': this.keys.f = false; break;
1029
  }
1030
+ });
1031
+
1032
+ document.addEventListener('mousemove', (event) => {
1033
+ if (!document.pointerLockElement || this.isGameOver || !gameStarted) return;
1034
+
1035
+ const deltaX = event.movementX || 0;
1036
+ const deltaY = event.movementY || 0;
1037
+
1038
+ this.fighter.updateMouseInput(deltaX, deltaY);
1039
+ });
1040
+
1041
+ window.addEventListener('resize', () => {
1042
+ this.camera.aspect = window.innerWidth / window.innerHeight;
1043
+ this.camera.updateProjectionMatrix();
1044
+ this.renderer.setSize(window.innerWidth, window.innerHeight);
1045
+ });
1046
+ }
1047
+
1048
+ startGame() {
1049
+ if (!this.isLoaded) {
1050
+ console.log('게임이 아직 로딩 중입니다...');
1051
+ return;
1052
  }
1053
+
1054
+ this.isStarted = true;
1055
+ this.startGameTimer();
1056
+
1057
+ // 엔진 소리 시작
1058
+ this.fighter.startEngineSound();
1059
+
1060
+ console.log('게임 시작!');
1061
+ }
1062
+
1063
+ startBGM() {
1064
+ if (this.bgmPlaying || !this.bgm) return;
1065
+
1066
+ console.log('BGM 재생 시도...');
1067
+
1068
+ const playPromise = this.bgm.play();
1069
+
1070
+ if (playPromise !== undefined) {
1071
+ playPromise.then(() => {
1072
+ this.bgmPlaying = true;
1073
+ console.log('BGM 재생 시작 성공!');
1074
+ }).catch(error => {
1075
+ console.log('자동 재생이 차단됨:', error);
1076
+ console.log('클릭 후 재생 시도 대기 중...');
1077
 
1078
+ const tryPlayOnInteraction = () => {
1079
+ if (!this.bgmPlaying && this.bgm) {
1080
+ console.log('사용자 상호작용으로 BGM 재생 시도...');
1081
+ this.bgm.play().then(() => {
1082
+ this.bgmPlaying = true;
1083
+ console.log('BGM 재생 시작 성공 (클릭 후)!');
1084
+ document.removeEventListener('click', tryPlayOnInteraction);
1085
+ document.removeEventListener('keydown', tryPlayOnInteraction);
1086
+ }).catch(e => console.log('BGM 재생 실패:', e));
1087
+ }
1088
  };
1089
 
1090
+ document.addEventListener('click', tryPlayOnInteraction);
1091
+ document.addEventListener('keydown', tryPlayOnInteraction);
1092
+ });
1093
+ }
1094
+ }
1095
+
1096
+ startGameTimer() {
1097
+ this.gameTimer = setInterval(() => {
1098
+ if (!this.isGameOver) {
1099
+ this.gameTime--;
1100
+
1101
+ if (this.gameTime <= 0) {
1102
+ this.endGame(true);
1103
+ }
1104
  }
1105
+ }, 1000);
1106
+ }
1107
+
1108
+ updateUI() {
1109
+ if (this.fighter.isLoaded) {
1110
+ const speedKnots = Math.round(this.fighter.speed * 1.94384);
1111
+ const altitudeFeet = Math.round(this.fighter.altitude * 3.28084);
1112
+ const altitudeMeters = Math.round(this.fighter.altitude);
1113
 
1114
+ const scoreElement = document.getElementById('score');
1115
+ const timeElement = document.getElementById('time');
1116
+ const healthElement = document.getElementById('health');
1117
+ const ammoElement = document.getElementById('ammoDisplay');
1118
+ const gameStatsElement = document.getElementById('gameStats');
 
 
 
 
 
 
 
1119
 
1120
+ if (scoreElement) scoreElement.textContent = `Score: ${this.score}`;
1121
+ if (timeElement) timeElement.textContent = `Time: ${this.gameTime}s`;
1122
+ if (healthElement) healthElement.style.width = `${this.fighter.health}%`;
1123
+ if (ammoElement) ammoElement.textContent = `AMMO: ${this.fighter.ammo}`;
1124
+
1125
+ if (gameStatsElement) {
1126
+ gameStatsElement.innerHTML = `
1127
+ <div>Score: ${this.score}</div>
1128
+ <div>Time: ${this.gameTime}s</div>
1129
+ <div>Speed: ${speedKnots} KT</div>
1130
+ <div>Alt: ${altitudeMeters}m (${altitudeFeet} FT)</div>
1131
+ <div>Throttle: ${Math.round(this.fighter.throttle * 100)}%</div>
1132
+ <div>G-Force: ${this.fighter.gForce.toFixed(1)}</div>
1133
+ <div>Targets: ${this.enemies.length}</div>
1134
+ `;
1135
+ }
1136
+
1137
+ this.updateWarnings();
1138
+ this.updateHUD();
1139
+ }
1140
+ }
1141
+
1142
+ updateHUD() {
1143
+ // HUD 크로스헤어 업데이트
1144
+ const hudElement = document.getElementById('hudCrosshair');
1145
+ if (!hudElement) return;
1146
+
1147
+ // 롤 각도에 따라 HUD 회전
1148
+ const rollDegrees = this.fighter.rotation.z * (180 / Math.PI);
1149
+ hudElement.style.transform = `translate(-50%, -50%) rotate(${-rollDegrees}deg)`;
1150
+
1151
+ // 피치 래더 업데이트 - 수정된 부분
1152
+ const pitchLadder = document.getElementById('pitchLadder');
1153
+ if (pitchLadder) {
1154
+ const pitchDegrees = this.fighter.rotation.x * (180 / Math.PI);
1155
+ // 수정: 음수를 곱해서 반대 방향으로, 10도당 20픽셀
1156
+ const pitchOffset = -pitchDegrees * 2;
1157
+ pitchLadder.style.transform = `translateY(${pitchOffset}px)`;
1158
+ }
1159
+
1160
+ // 비행 정보 업데이트
1161
+ const speedKnots = Math.round(this.fighter.speed * 1.94384);
1162
+ const altitudeMeters = Math.round(this.fighter.altitude);
1163
+ const pitchDegrees = Math.round(this.fighter.rotation.x * (180 / Math.PI));
1164
+ const rollDegreesRounded = Math.round(rollDegrees);
1165
+ const headingDegrees = Math.round(((this.fighter.rotation.y * (180 / Math.PI)) + 360) % 360);
1166
+
1167
+ // 선회율 계산 (도/초)
1168
+ if (!this.lastHeading) this.lastHeading = headingDegrees;
1169
+ let turnRate = (headingDegrees - this.lastHeading);
1170
+ if (turnRate > 180) turnRate -= 360;
1171
+ if (turnRate < -180) turnRate += 360;
1172
+ this.lastHeading = headingDegrees;
1173
+ const turnRateDegPerSec = Math.round(turnRate / (1/60)); // 60 FPS 기준
1174
+
1175
+ // HUD 정보 업데이트
1176
+ const hudSpeed = document.getElementById('hudSpeed');
1177
+ const hudAltitude = document.getElementById('hudAltitude');
1178
+ const hudHeading = document.getElementById('hudHeading');
1179
+ const hudPitch = document.getElementById('hudPitch');
1180
+ const hudRoll = document.getElementById('hudRoll');
1181
+ const hudTurnRate = document.getElementById('hudTurnRate');
1182
+
1183
+ if (hudSpeed) hudSpeed.textContent = `SPD: ${speedKnots} KT`;
1184
+ if (hudAltitude) hudAltitude.textContent = `ALT: ${altitudeMeters} M`;
1185
+ if (hudHeading) hudHeading.textContent = `HDG: ${String(headingDegrees).padStart(3, '0')}°`;
1186
+ if (hudPitch) hudPitch.textContent = `PITCH: ${pitchDegrees}°`;
1187
+ if (hudRoll) hudRoll.textContent = `ROLL: ${rollDegreesRounded}°`;
1188
+ if (hudTurnRate) hudTurnRate.textContent = `TURN: ${Math.abs(turnRateDegPerSec) > 1 ? turnRateDegPerSec : 0}°/s`;
1189
+
1190
+ // 적 타겟 마커 업데이트
1191
+ const targetMarkers = document.getElementById('targetMarkers');
1192
+ if (targetMarkers) {
1193
+ targetMarkers.innerHTML = '';
1194
+
1195
+ // 모든 적에 대해 처리
1196
+ this.enemies.forEach(enemy => {
1197
+ if (!enemy.mesh || !enemy.isLoaded) return;
1198
+
1199
+ const distance = this.fighter.position.distanceTo(enemy.position);
1200
+ if (distance > 10000) return; // 10km 이상은 표시하지 않음
1201
+
1202
+ // 적의 화면 좌표 계산
1203
+ const enemyScreenPos = this.getScreenPosition(enemy.position);
1204
+ if (!enemyScreenPos) return;
1205
+
1206
+ // 화면 중앙으로부터의 거리 계산
1207
+ const centerX = window.innerWidth / 2;
1208
+ const centerY = window.innerHeight / 2;
1209
+ const distFromCenter = Math.sqrt(
1210
+ Math.pow(enemyScreenPos.x - centerX, 2) +
1211
+ Math.pow(enemyScreenPos.y - centerY, 2)
1212
+ );
1213
+
1214
+ // 크로스헤어 반경 (75px)
1215
+ const crosshairRadius = 75;
1216
+ const isInCrosshair = distFromCenter < crosshairRadius;
1217
+
1218
+ // 타겟 마커 생성
1219
+ const marker = document.createElement('div');
1220
+ marker.className = 'target-marker';
1221
+
1222
+ if (isInCrosshair) {
1223
+ marker.classList.add('in-crosshair');
1224
 
1225
+ // 2000m 이내면 락온
1226
+ if (distance < 2000) {
1227
+ marker.classList.add('locked');
 
 
 
 
 
 
 
 
 
 
1228
  }
 
1229
 
1230
+ // 타겟 박스 추가
1231
+ const targetBox = document.createElement('div');
1232
+ targetBox.className = 'target-box';
1233
+ marker.appendChild(targetBox);
 
 
 
 
1234
 
1235
+ // 거리 정보 추가
1236
+ const targetInfo = document.createElement('div');
1237
+ targetInfo.className = 'target-info';
1238
+ targetInfo.textContent = `${Math.round(distance)}m`;
1239
+ marker.appendChild(targetInfo);
 
 
 
1240
  }
1241
+
1242
+ marker.style.left = `${enemyScreenPos.x}px`;
1243
+ marker.style.top = `${enemyScreenPos.y}px`;
1244
+
1245
+ targetMarkers.appendChild(marker);
1246
+ });
1247
+ }
1248
+ }
1249
+
1250
+ getScreenPosition(worldPosition) {
1251
+ // 3D 좌표를 화면 좌표로 변환
1252
+ const vector = worldPosition.clone();
1253
+ vector.project(this.camera);
1254
+
1255
+ // 카메라 뒤에 있는 객체는 표시하지 않음
1256
+ if (vector.z > 1) return null;
1257
 
1258
+ const x = (vector.x * 0.5 + 0.5) * window.innerWidth;
1259
+ const y = (-vector.y * 0.5 + 0.5) * window.innerHeight;
1260
+
1261
+ return { x, y };
1262
+ }
1263
+
1264
+ updateWarnings() {
1265
+ // 기존 경고 메시지 제거
1266
+ const existingWarnings = document.querySelectorAll('.warning-message');
1267
+ existingWarnings.forEach(w => w.remove());
1268
+
1269
+ // 스톨 탈출 경고 제거
1270
+ const existingStallWarnings = document.querySelectorAll('.stall-escape-warning, .stall-escape-progress');
1271
+ existingStallWarnings.forEach(w => w.remove());
1272
+
1273
+ if (this.fighter.warningBlinkState) {
1274
+ const warningContainer = document.createElement('div');
1275
+ warningContainer.className = 'warning-message';
1276
+ warningContainer.style.cssText = `
1277
+ position: fixed;
1278
+ top: 30%;
1279
+ left: 50%;
1280
+ transform: translateX(-50%);
1281
+ color: #ff0000;
1282
+ font-size: 24px;
1283
+ font-weight: bold;
1284
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
1285
+ z-index: 1500;
1286
+ text-align: center;
1287
+ `;
1288
 
1289
+ let warningText = '';
 
1290
 
1291
+ if (this.fighter.altitude < 250) {
1292
+ warningText += 'PULL UP! PULL UP!\n';
1293
+ } else if (this.fighter.altitude < 500) {
1294
+ warningText += 'LOW ALTITUDE WARNING\n';
1295
+ }
1296
+
1297
+ if (this.fighter.altitudeWarning) {
1298
+ warningText += 'ALTITUDE LIMIT\n';
1299
+ }
1300
+
1301
+ if (this.fighter.stallWarning) {
1302
+ warningText += 'STALL WARNING\n';
1303
+ }
1304
+
1305
+ if (this.fighter.overG) {
1306
+ warningText += 'OVER-G! OVER-G!\n';
1307
+ }
1308
+
1309
+ if (warningText) {
1310
+ warningContainer.innerHTML = warningText.replace(/\n/g, '<br>');
1311
+ document.body.appendChild(warningContainer);
1312
+ }
1313
+ }
1314
+
1315
+ // 스톨 상태일 때만 "Press F to Escape" 경고 표시
1316
+ if (this.fighter.stallWarning) {
1317
+ const stallEscapeWarning = document.createElement('div');
1318
+ stallEscapeWarning.className = 'stall-escape-warning';
1319
+ stallEscapeWarning.style.cssText = `
1320
+ position: fixed;
1321
+ bottom: 100px;
1322
+ left: 50%;
1323
+ transform: translateX(-50%);
1324
+ background: rgba(255, 0, 0, 0.8);
1325
+ color: #ffffff;
1326
+ font-size: 28px;
1327
+ font-weight: bold;
1328
+ padding: 15px 30px;
1329
+ border: 3px solid #ff0000;
1330
+ border-radius: 10px;
1331
+ z-index: 1600;
1332
+ text-align: center;
1333
+ animation: blink 0.5s infinite;
1334
+ `;
1335
+ stallEscapeWarning.innerHTML = 'PRESS F TO ESCAPE';
1336
+ document.body.appendChild(stallEscapeWarning);
1337
+
1338
+ // 애니메이션 스타일 추가
1339
+ if (!document.getElementById('blinkAnimation')) {
1340
+ const style = document.createElement('style');
1341
+ style.id = 'blinkAnimation';
1342
+ style.innerHTML = `
1343
+ @keyframes blink {
1344
+ 0%, 50% { opacity: 1; }
1345
+ 51%, 100% { opacity: 0.3; }
1346
  }
1347
+ `;
1348
+ document.head.appendChild(style);
1349
  }
1350
  }
1351
 
1352
+ // Over-G 3초 이상일 때 시야 흐림 효과
1353
+ if (this.fighter.overG && this.fighter.overGTimer > 3.0) {
1354
+ let blurEffect = document.getElementById('overGBlurEffect');
1355
+ if (!blurEffect) {
1356
+ blurEffect = document.createElement('div');
1357
+ blurEffect.id = 'overGBlurEffect';
1358
+ document.body.appendChild(blurEffect);
1359
  }
1360
 
1361
+ // Over-G 지속 시간에 따라 흐림 정도 증가
1362
+ const blurIntensity = Math.min((this.fighter.overGTimer - 3.0) * 0.3, 0.8);
1363
+ blurEffect.style.cssText = `
1364
+ position: fixed;
1365
+ top: 0;
1366
+ left: 0;
1367
+ width: 100%;
1368
+ height: 100%;
1369
+ background: radial-gradient(ellipse at center,
1370
+ transparent 20%,
1371
+ rgba(0, 0, 0, ${blurIntensity}) 50%,
1372
+ rgba(0, 0, 0, ${blurIntensity + 0.2}) 100%);
1373
+ pointer-events: none;
1374
+ z-index: 1400;
1375
+ `;
1376
+ } else {
1377
+ // Over-G 상태가 아니면 효과 제거
1378
+ const blurEffect = document.getElementById('overGBlurEffect');
1379
+ if (blurEffect) {
1380
+ blurEffect.remove();
1381
+ }
1382
+ }
1383
+ }
1384
+
1385
+ updateRadar() {
1386
+ const radar = document.getElementById('radar');
1387
+ if (!radar) return;
1388
+
1389
+ const oldDots = radar.getElementsByClassName('enemy-dot');
1390
+ while (oldDots[0]) {
1391
+ oldDots[0].remove();
1392
+ }
1393
+
1394
+ const radarCenter = { x: 100, y: 100 };
1395
+ const radarRange = 10000;
1396
+
1397
+ this.enemies.forEach(enemy => {
1398
+ if (!enemy.mesh || !enemy.isLoaded) return;
1399
 
1400
+ const distance = this.fighter.position.distanceTo(enemy.position);
1401
+ if (distance <= radarRange) {
1402
+ const relativePos = enemy.position.clone().sub(this.fighter.position);
1403
+ const angle = Math.atan2(relativePos.x, relativePos.z);
1404
+ const relativeDistance = distance / radarRange;
1405
+
1406
+ const dotX = radarCenter.x + Math.sin(angle) * (radarCenter.x * relativeDistance);
1407
+ const dotY = radarCenter.y + Math.cos(angle) * (radarCenter.y * relativeDistance);
1408
+
1409
+ const dot = document.createElement('div');
1410
+ dot.className = 'enemy-dot';
1411
+ dot.style.left = `${dotX}px`;
1412
+ dot.style.top = `${dotY}px`;
1413
+ radar.appendChild(dot);
1414
  }
1415
+ });
1416
+ }
1417
+
1418
+ checkCollisions() {
1419
+ for (let i = this.fighter.bullets.length - 1; i >= 0; i--) {
1420
+ const bullet = this.fighter.bullets[i];
1421
 
1422
+ for (let j = this.enemies.length - 1; j >= 0; j--) {
1423
+ const enemy = this.enemies[j];
1424
+ if (!enemy.mesh || !enemy.isLoaded) continue;
1425
+
1426
+ const distance = bullet.position.distanceTo(enemy.position);
1427
+ if (distance < 25) {
1428
+ this.scene.remove(bullet);
1429
+ this.fighter.bullets.splice(i, 1);
1430
+
1431
+ if (enemy.takeDamage(30)) {
1432
+ enemy.destroy();
1433
+ this.enemies.splice(j, 1);
1434
+ this.score += 100;
1435
+ }
1436
+ break;
1437
+ }
1438
+ }
1439
+ }
1440
+
1441
+ this.enemies.forEach(enemy => {
1442
+ enemy.bullets.forEach((bullet, index) => {
1443
+ const distance = bullet.position.distanceTo(this.fighter.position);
1444
+ if (distance < 30) {
1445
+ this.scene.remove(bullet);
1446
+ enemy.bullets.splice(index, 1);
1447
+
1448
+ if (this.fighter.takeDamage(20)) {
1449
+ this.endGame(false);
1450
+ }
1451
+ }
1452
+ });
1453
+ });
1454
+ }
1455
+
1456
+ animate() {
1457
+ if (this.isGameOver) return;
1458
+
1459
+ this.animationFrameId = requestAnimationFrame(() => this.animate());
1460
+
1461
+ const currentTime = performance.now();
1462
+ const deltaTime = Math.min((currentTime - this.lastTime) / 1000, 0.1);
1463
+ this.lastTime = currentTime;
1464
+
1465
+ if (this.isLoaded && this.fighter.isLoaded) {
1466
+ // F키 상태를 Fighter에 전달
1467
+ this.fighter.escapeKeyPressed = this.keys.f;
1468
+
1469
+ this.fighter.updateControls(this.keys, deltaTime);
1470
+ this.fighter.updatePhysics(deltaTime);
1471
+ this.fighter.updateBullets(this.scene, deltaTime);
1472
+
1473
+ if (this.isStarted) {
1474
+ this.enemies.forEach(enemy => {
1475
+ enemy.update(this.fighter.position, deltaTime);
1476
+ });
1477
+
1478
+ this.checkCollisions();
1479
+
1480
+ if (this.fighter.health <= 0 && this.fighter.position.y <= 0) {
1481
+ this.endGame(false, "GROUND COLLISION");
1482
+ return;
1483
+ }
1484
+
1485
+ this.updateUI();
1486
+ this.updateRadar();
1487
+
1488
+ if (this.enemies.length === 0) {
1489
+ this.endGame(true);
1490
+ }
1491
+ }
1492
+
1493
+ const targetCameraPos = this.fighter.getCameraPosition();
1494
+ const targetCameraTarget = this.fighter.getCameraTarget();
1495
 
1496
+ this.camera.position.lerp(targetCameraPos, this.fighter.cameraLag);
1497
+
1498
+ this.camera.lookAt(targetCameraTarget);
1499
+ } else {
1500
+ if (this.fighter.isLoaded) {
1501
+ const initialCameraPos = this.fighter.getCameraPosition();
1502
+ const initialTarget = this.fighter.getCameraTarget();
1503
+ this.camera.position.copy(initialCameraPos);
1504
+ this.camera.lookAt(initialTarget);
1505
+ }
1506
+ }
1507
+
1508
+ this.renderer.render(this.scene, this.camera);
1509
+ }
1510
+
1511
+ endGame(victory = false, reason = "") {
1512
+ this.isGameOver = true;
1513
+
1514
+ if (this.fighter && this.fighter.stopAllWarningAudios) {
1515
+ this.fighter.stopAllWarningAudios();
1516
  }
1517
+
1518
+ if (this.bgm) {
1519
+ this.bgm.pause();
1520
+ this.bgm = null;
1521
+ this.bgmPlaying = false;
1522
+ }
1523
+
1524
+ if (this.gameTimer) {
1525
+ clearInterval(this.gameTimer);
1526
+ }
1527
+
1528
+ document.exitPointerLock();
1529
+
1530
+ // 모든 경고 및 효과 제거
1531
+ const existingWarnings = document.querySelectorAll('.warning-message, .stall-escape-warning');
1532
+ existingWarnings.forEach(w => w.remove());
1533
+
1534
+ const blurEffect = document.getElementById('overGBlurEffect');
1535
+ if (blurEffect) {
1536
+ blurEffect.remove();
1537
+ }
1538
+
1539
+ const gameOverDiv = document.createElement('div');
1540
+ gameOverDiv.className = 'start-screen';
1541
+ gameOverDiv.style.display = 'flex';
1542
+ gameOverDiv.innerHTML = `
1543
+ <h1 style="color: ${victory ? '#0f0' : '#f00'}; font-size: 48px;">
1544
+ ${victory ? 'MISSION ACCOMPLISHED!' : 'SHOT DOWN!'}
1545
+ </h1>
1546
+ ${reason ? `<div style="color: #ff0000; font-size: 20px; margin: 10px 0;">${reason}</div>` : ''}
1547
+ <div style="color: #0f0; font-size: 24px; margin: 20px 0;">
1548
+ Final Score: ${this.score}<br>
1549
+ Enemies Destroyed: ${GAME_CONSTANTS.ENEMY_COUNT - this.enemies.length}<br>
1550
+ Mission Time: ${GAME_CONSTANTS.MISSION_DURATION - this.gameTime}s
1551
+ </div>
1552
+ <button class="start-button" onclick="location.reload()">
1553
+ New Mission
1554
+ </button>
1555
+ `;
1556
+
1557
+ document.body.appendChild(gameOverDiv);
1558
+ }
1559
+ }
1560
+
1561
+ // 전역 함수 및 이벤트
1562
+ window.gameInstance = null;
1563
+
1564
+ window.startGame = function() {
1565
+ if (!window.gameInstance || !window.gameInstance.isLoaded || !window.gameInstance.isBGMReady) {
1566
+ console.log('게임이 아직 준비되지 않았습니다...');
1567
+ return;
1568
+ }
1569
+
1570
+ gameStarted = true;
1571
+ document.getElementById('startScreen').style.display = 'none';
1572
+
1573
+ document.body.requestPointerLock();
1574
+
1575
+ window.gameInstance.startBGM();
1576
+ window.gameInstance.startGame();
1577
+ }
1578
+
1579
+ function showPointerLockNotification() {
1580
+ const existingNotification = document.getElementById('pointerLockNotification');
1581
+ if (existingNotification) {
1582
+ existingNotification.remove();
1583
+ }
1584
+
1585
+ const notification = document.createElement('div');
1586
+ notification.id = 'pointerLockNotification';
1587
+ notification.innerHTML = 'Click to resume control';
1588
+ notification.style.cssText = `
1589
+ position: fixed;
1590
+ top: 50%;
1591
+ left: 50%;
1592
+ transform: translate(-50%, -50%);
1593
+ background: rgba(0, 0, 0, 0.8);
1594
+ color: #00ff00;
1595
+ padding: 20px;
1596
+ border: 2px solid #00ff00;
1597
+ border-radius: 10px;
1598
+ font-size: 18px;
1599
+ z-index: 2001;
1600
+ text-align: center;
1601
+ pointer-events: none;
1602
+ `;
1603
+
1604
+ document.body.appendChild(notification);
1605
+
1606
+ const removeNotification = () => {
1607
+ if (document.pointerLockElement) {
1608
+ notification.remove();
1609
+ document.removeEventListener('pointerlockchange', removeNotification);
1610
+ }
1611
+ };
1612
+ document.addEventListener('pointerlockchange', removeNotification);
1613
+ }
1614
+
1615
+ document.addEventListener('pointerlockchange', () => {
1616
+ if (document.pointerLockElement === document.body) {
1617
+ console.log('Pointer locked');
1618
+ } else {
1619
+ console.log('Pointer unlocked');
1620
+ if (gameStarted && window.gameInstance && !window.gameInstance.isGameOver) {
1621
+ console.log('게임 중 포인터 락 해제됨 - 클릭하여 다시 잠그세요');
1622
+ showPointerLockNotification();
1623
+ }
1624
+ }
1625
+ });
1626
+
1627
+ document.addEventListener('click', (event) => {
1628
+ if (!gameStarted && !event.target.classList.contains('start-button')) {
1629
+ event.preventDefault();
1630
+ event.stopPropagation();
1631
+ return false;
1632
+ }
1633
 
1634
+ if (gameStarted && window.gameInstance && !window.gameInstance.isGameOver) {
1635
+ if (!document.pointerLockElement) {
1636
+ console.log('게임 중 클릭 - 포인터 락 재요청');
1637
+ document.body.requestPointerLock();
1638
+ }
1639
+ else if (window.gameInstance.fighter.isLoaded) {
1640
+ window.gameInstance.fighter.shoot(window.gameInstance.scene);
1641
+ }
1642
+ }
1643
+ }, true);
1644
+
1645
+ window.addEventListener('gameReady', () => {
1646
+ gameCanStart = true;
1647
+ });
1648
+
1649
+ document.addEventListener('DOMContentLoaded', () => {
1650
+ console.log('전투기 시뮬레이터 초기화 중...');
1651
+ window.gameInstance = new Game();
1652
+ });