cutechicken commited on
Commit
9c39181
โ€ข
1 Parent(s): eade4f6

Update game.js

Browse files
Files changed (1) hide show
  1. game.js +618 -559
game.js CHANGED
@@ -32,9 +32,11 @@ class TankPlayer {
32
  this.turretGroup = new THREE.Group();
33
  this.health = MAX_HEALTH;
34
  this.isLoaded = false;
35
- this.ammo = 10;
 
 
 
36
  this.lastShootTime = 0;
37
- this.shootInterval = 1000;
38
  this.bullets = [];
39
  }
40
 
@@ -67,6 +69,7 @@ class TankPlayer {
67
 
68
  scene.add(this.body);
69
  this.isLoaded = true;
 
70
 
71
  } catch (error) {
72
  console.error('Error loading tank models:', error);
@@ -75,9 +78,18 @@ class TankPlayer {
75
  }
76
 
77
  shoot(scene) {
78
- const currentTime = Date.now();
79
- if (currentTime - this.lastShootTime < this.shootInterval || this.ammo <= 0) return null;
 
 
 
 
 
 
 
 
80
 
 
81
  const bulletGeometry = new THREE.SphereGeometry(0.2);
82
  const bulletMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
83
  const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
@@ -94,44 +106,30 @@ class TankPlayer {
94
 
95
  scene.add(bullet);
96
  this.bullets.push(bullet);
97
- this.ammo--;
98
- this.lastShootTime = currentTime;
99
 
100
- document.getElementById('ammo').textContent = `Ammo: ${this.ammo}/10`;
101
-
102
  return bullet;
103
  }
104
 
105
- // TankPlayer ํด๋ž˜์Šค ๋‚ด๋ถ€
106
- update(mouseX, mouseY, scene) {
107
- if (!this.body || !this.turretGroup) return;
108
 
109
- // ํฌํƒ‘์ด ๋ฐ”๋ผ๋ณด๋Š” ์ ˆ๋Œ€ ๋ฐฉํ–ฅ ๊ณ„์‚ฐ
110
- const absoluteTurretRotation = mouseX;
111
-
112
- // ์ฐจ์ฒด ํšŒ์ „์„ ์ƒ์‡„ํ•˜๋Š” ํฌํƒ‘ ํšŒ์ „ ๊ณ„์‚ฐ
113
- // ์ฐจ์ฒด๊ฐ€ ํšŒ์ „ํ•˜๋ฉด ๊ทธ๋งŒํผ ๋ฐ˜๋Œ€๋กœ ํšŒ์ „ํ•˜์—ฌ ์ ˆ๋Œ€ ๋ฐฉํ–ฅ ์œ ์ง€
114
- this.turretGroup.rotation.y = absoluteTurretRotation - this.body.rotation.y;
115
-
116
- // ์ „์ฒด ํšŒ์ „๊ฐ์€ ์ ˆ๋Œ€ ๋ฐฉํ–ฅ ๊ทธ๋Œ€๋กœ ์œ ์ง€
117
- this.turretRotation = absoluteTurretRotation;
118
-
119
- // ํ”Œ๋ ˆ์ด์–ด ์ด์•Œ ์—…๋ฐ์ดํŠธ
120
- for (let i = this.bullets.length - 1; i >= 0; i--) {
121
- const bullet = this.bullets[i];
122
- bullet.position.add(bullet.velocity);
123
-
124
- if (
125
- Math.abs(bullet.position.x) > MAP_SIZE / 2 ||
126
- Math.abs(bullet.position.z) > MAP_SIZE / 2
127
- ) {
128
- scene.remove(bullet);
129
- this.bullets.splice(i, 1);
130
- }
131
- }
132
- }
133
 
 
 
 
134
 
 
 
 
 
 
 
 
 
 
135
 
136
  move(direction) {
137
  if (!this.body) return;
@@ -156,6 +154,24 @@ update(mouseX, mouseY, scene) {
156
  this.health -= damage;
157
  return this.health <= 0;
158
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  }
160
 
161
  // Enemy ํด๋ž˜์Šค
@@ -306,6 +322,11 @@ class Game {
306
  this.renderer.shadowMap.enabled = true;
307
  document.getElementById('gameContainer').appendChild(this.renderer.domElement);
308
 
 
 
 
 
 
309
  this.tank = new TankPlayer();
310
  this.enemies = [];
311
  this.particles = [];
@@ -334,579 +355,617 @@ class Game {
334
  }
335
 
336
  async initialize() {
337
- try {
338
- // ์•ˆ๊ฐœ ํšจ๊ณผ ์ œ๊ฑฐ
339
- this.scene.fog = null;
340
- this.scene.background = new THREE.Color(0x87CEEB);
341
-
342
- // ์ฃผ๋ณ€๊ด‘ ์„ค์ • - ๋” ๋ฐ๊ฒŒ
343
- const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
344
- this.scene.add(ambientLight);
345
-
346
- // ํƒœ์–‘๊ด‘ ์„ค์ •
347
- const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
348
- directionalLight.position.set(100, 100, 50);
349
- directionalLight.castShadow = true;
350
- directionalLight.shadow.mapSize.width = 1024;
351
- directionalLight.shadow.mapSize.height = 1024;
352
- this.scene.add(directionalLight);
353
-
354
- // ์‚ฌ๋ง‰ ์ง€ํ˜• ์ƒ์„ฑ (ํ—ฌ๋ฆฌ์ฝฅํ„ฐ ๊ฒŒ์ž„ ์Šคํƒ€์ผ)
355
- const groundGeometry = new THREE.PlaneGeometry(MAP_SIZE, MAP_SIZE, 100, 100);
356
- const groundMaterial = new THREE.MeshStandardMaterial({
357
- color: 0xD2B48C,
358
- roughness: 0.8,
359
- metalness: 0.2
360
- });
361
-
362
- const ground = new THREE.Mesh(groundGeometry, groundMaterial);
363
- ground.rotation.x = -Math.PI / 2;
364
- ground.receiveShadow = true;
365
-
366
- // ์ง€ํ˜•์˜ ๊ธฐ๋ณต ์ถ”๊ฐ€ (๋‹จ์ˆœํ™”)
367
- const vertices = ground.geometry.attributes.position.array;
368
- let seed = Math.random() * 100;
369
- for (let i = 0; i < vertices.length; i += 3) {
370
- vertices[i + 2] = Math.sin(vertices[i] * 0.01) * Math.cos(vertices[i + 1] * 0.01) * 20;
371
- }
372
-
373
- ground.geometry.attributes.position.needsUpdate = true;
374
- ground.geometry.computeVertexNormals();
375
-
376
- this.scene.add(ground);
377
-
378
- // ๋‚˜๋จธ์ง€ ์ดˆ๊ธฐํ™” ์ฝ”๋“œ
379
- await this.addDesertDecorations();
380
- await this.tank.initialize(this.scene, this.loader);
381
-
382
- if (!this.tank.isLoaded) {
383
- throw new Error('Tank loading failed');
384
- }
385
-
386
- const tankPosition = this.tank.getPosition();
387
- this.camera.position.set(
388
- tankPosition.x,
389
- tankPosition.y + 15,
390
- tankPosition.z - 30
391
- );
392
- this.camera.lookAt(tankPosition);
393
-
394
- this.isLoading = false;
395
- document.getElementById('loading').style.display = 'none';
396
-
397
- this.animate();
398
- this.spawnEnemies();
399
- this.startGameTimer();
400
-
401
- } catch (error) {
402
- console.error('Game initialization error:', error);
403
- this.handleLoadingError();
404
- }
405
- }
406
-
407
- // ์‚ฌ๋ง‰ ์žฅ์‹๋ฌผ ์ถ”๊ฐ€ ๋ฉ”์†Œ๋“œ
408
- async addDesertDecorations() {
409
- // ๋ฐ”์œ„ ์ƒ์„ฑ
410
- const rockGeometries = [
411
- new THREE.DodecahedronGeometry(3),
412
- new THREE.DodecahedronGeometry(2),
413
- new THREE.DodecahedronGeometry(4)
414
- ];
415
-
416
- const rockMaterial = new THREE.MeshStandardMaterial({
417
- color: 0x8B4513,
418
- roughness: 0.9,
419
- metalness: 0.1
420
- });
421
-
422
- for (let i = 0; i < 100; i++) {
423
- const rockGeometry = rockGeometries[Math.floor(Math.random() * rockGeometries.length)];
424
- const rock = new THREE.Mesh(rockGeometry, rockMaterial);
425
-
426
- rock.position.set(
427
- (Math.random() - 0.5) * MAP_SIZE * 0.9,
428
- Math.random() * 2,
429
- (Math.random() - 0.5) * MAP_SIZE * 0.9
430
- );
431
-
432
- rock.rotation.set(
433
- Math.random() * Math.PI,
434
- Math.random() * Math.PI,
435
- Math.random() * Math.PI
436
- );
437
-
438
- rock.scale.set(
439
- 1 + Math.random() * 0.5,
440
- 1 + Math.random() * 0.5,
441
- 1 + Math.random() * 0.5
442
- );
443
-
444
- rock.castShadow = true;
445
- rock.receiveShadow = true;
446
- this.scene.add(rock);
447
- }
448
 
449
- // ์„ ์ธ์žฅ ์ถ”๊ฐ€ (๊ฐ„๋‹จํ•œ geometry๋กœ ํ‘œํ˜„)
450
- const cactusGeometry = new THREE.CylinderGeometry(0.5, 0.7, 4, 8);
451
- const cactusMaterial = new THREE.MeshStandardMaterial({
452
- color: 0x2F4F2F,
453
- roughness: 0.8
454
- });
455
-
456
- for (let i = 0; i < 50; i++) {
457
- const cactus = new THREE.Mesh(cactusGeometry, cactusMaterial);
458
- cactus.position.set(
459
- (Math.random() - 0.5) * MAP_SIZE * 0.8,
460
- 2,
461
- (Math.random() - 0.5) * MAP_SIZE * 0.8
462
- );
463
- cactus.castShadow = true;
464
- cactus.receiveShadow = true;
465
- this.scene.add(cactus);
466
- }
467
- }
468
 
469
- setupEventListeners() {
470
- document.addEventListener('keydown', (event) => {
471
- if (this.isLoading || this.isGameOver) return;
472
- switch(event.code) {
473
- case 'KeyW': this.keys.forward = true; break;
474
- case 'KeyS': this.keys.backward = true; break;
475
- case 'KeyA': this.keys.left = true; break;
476
- case 'KeyD': this.keys.right = true; break;
477
  }
478
- });
479
 
480
- document.addEventListener('keyup', (event) => {
481
- if (this.isLoading || this.isGameOver) return;
482
- switch(event.code) {
483
- case 'KeyW': this.keys.forward = false; break;
484
- case 'KeyS': this.keys.backward = false; break;
485
- case 'KeyA': this.keys.left = false; break;
486
- case 'KeyD': this.keys.right = false; break;
487
- }
488
- });
489
 
490
- document.addEventListener('mousemove', (event) => {
491
- if (this.isLoading || this.isGameOver || !document.pointerLockElement) return;
492
-
493
- const movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0;
494
- // ์นด๋ฉ”๋ผ ํšŒ์ „ ๋ˆ„์ 
495
- this.mouse.x += movementX * 0.002;
496
- // X์ถ• ํšŒ์ „๋งŒ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ Y์ถ•์€ 0์œผ๋กœ ์œ ์ง€
497
- this.mouse.y = 0;
498
-
499
- // ๊ฐ๋„ ์ •๊ทœํ™” (-ฯ€ ~ ฯ€)
500
- while (this.mouse.x > Math.PI) this.mouse.x -= Math.PI * 2;
501
- while (this.mouse.x < -Math.PI) this.mouse.x += Math.PI * 2;
502
- });
503
-
504
-
505
- document.addEventListener('click', () => {
506
- if (!document.pointerLockElement) {
507
- document.body.requestPointerLock();
508
- } else if (!this.isGameOver) {
509
- const bullet = this.tank.shoot(this.scene);
510
- if (bullet) {
511
- // Shooting effects could be added here
512
- }
513
  }
514
- });
515
 
516
- document.addEventListener('pointerlockchange', () => {
517
- if (!document.pointerLockElement) {
518
- this.mouse.x = 0;
519
- this.mouse.y = 0;
520
- }
521
- });
 
 
 
 
 
 
 
 
522
 
523
- window.addEventListener('resize', () => {
524
- this.camera.aspect = window.innerWidth / window.innerHeight;
525
- this.camera.updateProjectionMatrix();
526
- this.renderer.setSize(window.innerWidth, window.innerHeight);
527
- });
528
  }
529
 
530
- handleMovement() {
531
- if (!this.tank.isLoaded || this.isGameOver) return;
532
-
533
- const direction = new THREE.Vector3();
534
-
535
- // ์ฐจ์ฒด ์ด๋™๊ณผ ํšŒ์ „์€ ์œ ์ง€
536
- if (this.keys.forward) direction.z += 1;
537
- if (this.keys.backward) direction.z -= 1;
538
- if (this.keys.left) this.tank.rotate(-1);
539
- if (this.keys.right) this.tank.rotate(1);
540
-
541
- if (direction.length() > 0) {
542
- direction.normalize();
543
- direction.applyEuler(this.tank.body.rotation);
544
- this.tank.move(direction);
545
- }
546
 
547
- // ํƒฑํฌ ์œ„์น˜ ๊ฐ€์ ธ์˜ค๊ธฐ
548
- const tankPos = this.tank.getPosition();
549
-
550
- // ์นด๋ฉ”๋ผ๋Š” ๋งˆ์šฐ์Šค X ํšŒ์ „์—๋งŒ ๋”ฐ๋ผ๊ฐ
551
- const cameraDistance = 30;
552
- const cameraHeight = 15;
553
- const cameraAngle = this.mouse.x + Math.PI; // ํ•ญ์ƒ ํฌํƒ‘์˜ ๋’ค์ชฝ์— ์œ„์น˜
554
-
555
- const cameraX = tankPos.x + Math.sin(cameraAngle) * cameraDistance;
556
- const cameraZ = tankPos.z + Math.cos(cameraAngle) * cameraDistance;
557
-
558
- this.camera.position.set(
559
- cameraX,
560
- tankPos.y + cameraHeight,
561
- cameraZ
562
- );
563
-
564
- // ์นด๋ฉ”๋ผ๊ฐ€ ํƒฑํฌ๋ฅผ ๋ฐ”๋ผ๋ณด๋„๋ก ์„ค์ •
565
- const lookAtPoint = new THREE.Vector3(
566
- tankPos.x,
567
- tankPos.y + 2,
568
- tankPos.z
569
- );
570
- this.camera.lookAt(lookAtPoint);
571
- }
572
 
573
- //this.camera.lookAt(lookAtPoint);
 
574
 
 
 
 
575
 
576
- createBuildings() {
577
- const buildingTypes = [
578
- { width: 10, height: 30, depth: 10, color: 0x808080 },
579
- { width: 15, height: 40, depth: 15, color: 0x606060 },
580
- { width: 20, height: 50, depth: 20, color: 0x404040 }
581
- ];
582
 
583
- for (let i = 0; i < BUILDING_COUNT; i++) {
584
- const type = buildingTypes[Math.floor(Math.random() * buildingTypes.length)];
585
- const building = this.createBuilding(type);
586
-
587
- let position;
588
- let attempts = 0;
589
- do {
590
- position = new THREE.Vector3(
591
- (Math.random() - 0.5) * (MAP_SIZE - type.width),
592
- type.height / 2,
593
- (Math.random() - 0.5) * (MAP_SIZE - type.depth)
594
  );
595
- attempts++;
596
- } while (this.checkBuildingCollision(position, type) && attempts < 50);
597
 
598
- if (attempts < 50) {
599
- building.position.copy(position);
600
- this.buildings.push(building);
601
- this.scene.add(building);
 
 
 
 
 
 
 
602
  }
603
- }
604
- return Promise.resolve();
605
- }
606
-
607
- createBuilding(type) {
608
- const geometry = new THREE.BoxGeometry(type.width, type.height, type.depth);
609
- const material = new THREE.MeshPhongMaterial({
610
- color: type.color,
611
- emissive: 0x222222,
612
- specular: 0x111111,
613
- shininess: 30
614
  });
615
- const building = new THREE.Mesh(geometry, material);
616
- building.castShadow = true;
617
- building.receiveShadow = true;
618
- return building;
619
- }
620
-
621
- checkBuildingCollision(position, type) {
622
- const margin = 5;
623
- const bbox = new THREE.Box3(
624
- new THREE.Vector3(
625
- position.x - (type.width / 2 + margin),
626
- 0,
627
- position.z - (type.depth / 2 + margin)
628
- ),
629
- new THREE.Vector3(
630
- position.x + (type.width / 2 + margin),
631
- type.height,
632
- position.z + (type.depth / 2 + margin)
633
- )
634
- );
635
 
636
- return this.buildings.some(building => {
637
- const buildingBox = new THREE.Box3().setFromObject(building);
638
- return bbox.intersectsBox(buildingBox);
639
- });
640
  }
641
 
642
- handleLoadingError() {
643
- this.isLoading = false;
644
- const loadingElement = document.getElementById('loading');
645
- if (loadingElement) {
646
- loadingElement.innerHTML = `
647
- <div class="loading-text" style="color: red;">
648
- Loading failed. Please refresh the page.
649
- </div>
650
- `;
651
- }
652
- }
653
-
654
- startGameTimer() {
655
- if (this.gameTimer) {
656
- clearInterval(this.gameTimer);
657
- }
658
-
659
- this.gameTimer = setInterval(() => {
660
- if (this.isLoading || this.isGameOver) {
661
- clearInterval(this.gameTimer);
662
- return;
663
- }
664
-
665
- this.gameTime--;
666
- document.getElementById('time').textContent = `Time: ${this.gameTime}s`;
667
-
668
- if (this.gameTime <= 0) {
669
- clearInterval(this.gameTimer);
670
- this.endGame();
671
- }
672
- }, 1000);
673
- }
674
-
675
- spawnEnemies() {
676
- const spawnEnemy = () => {
677
- if (this.enemies.length < 3 && !this.isGameOver) { // ์ตœ๋Œ€ 3๋Œ€๋กœ ์ œํ•œ
678
- const position = this.getValidEnemySpawnPosition();
679
- if (position) {
680
- const type = Math.random() < 0.7 ? 'tank' : 'heavy';
681
- const enemy = new Enemy(this.scene, position, type);
682
- enemy.initialize(this.loader);
683
- this.enemies.push(enemy);
684
- }
685
- }
686
- if (!this.isGameOver) {
687
- setTimeout(spawnEnemy, 10000); // 10์ดˆ๋งˆ๋‹ค ์Šคํฐ
688
- }
689
- };
690
-
691
- spawnEnemy();
692
- }
693
-
694
-
695
- getValidEnemySpawnPosition() {
696
- const margin = 20;
697
- let position;
698
- let attempts = 0;
699
- const maxAttempts = 50;
700
-
701
- do {
702
- position = new THREE.Vector3(
703
- (Math.random() - 0.5) * (MAP_SIZE - margin * 2),
704
- ENEMY_GROUND_HEIGHT,
705
- (Math.random() - 0.5) * (MAP_SIZE - margin * 2)
706
- );
707
-
708
- const distanceToPlayer = position.distanceTo(this.tank.getPosition());
709
- if (distanceToPlayer < 100) continue;
710
-
711
- let collisionFound = false;
712
- for (const building of this.buildings) {
713
- const buildingBox = new THREE.Box3().setFromObject(building);
714
- if (buildingBox.containsPoint(position)) {
715
- collisionFound = true;
716
- break;
717
- }
718
- }
719
 
720
- if (!collisionFound) return position;
 
 
 
 
721
 
722
- attempts++;
723
- } while (attempts < maxAttempts);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
724
 
725
- return null;
726
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
727
 
728
- updateParticles() {
729
- for (let i = this.particles.length - 1; i >= 0; i--) {
730
- const particle = this.particles[i];
731
- if (!particle.update()) {
732
- particle.destroy(this.scene);
733
- this.particles.splice(i, 1);
734
- }
735
- }
736
- }
 
 
 
 
 
 
 
 
 
 
 
737
 
738
- createExplosion(position) {
739
- for (let i = 0; i < PARTICLE_COUNT; i++) {
740
- this.particles.push(new Particle(this.scene, position));
741
- }
742
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
743
 
744
- checkCollisions() {
745
- if (this.isLoading || !this.tank.isLoaded) return;
746
 
747
- const tankPosition = this.tank.getPosition();
 
 
 
 
748
 
749
- // ์  ์ด์•Œ๊ณผ ํ”Œ๋ ˆ์ด์–ด ํƒฑํฌ ์ถฉ๋Œ ์ฒดํฌ
750
- this.enemies.forEach(enemy => {
751
- if (!enemy.mesh || !enemy.isLoaded) return;
 
 
752
 
753
- enemy.bullets.forEach(bullet => {
754
- const distance = bullet.position.distanceTo(tankPosition);
755
- if (distance < 1) {
756
- if (this.tank.takeDamage(10)) {
757
- this.endGame();
758
- }
759
- this.scene.remove(bullet);
760
- enemy.bullets = enemy.bullets.filter(b => b !== bullet);
761
-
762
- this.createExplosion(bullet.position);
763
- document.getElementById('health').style.width =
764
- `${(this.tank.health / MAX_HEALTH) * 100}%`;
765
- }
766
- });
767
- });
 
 
 
 
 
 
 
 
 
 
768
 
769
- // ํ”Œ๋ ˆ์ด์–ด ์ด์•Œ๊ณผ ์  ์ถฉ๋Œ ์ฒดํฌ
770
- this.tank.bullets.forEach((bullet, bulletIndex) => {
771
- this.enemies.forEach((enemy, enemyIndex) => {
772
- if (!enemy.mesh || !enemy.isLoaded) return;
773
-
774
- const distance = bullet.position.distanceTo(enemy.mesh.position);
775
- if (distance < 2) {
776
- if (enemy.takeDamage(50)) {
777
- enemy.destroy();
778
- this.enemies.splice(enemyIndex, 1);
779
- this.score += 100;
780
- document.getElementById('score').textContent = `Score: ${this.score}`;
781
- }
782
- this.scene.remove(bullet);
783
- this.tank.bullets.splice(bulletIndex, 1);
784
- this.createExplosion(bullet.position);
785
- }
786
- });
787
- });
 
 
 
 
 
 
 
 
 
 
 
788
 
789
- // ํ”Œ๋ ˆ์ด์–ด ํƒฑํฌ์™€ ๊ฑด๋ฌผ ์ถฉ๋Œ ์ฒดํฌ
790
- const tankBoundingBox = new THREE.Box3().setFromObject(this.tank.body);
791
- for (const building of this.buildings) {
792
- const buildingBox = new THREE.Box3().setFromObject(building);
793
- if (tankBoundingBox.intersectsBox(buildingBox)) {
794
- this.tank.body.position.copy(this.previousTankPosition);
795
- break;
796
- }
797
- }
798
-
799
- // ์ด์ „ ์œ„์น˜ ์ €์žฅ
800
- this.previousTankPosition.copy(this.tank.body.position);
801
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
802
 
803
- endGame() {
804
- if (this.isGameOver) return;
805
-
806
- this.isGameOver = true;
807
-
808
- if (this.gameTimer) {
809
- clearInterval(this.gameTimer);
810
- }
 
 
 
811
 
812
- if (this.animationFrameId) {
813
- cancelAnimationFrame(this.animationFrameId);
814
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
815
 
816
- document.exitPointerLock();
817
-
818
- const gameOverDiv = document.createElement('div');
819
- gameOverDiv.style.position = 'absolute';
820
- gameOverDiv.style.top = '50%';
821
- gameOverDiv.style.left = '50%';
822
- gameOverDiv.style.transform = 'translate(-50%, -50%)';
823
- gameOverDiv.style.color = '#0f0';
824
- gameOverDiv.style.fontSize = '48px';
825
- gameOverDiv.style.backgroundColor = 'rgba(0, 20, 0, 0.7)';
826
- gameOverDiv.style.padding = '20px';
827
- gameOverDiv.style.borderRadius = '10px';
828
- gameOverDiv.style.textAlign = 'center';
829
- gameOverDiv.innerHTML = `
830
- Game Over<br>
831
- Score: ${this.score}<br>
832
- Time Survived: ${GAME_DURATION - this.gameTime}s<br>
833
- <button onclick="location.reload()"
834
- style="font-size: 24px; padding: 10px; margin-top: 20px;
835
- cursor: pointer; background: #0f0; border: none;
836
- color: black; border-radius: 5px;">
837
- Play Again
838
- </button>
839
- `;
840
- document.body.appendChild(gameOverDiv);
841
- }
842
 
843
- updateUI() {
844
- if (!this.isGameOver) {
845
- const healthBar = document.getElementById('health');
846
- if (healthBar) {
847
- healthBar.style.width = `${(this.tank.health / MAX_HEALTH) * 100}%`;
848
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
849
 
850
- const timeElement = document.getElementById('time');
851
- if (timeElement) {
852
- timeElement.textContent = `Time: ${this.gameTime}s`;
853
- }
 
854
 
855
- const scoreElement = document.getElementById('score');
856
- if (scoreElement) {
857
- scoreElement.textContent = `Score: ${this.score}`;
858
- }
859
- }
860
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
861
 
862
- animate() {
863
- if (this.isGameOver) {
864
- if (this.animationFrameId) {
865
- cancelAnimationFrame(this.animationFrameId);
866
- }
867
- return;
868
- }
869
 
870
- this.animationFrameId = requestAnimationFrame(() => this.animate());
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
871
 
872
- const currentTime = performance.now();
873
- const deltaTime = (currentTime - this.lastTime) / 1000;
874
- this.lastTime = currentTime;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
875
 
876
- if (!this.isLoading) {
877
- this.handleMovement();
878
- this.tank.update(this.mouse.x, this.mouse.y, this.scene); // scene ๊ฐ์ฒด ์ „๋‹ฌ
879
-
880
- const tankPosition = this.tank.getPosition();
881
- this.enemies.forEach(enemy => {
882
- enemy.update(tankPosition);
883
-
884
- if (enemy.isLoaded && enemy.mesh.position.distanceTo(tankPosition) < ENEMY_CONFIG.ATTACK_RANGE) {
885
- enemy.shoot(tankPosition);
886
- }
887
- });
888
 
889
- this.updateParticles();
890
- this.checkCollisions();
891
- this.updateUI();
892
- }
893
-
894
- this.renderer.render(this.scene, this.camera);
895
- }
896
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
897
 
898
  // Start game
899
  window.startGame = function() {
900
- document.getElementById('startScreen').style.display = 'none';
901
- document.body.requestPointerLock();
902
-
903
- // Create new game instance or restart existing one
904
- if (!window.gameInstance) {
905
- window.gameInstance = new Game();
906
- }
907
  };
908
 
909
  // Initialize game
910
  document.addEventListener('DOMContentLoaded', () => {
911
- const game = new Game();
912
  });
 
32
  this.turretGroup = new THREE.Group();
33
  this.health = MAX_HEALTH;
34
  this.isLoaded = false;
35
+ this.ammo = 1; // ๋ณ€๊ฒฝ: 10 -> 1
36
+ this.maxAmmo = 1; // ์ถ”๊ฐ€: ์ตœ๋Œ€ ํฌํƒ„ ์ˆ˜
37
+ this.isReloading = false; // ์ถ”๊ฐ€: ์žฌ์žฅ์ „ ์ƒํƒœ
38
+ this.reloadTime = 3000; // ์ถ”๊ฐ€: 3์ดˆ ์žฌ์žฅ์ „ ์‹œ๊ฐ„
39
  this.lastShootTime = 0;
 
40
  this.bullets = [];
41
  }
42
 
 
69
 
70
  scene.add(this.body);
71
  this.isLoaded = true;
72
+ this.updateAmmoDisplay(); // ์ถ”๊ฐ€: ์ดˆ๊ธฐ ํƒ„์•ฝ ํ‘œ์‹œ
73
 
74
  } catch (error) {
75
  console.error('Error loading tank models:', error);
 
78
  }
79
 
80
  shoot(scene) {
81
+ if (this.isReloading || this.ammo <= 0) return null;
82
+
83
+ const bullet = this.createBullet(scene);
84
+ if (bullet) {
85
+ this.ammo--;
86
+ this.updateAmmoDisplay();
87
+ this.startReload();
88
+ }
89
+ return bullet;
90
+ }
91
 
92
+ createBullet(scene) {
93
  const bulletGeometry = new THREE.SphereGeometry(0.2);
94
  const bulletMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
95
  const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
 
106
 
107
  scene.add(bullet);
108
  this.bullets.push(bullet);
 
 
109
 
 
 
110
  return bullet;
111
  }
112
 
113
+ update(mouseX, mouseY, scene) {
114
+ if (!this.body || !this.turretGroup) return;
 
115
 
116
+ const absoluteTurretRotation = mouseX;
117
+ this.turretGroup.rotation.y = absoluteTurretRotation - this.body.rotation.y;
118
+ this.turretRotation = absoluteTurretRotation;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
+ for (let i = this.bullets.length - 1; i >= 0; i--) {
121
+ const bullet = this.bullets[i];
122
+ bullet.position.add(bullet.velocity);
123
 
124
+ if (
125
+ Math.abs(bullet.position.x) > MAP_SIZE / 2 ||
126
+ Math.abs(bullet.position.z) > MAP_SIZE / 2
127
+ ) {
128
+ scene.remove(bullet);
129
+ this.bullets.splice(i, 1);
130
+ }
131
+ }
132
+ }
133
 
134
  move(direction) {
135
  if (!this.body) return;
 
154
  this.health -= damage;
155
  return this.health <= 0;
156
  }
157
+
158
+ // ์ƒˆ๋กœ ์ถ”๊ฐ€๋œ ๋ฉ”์„œ๋“œ๋“ค
159
+ startReload() {
160
+ this.isReloading = true;
161
+ const reloadingText = document.getElementById('reloadingText');
162
+ reloadingText.style.display = 'block';
163
+
164
+ setTimeout(() => {
165
+ this.ammo = this.maxAmmo;
166
+ this.isReloading = false;
167
+ reloadingText.style.display = 'none';
168
+ this.updateAmmoDisplay();
169
+ }, this.reloadTime);
170
+ }
171
+
172
+ updateAmmoDisplay() {
173
+ document.getElementById('ammoDisplay').textContent = `APFSDS: ${this.ammo}/${this.maxAmmo}`;
174
+ }
175
  }
176
 
177
  // Enemy ํด๋ž˜์Šค
 
322
  this.renderer.shadowMap.enabled = true;
323
  document.getElementById('gameContainer').appendChild(this.renderer.domElement);
324
 
325
+ // ๋ ˆ์ด๋” ๊ด€๋ จ ์†์„ฑ ์ถ”๊ฐ€
326
+ this.radarUpdateInterval = 100; // 100ms๋งˆ๋‹ค ๋ ˆ์ด๋” ์—…๋ฐ์ดํŠธ
327
+ this.lastRadarUpdate = 0;
328
+ this.radarRange = 200; // ๋ ˆ์ด๋” ๊ฐ์ง€ ๋ฒ”์œ„
329
+
330
  this.tank = new TankPlayer();
331
  this.enemies = [];
332
  this.particles = [];
 
355
  }
356
 
357
  async initialize() {
358
+ try {
359
+ // ์•ˆ๊ฐœ ํšจ๊ณผ ์ œ๊ฑฐ
360
+ this.scene.fog = null;
361
+ this.scene.background = new THREE.Color(0x87CEEB);
362
+
363
+ // ์ฃผ๋ณ€๊ด‘ ์„ค์ • - ๋” ๋ฐ๊ฒŒ
364
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
365
+ this.scene.add(ambientLight);
366
+
367
+ // ํƒœ์–‘๊ด‘ ์„ค์ •
368
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
369
+ directionalLight.position.set(100, 100, 50);
370
+ directionalLight.castShadow = true;
371
+ directionalLight.shadow.mapSize.width = 1024;
372
+ directionalLight.shadow.mapSize.height = 1024;
373
+ this.scene.add(directionalLight);
374
+
375
+ // ์‚ฌ๋ง‰ ์ง€ํ˜• ์ƒ์„ฑ
376
+ const groundGeometry = new THREE.PlaneGeometry(MAP_SIZE, MAP_SIZE, 100, 100);
377
+ const groundMaterial = new THREE.MeshStandardMaterial({
378
+ color: 0xD2B48C,
379
+ roughness: 0.8,
380
+ metalness: 0.2
381
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
 
383
+ const ground = new THREE.Mesh(groundGeometry, groundMaterial);
384
+ ground.rotation.x = -Math.PI / 2;
385
+ ground.receiveShadow = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386
 
387
+ // ์ง€ํ˜•์˜ ๊ธฐ๋ณต ์ถ”๊ฐ€
388
+ const vertices = ground.geometry.attributes.position.array;
389
+ let seed = Math.random() * 100;
390
+ for (let i = 0; i < vertices.length; i += 3) {
391
+ vertices[i + 2] = Math.sin(vertices[i] * 0.01) * Math.cos(vertices[i + 1] * 0.01) * 20;
 
 
 
392
  }
 
393
 
394
+ ground.geometry.attributes.position.needsUpdate = true;
395
+ ground.geometry.computeVertexNormals();
396
+
397
+ this.scene.add(ground);
 
 
 
 
 
398
 
399
+ await this.addDesertDecorations();
400
+ await this.tank.initialize(this.scene, this.loader);
401
+ if (!this.tank.isLoaded) {
402
+ throw new Error('Tank loading failed');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
403
  }
 
404
 
405
+ const tankPosition = this.tank.getPosition();
406
+ this.camera.position.set(
407
+ tankPosition.x,
408
+ tankPosition.y + 15,
409
+ tankPosition.z - 30
410
+ );
411
+ this.camera.lookAt(tankPosition);
412
+
413
+ this.isLoading = false;
414
+ document.getElementById('loading').style.display = 'none';
415
+
416
+ this.animate();
417
+ this.spawnEnemies();
418
+ this.startGameTimer();
419
 
420
+ } catch (error) {
421
+ console.error('Game initialization error:', error);
422
+ this.handleLoadingError();
423
+ }
 
424
  }
425
 
426
+ // ๋ ˆ์ด๋” ์—…๋ฐ์ดํŠธ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€
427
+ updateRadar() {
428
+ const currentTime = Date.now();
429
+ if (currentTime - this.lastRadarUpdate < this.radarUpdateInterval) return;
430
+
431
+ const radar = document.getElementById('radar');
432
+ const radarRect = radar.getBoundingClientRect();
433
+ const radarCenter = {
434
+ x: radarRect.width / 2,
435
+ y: radarRect.height / 2
436
+ };
 
 
 
 
 
437
 
438
+ // ๊ธฐ์กด ์  ๋„ํŠธ ์ œ๊ฑฐ
439
+ const oldDots = radar.getElementsByClassName('enemy-dot');
440
+ while (oldDots[0]) {
441
+ oldDots[0].remove();
442
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
443
 
444
+ // ํƒฑํฌ ์œ„์น˜ ๊ฐ€์ ธ์˜ค๊ธฐ
445
+ const tankPos = this.tank.getPosition();
446
 
447
+ // ๋ชจ๋“  ์ ์— ๋Œ€ํ•ด ๋ ˆ์ด๋”์— ํ‘œ์‹œ
448
+ this.enemies.forEach(enemy => {
449
+ if (!enemy.mesh || !enemy.isLoaded) return;
450
 
451
+ const enemyPos = enemy.mesh.position;
452
+ const distance = tankPos.distanceTo(enemyPos);
 
 
 
 
453
 
454
+ // ๋ ˆ์ด๋” ๋ฒ”์œ„ ๋‚ด์— ์žˆ๋Š” ๊ฒฝ์šฐ๋งŒ ํ‘œ์‹œ
455
+ if (distance <= this.radarRange) {
456
+ // ํƒฑํฌ ๊ธฐ์ค€ ์ƒ๋Œ€ ๊ฐ๋„ ๊ณ„์‚ฐ
457
+ const angle = Math.atan2(
458
+ enemyPos.x - tankPos.x,
459
+ enemyPos.z - tankPos.z
 
 
 
 
 
460
  );
 
 
461
 
462
+ // ์ƒ๋Œ€ ๊ฑฐ๋ฆฌ๋ฅผ ๋ ˆ์ด๋” ํฌ๊ธฐ์— ๋งž๊ฒŒ ์Šค์ผ€์ผ๋ง
463
+ const relativeDistance = distance / this.radarRange;
464
+ const dotX = radarCenter.x + Math.sin(angle) * (radarCenter.x * relativeDistance);
465
+ const dotY = radarCenter.y + Math.cos(angle) * (radarCenter.y * relativeDistance);
466
+
467
+ // ์  ๋„ํŠธ ์ƒ์„ฑ ๋ฐ ์ถ”๊ฐ€
468
+ const dot = document.createElement('div');
469
+ dot.className = 'enemy-dot';
470
+ dot.style.left = `${dotX}px`;
471
+ dot.style.top = `${dotY}px`;
472
+ radar.appendChild(dot);
473
  }
 
 
 
 
 
 
 
 
 
 
 
474
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
475
 
476
+ this.lastRadarUpdate = currentTime;
 
 
 
477
  }
478
 
479
+ async addDesertDecorations() {
480
+ // ๋ฐ”์œ„ ์ƒ์„ฑ
481
+ const rockGeometries = [
482
+ new THREE.DodecahedronGeometry(3),
483
+ new THREE.DodecahedronGeometry(2),
484
+ new THREE.DodecahedronGeometry(4)
485
+ ];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
486
 
487
+ const rockMaterial = new THREE.MeshStandardMaterial({
488
+ color: 0x8B4513,
489
+ roughness: 0.9,
490
+ metalness: 0.1
491
+ });
492
 
493
+ for (let i = 0; i < 100; i++) {
494
+ const rockGeometry = rockGeometries[Math.floor(Math.random() * rockGeometries.length)];
495
+ const rock = new THREE.Mesh(rockGeometry, rockMaterial);
496
+ rock.position.set(
497
+ (Math.random() - 0.5) * MAP_SIZE * 0.9,
498
+ Math.random() * 2,
499
+ (Math.random() - 0.5) * MAP_SIZE * 0.9
500
+ );
501
+
502
+ rock.rotation.set(
503
+ Math.random() * Math.PI,
504
+ Math.random() * Math.PI,
505
+ Math.random() * Math.PI
506
+ );
507
+
508
+ rock.scale.set(
509
+ 1 + Math.random() * 0.5,
510
+ 1 + Math.random() * 0.5,
511
+ 1 + Math.random() * 0.5
512
+ );
513
+
514
+ rock.castShadow = true;
515
+ rock.receiveShadow = true;
516
+ this.scene.add(rock);
517
+ }
518
 
519
+ // ์„ ์ธ์žฅ ์ถ”๊ฐ€ (๊ฐ„๋‹จํ•œ geometry๋กœ ํ‘œํ˜„)
520
+ const cactusGeometry = new THREE.CylinderGeometry(0.5, 0.7, 4, 8);
521
+ const cactusMaterial = new THREE.MeshStandardMaterial({
522
+ color: 0x2F4F2F,
523
+ roughness: 0.8
524
+ });
525
+
526
+ for (let i = 0; i < 50; i++) {
527
+ const cactus = new THREE.Mesh(cactusGeometry, cactusMaterial);
528
+ cactus.position.set(
529
+ (Math.random() - 0.5) * MAP_SIZE * 0.8,
530
+ 2,
531
+ (Math.random() - 0.5) * MAP_SIZE * 0.8
532
+ );
533
+ cactus.castShadow = true;
534
+ cactus.receiveShadow = true;
535
+ this.scene.add(cactus);
536
+ }
537
+ }
538
 
539
+ setupEventListeners() {
540
+ document.addEventListener('keydown', (event) => {
541
+ if (this.isLoading || this.isGameOver) return;
542
+ switch(event.code) {
543
+ case 'KeyW': this.keys.forward = true; break;
544
+ case 'KeyS': this.keys.backward = true; break;
545
+ case 'KeyA': this.keys.left = true; break;
546
+ case 'KeyD': this.keys.right = true; break;
547
+ }
548
+ });
549
+
550
+ document.addEventListener('keyup', (event) => {
551
+ if (this.isLoading || this.isGameOver) return;
552
+ switch(event.code) {
553
+ case 'KeyW': this.keys.forward = false; break;
554
+ case 'KeyS': this.keys.backward = false; break;
555
+ case 'KeyA': this.keys.left = false; break;
556
+ case 'KeyD': this.keys.right = false; break;
557
+ }
558
+ });
559
 
560
+ document.addEventListener('mousemove', (event) => {
561
+ if (this.isLoading || this.isGameOver || !document.pointerLockElement) return;
562
+
563
+ const movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0;
564
+ this.mouse.x += movementX * 0.002;
565
+ this.mouse.y = 0;
566
+
567
+ while (this.mouse.x > Math.PI) this.mouse.x -= Math.PI * 2;
568
+ while (this.mouse.x < -Math.PI) this.mouse.x += Math.PI * 2;
569
+ });
570
+
571
+ document.addEventListener('click', () => {
572
+ if (!document.pointerLockElement) {
573
+ document.body.requestPointerLock();
574
+ } else if (!this.isGameOver) {
575
+ const bullet = this.tank.shoot(this.scene);
576
+ if (bullet) {
577
+ // Shooting effects could be added here
578
+ }
579
+ }
580
+ });
581
+
582
+ document.addEventListener('pointerlockchange', () => {
583
+ if (!document.pointerLockElement) {
584
+ this.mouse.x = 0;
585
+ this.mouse.y = 0;
586
+ }
587
+ });
588
+
589
+ window.addEventListener('resize', () => {
590
+ this.camera.aspect = window.innerWidth / window.innerHeight;
591
+ this.camera.updateProjectionMatrix();
592
+ this.renderer.setSize(window.innerWidth, window.innerHeight);
593
+ });
594
+ }
595
+ handleMovement() {
596
+ if (!this.tank.isLoaded || this.isGameOver) return;
597
 
598
+ const direction = new THREE.Vector3();
 
599
 
600
+ // ์ฐจ์ฒด ์ด๋™๊ณผ ํšŒ์ „์€ ์œ ์ง€
601
+ if (this.keys.forward) direction.z += 1;
602
+ if (this.keys.backward) direction.z -= 1;
603
+ if (this.keys.left) this.tank.rotate(-1);
604
+ if (this.keys.right) this.tank.rotate(1);
605
 
606
+ if (direction.length() > 0) {
607
+ direction.normalize();
608
+ direction.applyEuler(this.tank.body.rotation);
609
+ this.tank.move(direction);
610
+ }
611
 
612
+ // ํƒฑํฌ ์œ„์น˜ ๊ฐ€์ ธ์˜ค๊ธฐ
613
+ const tankPos = this.tank.getPosition();
614
+
615
+ // ์นด๋ฉ”๋ผ๋Š” ๋งˆ์šฐ์Šค X ํšŒ์ „์—๋งŒ ๋”ฐ๋ผ๊ฐ
616
+ const cameraDistance = 30;
617
+ const cameraHeight = 15;
618
+ const cameraAngle = this.mouse.x + Math.PI; // ํ•ญ์ƒ ํฌํƒ‘์˜ ๋’ค์ชฝ์— ์œ„์น˜
619
+
620
+ const cameraX = tankPos.x + Math.sin(cameraAngle) * cameraDistance;
621
+ const cameraZ = tankPos.z + Math.cos(cameraAngle) * cameraDistance;
622
+
623
+ this.camera.position.set(
624
+ cameraX,
625
+ tankPos.y + cameraHeight,
626
+ cameraZ
627
+ );
628
+
629
+ // ์นด๋ฉ”๋ผ๊ฐ€ ํƒฑํฌ๋ฅผ ๋ฐ”๋ผ๋ณด๋„๋ก ์„ค์ •
630
+ const lookAtPoint = new THREE.Vector3(
631
+ tankPos.x,
632
+ tankPos.y + 2,
633
+ tankPos.z
634
+ );
635
+ this.camera.lookAt(lookAtPoint);
636
+ }
637
 
638
+ createBuildings() {
639
+ const buildingTypes = [
640
+ { width: 10, height: 30, depth: 10, color: 0x808080 },
641
+ { width: 15, height: 40, depth: 15, color: 0x606060 },
642
+ { width: 20, height: 50, depth: 20, color: 0x404040 }
643
+ ];
644
+
645
+ for (let i = 0; i < BUILDING_COUNT; i++) {
646
+ const type = buildingTypes[Math.floor(Math.random() * buildingTypes.length)];
647
+ const building = this.createBuilding(type);
648
+
649
+ let position;
650
+ let attempts = 0;
651
+ do {
652
+ position = new THREE.Vector3(
653
+ (Math.random() - 0.5) * (MAP_SIZE - type.width),
654
+ type.height / 2,
655
+ (Math.random() - 0.5) * (MAP_SIZE - type.depth)
656
+ );
657
+ attempts++;
658
+ } while (this.checkBuildingCollision(position, type) && attempts < 50);
659
+
660
+ if (attempts < 50) {
661
+ building.position.copy(position);
662
+ this.buildings.push(building);
663
+ this.scene.add(building);
664
+ }
665
+ }
666
+ return Promise.resolve();
667
+ }
668
 
669
+ createBuilding(type) {
670
+ const geometry = new THREE.BoxGeometry(type.width, type.height, type.depth);
671
+ const material = new THREE.MeshPhongMaterial({
672
+ color: type.color,
673
+ emissive: 0x222222,
674
+ specular: 0x111111,
675
+ shininess: 30
676
+ });
677
+ const building = new THREE.Mesh(geometry, material);
678
+ building.castShadow = true;
679
+ building.receiveShadow = true;
680
+ return building;
681
+ }
682
+ checkBuildingCollision(position, type) {
683
+ const margin = 5;
684
+ const bbox = new THREE.Box3(
685
+ new THREE.Vector3(
686
+ position.x - (type.width / 2 + margin),
687
+ 0,
688
+ position.z - (type.depth / 2 + margin)
689
+ ),
690
+ new THREE.Vector3(
691
+ position.x + (type.width / 2 + margin),
692
+ type.height,
693
+ position.z + (type.depth / 2 + margin)
694
+ )
695
+ );
696
+
697
+ return this.buildings.some(building => {
698
+ const buildingBox = new THREE.Box3().setFromObject(building);
699
+ return bbox.intersectsBox(buildingBox);
700
+ });
701
+ }
702
 
703
+ handleLoadingError() {
704
+ this.isLoading = false;
705
+ const loadingElement = document.getElementById('loading');
706
+ if (loadingElement) {
707
+ loadingElement.innerHTML = `
708
+ <div class="loading-text" style="color: red;">
709
+ Loading failed. Please refresh the page.
710
+ </div>
711
+ `;
712
+ }
713
+ }
714
 
715
+ startGameTimer() {
716
+ if (this.gameTimer) {
717
+ clearInterval(this.gameTimer);
718
+ }
719
+
720
+ this.gameTimer = setInterval(() => {
721
+ if (this.isLoading || this.isGameOver) {
722
+ clearInterval(this.gameTimer);
723
+ return;
724
+ }
725
+
726
+ this.gameTime--;
727
+ document.getElementById('time').textContent = `Time: ${this.gameTime}s`;
728
+
729
+ if (this.gameTime <= 0) {
730
+ clearInterval(this.gameTimer);
731
+ this.endGame();
732
+ }
733
+ }, 1000);
734
+ }
735
 
736
+ spawnEnemies() {
737
+ const spawnEnemy = () => {
738
+ if (this.enemies.length < 3 && !this.isGameOver) { // ์ตœ๋Œ€ 3๋Œ€๋กœ ์ œํ•œ
739
+ const position = this.getValidEnemySpawnPosition();
740
+ if (position) {
741
+ const type = Math.random() < 0.7 ? 'tank' : 'heavy';
742
+ const enemy = new Enemy(this.scene, position, type);
743
+ enemy.initialize(this.loader);
744
+ this.enemies.push(enemy);
745
+ }
746
+ }
747
+ if (!this.isGameOver) {
748
+ setTimeout(spawnEnemy, 10000); // 10์ดˆ๋งˆ๋‹ค ์Šคํฐ
749
+ }
750
+ };
751
+
752
+ spawnEnemy();
753
+ }
 
 
 
 
 
 
 
 
754
 
755
+ getValidEnemySpawnPosition() {
756
+ const margin = 20;
757
+ let position;
758
+ let attempts = 0;
759
+ const maxAttempts = 50;
760
+
761
+ do {
762
+ position = new THREE.Vector3(
763
+ (Math.random() - 0.5) * (MAP_SIZE - margin * 2),
764
+ ENEMY_GROUND_HEIGHT,
765
+ (Math.random() - 0.5) * (MAP_SIZE - margin * 2)
766
+ );
767
+
768
+ const distanceToPlayer = position.distanceTo(this.tank.getPosition());
769
+ if (distanceToPlayer < 100) continue;
770
+
771
+ let collisionFound = false;
772
+ for (const building of this.buildings) {
773
+ const buildingBox = new THREE.Box3().setFromObject(building);
774
+ if (buildingBox.containsPoint(position)) {
775
+ collisionFound = true;
776
+ break;
777
+ }
778
+ }
779
+
780
+ if (!collisionFound) return position;
781
+
782
+ attempts++;
783
+ } while (attempts < maxAttempts);
784
+
785
+ return null;
786
+ }
787
+ updateParticles() {
788
+ for (let i = this.particles.length - 1; i >= 0; i--) {
789
+ const particle = this.particles[i];
790
+ if (!particle.update()) {
791
+ particle.destroy(this.scene);
792
+ this.particles.splice(i, 1);
793
+ }
794
+ }
795
+ }
796
 
797
+ createExplosion(position) {
798
+ for (let i = 0; i < PARTICLE_COUNT; i++) {
799
+ this.particles.push(new Particle(this.scene, position));
800
+ }
801
+ }
802
 
803
+ checkCollisions() {
804
+ if (this.isLoading || !this.tank.isLoaded) return;
805
+
806
+ const tankPosition = this.tank.getPosition();
807
+
808
+ // ์  ์ด์•Œ๊ณผ ํ”Œ๋ ˆ์ด์–ด ํƒฑํฌ ์ถฉ๋Œ ์ฒดํฌ
809
+ this.enemies.forEach(enemy => {
810
+ if (!enemy.mesh || !enemy.isLoaded) return;
811
+
812
+ enemy.bullets.forEach(bullet => {
813
+ const distance = bullet.position.distanceTo(tankPosition);
814
+ if (distance < 1) {
815
+ if (this.tank.takeDamage(10)) {
816
+ this.endGame();
817
+ }
818
+ this.scene.remove(bullet);
819
+ enemy.bullets = enemy.bullets.filter(b => b !== bullet);
820
+
821
+ this.createExplosion(bullet.position);
822
+ document.getElementById('health').style.width =
823
+ `${(this.tank.health / MAX_HEALTH) * 100}%`;
824
+ }
825
+ });
826
+ });
827
+
828
+ // ํ”Œ๋ ˆ์ด์–ด ์ด์•Œ๊ณผ ์  ์ถฉ๋Œ ์ฒดํฌ
829
+ this.tank.bullets.forEach((bullet, bulletIndex) => {
830
+ this.enemies.forEach((enemy, enemyIndex) => {
831
+ if (!enemy.mesh || !enemy.isLoaded) return;
832
+
833
+ const distance = bullet.position.distanceTo(enemy.mesh.position);
834
+ if (distance < 2) {
835
+ if (enemy.takeDamage(50)) {
836
+ enemy.destroy();
837
+ this.enemies.splice(enemyIndex, 1);
838
+ this.score += 100;
839
+ document.getElementById('score').textContent = `Score: ${this.score}`;
840
+ }
841
+ this.scene.remove(bullet);
842
+ this.tank.bullets.splice(bulletIndex, 1);
843
+ this.createExplosion(bullet.position);
844
+ }
845
+ });
846
+ });
847
+
848
+ // ํ”Œ๋ ˆ์ด์–ด ํƒฑํฌ์™€ ๊ฑด๋ฌผ ์ถฉ๋Œ ์ฒดํฌ
849
+ const tankBoundingBox = new THREE.Box3().setFromObject(this.tank.body);
850
+ for (const building of this.buildings) {
851
+ const buildingBox = new THREE.Box3().setFromObject(building);
852
+ if (tankBoundingBox.intersectsBox(buildingBox)) {
853
+ this.tank.body.position.copy(this.previousTankPosition);
854
+ break;
855
+ }
856
+ }
857
+
858
+ // ์ด์ „ ์œ„์น˜ ์ €์žฅ
859
+ this.previousTankPosition.copy(this.tank.body.position);
860
+ }
861
+ endGame() {
862
+ if (this.isGameOver) return;
863
+
864
+ this.isGameOver = true;
865
+
866
+ if (this.gameTimer) {
867
+ clearInterval(this.gameTimer);
868
+ }
869
 
870
+ if (this.animationFrameId) {
871
+ cancelAnimationFrame(this.animationFrameId);
872
+ }
 
 
 
 
873
 
874
+ document.exitPointerLock();
875
+
876
+ const gameOverDiv = document.createElement('div');
877
+ gameOverDiv.style.position = 'absolute';
878
+ gameOverDiv.style.top = '50%';
879
+ gameOverDiv.style.left = '50%';
880
+ gameOverDiv.style.transform = 'translate(-50%, -50%)';
881
+ gameOverDiv.style.color = '#0f0';
882
+ gameOverDiv.style.fontSize = '48px';
883
+ gameOverDiv.style.backgroundColor = 'rgba(0, 20, 0, 0.7)';
884
+ gameOverDiv.style.padding = '20px';
885
+ gameOverDiv.style.borderRadius = '10px';
886
+ gameOverDiv.style.textAlign = 'center';
887
+ gameOverDiv.innerHTML = `
888
+ Game Over<br>
889
+ Score: ${this.score}<br>
890
+ Time Survived: ${GAME_DURATION - this.gameTime}s<br>
891
+ <button onclick="location.reload()"
892
+ style="font-size: 24px; padding: 10px; margin-top: 20px;
893
+ cursor: pointer; background: #0f0; border: none;
894
+ color: black; border-radius: 5px;">
895
+ Play Again
896
+ </button>
897
+ `;
898
+ document.body.appendChild(gameOverDiv);
899
+ }
900
 
901
+ updateUI() {
902
+ if (!this.isGameOver) {
903
+ const healthBar = document.getElementById('health');
904
+ if (healthBar) {
905
+ healthBar.style.width = `${(this.tank.health / MAX_HEALTH) * 100}%`;
906
+ }
907
+
908
+ const timeElement = document.getElementById('time');
909
+ if (timeElement) {
910
+ timeElement.textContent = `Time: ${this.gameTime}s`;
911
+ }
912
+
913
+ const scoreElement = document.getElementById('score');
914
+ if (scoreElement) {
915
+ scoreElement.textContent = `Score: ${this.score}`;
916
+ }
917
+ }
918
+ }
919
 
920
+ animate() {
921
+ if (this.isGameOver) {
922
+ if (this.animationFrameId) {
923
+ cancelAnimationFrame(this.animationFrameId);
924
+ }
925
+ return;
926
+ }
 
 
 
 
 
927
 
928
+ this.animationFrameId = requestAnimationFrame(() => this.animate());
929
+
930
+ const currentTime = performance.now();
931
+ const deltaTime = (currentTime - this.lastTime) / 1000;
932
+ this.lastTime = currentTime;
933
+
934
+ if (!this.isLoading) {
935
+ this.handleMovement();
936
+ this.tank.update(this.mouse.x, this.mouse.y, this.scene);
937
+
938
+ const tankPosition = this.tank.getPosition();
939
+ this.enemies.forEach(enemy => {
940
+ enemy.update(tankPosition);
941
+
942
+ if (enemy.isLoaded && enemy.mesh.position.distanceTo(tankPosition) < ENEMY_CONFIG.ATTACK_RANGE) {
943
+ enemy.shoot(tankPosition);
944
+ }
945
+ });
946
+
947
+ this.updateParticles();
948
+ this.checkCollisions();
949
+ this.updateUI();
950
+ this.updateRadar(); // ๋ ˆ์ด๋” ์—…๋ฐ์ดํŠธ ์ถ”๊ฐ€
951
+ }
952
+
953
+ this.renderer.render(this.scene, this.camera);
954
+ }
955
+ }
956
 
957
  // Start game
958
  window.startGame = function() {
959
+ document.getElementById('startScreen').style.display = 'none';
960
+ document.body.requestPointerLock();
961
+
962
+ // Create new game instance or restart existing one
963
+ if (!window.gameInstance) {
964
+ window.gameInstance = new Game();
965
+ }
966
  };
967
 
968
  // Initialize game
969
  document.addEventListener('DOMContentLoaded', () => {
970
+ const game = new Game();
971
  });