cutechicken commited on
Commit
144ca52
·
verified ·
1 Parent(s): 6397340

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +817 -1590
index.html CHANGED
@@ -1,1652 +1,879 @@
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
- });
 
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>