cutechicken commited on
Commit
96897f0
โ€ข
1 Parent(s): 9c0f025

Update game.js

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