cutechicken commited on
Commit
916e77f
ยท
verified ยท
1 Parent(s): 280b38d

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +895 -350
index.html CHANGED
@@ -1,399 +1,944 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <script src="https://cdnjs.cloudflare.com/ajax/libs/stats.js/r17/Stats.min.js"></script>
7
- <title>JET FIGHT SIMULATER - FPS Mode</title>
8
- <style>
9
- body {
10
- margin: 0;
11
- overflow: hidden;
12
- background: #000;
13
- font-family: 'Courier New', monospace;
14
- }
15
- #enemyLabels {
16
- position: absolute;
17
- top: 0;
18
- left: 0;
19
- width: 100%;
20
- height: 100%;
21
- pointer-events: none;
22
- }
 
 
23
 
24
- .enemy-label {
25
- position: absolute;
26
- background-color: rgba(255, 0, 0, 0.7);
27
- color: white;
28
- padding: 2px 6px;
29
- border-radius: 3px;
30
- font-size: 12px;
31
- font-family: Arial, sans-serif;
32
- transform: translate(-50%, -50%);
33
- white-space: nowrap;
34
- }
35
- #loading {
36
- position: fixed;
37
- top: 50%;
38
- left: 50%;
39
- transform: translate(-50%, -50%);
40
- background: rgba(0,0,0,0.8);
41
- padding: 20px;
42
- border-radius: 10px;
43
- z-index: 2000;
44
- text-align: center;
45
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
- .loading-spinner {
48
- width: 50px;
49
- height: 50px;
50
- border: 5px solid #0f0;
51
- border-top: 5px solid transparent;
52
- border-radius: 50%;
53
- animation: spin 1s linear infinite;
54
- margin: 0 auto 20px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
- @keyframes spin {
58
- 0% { transform: rotate(0deg); }
59
- 100% { transform: rotate(360deg); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
- .loading-text {
63
- color: #0f0;
64
- font-size: 24px;
65
- text-align: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
 
68
- #gameContainer {
69
- position: relative;
70
- width: 100vw;
71
- height: 100vh;
72
- cursor: crosshair;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  }
 
74
 
75
- #info {
76
- position: absolute;
77
- top: 10px;
78
- left: 10px;
79
- color: #0f0;
80
- background: rgba(0,20,0,0.7);
81
- padding: 10px;
82
- font-size: 14px;
83
- z-index: 1001;
84
- border: 1px solid #0f0;
85
- border-radius: 5px;
86
- user-select: none;
 
87
  }
 
88
 
89
- #crosshair {
90
- position: fixed;
91
- top: 25%;
92
- left: 50%;
93
- transform: translate(-50%, -50%);
94
- width: 40px;
95
- height: 40px;
96
- border: 2px solid #00ff00;
97
- border-radius: 50%;
98
- z-index: 1001;
99
- pointer-events: none;
100
- transition: all 0.2s ease;
101
- }
102
 
103
- /* ๋ฌผ์ฒด๊ฐ€ ๊ฐ์ง€๋˜์—ˆ์„ ๋•Œ์˜ ์Šคํƒ€์ผ */
104
- #crosshair.target-detected {
105
- border-color: #ff0000;
106
- border-width: 3px;
107
- box-shadow: 0 0 10px rgba(255, 0, 0, 0.5);
108
- }
 
 
 
 
 
 
 
109
 
110
- #crosshair::before,
111
- #crosshair::after {
112
- content: '';
113
- position: absolute;
114
- background: #00ff00;
115
- transition: all 0.2s ease;
116
  }
117
 
118
- #crosshair::before {
119
- top: 50%;
120
- left: -10px;
121
- right: -10px;
122
- height: 2px;
123
- transform: translateY(-50%);
124
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
- #crosshair::after {
127
- left: 50%;
128
- top: -10px;
129
- bottom: -10px;
130
- width: 2px;
131
- transform: translateX(-50%);
132
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
- /* ๋ฌผ์ฒด๊ฐ€ ๊ฐ์ง€๋˜์—ˆ์„ ๋•Œ์˜ ํฌ๋กœ์Šค ๋ผ์ธ ์Šคํƒ€์ผ */
135
- #crosshair.target-detected::before,
136
- #crosshair.target-detected::after {
137
- background: #ff0000;
138
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
 
140
- #healthBar {
141
- position: absolute;
142
- bottom: 20px;
143
- left: 20px;
144
- width: 200px;
145
- height: 20px;
146
- background: rgba(0,20,0,0.7);
147
- border: 2px solid #0f0;
148
- z-index: 1001;
149
- border-radius: 10px;
150
- overflow: hidden;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  }
152
-
153
- #health {
154
- width: 100%;
155
- height: 100%;
156
- background: linear-gradient(90deg, #0f0, #00ff00);
157
- transition: width 0.3s;
 
158
  }
159
-
160
- #ammo {
161
- position: absolute;
162
- bottom: 20px;
163
- right: 20px;
164
- color: #0f0;
165
- background: rgba(0,20,0,0.7);
166
- padding: 10px;
167
- font-size: 20px;
168
- z-index: 1001;
169
- border: 1px solid #0f0;
170
- border-radius: 5px;
171
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
 
173
-
174
- #gameTitle {
175
- position: absolute;
176
- top: 10px;
177
- left: 50%;
178
- transform: translateX(-50%);
179
- color: #0f0;
180
- background: rgba(0,20,0,0.7);
181
- padding: 10px 20px;
182
- font-size: 20px;
183
- z-index: 1001;
184
- border: 1px solid #0f0;
185
- border-radius: 5px;
186
- text-transform: uppercase;
187
- letter-spacing: 2px;
188
  }
189
- #ammoDisplay {
190
- position: absolute;
191
- bottom: 20px;
192
- right: 20px;
193
- color: #0f0;
194
- background: rgba(0,20,0,0.7);
195
- padding: 10px;
196
- font-size: 20px;
197
- z-index: 1001;
198
- border: 1px solid #0f0;
199
- border-radius: 5px;
 
 
200
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
 
202
- #reloadingText {
203
- position: absolute;
204
- top: 50%;
205
- left: 50%;
206
- transform: translate(-50%, -50%);
207
- color: #ff0000;
208
- font-size: 24px;
209
- font-weight: bold;
210
- display: none;
211
- z-index: 1002;
212
- }
213
 
214
- #radar {
215
- position: absolute;
216
- bottom: 60px; /* ์ฒด๋ ฅ๋ฐ” ์œ„๋กœ ์ด๋™ */
217
- left: 20px;
218
- width: 200px;
219
- height: 200px;
220
- background: rgba(0,20,0,0.3);
221
- border: 2px solid #0f0;
222
- border-radius: 50%;
223
- z-index: 1001;
224
- overflow: hidden;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  }
226
-
227
- #mission {
228
- position: absolute;
229
- top: 10px;
230
- left: 10px;
231
- color: #0f0;
232
- background: rgba(0,20,0,0.7);
233
- padding: 10px;
234
- font-size: 16px;
235
- z-index: 1001;
236
- border: 1px solid #0f0;
237
- border-radius: 5px;
 
 
 
 
238
  }
 
 
 
 
 
 
239
 
240
- #radarLine {
241
- position: absolute;
242
- top: 50%;
243
- left: 50%;
244
- width: 50%;
245
- height: 2px;
246
- background: #0f0;
247
- transform-origin: left center;
248
- animation: radar-sweep 4s infinite linear;
249
- }
250
 
