cutechicken commited on
Commit
a30e88c
โ€ข
1 Parent(s): 576e1ec

Create game.js

Browse files
Files changed (1) hide show
  1. game.js +729 -0
game.js ADDED
@@ -0,0 +1,729 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as THREE from 'three';
2
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
3
+ import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';
4
+
5
+ // ๊ฒŒ์ž„ ์ƒ์ˆ˜
6
+ const GAME_DURATION = 180;
7
+ const MAP_SIZE = 2000;
8
+ const HELICOPTER_HEIGHT = 30;
9
+ const ENEMY_GROUND_HEIGHT = 0;
10
+ const ENEMY_SCALE = 10;
11
+ const MAX_HEALTH = 1000;
12
+ const ENEMY_MOVE_SPEED = 0.1;
13
+ const ENEMY_COUNT_MAX = 5;
14
+ const PARTICLE_COUNT = 15;
15
+ const OBSTACLE_COUNT = 50;
16
+ const ENEMY_CONFIG = {
17
+ ATTACK_RANGE: 100,
18
+ ATTACK_INTERVAL: 2000,
19
+ BULLET_SPEED: 2
20
+ };
21
+
22
+ // ๊ฒŒ์ž„ ๋ณ€์ˆ˜
23
+ let scene, camera, renderer, controls;
24
+ let enemies = [];
25
+ let bullets = [];
26
+ let enemyBullets = [];
27
+ let playerHealth = MAX_HEALTH;
28
+ let ammo = 30;
29
+ let currentStage = 1;
30
+ let isGameOver = false;
31
+ let lastTime = performance.now();
32
+ let lastRender = 0;
33
+
34
+ // ์˜ค์‹ค๋ ˆ์ดํ„ฐ ๊ธฐ๋ฐ˜ ์ด์†Œ๋ฆฌ ์ƒ์„ฑ๊ธฐ
35
+ class GunSoundGenerator {
36
+ constructor() {
37
+ this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
38
+ }
39
+
40
+ createGunshot() {
41
+ const currentTime = this.audioContext.currentTime;
42
+
43
+ // ๋ฉ”์ธ ์˜ค์‹ค๋ ˆ์ดํ„ฐ
44
+ const osc = this.audioContext.createOscillator();
45
+ const gainNode = this.audioContext.createGain();
46
+
47
+ osc.type = 'square';
48
+ osc.frequency.setValueAtTime(200, currentTime);
49
+ osc.frequency.exponentialRampToValueAtTime(50, currentTime + 0.1);
50
+
51
+ gainNode.gain.setValueAtTime(0.5, currentTime);
52
+ gainNode.gain.exponentialRampToValueAtTime(0.01, currentTime + 0.1);
53
+
54
+ osc.connect(gainNode);
55
+ gainNode.connect(this.audioContext.destination);
56
+
57
+ osc.start(currentTime);
58
+ osc.stop(currentTime + 0.1);
59
+
60
+ // ๋…ธ์ด์ฆˆ ์ถ”๊ฐ€
61
+ const bufferSize = this.audioContext.sampleRate * 0.1;
62
+ const buffer = this.audioContext.createBuffer(1, bufferSize, this.audioContext.sampleRate);
63
+ const data = buffer.getChannelData(0);
64
+
65
+ for (let i = 0; i < bufferSize; i++) {
66
+ data[i] = Math.random() * 2 - 1;
67
+ }
68
+
69
+ const noise = this.audioContext.createBufferSource();
70
+ const noiseGain = this.audioContext.createGain();
71
+
72
+ noise.buffer = buffer;
73
+ noiseGain.gain.setValueAtTime(0.2, currentTime);
74
+ noiseGain.gain.exponentialRampToValueAtTime(0.01, currentTime + 0.05);
75
+
76
+ noise.connect(noiseGain);
77
+ noiseGain.connect(this.audioContext.destination);
78
+
79
+ noise.start(currentTime);
80
+ }
81
+
82
+ resume() {
83
+ if (this.audioContext.state === 'suspended') {
84
+ this.audioContext.resume();
85
+ }
86
+ }
87
+ }
88
+
89
+ // ์‚ฌ์šด๋“œ ์‹œ์Šคํ…œ ์ดˆ๊ธฐํ™”
90
+ const gunSound = new GunSoundGenerator();
91
+
92
+ async function init() {
93
+ document.getElementById('loading').style.display = 'block';
94
+
95
+ try {
96
+ // Scene ์ดˆ๊ธฐํ™”
97
+ scene = new THREE.Scene();
98
+ scene.background = new THREE.Color(0x87ceeb);
99
+ scene.fog = new THREE.Fog(0x87ceeb, 0, 1000);
100
+
101
+ // Renderer ์ตœ์ ํ™”
102
+ renderer = new THREE.WebGLRenderer({
103
+ antialias: false,
104
+ powerPreference: "high-performance"
105
+ });
106
+ renderer.setSize(window.innerWidth, window.innerHeight);
107
+ renderer.shadowMap.enabled = true;
108
+ renderer.shadowMap.type = THREE.BasicShadowMap;
109
+ document.body.appendChild(renderer.domElement);
110
+
111
+ // Camera ์„ค์ •
112
+ camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
113
+ camera.position.set(0, HELICOPTER_HEIGHT, 0);
114
+
115
+ // ๊ธฐ๋ณธ ์กฐ๋ช…
116
+ scene.add(new THREE.AmbientLight(0xffffff, 0.6));
117
+
118
+ const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
119
+ dirLight.position.set(100, 100, 50);
120
+ dirLight.castShadow = true;
121
+ dirLight.shadow.mapSize.width = 1024;
122
+ dirLight.shadow.mapSize.height = 1024;
123
+ scene.add(dirLight);
124
+
125
+ // Controls ์„ค์ •
126
+ controls = new PointerLockControls(camera, document.body);
127
+
128
+ // ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
129
+ setupEventListeners();
130
+
131
+ // ๋ชจ๋ธ ํ…Œ์ŠคํŠธ ๋จผ์ € ์‹คํ–‰
132
+ await testModelLoading();
133
+
134
+ // ๊ฒŒ์ž„ ์š”์†Œ ์ดˆ๊ธฐํ™”
135
+ await Promise.all([
136
+ createTerrain(),
137
+ createEnemies()
138
+ ]);
139
+
140
+ document.getElementById('loading').style.display = 'none';
141
+ console.log('Game initialized successfully');
142
+ } catch (error) {
143
+ console.error('Initialization error:', error);
144
+ document.getElementById('loading').innerHTML = `
145
+ <div class="loading-text" style="color: #ff0000;">
146
+ Error loading models. Please check console and file paths.
147
+ </div>
148
+ `;
149
+ throw error;
150
+ }
151
+ }
152
+
153
+ function setupEventListeners() {
154
+ document.addEventListener('click', onClick);
155
+ document.addEventListener('keydown', onKeyDown);
156
+ document.addEventListener('keyup', onKeyUp);
157
+ window.addEventListener('resize', onWindowResize);
158
+ }
159
+
160
+ async function testModelLoading() {
161
+ const loader = new GLTFLoader();
162
+ try {
163
+ const modelPath = 'models/enemy1.glb';
164
+ console.log('Testing model loading:', modelPath);
165
+ const gltf = await loader.loadAsync(modelPath);
166
+ console.log('Test model loaded successfully:', gltf);
167
+ } catch (error) {
168
+ console.error('Test model loading failed:', error);
169
+ throw error;
170
+ }
171
+ }
172
+
173
+ function createTerrain() {
174
+ return new Promise((resolve) => {
175
+ const geometry = new THREE.PlaneGeometry(MAP_SIZE, MAP_SIZE, 100, 100);
176
+ const material = new THREE.MeshStandardMaterial({
177
+ color: 0xD2B48C,
178
+ roughness: 0.8,
179
+ metalness: 0.2
180
+ });
181
+
182
+ const vertices = geometry.attributes.position.array;
183
+ for (let i = 0; i < vertices.length; i += 3) {
184
+ vertices[i + 2] = Math.sin(vertices[i] * 0.01) * Math.cos(vertices[i + 1] * 0.01) * 20;
185
+ }
186
+
187
+ geometry.attributes.position.needsUpdate = true;
188
+ geometry.computeVertexNormals();
189
+
190
+ const terrain = new THREE.Mesh(geometry, material);
191
+ terrain.rotation.x = -Math.PI / 2;
192
+ terrain.receiveShadow = true;
193
+ scene.add(terrain);
194
+
195
+ addObstacles();
196
+ resolve();
197
+ });
198
+ }
199
+
200
+ function addObstacles() {
201
+ const rockGeometry = new THREE.DodecahedronGeometry(10);
202
+ const rockMaterial = new THREE.MeshStandardMaterial({
203
+ color: 0x8B4513,
204
+ roughness: 0.9
205
+ });
206
+
207
+ for (let i = 0; i < OBSTACLE_COUNT; i++) {
208
+ const rock = new THREE.Mesh(rockGeometry, rockMaterial);
209
+ rock.position.set(
210
+ (Math.random() - 0.5) * MAP_SIZE * 0.9,
211
+ Math.random() * 10,
212
+ (Math.random() - 0.5) * MAP_SIZE * 0.9
213
+ );
214
+ rock.rotation.set(
215
+ Math.random() * Math.PI,
216
+ Math.random() * Math.PI,
217
+ Math.random() * Math.PI
218
+ );
219
+ rock.castShadow = true;
220
+ rock.receiveShadow = true;
221
+ scene.add(rock);
222
+ }
223
+ }
224
+
225
+ async function createEnemies() {
226
+ console.log('Creating enemies...');
227
+ const loader = new GLTFLoader();
228
+ const enemyCount = Math.min(3 + currentStage, ENEMY_COUNT_MAX);
229
+
230
+ for (let i = 0; i < enemyCount; i++) {
231
+ const angle = (i / enemyCount) * Math.PI * 2;
232
+ const radius = 200;
233
+ const position = new THREE.Vector3(
234
+ Math.cos(angle) * radius,
235
+ ENEMY_GROUND_HEIGHT,
236
+ Math.sin(angle) * radius
237
+ );
238
+
239
+ // ์ž„์‹œ ์  ์ƒ์„ฑ
240
+ const tempEnemy = createTemporaryEnemy(position);
241
+ scene.add(tempEnemy.model);
242
+ enemies.push(tempEnemy);
243
+
244
+ // GLB ๋ชจ๋ธ ๋กœ๋“œ
245
+ try {
246
+ const modelIndex = i % 4 + 1;
247
+ const modelPath = `models/enemy${modelIndex}.glb`;
248
+ console.log(`Loading model: ${modelPath}`);
249
+
250
+ const gltf = await loader.loadAsync(modelPath);
251
+ const model = gltf.scene;
252
+
253
+ // ๋ชจ๋ธ ์„ค์ •
254
+ model.scale.set(ENEMY_SCALE, ENEMY_SCALE, ENEMY_SCALE);
255
+ model.position.copy(position);
256
+
257
+ // ๋ชจ๋ธ ์žฌ์งˆ ๋ฐ ๊ทธ๋ฆผ์ž ์„ค์ •
258
+ model.traverse((node) => {
259
+ if (node.isMesh) {
260
+ node.castShadow = true;
261
+ node.receiveShadow = true;
262
+ node.material.metalness = 0.2;
263
+ node.material.roughness = 0.8;
264
+ }
265
+ });
266
+
267
+ // ์ž„์‹œ ๋ชจ๋ธ ๊ต์ฒด
268
+ scene.remove(tempEnemy.model);
269
+ scene.add(model);
270
+ enemies[enemies.indexOf(tempEnemy)].model = model;
271
+
272
+ console.log(`Successfully loaded enemy model ${modelIndex}`);
273
+ } catch (error) {
274
+ console.error(`Error loading enemy model:`, error);
275
+ }
276
+ }
277
+ }
278
+
279
+ function createTemporaryEnemy(position) {
280
+ const geometry = new THREE.BoxGeometry(5, 10, 5);
281
+ const material = new THREE.MeshPhongMaterial({
282
+ color: 0xff0000,
283
+ transparent: true,
284
+ opacity: 0.8
285
+ });
286
+
287
+ const model = new THREE.Mesh(geometry, material);
288
+ model.position.copy(position);
289
+ model.castShadow = true;
290
+ model.receiveShadow = true;
291
+
292
+ return {
293
+ model: model,
294
+ health: 100,
295
+ speed: ENEMY_MOVE_SPEED,
296
+ lastAttackTime: 0
297
+ };
298
+ }
299
+
300
+ function createExplosion(position) {
301
+ const particles = [];
302
+ for (let i = 0; i < PARTICLE_COUNT; i++) {
303
+ const particle = new THREE.Mesh(
304
+ new THREE.SphereGeometry(0.3),
305
+ new THREE.MeshBasicMaterial({
306
+ color: 0xff4400,
307
+ transparent: true,
308
+ opacity: 1
309
+ })
310
+ );
311
+
312
+ particle.position.copy(position);
313
+ particle.velocity = new THREE.Vector3(
314
+ (Math.random() - 0.5) * 2,
315
+ Math.random() * 2,
316
+ (Math.random() - 0.5) * 2
317
+ );
318
+
319
+ particles.push(particle);
320
+ scene.add(particle);
321
+ }
322
+
323
+ // ํญ๋ฐœ ๊ด‘์› ํšจ๊ณผ
324
+ const explosionLight = new THREE.PointLight(0xff4400, 2, 20);
325
+ explosionLight.position.copy(position);
326
+ scene.add(explosionLight);
327
+
328
+ let opacity = 1;
329
+ const animate = () => {
330
+ opacity -= 0.05;
331
+ if (opacity <= 0) {
332
+ particles.forEach(p => scene.remove(p));
333
+ scene.remove(explosionLight);
334
+ return;
335
+ }
336
+
337
+ particles.forEach(particle => {
338
+ particle.position.add(particle.velocity);
339
+ particle.material.opacity = opacity;
340
+ });
341
+
342
+ requestAnimationFrame(animate);
343
+ };
344
+
345
+ animate();
346
+ }
347
+
348
+ function onClick() {
349
+ if (!controls.isLocked) {
350
+ controls.lock();
351
+ gunSound.resume();
352
+ } else if (ammo > 0) {
353
+ shoot();
354
+ }
355
+ }
356
+
357
+ function onKeyDown(event) {
358
+ switch(event.code) {
359
+ case 'KeyW': moveState.forward = true; break;
360
+ case 'KeyS': moveState.backward = true; break;
361
+ case 'KeyA': moveState.left = true; break;
362
+ case 'KeyD': moveState.right = true; break;
363
+ case 'KeyR': reload(); break;
364
+ }
365
+ }
366
+
367
+ function onKeyUp(event) {
368
+ switch(event.code) {
369
+ case 'KeyW': moveState.forward = false; break;
370
+ case 'KeyS': moveState.backward = false; break;
371
+ case 'KeyA': moveState.left = false; break;
372
+ case 'KeyD': moveState.right = false; break;
373
+ }
374
+ }
375
+
376
+ function onWindowResize() {
377
+ camera.aspect = window.innerWidth / window.innerHeight;
378
+ camera.updateProjectionMatrix();
379
+ renderer.setSize(window.innerWidth, window.innerHeight);
380
+ }
381
+
382
+ // ์ด๋™ ์ƒํƒœ
383
+ const moveState = {
384
+ forward: false,
385
+ backward: false,
386
+ left: false,
387
+ right: false
388
+ };
389
+
390
+ function shoot() {
391
+ if (ammo <= 0) return;
392
+
393
+ ammo--;
394
+ updateAmmoDisplay();
395
+
396
+ const bullet = createBullet();
397
+ bullets.push(bullet);
398
+
399
+ gunSound.createGunshot();
400
+
401
+ // ์ด๊ตฌ ํ™”์—ผ ํšจ๊ณผ
402
+ const muzzleFlash = new THREE.PointLight(0xffff00, 3, 10);
403
+ muzzleFlash.position.copy(camera.position);
404
+ scene.add(muzzleFlash);
405
+ setTimeout(() => scene.remove(muzzleFlash), 50);
406
+ }
407
+
408
+ function createBullet() {
409
+ const bullet = new THREE.Mesh(
410
+ new THREE.SphereGeometry(0.5),
411
+ new THREE.MeshBasicMaterial({
412
+ color: 0xffff00,
413
+ emissive: 0xffff00,
414
+ emissiveIntensity: 1
415
+ })
416
+ );
417
+
418
+ bullet.position.copy(camera.position);
419
+ const direction = new THREE.Vector3();
420
+ camera.getWorldDirection(direction);
421
+ bullet.velocity = direction.multiplyScalar(5);
422
+
423
+ scene.add(bullet);
424
+ return bullet;
425
+ }
426
+
427
+ function createEnemyBullet(enemy) {
428
+ const bullet = new THREE.Mesh(
429
+ new THREE.SphereGeometry(0.5),
430
+ new THREE.MeshBasicMaterial({
431
+ color: 0xff0000,
432
+ emissive: 0xff0000,
433
+ emissiveIntensity: 1
434
+ })
435
+ );
436
+
437
+ bullet.position.copy(enemy.model.position);
438
+ bullet.position.y += 5;
439
+
440
+ const direction = new THREE.Vector3();
441
+ direction.subVectors(camera.position, enemy.model.position).normalize();
442
+ bullet.velocity = direction.multiplyScalar(ENEMY_CONFIG.BULLET_SPEED);
443
+
444
+ scene.add(bullet);
445
+ return bullet;
446
+ }
447
+
448
+ function updateMovement() {
449
+ if (controls.isLocked) {
450
+ const speed = 2.0;
451
+ if (moveState.forward) controls.moveForward(speed);
452
+ if (moveState.backward) controls.moveForward(-speed);
453
+ if (moveState.left) controls.moveRight(-speed);
454
+ if (moveState.right) controls.moveRight(speed);
455
+
456
+ // ๊ณ ๋„ ์ œํ•œ
457
+ if (camera.position.y < HELICOPTER_HEIGHT) {
458
+ camera.position.y = HELICOPTER_HEIGHT;
459
+ } else if (camera.position.y > HELICOPTER_HEIGHT + 10) {
460
+ camera.position.y = HELICOPTER_HEIGHT + 10;
461
+ }
462
+ }
463
+ }
464
+
465
+ function updateBullets() {
466
+ for (let i = bullets.length - 1; i >= 0; i--) {
467
+ if (!bullets[i]) continue;
468
+
469
+ bullets[i].position.add(bullets[i].velocity);
470
+
471
+ // ์ ๊ณผ์˜ ์ถฉ๋Œ ๊ฒ€์‚ฌ
472
+ for (let j = enemies.length - 1; j >= 0; j--) {
473
+ const enemy = enemies[j];
474
+ if (!enemy || !enemy.model) continue;
475
+
476
+ if (bullets[i] && bullets[i].position.distanceTo(enemy.model.position) < 10) {
477
+ scene.remove(bullets[i]);
478
+ bullets.splice(i, 1);
479
+ enemy.health -= 25;
480
+
481
+ createExplosion(enemy.model.position.clone());
482
+
483
+ if (enemy.health <= 0) {
484
+ createExplosion(enemy.model.position.clone());
485
+ scene.remove(enemy.model);
486
+ enemies.splice(j, 1);
487
+ }
488
+ break;
489
+ }
490
+ }
491
+
492
+ // ๋ฒ”์œ„ ๋ฒ—์–ด๋‚œ ์ด์•Œ ์ œ๊ฑฐ
493
+ if (bullets[i] && bullets[i].position.distanceTo(camera.position) > 1000) {
494
+ scene.remove(bullets[i]);
495
+ bullets.splice(i, 1);
496
+ }
497
+ }
498
+ }
499
+
500
+ function updateEnemyBullets() {
501
+ for (let i = enemyBullets.length - 1; i >= 0; i--) {
502
+ if (!enemyBullets[i]) continue;
503
+
504
+ enemyBullets[i].position.add(enemyBullets[i].velocity);
505
+
506
+ if (enemyBullets[i].position.distanceTo(camera.position) < 3) {
507
+ playerHealth -= 10;
508
+ updateHealthBar();
509
+ createExplosion(enemyBullets[i].position.clone());
510
+ scene.remove(enemyBullets[i]);
511
+ enemyBullets.splice(i, 1);
512
+
513
+ if (playerHealth <= 0) {
514
+ gameOver(false);
515
+ }
516
+ continue;
517
+ }
518
+
519
+ if (enemyBullets[i].position.distanceTo(camera.position) > 1000) {
520
+ scene.remove(enemyBullets[i]);
521
+ enemyBullets.splice(i, 1);
522
+ }
523
+ }
524
+ }
525
+
526
+ function updateEnemies() {
527
+ const currentTime = Date.now();
528
+
529
+ enemies.forEach(enemy => {
530
+ if (!enemy || !enemy.model) return;
531
+
532
+ // ์  ์ด๋™ ๋กœ์ง
533
+ const direction = new THREE.Vector3();
534
+ direction.subVectors(camera.position, enemy.model.position);
535
+ direction.y = 0;
536
+ direction.normalize();
537
+
538
+ const newPosition = enemy.model.position.clone()
539
+ .add(direction.multiplyScalar(enemy.speed));
540
+ newPosition.y = ENEMY_GROUND_HEIGHT;
541
+ enemy.model.position.copy(newPosition);
542
+
543
+ // ์ ์ด ํ”Œ๋ ˆ์ด์–ด๋ฅผ ๋ฐ”๋ผ๋ณด๋„๋ก ์„ค์ •
544
+ enemy.model.lookAt(new THREE.Vector3(
545
+ camera.position.x,
546
+ enemy.model.position.y,
547
+ camera.position.z
548
+ ));
549
+
550
+ // ๊ณต๊ฒฉ ๋กœ์ง
551
+ const distanceToPlayer = enemy.model.position.distanceTo(camera.position);
552
+ if (distanceToPlayer < ENEMY_CONFIG.ATTACK_RANGE &&
553
+ currentTime - enemy.lastAttackTime > ENEMY_CONFIG.ATTACK_INTERVAL) {
554
+
555
+ enemyBullets.push(createEnemyBullet(enemy));
556
+ enemy.lastAttackTime = currentTime;
557
+
558
+ // ๊ณต๊ฒฉ ์‹œ ๋ฐœ๊ด‘ ํšจ๊ณผ
559
+ const attackFlash = new THREE.PointLight(0xff0000, 2, 20);
560
+ attackFlash.position.copy(enemy.model.position);
561
+ scene.add(attackFlash);
562
+ setTimeout(() => scene.remove(attackFlash), 100);
563
+ }
564
+ });
565
+ }
566
+
567
+ function reload() {
568
+ ammo = 30;
569
+ updateAmmoDisplay();
570
+ }
571
+
572
+ function updateAmmoDisplay() {
573
+ document.getElementById('ammo').textContent = `Ammo: ${ammo}/30`;
574
+ }
575
+
576
+ function updateHealthBar() {
577
+ const healthElement = document.getElementById('health');
578
+ const healthPercentage = (playerHealth / MAX_HEALTH) * 100;
579
+ healthElement.style.width = `${healthPercentage}%`;
580
+ }
581
+
582
+ function updateHelicopterHUD() {
583
+ document.querySelector('#altitude-indicator span').textContent =
584
+ Math.round(camera.position.y);
585
+
586
+ const speed = Math.round(
587
+ Math.sqrt(
588
+ moveState.forward * moveState.forward +
589
+ moveState.right * moveState.right
590
+ ) * 100
591
+ );
592
+ document.querySelector('#speed-indicator span').textContent = speed;
593
+
594
+ const heading = Math.round(
595
+ (camera.rotation.y * (180 / Math.PI) + 360) % 360
596
+ );
597
+ document.querySelector('#compass span').textContent = heading;
598
+
599
+ updateRadar();
600
+ }
601
+
602
+ function updateRadar() {
603
+ const radarTargets = document.querySelector('.radar-targets');
604
+ radarTargets.innerHTML = '';
605
+
606
+ enemies.forEach(enemy => {
607
+ if (!enemy || !enemy.model) return;
608
+
609
+ const relativePos = enemy.model.position.clone().sub(camera.position);
610
+ const distance = relativePos.length();
611
+
612
+ if (distance < 500) {
613
+ const playerAngle = camera.rotation.y;
614
+ const enemyAngle = Math.atan2(relativePos.x, relativePos.z);
615
+ const relativeAngle = enemyAngle - playerAngle;
616
+
617
+ const normalizedDistance = distance / 500;
618
+
619
+ const dot = document.createElement('div');
620
+ dot.className = 'radar-dot';
621
+ dot.style.left = `${50 + Math.sin(relativeAngle) * normalizedDistance * 45}%`;
622
+ dot.style.top = `${50 + Math.cos(relativeAngle) * normalizedDistance * 45}%`;
623
+ radarTargets.appendChild(dot);
624
+ }
625
+ });
626
+ }
627
+
628
+ function checkGameStatus() {
629
+ if (enemies.length === 0 && currentStage < 5) {
630
+ currentStage++;
631
+ document.getElementById('stage').style.display = 'block';
632
+ document.getElementById('stage').textContent = `Stage ${currentStage}`;
633
+ setTimeout(() => {
634
+ document.getElementById('stage').style.display = 'none';
635
+ createEnemies();
636
+ }, 2000);
637
+ }
638
+ }
639
+
640
+ function cleanupResources() {
641
+ bullets.forEach(bullet => scene.remove(bullet));
642
+ bullets = [];
643
+
644
+ enemyBullets.forEach(bullet => scene.remove(bullet));
645
+ enemyBullets = [];
646
+
647
+ enemies.forEach(enemy => {
648
+ if (enemy && enemy.model) {
649
+ scene.remove(enemy.model);
650
+ }
651
+ });
652
+ enemies = [];
653
+ }
654
+
655
+ function gameOver(won) {
656
+ isGameOver = true;
657
+ controls.unlock();
658
+ cleanupResources();
659
+ setTimeout(() => {
660
+ alert(won ? 'Mission Complete!' : 'Game Over!');
661
+ location.reload();
662
+ }, 100);
663
+ }
664
+
665
+ function gameLoop(timestamp) {
666
+ requestAnimationFrame(gameLoop);
667
+
668
+ // ํ”„๋ ˆ์ž„ ์ œํ•œ (60fps)
669
+ if (timestamp - lastRender < 16) {
670
+ return;
671
+ }
672
+ lastRender = timestamp;
673
+
674
+ if (controls.isLocked && !isGameOver) {
675
+ updateMovement();
676
+ updateBullets();
677
+ updateEnemies();
678
+ updateEnemyBullets();
679
+ updateHelicopterHUD();
680
+ checkGameStatus();
681
+ }
682
+
683
+ renderer.render(scene, camera);
684
+ }
685
+
686
+ // ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง
687
+ let lastFpsUpdate = 0;
688
+ let frameCount = 0;
689
+
690
+ function updateFPS(timestamp) {
691
+ frameCount++;
692
+
693
+ if (timestamp - lastFpsUpdate >= 1000) {
694
+ const fps = Math.round(frameCount * 1000 / (timestamp - lastFpsUpdate));
695
+ console.log('FPS:', fps);
696
+
697
+ frameCount = 0;
698
+ lastFpsUpdate = timestamp;
699
+ }
700
+
701
+ requestAnimationFrame(updateFPS);
702
+ }
703
+
704
+ // ๊ฒŒ์ž„ ์‹œ์ž‘
705
+ window.addEventListener('load', async () => {
706
+ try {
707
+ await init();
708
+ console.log('Game started');
709
+ console.log('Active enemies:', enemies.length);
710
+ gameLoop(performance.now());
711
+ updateFPS(performance.now());
712
+ } catch (error) {
713
+ console.error('Game initialization error:', error);
714
+ document.getElementById('loading').innerHTML = `
715
+ <div class="loading-text" style="color: #ff0000;">
716
+ Error loading game. Please check console and file paths.
717
+ </div>
718
+ `;
719
+ }
720
+ });
721
+
722
+ // ๋””๋ฒ„๊น…์„ ์œ„ํ•œ ์ „์—ญ ์ ‘๊ทผ
723
+ window.debugGame = {
724
+ scene,
725
+ camera,
726
+ enemies,
727
+ gunSound,
728
+ reloadEnemies: createEnemies
729
+ };