251
- .enemy-dot {
252
- position: absolute;
253
- width: 6px;
254
- height: 6px;
255
- background: #ff0000;
256
- border-radius: 50%;
257
- transform: translate(-50%, -50%);
258
- }
 
 
 
 
259
 
260
- @keyframes radar-sweep {
261
- from {
262
- transform: rotate(0deg);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
  }
264
- to {
265
- transform: rotate(360deg);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  }
267
- }
 
 
 
 
 
 
 
 
268
 
269
- #gameStats {
270
- position: absolute;
271
- top: 10px;
272
- right: 20px;
273
- color: #0f0;
274
- background: rgba(0,20,0,0.7);
275
- padding: 10px;
276
- font-size: 16px;
277
- z-index: 1001;
278
- border: 1px solid #0f0;
279
- border-radius: 5px;
280
- text-align: right;
281
  }
 
 
 
 
 
 
 
282
 
283
- .start-screen {
284
- position: fixed;
285
- top: 0;
286
- left: 0;
287
- width: 100%;
288
- height: 100%;
289
- background: rgba(0,0,0,0.8);
290
- display: flex;
291
- justify-content: center;
292
- align-items: center;
293
- flex-direction: column;
294
- z-index: 2000;
295
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
296
 
297
- .start-button {
298
- padding: 15px 30px;
299
- font-size: 24px;
300
- background: #0f0;
301
- color: #000;
302
- border: none;
303
- border-radius: 5px;
304
- cursor: pointer;
305
- margin-top: 20px;
306
- transition: transform 0.2s;
 
 
 
 
 
 
 
 
 
 
 
 
307
  }
 
 
 
 
 
308
 
309
- .start-button:hover {
310
- transform: scale(1.1);
 
311
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
 
313
- #minimap {
314
- position: absolute;
315
- bottom: 20px;
316
- right: 20px;
317
- width: 200px;
318
- height: 200px;
319
- background: rgba(0,20,0,0.7);
320
- border: 2px solid #0f0;
321
- border-radius: 5px;
322
- z-index: 1001;
 
 
 
 
 
 
 
 
 
 
 
 
323
  }
324
- </style>
325
- </head>
326
- <body>
327
- <div id="loading">
328
- <div class="loading-spinner"></div>
329
- <div class="loading-text">Loading tank assets...</div>
330
- </div>
331
-
332
- <div class="start-screen" id="startScreen">
333
- <h1 style="color: #0f0; font-size: 48px; margin-bottom: 20px;">JET FIGHT SIMULATER</h1>
334
- <button class="start-button" onclick="startGame()">Start Game</button>
335
- <div style="color: #0f0; margin-top: 20px; text-align: center;">
336
- <p>Controls:</p>
337
- <p>W,A,S,D - Move Tank</p>
338
- <p>Mouse - Look Around</p>
339
- <p>Left Click - Fire</p>
340
- <p>ESC - Pause</p>
341
- </div>
342
- </div>
343
 
344
-
345
- <div id="gameContainer">
346
- <div id="gameTitle">JET FIGHT SIMULATER</div>
347
- <div id="mission">MISSION: DESTROY ENEMY JET</div>
348
- <div id="gameStats">
349
- <div id="score">Score: 0</div>
350
- <div id="time">Time: 180s</div>
351
- </div>
352
- <div id="crosshair"></div>
353
- <div id="detected" style="position: absolute; top: 55%; left: 50%; transform: translate(-50%, -50%); color: red; font-size: 16px; display: none;">DETECTED</div>
354
- <div id="enemyLabels"></div>
355
-
356
- <!-- ์ฒด๋ ฅ๋ฐ” -->
357
- <div id="healthBar">
358
- <div id="health"></div>
359
- </div>
360
-
361
- <!-- ์ƒˆ๋กœ์šด ํƒ„์•ฝ ์‹œ์Šคํ…œ -->
362
- <div id="ammoDisplay">APFSDS: 1/1</div>
363
-
364
- <!-- ๋ฆฌ๋กœ๋”ฉ ํ…์ŠคํŠธ -->
365
- <div id="reloadingText">RELOADING...</div>
366
-
367
- <!-- ๋ ˆ์ด๋” -->
368
- <div id="radar">
369
- <div id="radarLine"></div>
370
- </div>
371
- </div>
372
-
373
- <script type="importmap">
374
- {
375
- "imports": {
376
- "three": "https://unpkg.com/[email protected]/build/three.module.js",
377
- "three/addons/": "https://unpkg.com/[email protected]/examples/jsm/"
378
  }
 
 
 
 
 
 
 
379
  }
380
- </script>
381
- <script>
382
- function startGame() {
383
- document.getElementById('startScreen').style.display = 'none';
384
- // ์—ฌ๊ธฐ์— ๊ฒŒ์ž„ ์‹œ์ž‘ ๋กœ์ง ์ถ”๊ฐ€
385
- document.body.requestPointerLock();
 
 
 
 
 
 
 
 
 
386
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
 
388
- // ํฌ์ธํ„ฐ ๋ฝ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ
389
- document.addEventListener('pointerlockchange', () => {
390
- if (document.pointerLockElement === document.body) {
391
- console.log('Pointer locked');
392
- } else {
393
- console.log('Pointer unlocked');
394
- }
395
- });
396
- </script>
397
- <script type="module" src="game.js"></script>
398
- </body>
399
- </html>
 
 
 
 
 
 
 
 
 
1
+ import * as THREE from 'three';
2
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
3
+
4
+ // ๊ฒŒ์ž„ ์ƒ์ˆ˜
5
+ const GAME_CONSTANTS = {
6
+ MISSION_DURATION: 180,
7
+ MAP_SIZE: 15000,
8
+ MAX_ALTITUDE: 12000,
9
+ MIN_ALTITUDE: 100,
10
+ MAX_SPEED: 800,
11
+ STALL_SPEED: 120,
12
+ GRAVITY: 9.8,
13
+ MOUSE_SENSITIVITY: 0.001, // ๋งˆ์šฐ์Šค ๊ฐ๋„ ๋‚ฎ์ถค
14
+ MAX_G_FORCE: 12.0,
15
+ ENEMY_COUNT: 4,
16
+ MISSILE_COUNT: 6,
17
+ AMMO_COUNT: 300
18
+ };
19
+
20
+ // ์ „ํˆฌ๊ธฐ ํด๋ž˜์Šค
21
+ class Fighter {
22
+ constructor() {
23
+ this.mesh = null;
24
+ this.isLoaded = false;
25
 
26
+ // ๋ฌผ๋ฆฌ ์†์„ฑ
27
+ this.position = new THREE.Vector3(0, 2000, 0);
28
+ this.velocity = new THREE.Vector3(0, 0, 150); // ์ดˆ๊ธฐ ์ „์ง„ ์†๋„
29
+ this.acceleration = new THREE.Vector3(0, 0, 0);
30
+ this.rotation = new THREE.Euler(0, 0, 0);
31
+
32
+ // ๋น„ํ–‰ ์ œ์–ด (์›Œ์ฌ๋” ์Šคํƒ€์ผ)
33
+ this.baseSpeed = 150; // ๊ธฐ๋ณธ ์†๋„
34
+ this.throttle = 0.5; // ๊ธฐ๋ณธ ์Šค๋กœํ‹€ 50%
35
+ this.speed = 150;
36
+ this.altitude = 2000;
37
+ this.gForce = 1.0;
38
+ this.health = 100;
39
+
40
+ // ์กฐ์ข… ์ž…๋ ฅ ์‹œ์Šคํ…œ
41
+ this.pitchInput = 0; // ๋งˆ์šฐ์Šค Y -> ๊ธฐ์ˆ˜ ์ƒํ•˜
42
+ this.rollInput = 0; // ๋งˆ์šฐ์Šค X -> ์ขŒ์šฐ ํšŒ์ „ (๋กค)
43
+ this.yawInput = 0; // A/D ํ‚ค -> ๋Ÿฌ๋”
44
+
45
+ // ๋งˆ์šฐ์Šค ๋ˆ„์  ์ž…๋ ฅ
46
+ this.mousePitch = 0;
47
+ this.mouseRoll = 0;
48
+
49
+ // ๋ถ€๋“œ๋Ÿฌ์šด ํšŒ์ „์„ ์œ„ํ•œ ๋ชฉํ‘œ๊ฐ’
50
+ this.targetPitch = 0;
51
+ this.targetRoll = 0;
52
+ this.targetYaw = 0;
53
+
54
+ // ๋ฌด๊ธฐ
55
+ this.missiles = GAME_CONSTANTS.MISSILE_COUNT;
56
+ this.ammo = GAME_CONSTANTS.AMMO_COUNT;
57
+ this.bullets = [];
58
+ this.lastShootTime = 0;
59
+
60
+ // ์นด๋ฉ”๋ผ ์„ค์ •
61
+ this.cameraDistance = 40;
62
+ this.cameraHeight = 10;
63
+ this.cameraLag = 0.08; // ์นด๋ฉ”๋ผ ๋”ฐ๋ผ์˜ค๋Š” ์†๋„
64
+ }
65
 
66
+ async initialize(scene, loader) {
67
+ try {
68
+ const result = await loader.loadAsync('models/f-15.glb');
69
+ this.mesh = result.scene;
70
+ this.mesh.position.copy(this.position);
71
+ this.mesh.scale.set(2, 2, 2);
72
+
73
+ // ๊ทธ๋ฆผ์ž ์„ค์ •
74
+ this.mesh.traverse((child) => {
75
+ if (child.isMesh) {
76
+ child.castShadow = true;
77
+ child.receiveShadow = true;
78
+ }
79
+ });
80
+
81
+ scene.add(this.mesh);
82
+ this.isLoaded = true;
83
+
84
+ console.log('F-15 ์ „ํˆฌ๊ธฐ ๋กœ๋”ฉ ์™„๋ฃŒ');
85
+ } catch (error) {
86
+ console.error('F-15 ๋ชจ๋ธ ๋กœ๋”ฉ ์‹คํŒจ:', error);
87
+ this.createFallbackModel(scene);
88
  }
89
+ }
90
+
91
+ createFallbackModel(scene) {
92
+ const group = new THREE.Group();
93
+
94
+ // ๋™์ฒด
95
+ const fuselageGeometry = new THREE.CylinderGeometry(0.8, 1.5, 12, 8);
96
+ const fuselageMaterial = new THREE.MeshLambertMaterial({ color: 0x606060 });
97
+ const fuselage = new THREE.Mesh(fuselageGeometry, fuselageMaterial);
98
+ fuselage.rotation.x = Math.PI / 2;
99
+ group.add(fuselage);
100
+
101
+ // ์ฃผ์ต
102
+ const wingGeometry = new THREE.BoxGeometry(16, 0.3, 4);
103
+ const wingMaterial = new THREE.MeshLambertMaterial({ color: 0x404040 });
104
+ const wings = new THREE.Mesh(wingGeometry, wingMaterial);
105
+ wings.position.z = -1;
106
+ group.add(wings);
107
+
108
+ // ์ˆ˜์ง ์•ˆ์ •ํŒ
109
+ const tailGeometry = new THREE.BoxGeometry(0.3, 4, 3);
110
+ const tailMaterial = new THREE.MeshLambertMaterial({ color: 0x404040 });
111
+ const tail = new THREE.Mesh(tailGeometry, tailMaterial);
112
+ tail.position.z = -5;
113
+ tail.position.y = 1.5;
114
+ group.add(tail);
115
+
116
+ // ์ˆ˜ํ‰ ์•ˆ์ •ํŒ
117
+ const horizontalTailGeometry = new THREE.BoxGeometry(6, 0.2, 2);
118
+ const horizontalTail = new THREE.Mesh(horizontalTailGeometry, tailMaterial);
119
+ horizontalTail.position.z = -5;
120
+ horizontalTail.position.y = 0.5;
121
+ group.add(horizontalTail);
122
+
123
+ this.mesh = group;
124
+ this.mesh.position.copy(this.position);
125
+ this.mesh.scale.set(2, 2, 2);
126
+ scene.add(this.mesh);
127
+ this.isLoaded = true;
128
+
129
+ console.log('Fallback ์ „ํˆฌ๊ธฐ ๋ชจ๋ธ ์ƒ์„ฑ ์™„๋ฃŒ');
130
+ }
131
 
132
+ // ์›Œ์ฌ๋” ์Šคํƒ€์ผ ๋งˆ์šฐ์Šค ์ž…๋ ฅ ์ฒ˜๋ฆฌ
133
+ updateMouseInput(deltaX, deltaY) {
134
+ // ๋งˆ์šฐ์Šค X = ๋กค (์ขŒ์šฐ ๊ธฐ์šธ๊ธฐ)
135
+ // ๋งˆ์šฐ์Šค Y = ํ”ผ์น˜ (๊ธฐ์ˆ˜ ์ƒํ•˜)
136
+ this.mouseRoll += deltaX * GAME_CONSTANTS.MOUSE_SENSITIVITY;
137
+ this.mousePitch += deltaY * GAME_CONSTANTS.MOUSE_SENSITIVITY;
138
+
139
+ // ์ž…๋ ฅ ์ œํ•œ (ยฑ1)
140
+ this.mouseRoll = Math.max(-1, Math.min(1, this.mouseRoll));
141
+ this.mousePitch = Math.max(-1, Math.min(1, this.mousePitch));
142
+ }
143
+
144
+ // ํ‚ค๋ณด๋“œ ์ž…๋ ฅ ์ฒ˜๋ฆฌ
145
+ updateControls(keys, deltaTime) {
146
+ // W/S: ์Šค๋กœํ‹€ ์กฐ์ ˆ
147
+ if (keys.w) {
148
+ this.throttle = Math.min(1.0, this.throttle + deltaTime * 0.8);
149
+ }
150
+ if (keys.s) {
151
+ this.throttle = Math.max(0.1, this.throttle - deltaTime * 0.8);
152
  }
153
+
154
+ // A/D: ๋Ÿฌ๋” (์š” ํšŒ์ „)
155
+ this.yawInput = 0;
156
+ if (keys.a) this.yawInput = -0.5;
157
+ if (keys.d) this.yawInput = 0.5;
158
+
159
+ // ํ˜„์žฌ ์ž…๋ ฅ์„ ์กฐ์ข… ์ž…๋ ฅ์œผ๋กœ ์„ค์ •
160
+ this.pitchInput = this.mousePitch;
161
+ this.rollInput = this.mouseRoll;
162
+
163
+ // ๋ชฉํ‘œ ํšŒ์ „๊ฐ ๊ณ„์‚ฐ (๋ถ€๋“œ๋Ÿฌ์šด ์ œ์–ด)
164
+ const maxPitchAngle = Math.PI / 6; // 30๋„
165
+ const maxRollAngle = Math.PI / 4; // 45๋„
166
+
167
+ this.targetPitch = this.pitchInput * maxPitchAngle;
168
+ this.targetRoll = this.rollInput * maxRollAngle;
169
+ this.targetYaw += this.yawInput * deltaTime * 1.5;
170
+
171
+ // ๋งˆ์šฐ์Šค ์ž…๋ ฅ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๊ฐ์‡ 
172
+ this.mouseRoll *= 0.95;
173
+ this.mousePitch *= 0.95;
174
+ }
175
 
176
+ // ๋ฌผ๋ฆฌ ์‹œ๋ฎฌ๋ ˆ์ด์…˜
177
+ updatePhysics(deltaTime) {
178
+ if (!this.mesh) return;
179
+
180
+ // ํšŒ์ „ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ๋ณด๊ฐ„
181
+ const rotationSpeed = deltaTime * 3.0;
182
+ this.rotation.x = THREE.MathUtils.lerp(this.rotation.x, this.targetPitch, rotationSpeed);
183
+ this.rotation.y = THREE.MathUtils.lerp(this.rotation.y, this.targetYaw, rotationSpeed);
184
+ this.rotation.z = THREE.MathUtils.lerp(this.rotation.z, this.targetRoll, rotationSpeed);
185
+
186
+ // ์Šค๋กœํ‹€์— ๋”ฐ๋ฅธ ์†๋„ ๊ณ„์‚ฐ
187
+ const minSpeed = 100; // ์ตœ์†Œ ์†๋„
188
+ const maxSpeed = 300; // ์ตœ๋Œ€ ์†๋„
189
+ const targetSpeed = minSpeed + (maxSpeed - minSpeed) * this.throttle;
190
+
191
+ // ์†๋„ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ๋ณ€ํ™”
192
+ this.speed = THREE.MathUtils.lerp(this.speed, targetSpeed, deltaTime * 2);
193
+
194
+ // ์ „์ง„ ๋ฐฉํ–ฅ ๋ฒกํ„ฐ ๊ณ„์‚ฐ
195
+ const forward = new THREE.Vector3(0, 0, 1);
196
+ forward.applyEuler(this.rotation);
197
+
198
+ // ์†๋„ ๋ฒกํ„ฐ ์„ค์ • (ํ•ญ์ƒ ์ „์ง„)
199
+ this.velocity = forward.multiplyScalar(this.speed);
200
+
201
+ // ์œ„์น˜ ์—…๋ฐ์ดํŠธ
202
+ this.position.add(this.velocity.clone().multiplyScalar(deltaTime));
203
+
204
+ // ๊ณ ๋„ ์ œํ•œ
205
+ if (this.position.y < GAME_CONSTANTS.MIN_ALTITUDE) {
206
+ this.position.y = GAME_CONSTANTS.MIN_ALTITUDE;
207
+ if (this.velocity.y < 0) this.velocity.y = 0;
208
+ }
209
+
210
+ if (this.position.y > GAME_CONSTANTS.MAX_ALTITUDE) {
211
+ this.position.y = GAME_CONSTANTS.MAX_ALTITUDE;
212
+ if (this.velocity.y > 0) this.velocity.y = 0;
213
  }
214
+
215
+ // ๋งต ๊ฒฝ๊ณ„ ์ˆœํ™˜ (๋ฌดํ•œ ๋งต)
216
+ const mapLimit = GAME_CONSTANTS.MAP_SIZE / 2;
217
+ if (this.position.x > mapLimit) this.position.x = -mapLimit;
218
+ if (this.position.x < -mapLimit) this.position.x = mapLimit;
219
+ if (this.position.z > mapLimit) this.position.z = -mapLimit;
220
+ if (this.position.z < -mapLimit) this.position.z = mapLimit;
221
+
222
+ // ๋ฉ”์‹œ ์—…๋ฐ์ดํŠธ
223
+ this.mesh.position.copy(this.position);
224
+ this.mesh.rotation.copy(this.rotation);
225
+
226
+ // ํ˜„์žฌ ๊ฐ’ ์—…๋ฐ์ดํŠธ
227
+ this.altitude = this.position.y;
228
+
229
+ // G-Force ๊ณ„์‚ฐ (ํšŒ์ „ ๊ฐ•๋„์— ๋”ฐ๋ผ)
230
+ const rotationIntensity = Math.abs(this.targetPitch) + Math.abs(this.targetRoll);
231
+ this.gForce = 1.0 + rotationIntensity * 3;
232
+ }
233
 
234
+ // ๋ฐœ์‚ฌ ์‹œ์Šคํ…œ
235
+ shoot(scene) {
236
+ const currentTime = Date.now();
237
+ if (currentTime - this.lastShootTime < 100 || this.ammo <= 0) return;
238
+
239
+ this.lastShootTime = currentTime;
240
+ this.ammo--;
241
+
242
+ // ์ด์•Œ ์ƒ์„ฑ
243
+ const bulletGeometry = new THREE.SphereGeometry(0.2);
244
+ const bulletMaterial = new THREE.MeshBasicMaterial({
245
+ color: 0xffff00,
246
+ emissive: 0xffff00,
247
+ emissiveIntensity: 0.7
248
+ });
249
+ const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
250
+
251
+ // ์ด๊ตฌ ์œ„์น˜ (์ „ํˆฌ๊ธฐ ์•ž์ชฝ)
252
+ const muzzleOffset = new THREE.Vector3(0, 0, 8);
253
+ muzzleOffset.applyEuler(this.rotation);
254
+ bullet.position.copy(this.position).add(muzzleOffset);
255
+
256
+ // ์ด์•Œ ์†๋„ (์ „ํˆฌ๊ธฐ ์†๋„ + ์ด์•Œ ์†๋„)
257
+ const bulletSpeed = 1000;
258
+ const direction = new THREE.Vector3(0, 0, 1);
259
+ direction.applyEuler(this.rotation);
260
+ bullet.velocity = direction.multiplyScalar(bulletSpeed).add(this.velocity);
261
+
262
+ scene.add(bullet);
263
+ this.bullets.push(bullet);
264
+
265
+ // ๋ฐœ์‚ฌ์Œ
266
+ try {
267
+ const audio = new Audio('sounds/gunfire.ogg');
268
+ audio.volume = 0.3;
269
+ audio.play();
270
+ } catch (e) {
271
+ console.log('Sound file not found');
272
  }
273
+ }
274
 
275
+ // ์ด์•Œ ์—…๋ฐ์ดํŠธ
276
+ updateBullets(scene, deltaTime) {
277
+ for (let i = this.bullets.length - 1; i >= 0; i--) {
278
+ const bullet = this.bullets[i];
279
+ bullet.position.add(bullet.velocity.clone().multiplyScalar(deltaTime));
280
+
281
+ // ์ด์•Œ ์ œ๊ฑฐ ์กฐ๊ฑด
282
+ if (bullet.position.distanceTo(this.position) > 8000 ||
283
+ bullet.position.y < 0 ||
284
+ bullet.position.y > GAME_CONSTANTS.MAX_ALTITUDE + 500) {
285
+ scene.remove(bullet);
286
+ this.bullets.splice(i, 1);
287
+ }
288
  }
289
+ }
290
 
291
+ // ํ”ผํ•ด ์‹œ์Šคํ…œ
292
+ takeDamage(damage) {
293
+ this.health -= damage;
294
+ return this.health <= 0;
295
+ }
 
 
 
 
 
 
 
 
296
 
297
+ // ์นด๋ฉ”๋ผ ์œ„์น˜ ๊ณ„์‚ฐ (์ „ํˆฌ๊ธฐ ๋’ค์ชฝ์—์„œ ์ถ”์ )
298
+ getCameraPosition() {
299
+ // ์ „ํˆฌ๊ธฐ ๋’ค์ชฝ ๋ฐฉํ–ฅ ๊ณ„์‚ฐ
300
+ const backward = new THREE.Vector3(0, 0, -1);
301
+ backward.applyEuler(this.rotation);
302
+
303
+ // ์นด๋ฉ”๋ผ ์œ„์น˜ = ์ „ํˆฌ๊ธฐ ์œ„์น˜ + ๋’ค์ชฝ ๋ฐฉํ–ฅ * ๊ฑฐ๋ฆฌ + ๋†’์ด
304
+ const cameraPos = this.position.clone()
305
+ .add(backward.multiplyScalar(this.cameraDistance))
306
+ .add(new THREE.Vector3(0, this.cameraHeight, 0));
307
+
308
+ return cameraPos;
309
+ }
310
 
311
+ // ์นด๋ฉ”๋ผ ํƒ€๊ฒŸ (์ „ํˆฌ๊ธฐ๋ฅผ ๋ฐ”๋ผ๋ด„)
312
+ getCameraTarget() {
313
+ return this.position.clone().add(new THREE.Vector3(0, 0, 0));
314
+ }
 
 
315
  }
316
 
317
+ // ์  ์ „ํˆฌ๊ธฐ ํด๋ž˜์Šค (๊ฐ„๋‹จํ•œ ๋ฒ„์ „)
318
+ class EnemyFighter {
319
+ constructor(scene, position) {
320
+ this.mesh = null;
321
+ this.isLoaded = false;
322
+ this.scene = scene;
323
+ this.position = position.clone();
324
+ this.velocity = new THREE.Vector3(0, 0, 120);
325
+ this.rotation = new THREE.Euler(0, 0, 0);
326
+ this.health = 100;
327
+ this.speed = 120;
328
+ this.bullets = [];
329
+ this.lastShootTime = 0;
330
+
331
+ // AI ์ƒํƒœ
332
+ this.aiState = 'patrol';
333
+ this.targetPosition = position.clone();
334
+ this.patrolCenter = position.clone();
335
+ this.patrolRadius = 2000;
336
+ this.lastStateChange = 0;
337
+ }
338
 
339
+ async initialize(loader) {
340
+ try {
341
+ const result = await loader.loadAsync('models/mig-29.glb');
342
+ this.mesh = result.scene;
343
+ this.mesh.position.copy(this.position);
344
+ this.mesh.scale.set(1.5, 1.5, 1.5);
345
+
346
+ this.mesh.traverse((child) => {
347
+ if (child.isMesh) {
348
+ child.castShadow = true;
349
+ child.receiveShadow = true;
350
+ }
351
+ });
352
+
353
+ this.scene.add(this.mesh);
354
+ this.isLoaded = true;
355
+
356
+ console.log('MiG-29 ์ ๊ธฐ ๋กœ๋”ฉ ์™„๋ฃŒ');
357
+ } catch (error) {
358
+ console.error('MiG-29 ๋ชจ๋ธ ๋กœ๋”ฉ ์‹คํŒจ:', error);
359
+ this.createFallbackModel();
360
+ }
361
+ }
362
 
363
+ createFallbackModel() {
364
+ const group = new THREE.Group();
365
+
366
+ const fuselageGeometry = new THREE.CylinderGeometry(0.6, 1.0, 8, 8);
367
+ const fuselageMaterial = new THREE.MeshLambertMaterial({ color: 0x800000 });
368
+ const fuselage = new THREE.Mesh(fuselageGeometry, fuselageMaterial);
369
+ fuselage.rotation.x = Math.PI / 2;
370
+ group.add(fuselage);
371
+
372
+ const wingGeometry = new THREE.BoxGeometry(12, 0.3, 3);
373
+ const wingMaterial = new THREE.MeshLambertMaterial({ color: 0x600000 });
374
+ const wings = new THREE.Mesh(wingGeometry, wingMaterial);
375
+ wings.position.z = -0.5;
376
+ group.add(wings);
377
+
378
+ this.mesh = group;
379
+ this.mesh.position.copy(this.position);
380
+ this.mesh.scale.set(1.5, 1.5, 1.5);
381
+ this.scene.add(this.mesh);
382
+ this.isLoaded = true;
383
+
384
+ console.log('Fallback ์ ๊ธฐ ๋ชจ๋ธ ์ƒ์„ฑ ์™„๋ฃŒ');
385
+ }
386
 
387
+ update(playerPosition, deltaTime) {
388
+ if (!this.mesh || !this.isLoaded) return;
389
+
390
+ const currentTime = Date.now();
391
+ const distanceToPlayer = this.position.distanceTo(playerPosition);
392
+
393
+ // ๊ฐ„๋‹จํ•œ AI: ํ”Œ๋ ˆ์ด์–ด ์ถ”์ 
394
+ if (distanceToPlayer < 5000) {
395
+ const direction = new THREE.Vector3()
396
+ .subVectors(playerPosition, this.position)
397
+ .normalize();
398
+
399
+ this.velocity = direction.multiplyScalar(this.speed);
400
+ this.rotation.y = Math.atan2(direction.x, direction.z);
401
+
402
+ // ๊ณต๊ฒฉ
403
+ if (distanceToPlayer < 2000 && currentTime - this.lastShootTime > 1500) {
404
+ this.shoot();
405
+ }
406
+ } else {
407
+ // ์ˆœ์ฐฐ
408
+ if (this.position.distanceTo(this.targetPosition) < 300) {
409
+ const angle = Math.random() * Math.PI * 2;
410
+ this.targetPosition = this.patrolCenter.clone().add(
411
+ new THREE.Vector3(
412
+ Math.cos(angle) * this.patrolRadius,
413
+ (Math.random() - 0.5) * 1000,
414
+ Math.sin(angle) * this.patrolRadius
415
+ )
416
+ );
417
+ }
418
+
419
+ const direction = new THREE.Vector3()
420
+ .subVectors(this.targetPosition, this.position)
421
+ .normalize();
422
+
423
+ this.velocity = direction.multiplyScalar(this.speed * 0.7);
424
+ this.rotation.y = Math.atan2(direction.x, direction.z);
425
  }
426
+
427
+ // ์œ„์น˜ ์—…๋ฐ์ดํŠธ
428
+ this.position.add(this.velocity.clone().multiplyScalar(deltaTime));
429
+
430
+ // ๊ณ ๋„ ์ œํ•œ
431
+ if (this.position.y < GAME_CONSTANTS.MIN_ALTITUDE) {
432
+ this.position.y = GAME_CONSTANTS.MIN_ALTITUDE;
433
  }
434
+ if (this.position.y > GAME_CONSTANTS.MAX_ALTITUDE) {
435
+ this.position.y = GAME_CONSTANTS.MAX_ALTITUDE;
 
 
 
 
 
 
 
 
 
 
436
  }
437
+
438
+ // ๋งต ๊ฒฝ๊ณ„ ์ˆœํ™˜
439
+ const mapLimit = GAME_CONSTANTS.MAP_SIZE / 2;
440
+ if (this.position.x > mapLimit) this.position.x = -mapLimit;
441
+ if (this.position.x < -mapLimit) this.position.x = mapLimit;
442
+ if (this.position.z > mapLimit) this.position.z = -mapLimit;
443
+ if (this.position.z < -mapLimit) this.position.z = mapLimit;
444
+
445
+ // ๋ฉ”์‹œ ์—…๋ฐ์ดํŠธ
446
+ this.mesh.position.copy(this.position);
447
+ this.mesh.rotation.copy(this.rotation);
448
+
449
+ // ์ด์•Œ ์—…๋ฐ์ดํŠธ
450
+ this.updateBullets(deltaTime);
451
+ }
452
+
453
+ shoot() {
454
+ this.lastShootTime = Date.now();
455
+
456
+ const bulletGeometry = new THREE.SphereGeometry(0.15);
457
+ const bulletMaterial = new THREE.MeshBasicMaterial({
458
+ color: 0xff0000,
459
+ emissive: 0xff0000,
460
+ emissiveIntensity: 0.5
461
+ });
462
+ const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
463
+
464
+ const muzzleOffset = new THREE.Vector3(0, 0, 6);
465
+ muzzleOffset.applyEuler(this.rotation);
466
+ bullet.position.copy(this.position).add(muzzleOffset);
467
+
468
+ const direction = new THREE.Vector3(0, 0, 1);
469
+ direction.applyEuler(this.rotation);
470
+ bullet.velocity = direction.multiplyScalar(800);
471
+
472
+ this.scene.add(bullet);
473
+ this.bullets.push(bullet);
474
+ }
475
 
476
+ updateBullets(deltaTime) {
477
+ for (let i = this.bullets.length - 1; i >= 0; i--) {
478
+ const bullet = this.bullets[i];
479
+ bullet.position.add(bullet.velocity.clone().multiplyScalar(deltaTime));
480
+
481
+ if (bullet.position.distanceTo(this.position) > 5000 ||
482
+ bullet.position.y < 0) {
483
+ this.scene.remove(bullet);
484
+ this.bullets.splice(i, 1);
485
+ }
 
 
 
 
 
486
  }
487
+ }
488
+
489
+ takeDamage(damage) {
490
+ this.health -= damage;
491
+ return this.health <= 0;
492
+ }
493
+
494
+ destroy() {
495
+ if (this.mesh) {
496
+ this.scene.remove(this.mesh);
497
+ this.bullets.forEach(bullet => this.scene.remove(bullet));
498
+ this.bullets = [];
499
+ this.isLoaded = false;
500
  }
501
+ }
502
+ }
503
+
504
+ // ๋ฉ”์ธ ๊ฒŒ์ž„ ํด๋ž˜์Šค
505
+ class Game {
506
+ constructor() {
507
+ this.scene = new THREE.Scene();
508
+ this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 50000);
509
+ this.renderer = new THREE.WebGLRenderer({ antialias: true });
510
+ this.renderer.setSize(window.innerWidth, window.innerHeight);
511
+ this.renderer.shadowMap.enabled = true;
512
+ this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
513
+ this.renderer.setClearColor(0x87CEEB);
514
+ this.renderer.setPixelRatio(window.devicePixelRatio);
515
 
516
+ document.getElementById('gameContainer').appendChild(this.renderer.domElement);
 
 
 
 
 
 
 
 
 
 
517
 
518
+ this.loader = new GLTFLoader();
519
+ this.fighter = new Fighter();
520
+ this.enemies = [];
521
+ this.isLoaded = false;
522
+ this.isGameOver = false;
523
+ this.gameTime = GAME_CONSTANTS.MISSION_DURATION;
524
+ this.score = 0;
525
+ this.lastTime = performance.now();
526
+ this.gameTimer = null;
527
+ this.animationFrameId = null;
528
+
529
+ // ์˜ค๋””์˜ค ์‹œ์Šคํ…œ
530
+ this.bgm = null;
531
+ this.bgmPlaying = false;
532
+
533
+ // ์ž…๋ ฅ ์ƒํƒœ
534
+ this.keys = { w: false, a: false, s: false, d: false };
535
+ this.isStarted = false;
536
+
537
+ // ๊ธฐ๋ณธ ์ดˆ๊ธฐํ™”
538
+ this.setupScene();
539
+ this.setupEventListeners();
540
+ this.preloadGame();
541
+ }
542
+
543
+ async preloadGame() {
544
+ try {
545
+ console.log('๊ฒŒ์ž„ ๋ฆฌ์†Œ์Šค ์‚ฌ์ „ ๋กœ๋”ฉ ์ค‘...');
546
+
547
+ // ์ „ํˆฌ๊ธฐ ์‚ฌ์ „ ๋กœ๋”ฉ
548
+ await this.fighter.initialize(this.scene, this.loader);
549
+
550
+ if (!this.fighter.isLoaded) {
551
+ throw new Error('์ „ํˆฌ๊ธฐ ๋กœ๋”ฉ ์‹คํŒจ');
552
+ }
553
+
554
+ // ์ดˆ๊ธฐ ์นด๋ฉ”๋ผ ์œ„์น˜ ์„ค์ •
555
+ const initialCameraPos = this.fighter.getCameraPosition();
556
+ this.camera.position.copy(initialCameraPos);
557
+ this.camera.lookAt(this.fighter.position);
558
+
559
+ // ์  ์ƒ์„ฑ
560
+ await this.preloadEnemies();
561
+
562
+ this.isLoaded = true;
563
+ console.log('๊ฒŒ์ž„ ๋ฆฌ์†Œ์Šค ๋กœ๋”ฉ ์™„๋ฃŒ');
564
+
565
+ // ๋กœ๋”ฉ ํ™”๋ฉด ์ˆจ๊ธฐ๊ธฐ
566
+ document.getElementById('loading').style.display = 'none';
567
+
568
+ // ๋ Œ๋”๋ง ์‹œ์ž‘
569
+ this.animate();
570
+
571
+ } catch (error) {
572
+ console.error('๊ฒŒ์ž„ ์‚ฌ์ „ ๋กœ๋”ฉ ์‹คํŒจ:', error);
573
+ document.getElementById('loading').innerHTML =
574
+ '<div class="loading-text" style="color: red;">๋กœ๋”ฉ ์‹คํŒจ. ํŽ˜์ด์ง€๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•ด์ฃผ์„ธ์š”.</div>';
575
  }
576
+ }
577
+
578
+ async preloadEnemies() {
579
+ for (let i = 0; i < GAME_CONSTANTS.ENEMY_COUNT; i++) {
580
+ const angle = (i / GAME_CONSTANTS.ENEMY_COUNT) * Math.PI * 2;
581
+ const distance = 6000 + Math.random() * 3000;
582
+
583
+ const position = new THREE.Vector3(
584
+ Math.cos(angle) * distance,
585
+ 2500 + Math.random() * 2000,
586
+ Math.sin(angle) * distance
587
+ );
588
+
589
+ const enemy = new EnemyFighter(this.scene, position);
590
+ await enemy.initialize(this.loader);
591
+ this.enemies.push(enemy);
592
  }
593
+ }
594
+
595
+ setupScene() {
596
+ // ํ•˜๋Š˜ ๋ฐฐ๊ฒฝ
597
+ this.scene.background = new THREE.Color(0x87CEEB);
598
+ this.scene.fog = new THREE.Fog(0x87CEEB, 1000, 30000);
599
 
600
+ // ์กฐ๋ช…
601
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
602
+ this.scene.add(ambientLight);
 
 
 
 
 
 
 
603
 
604
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
605
+ directionalLight.position.set(8000, 10000, 8000);
606
+ directionalLight.castShadow = true;
607
+ directionalLight.shadow.mapSize.width = 2048;
608
+ directionalLight.shadow.mapSize.height = 2048;
609
+ directionalLight.shadow.camera.near = 0.5;
610
+ directionalLight.shadow.camera.far = 20000;
611
+ directionalLight.shadow.camera.left = -10000;
612
+ directionalLight.shadow.camera.right = 10000;
613
+ directionalLight.shadow.camera.top = 10000;
614
+ directionalLight.shadow.camera.bottom = -10000;
615
+ this.scene.add(directionalLight);
616
 
617
+ // ์ง€ํ˜•
618
+ const groundGeometry = new THREE.PlaneGeometry(GAME_CONSTANTS.MAP_SIZE, GAME_CONSTANTS.MAP_SIZE);
619
+ const groundMaterial = new THREE.MeshLambertMaterial({
620
+ color: 0x8FBC8F,
621
+ transparent: true,
622
+ opacity: 0.8
623
+ });
624
+ const ground = new THREE.Mesh(groundGeometry, groundMaterial);
625
+ ground.rotation.x = -Math.PI / 2;
626
+ ground.receiveShadow = true;
627
+ this.scene.add(ground);
628
+
629
+ // ๊ตฌ๋ฆ„ ์ถ”๊ฐ€
630
+ this.addClouds();
631
+ }
632
+
633
+ addClouds() {
634
+ const cloudGeometry = new THREE.SphereGeometry(100, 8, 6);
635
+ const cloudMaterial = new THREE.MeshLambertMaterial({
636
+ color: 0xffffff,
637
+ transparent: true,
638
+ opacity: 0.5
639
+ });
640
+
641
+ for (let i = 0; i < 100; i++) {
642
+ const cloud = new THREE.Mesh(cloudGeometry, cloudMaterial);
643
+ cloud.position.set(
644
+ (Math.random() - 0.5) * GAME_CONSTANTS.MAP_SIZE,
645
+ Math.random() * 4000 + 1000,
646
+ (Math.random() - 0.5) * GAME_CONSTANTS.MAP_SIZE
647
+ );
648
+ cloud.scale.set(
649
+ Math.random() * 3 + 1,
650
+ Math.random() * 2 + 0.5,
651
+ Math.random() * 3 + 1
652
+ );
653
+ this.scene.add(cloud);
654
+ }
655
+ }
656
+
657
+ setupEventListeners() {
658
+ // ํ‚ค๋ณด๋“œ ์ด๋ฒคํŠธ
659
+ document.addEventListener('keydown', (event) => {
660
+ if (!this.isStarted || this.isGameOver) return;
661
+
662
+ switch(event.code) {
663
+ case 'KeyW': this.keys.w = true; break;
664
+ case 'KeyA': this.keys.a = true; break;
665
+ case 'KeyS': this.keys.s = true; break;
666
+ case 'KeyD': this.keys.d = true; break;
667
+ }
668
+ });
669
+
670
+ document.addEventListener('keyup', (event) => {
671
+ switch(event.code) {
672
+ case 'KeyW': this.keys.w = false; break;
673
+ case 'KeyA': this.keys.a = false; break;
674
+ case 'KeyS': this.keys.s = false; break;
675
+ case 'KeyD': this.keys.d = false; break;
676
  }
677
+ });
678
+
679
+ // ๋งˆ์šฐ์Šค ์ด๋ฒคํŠธ
680
+ document.addEventListener('mousemove', (event) => {
681
+ if (!document.pointerLockElement || !this.isStarted || this.isGameOver) return;
682
+
683
+ const deltaX = event.movementX || 0;
684
+ const deltaY = event.movementY || 0;
685
+
686
+ this.fighter.updateMouseInput(deltaX, deltaY);
687
+ });
688
+
689
+ document.addEventListener('click', () => {
690
+ if (!this.isStarted) return;
691
+
692
+ if (!document.pointerLockElement) {
693
+ document.body.requestPointerLock();
694
+ } else if (!this.isGameOver && this.fighter.isLoaded) {
695
+ this.fighter.shoot(this.scene);
696
  }
697
+ });
698
+
699
+ // ์œˆ๋„์šฐ ๋ฆฌ์‚ฌ์ด์ฆˆ
700
+ window.addEventListener('resize', () => {
701
+ this.camera.aspect = window.innerWidth / window.innerHeight;
702
+ this.camera.updateProjectionMatrix();
703
+ this.renderer.setSize(window.innerWidth, window.innerHeight);
704
+ });
705
+ }
706
 
707
+ startGame() {
708
+ if (!this.isLoaded) {
709
+ console.log('๊ฒŒ์ž„์ด ์•„์ง ๋กœ๋”ฉ ์ค‘์ž…๋‹ˆ๋‹ค...');
710
+ return;
 
 
 
 
 
 
 
 
711
  }
712
+
713
+ this.isStarted = true;
714
+ this.startBGM();
715
+ this.startGameTimer();
716
+
717
+ console.log('๊ฒŒ์ž„ ์‹œ์ž‘!');
718
+ }
719
 
720
+ startBGM() {
721
+ if (this.bgmPlaying) return;
722
+
723
+ try {
724
+ this.bgm = new Audio('sounds/main_bgm.ogg');
725
+ this.bgm.volume = 0.5;
726
+ this.bgm.loop = true;
727
+ this.bgm.play();
728
+ this.bgmPlaying = true;
729
+ console.log('BGM ์žฌ์ƒ ์‹œ์ž‘');
730
+ } catch (error) {
731
+ console.log('BGM ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค:', error);
732
  }
733
+ }
734
+
735
+ startGameTimer() {
736
+ this.gameTimer = setInterval(() => {
737
+ if (!this.isGameOver) {
738
+ this.gameTime--;
739
+
740
+ if (this.gameTime <= 0) {
741
+ this.endGame(true);
742
+ }
743
+ }
744
+ }, 1000);
745
+ }
746
 
747
+ updateUI() {
748
+ if (this.fighter.isLoaded) {
749
+ const speedKnots = Math.round(this.fighter.speed * 1.94384);
750
+ const altitudeFeet = Math.round(this.fighter.altitude * 3.28084);
751
+
752
+ document.getElementById('score').textContent = `Score: ${this.score}`;
753
+ document.getElementById('time').textContent = `Time: ${this.gameTime}s`;
754
+ document.getElementById('health').style.width = `${this.fighter.health}%`;
755
+ document.getElementById('ammoDisplay').textContent = `AMMO: ${this.fighter.ammo}`;
756
+
757
+ const gameStats = document.getElementById('gameStats');
758
+ if (gameStats) {
759
+ gameStats.innerHTML = `
760
+ <div>Score: ${this.score}</div>
761
+ <div>Time: ${this.gameTime}s</div>
762
+ <div>Speed: ${speedKnots} KT</div>
763
+ <div>Alt: ${altitudeFeet} FT</div>
764
+ <div>Throttle: ${Math.round(this.fighter.throttle * 100)}%</div>
765
+ <div>G-Force: ${this.fighter.gForce.toFixed(1)}</div>
766
+ <div>Targets: ${this.enemies.length}</div>
767
+ `;
768
+ }
769
  }
770
+ }
771
+
772
+ updateRadar() {
773
+ const radar = document.getElementById('radar');
774
+ if (!radar) return;
775
 
776
+ const oldDots = radar.getElementsByClassName('enemy-dot');
777
+ while (oldDots[0]) {
778
+ oldDots[0].remove();
779
  }
780
+
781
+ const radarCenter = { x: 100, y: 100 };
782
+ const radarRange = 10000;
783
+
784
+ this.enemies.forEach(enemy => {
785
+ if (!enemy.mesh || !enemy.isLoaded) return;
786
+
787
+ const distance = this.fighter.position.distanceTo(enemy.position);
788
+ if (distance <= radarRange) {
789
+ const relativePos = enemy.position.clone().sub(this.fighter.position);
790
+ const angle = Math.atan2(relativePos.x, relativePos.z);
791
+ const relativeDistance = distance / radarRange;
792
+
793
+ const dotX = radarCenter.x + Math.sin(angle) * (radarCenter.x * relativeDistance);
794
+ const dotY = radarCenter.y + Math.cos(angle) * (radarCenter.y * relativeDistance);
795
+
796
+ const dot = document.createElement('div');
797
+ dot.className = 'enemy-dot';
798
+ dot.style.left = `${dotX}px`;
799
+ dot.style.top = `${dotY}px`;
800
+ radar.appendChild(dot);
801
+ }
802
+ });
803
+ }
804
 
805
+ checkCollisions() {
806
+ // ํ”Œ๋ ˆ์ด์–ด ์ด์•Œ๊ณผ ์  ์ถฉ๋Œ
807
+ for (let i = this.fighter.bullets.length - 1; i >= 0; i--) {
808
+ const bullet = this.fighter.bullets[i];
809
+
810
+ for (let j = this.enemies.length - 1; j >= 0; j--) {
811
+ const enemy = this.enemies[j];
812
+ if (!enemy.mesh || !enemy.isLoaded) continue;
813
+
814
+ const distance = bullet.position.distanceTo(enemy.position);
815
+ if (distance < 25) {
816
+ this.scene.remove(bullet);
817
+ this.fighter.bullets.splice(i, 1);
818
+
819
+ if (enemy.takeDamage(30)) {
820
+ enemy.destroy();
821
+ this.enemies.splice(j, 1);
822
+ this.score += 100;
823
+ }
824
+ break;
825
+ }
826
+ }
827
  }
828
+
829
+ // ์  ์ด์•Œ๊ณผ ํ”Œ๋ ˆ์ด์–ด ์ถฉ๋Œ
830
+ this.enemies.forEach(enemy => {
831
+ enemy.bullets.forEach((bullet, index) => {
832
+ const distance = bullet.position.distanceTo(this.fighter.position);
833
+ if (distance < 30) {
834
+ this.scene.remove(bullet);
835
+ enemy.bullets.splice(index, 1);
836
+
837
+ if (this.fighter.takeDamage(20)) {
838
+ this.endGame(false);
839
+ }
840
+ }
841
+ });
842
+ });
843
+ }
 
 
 
844
 
845
+ animate() {
846
+ if (this.isGameOver) return;
847
+
848
+ this.animationFrameId = requestAnimationFrame(() => this.animate());
849
+
850
+ const currentTime = performance.now();
851
+ const deltaTime = Math.min((currentTime - this.lastTime) / 1000, 0.1);
852
+ this.lastTime = currentTime;
853
+
854
+ if (this.isLoaded && this.fighter.isLoaded) {
855
+ if (this.isStarted) {
856
+ // ์ „ํˆฌ๊ธฐ ์—…๋ฐ์ดํŠธ
857
+ this.fighter.updateControls(this.keys, deltaTime);
858
+ this.fighter.updatePhysics(deltaTime);
859
+ this.fighter.updateBullets(this.scene, deltaTime);
860
+
861
+ // ์  ์—…๋ฐ์ดํŠธ
862
+ this.enemies.forEach(enemy => {
863
+ enemy.update(this.fighter.position, deltaTime);
864
+ });
865
+
866
+ // ์ถฉ๋Œ ๊ฒ€์‚ฌ
867
+ this.checkCollisions();
868
+
869
+ // UI ์—…๋ฐ์ดํŠธ
870
+ this.updateUI();
871
+ this.updateRadar();
872
+
873
+ // ์Šน๋ฆฌ ์กฐ๊ฑด ํ™•์ธ
874
+ if (this.enemies.length === 0) {
875
+ this.endGame(true);
876
+ }
 
 
877
  }
878
+
879
+ // ์นด๋ฉ”๋ผ ์—…๋ฐ์ดํŠธ (๋ถ€๋“œ๋Ÿฝ๊ฒŒ ๋”ฐ๋ผ๊ฐ€๊ธฐ)
880
+ const targetCameraPos = this.fighter.getCameraPosition();
881
+ const targetCameraTarget = this.fighter.getCameraTarget();
882
+
883
+ this.camera.position.lerp(targetCameraPos, this.fighter.cameraLag);
884
+ this.camera.lookAt(targetCameraTarget);
885
  }
886
+
887
+ this.renderer.render(this.scene, this.camera);
888
+ }
889
+
890
+ endGame(victory = false) {
891
+ this.isGameOver = true;
892
+
893
+ if (this.bgm) {
894
+ this.bgm.pause();
895
+ this.bgm = null;
896
+ this.bgmPlaying = false;
897
+ }
898
+
899
+ if (this.gameTimer) {
900
+ clearInterval(this.gameTimer);
901
  }
902
+
903
+ document.exitPointerLock();
904
+
905
+ const gameOverDiv = document.createElement('div');
906
+ gameOverDiv.className = 'start-screen';
907
+ gameOverDiv.innerHTML = `
908
+ <h1 style="color: ${victory ? '#0f0' : '#f00'}; font-size: 48px;">
909
+ ${victory ? 'MISSION ACCOMPLISHED!' : 'SHOT DOWN!'}
910
+ </h1>
911
+ <div style="color: #0f0; font-size: 24px; margin: 20px 0;">
912
+ Final Score: ${this.score}<br>
913
+ Enemies Destroyed: ${GAME_CONSTANTS.ENEMY_COUNT - this.enemies.length}<br>
914
+ Mission Time: ${GAME_CONSTANTS.MISSION_DURATION - this.gameTime}s
915
+ </div>
916
+ <button class="start-button" onclick="location.reload()">
917
+ New Mission
918
+ </button>
919
+ `;
920
+
921
+ document.body.appendChild(gameOverDiv);
922
+ }
923
+ }
924
 
925
+ // ๊ฒŒ์ž„ ์ธ์Šคํ„ด์Šค
926
+ let gameInstance = null;
927
+
928
+ // ์ „์—ญ ํ•จ์ˆ˜
929
+ window.startGame = function() {
930
+ document.getElementById('startScreen').style.display = 'none';
931
+ document.body.requestPointerLock();
932
+
933
+ if (gameInstance && gameInstance.isLoaded) {
934
+ gameInstance.startGame();
935
+ } else {
936
+ console.log('๊ฒŒ์ž„์ด ์•„์ง ์ค€๋น„๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค...');
937
+ }
938
+ };
939
+
940
+ // ๊ฒŒ์ž„ ์ดˆ๊ธฐํ™”
941
+ document.addEventListener('DOMContentLoaded', () => {
942
+ console.log('์ „ํˆฌ๊ธฐ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ ์ดˆ๊ธฐํ™” ์ค‘...');
943
+ gameInstance = new Game();
944
+ });