cutechicken commited on
Commit
89d8144
โ€ข
1 Parent(s): 4b521cc

Update game.js

Browse files
Files changed (1) hide show
  1. game.js +232 -253
game.js CHANGED
@@ -12,7 +12,7 @@ const MAX_HEALTH = 1000;
12
  const ENEMY_MOVE_SPEED = 0.1;
13
  const ENEMY_COUNT_MAX = 5;
14
  const PARTICLE_COUNT = 15;
15
- const BUILDING_COUNT = 30; // ๊ฑด๋ฌผ ์ˆ˜ ์ถ”๊ฐ€
16
  const ENEMY_CONFIG = {
17
  ATTACK_RANGE: 100,
18
  ATTACK_INTERVAL: 2000,
@@ -73,64 +73,63 @@ class TankPlayer {
73
  this.isLoaded = false;
74
  }
75
  }
 
 
 
76
 
77
- shoot(scene) {
78
- const currentTime = Date.now();
79
- if (currentTime - this.lastShootTime < this.shootInterval || this.ammo <= 0) return null;
80
-
81
- // ์ด์•Œ ์ƒ์„ฑ
82
- const bulletGeometry = new THREE.SphereGeometry(0.2);
83
- const bulletMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
84
- const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
85
-
86
- // ์ด์•Œ ์‹œ์ž‘ ์œ„์น˜ (ํฌํƒ‘ ๋)
87
- const bulletOffset = new THREE.Vector3(0, 0.5, 2);
88
- // ํฌํƒ‘์˜ ํšŒ์ „์„ ์ ์šฉ
89
- bulletOffset.applyQuaternion(this.turretGroup.quaternion);
90
- bulletOffset.applyQuaternion(this.body.quaternion);
91
- bullet.position.copy(this.body.position).add(bulletOffset);
92
-
93
- // ์ด์•Œ ์†๋„ (ํฌํƒ‘ ๋ฐฉํ–ฅ)
94
- const direction = new THREE.Vector3(0, 0, 1);
95
- direction.applyQuaternion(this.turretGroup.quaternion);
96
- direction.applyQuaternion(this.body.quaternion);
97
- bullet.velocity = direction.multiplyScalar(2);
98
-
99
- scene.add(bullet);
100
- this.bullets.push(bullet);
101
- this.ammo--;
102
- this.lastShootTime = currentTime;
103
-
104
- document.getElementById('ammo').textContent = `Ammo: ${this.ammo}/10`;
105
-
106
- return bullet;
107
- }
108
 
109
  update(mouseX, mouseY) {
110
- if (!this.body || !this.turretGroup) return;
111
-
112
- // ์ด์•Œ ์—…๋ฐ์ดํŠธ๋งŒ ์ˆ˜ํ–‰ํ•˜๊ณ  ํฌํƒ‘ ํšŒ์ „์€ Game ํด๋ž˜์Šค์˜ handleMovement์—์„œ ์ฒ˜๋ฆฌ
113
- for (let i = this.bullets.length - 1; i >= 0; i--) {
114
- const bullet = this.bullets[i];
115
- bullet.position.add(bullet.velocity);
116
-
117
- // ์ด์•Œ์ด ๋งต ๋ฐ–์œผ๋กœ ๋‚˜๊ฐ€๋ฉด ์ œ๊ฑฐ
118
- if (Math.abs(bullet.position.x) > MAP_SIZE/2 ||
119
- Math.abs(bullet.position.z) > MAP_SIZE/2) {
120
- scene.remove(bullet);
121
- this.bullets.splice(i, 1);
122
- }
123
- }
124
- }
125
 
126
  move(direction) {
127
- if (!this.body) return;
128
-
129
- const moveVector = new THREE.Vector3();
130
- moveVector.x = direction.x * this.moveSpeed;
131
- moveVector.z = direction.z * this.moveSpeed;
132
-
133
- this.body.position.add(moveVector);
134
  }
135
 
136
  rotate(angle) {
@@ -147,23 +146,22 @@ class TankPlayer {
147
  return this.health <= 0;
148
  }
149
  }
150
- // Enemy ํด๋ž˜์Šค ์ˆ˜์ •
151
  class Enemy {
152
  constructor(scene, position, type = 'tank') {
153
  this.scene = scene;
154
  this.position = position;
155
  this.mesh = null;
156
- this.type = type; // 'tank' ๋˜๋Š” 'heavy'
157
- this.health = type === 'tank' ? 100 : 200; // heavy๋Š” ์ฒด๋ ฅ์ด ๋” ๋†’์Œ
158
  this.lastAttackTime = 0;
159
  this.bullets = [];
160
  this.isLoaded = false;
161
- this.moveSpeed = type === 'tank' ? ENEMY_MOVE_SPEED : ENEMY_MOVE_SPEED * 0.7; // heavy๋Š” ๋” ๋Š๋ฆผ
162
  }
163
 
164
  async initialize(loader) {
165
  try {
166
- // ํƒ€์ž…์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ๋ชจ๋ธ ๋กœ๋“œ
167
  const modelPath = this.type === 'tank' ? '/models/enemy1.glb' : '/models/enemy4.glb';
168
  const result = await loader.loadAsync(modelPath);
169
  this.mesh = result.scene;
@@ -188,14 +186,11 @@ class Enemy {
188
  update(playerPosition) {
189
  if (!this.mesh || !this.isLoaded) return;
190
 
191
- // ํ”Œ๋ ˆ์ด์–ด ๋ฐฉํ–ฅ์œผ๋กœ ํšŒ์ „
192
  const direction = new THREE.Vector3()
193
  .subVectors(playerPosition, this.mesh.position)
194
  .normalize();
195
 
196
  this.mesh.lookAt(playerPosition);
197
-
198
- // ํ”Œ๋ ˆ์ด์–ด ๋ฐฉํ–ฅ์œผ๋กœ ์ด๋™ (ํƒ€์ž…์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ์†๋„)
199
  this.mesh.position.add(direction.multiplyScalar(this.moveSpeed));
200
 
201
  // ์ด์•Œ ์—…๋ฐ์ดํŠธ
@@ -203,7 +198,6 @@ class Enemy {
203
  const bullet = this.bullets[i];
204
  bullet.position.add(bullet.velocity);
205
 
206
- // ์ด์•Œ์ด ๋งต ๋ฐ–์œผ๋กœ ๋‚˜๊ฐ€๋ฉด ์ œ๊ฑฐ
207
  if (Math.abs(bullet.position.x) > MAP_SIZE ||
208
  Math.abs(bullet.position.z) > MAP_SIZE) {
209
  this.scene.remove(bullet);
@@ -216,7 +210,7 @@ class Enemy {
216
  const currentTime = Date.now();
217
  const attackInterval = this.type === 'tank' ?
218
  ENEMY_CONFIG.ATTACK_INTERVAL :
219
- ENEMY_CONFIG.ATTACK_INTERVAL * 1.5; // heavy๋Š” ๋ฐœ์‚ฌ ๊ฐ„๊ฒฉ์ด ๋” ๊น€
220
 
221
  if (currentTime - this.lastAttackTime < attackInterval) return;
222
 
@@ -247,8 +241,7 @@ class Enemy {
247
  this.health -= damage;
248
  return this.health <= 0;
249
  }
250
-
251
- destroy() {
252
  if (this.mesh) {
253
  this.scene.remove(this.mesh);
254
  this.bullets.forEach(bullet => this.scene.remove(bullet));
@@ -258,7 +251,7 @@ class Enemy {
258
  }
259
  }
260
 
261
- // Particle ํด๋ž˜์Šค๋Š” ๊ทธ๋Œ€๋กœ ์œ ์ง€
262
  class Particle {
263
  constructor(scene, position) {
264
  const geometry = new THREE.SphereGeometry(0.1);
@@ -290,10 +283,10 @@ class Particle {
290
  scene.remove(this.mesh);
291
  }
292
  }
 
293
  // Game ํด๋ž˜์Šค
294
  class Game {
295
  constructor() {
296
- // ๊ธฐ๋ณธ Three.js ์„ค์ •
297
  this.scene = new THREE.Scene();
298
  this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
299
  this.renderer = new THREE.WebGLRenderer({ antialias: true });
@@ -301,7 +294,6 @@ class Game {
301
  this.renderer.shadowMap.enabled = true;
302
  document.getElementById('gameContainer').appendChild(this.renderer.domElement);
303
 
304
- // ๊ฒŒ์ž„ ์š”์†Œ ์ดˆ๊ธฐํ™”
305
  this.tank = new TankPlayer();
306
  this.enemies = [];
307
  this.particles = [];
@@ -314,8 +306,8 @@ class Game {
314
  this.isLoading = true;
315
  this.previousTankPosition = new THREE.Vector3();
316
  this.lastTime = performance.now();
 
317
 
318
- // ๋งˆ์šฐ์Šค/ํ‚ค๋ณด๋“œ ์ƒํƒœ
319
  this.mouse = { x: 0, y: 0 };
320
  this.keys = {
321
  forward: false,
@@ -324,193 +316,167 @@ class Game {
324
  right: false
325
  };
326
 
327
- // ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์„ค์ •
328
  this.setupEventListeners();
329
  this.initialize();
330
  }
331
 
332
  async initialize() {
333
- try {
334
- // ์กฐ๋ช… ์„ค์ •
335
- const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
336
- this.scene.add(ambientLight);
337
-
338
- const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
339
- directionalLight.position.set(50, 50, 50);
340
- directionalLight.castShadow = true;
341
- directionalLight.shadow.mapSize.width = 2048;
342
- directionalLight.shadow.mapSize.height = 2048;
343
- this.scene.add(directionalLight);
344
-
345
- // ์ง€ํ˜• ์ƒ์„ฑ
346
- const ground = new THREE.Mesh(
347
- new THREE.PlaneGeometry(MAP_SIZE, MAP_SIZE),
348
- new THREE.MeshStandardMaterial({
349
- color: 0x333333,
350
- roughness: 0.9,
351
- metalness: 0.1
352
- })
353
- );
354
- ground.rotation.x = -Math.PI / 2;
355
- ground.receiveShadow = true;
356
- this.scene.add(ground);
357
-
358
- // ๊ฑด๋ฌผ ์ƒ์„ฑ
359
- await this.createBuildings();
360
-
361
- // ํƒฑํฌ ์ดˆ๊ธฐํ™”
362
- await this.tank.initialize(this.scene, this.loader);
363
- if (!this.tank.isLoaded) {
364
- throw new Error('Tank loading failed');
365
- }
366
-
367
- // ์นด๋ฉ”๋ผ ์„ค์ • ์ˆ˜์ •
368
- // ํƒฑํฌ์˜ ํ˜„์žฌ ์œ„์น˜ ๊ฐ€์ ธ์˜ค๊ธฐ
369
- const tankPosition = this.tank.getPosition();
370
- // ์นด๋ฉ”๋ผ๋ฅผ ํƒฑํฌ ์œ„์น˜ ๊ธฐ์ค€์œผ๋กœ ์„ค์ •
371
- this.camera.position.set(
372
- tankPosition.x,
373
- tankPosition.y + 15, // ํƒฑํฌ๋ณด๋‹ค 15 ์œ ๋‹› ์œ„์—
374
- tankPosition.z - 30 // ํƒฑํฌ๋ณด๋‹ค 30 ์œ ๋‹› ๋’ค์—
375
- );
376
- // ๏ฟฝ๏ฟฝ๏ฟฝ๋ฉ”๋ผ๊ฐ€ ํƒฑํฌ๋ฅผ ๋ฐ”๋ผ๋ณด๋„๋ก ์„ค์ •
377
- this.camera.lookAt(new THREE.Vector3(
378
- tankPosition.x,
379
- tankPosition.y + 2, // ํƒฑํฌ์˜ ์ƒ๋‹จ ๋ถ€๋ถ„์„ ๋ฐ”๋ผ๋ณด๋„๋ก
380
- tankPosition.z
381
- ));
382
-
383
- // ๋กœ๋”ฉ ์™„๋ฃŒ
384
- this.isLoading = false;
385
- document.getElementById('loading').style.display = 'none';
386
-
387
- // ๊ฒŒ์ž„ ์‹œ์ž‘
388
- this.animate();
389
- this.spawnEnemies();
390
- this.startGameTimer();
391
-
392
- } catch (error) {
393
- console.error('Game initialization error:', error);
394
- this.handleLoadingError();
395
- }
396
- }
397
 
398
- setupEventListeners() {
399
- // ํ‚ค๋ณด๋“œ ์ด๋ฒคํŠธ๋Š” ๊ทธ๋Œ€๋กœ ์œ ์ง€
400
- document.addEventListener('keydown', (event) => {
401
- if (this.isLoading) return;
402
- switch(event.code) {
403
- case 'KeyW': this.keys.forward = true; break;
404
- case 'KeyS': this.keys.backward = true; break;
405
- case 'KeyA': this.keys.left = true; break;
406
- case 'KeyD': this.keys.right = true; break;
407
- }
408
- });
409
-
410
- document.addEventListener('keyup', (event) => {
411
- if (this.isLoading) return;
412
- switch(event.code) {
413
- case 'KeyW': this.keys.forward = false; break;
414
- case 'KeyS': this.keys.backward = false; break;
415
- case 'KeyA': this.keys.left = false; break;
416
- case 'KeyD': this.keys.right = false; break;
 
 
 
 
 
 
 
 
 
 
417
  }
418
- });
419
 
420
- // ๋งˆ์šฐ์Šค ์›€์ง์ž„ ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์ •
421
- document.addEventListener('mousemove', (event) => {
422
- if (this.isLoading || !document.pointerLockElement) return;
423
-
424
- // movementX/Y๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋งˆ์šฐ์Šค ํšŒ์ „ ๊ณ„์‚ฐ
425
- this.mouse.x += event.movementX * 0.002;
426
- this.mouse.y += event.movementY * 0.002;
427
- });
428
-
429
- // ํด๋ฆญ ์ด๋ฒคํŠธ ์ˆ˜์ • - ํฌ์ธํ„ฐ ๋ฝ๊ณผ ๋ฐœ์‚ฌ๋ฅผ ํ•จ๊ป˜ ์ฒ˜๋ฆฌ
430
- document.addEventListener('click', () => {
431
- if (!document.pointerLockElement) {
432
- document.body.requestPointerLock();
433
- } else {
434
- const bullet = this.tank.shoot(this.scene);
435
- if (bullet) {
436
- // ์ด์•Œ ๋ฐœ์‚ฌ ํšจ๊ณผ์Œ์ด๋‚˜ ์‹œ๊ฐํšจ๊ณผ ์ถ”๊ฐ€ ๊ฐ€๋Šฅ
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
437
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
438
  }
439
- });
440
-
441
- // ํฌ์ธํ„ฐ ๋ฝ ์ƒํƒœ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ ์ถ”๊ฐ€
442
- document.addEventListener('pointerlockchange', () => {
443
- if (!document.pointerLockElement) {
444
- // ํฌ์ธํ„ฐ ๋ฝ์ด ํ•ด์ œ๋˜์—ˆ์„ ๋•Œ์˜ ์ฒ˜๋ฆฌ
445
- this.mouse.x = 0;
446
- this.mouse.y = 0;
447
  }
448
- });
449
 
450
- // ์ฐฝ ํฌ๊ธฐ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ๋Š” ๊ทธ๋Œ€๋กœ ์œ ์ง€
451
- window.addEventListener('resize', () => {
452
- this.camera.aspect = window.innerWidth / window.innerHeight;
453
- this.camera.updateProjectionMatrix();
454
- this.renderer.setSize(window.innerWidth, window.innerHeight);
455
- });
456
- }
457
- handleMovement() {
458
- if (!this.tank.isLoaded) return;
459
-
460
- const direction = new THREE.Vector3();
461
-
462
- if (this.keys.forward) direction.z += 1;
463
- if (this.keys.backward) direction.z -= 1;
464
- if (this.keys.left) direction.x -= 1;
465
- if (this.keys.right) direction.x += 1;
466
-
467
- if (direction.length() > 0) {
468
- direction.normalize();
469
 
470
- // A,D ํ‚ค๋กœ ํƒฑํฌ ํšŒ์ „
471
- if (this.keys.left) this.tank.rotate(-1);
472
- if (this.keys.right) this.tank.rotate(1);
473
 
474
- // ํ˜„์žฌ ํƒฑํฌ์˜ ๋ฐฉํ–ฅ์œผ๋กœ ์ด๋™
475
- direction.applyEuler(this.tank.body.rotation);
476
- this.tank.move(direction);
477
- }
478
-
479
- // ๋งˆ์šฐ์Šค ์œ„์น˜๋ฅผ ํƒฑํฌ ๊ธฐ์ค€์œผ๋กœ ๋ณ€ํ™˜
480
- const mouseVector = new THREE.Vector2(this.mouse.x, -this.mouse.y);
481
- const rotationAngle = Math.atan2(mouseVector.x, mouseVector.y);
482
-
483
- // ํฌํƒ‘ ํšŒ์ „
484
- if (this.tank.turretGroup) {
485
- this.tank.turretGroup.rotation.y = rotationAngle;
486
- }
487
-
488
- // ์—ฌ๊ธฐ๋ถ€ํ„ฐ ์นด๋ฉ”๋ผ ๋กœ์ง์„ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค
489
- const tankPos = this.tank.getPosition();
490
- const cameraDistance = 30; // ์นด๋ฉ”๋ผ์™€ ํƒฑํฌ ์‚ฌ์ด์˜ ๊ฑฐ๋ฆฌ
491
- const cameraHeight = 15; // ์นด๋ฉ”๋ผ์˜ ๋†’์ด
492
- const lookAtHeight = 5; // ์นด๋ฉ”๋ผ๊ฐ€ ๋ฐ”๋ผ๋ณด๋Š” ๋†’์ด
493
-
494
- // ํƒฑํฌ์˜ ํšŒ์ „์— ๋”ฐ๋ผ ์นด๋ฉ”๋ผ ์œ„์น˜ ๊ณ„์‚ฐ
495
- const tankRotation = this.tank.body.rotation.y;
496
-
497
- // ์นด๋ฉ”๋ผ ์œ„์น˜ ๊ณ„์‚ฐ ์ˆ˜์ •
498
- this.camera.position.set(
499
- tankPos.x - Math.sin(tankRotation) * cameraDistance,
500
- tankPos.y + cameraHeight,
501
- tankPos.z - Math.cos(tankRotation) * cameraDistance
502
- );
503
-
504
- // ์นด๋ฉ”๋ผ๊ฐ€ ๋ฐ”๋ผ๋ณด๋Š” ์ง€์ ์„ ํƒฑํฌ ์œ„์น˜๋ณด๋‹ค ์•ฝ๊ฐ„ ์•ž์ชฝ์œผ๋กœ ์„ค์ •
505
- const lookAtPoint = new THREE.Vector3(
506
- tankPos.x + Math.sin(tankRotation) * 10, // ํƒฑํฌ ์•ž์ชฝ 10 ์œ ๋‹›
507
- tankPos.y + lookAtHeight, // ํƒฑํฌ๋ณด๋‹ค ์•ฝ๊ฐ„ ์œ„
508
- tankPos.z + Math.cos(tankRotation) * 10 // ํƒฑํฌ ์•ž์ชฝ 10 ์œ ๋‹›
509
- );
510
-
511
- this.camera.lookAt(lookAtPoint);
512
- }
513
- createBuildings() {
514
  const buildingTypes = [
515
  { width: 10, height: 30, depth: 10, color: 0x808080 },
516
  { width: 15, height: 40, depth: 15, color: 0x606060 },
@@ -538,6 +504,7 @@ createBuildings() {
538
  this.scene.add(building);
539
  }
540
  }
 
541
  }
542
 
543
  createBuilding(type) {
@@ -548,7 +515,7 @@ createBuildings() {
548
  specular: 0x111111,
549
  shininess: 30
550
  });
551
- const building = new THREE.Mesh(geometry, material);
552
  building.castShadow = true;
553
  building.receiveShadow = true;
554
  return building;
@@ -598,7 +565,9 @@ createBuildings() {
598
  this.enemies.push(enemy);
599
  }
600
  }
601
- setTimeout(spawnEnemy, 3000);
 
 
602
  };
603
 
604
  spawnEnemy();
@@ -639,10 +608,12 @@ createBuildings() {
639
 
640
  startGameTimer() {
641
  const timer = setInterval(() => {
642
- if (this.isLoading) return;
643
 
644
  this.gameTime--;
645
- if (this.gameTime <= 0 || this.isGameOver) {
 
 
646
  clearInterval(timer);
647
  this.endGame();
648
  }
@@ -658,8 +629,7 @@ createBuildings() {
658
  }
659
  }
660
  }
661
-
662
- createExplosion(position) {
663
  for (let i = 0; i < PARTICLE_COUNT; i++) {
664
  this.particles.push(new Particle(this.scene, position));
665
  }
@@ -670,6 +640,7 @@ createBuildings() {
670
 
671
  const tankPosition = this.tank.getPosition();
672
 
 
673
  this.enemies.forEach(enemy => {
674
  if (!enemy.mesh || !enemy.isLoaded) return;
675
 
@@ -689,6 +660,7 @@ createBuildings() {
689
  });
690
  });
691
 
 
692
  const tankBoundingBox = new THREE.Box3().setFromObject(this.tank.body);
693
  for (const building of this.buildings) {
694
  const buildingBox = new THREE.Box3().setFromObject(building);
@@ -698,11 +670,16 @@ createBuildings() {
698
  }
699
  }
700
 
 
701
  this.previousTankPosition = this.tank.body.position.clone();
702
  }
703
 
704
  endGame() {
705
  this.isGameOver = true;
 
 
 
 
706
  const gameOverDiv = document.createElement('div');
707
  gameOverDiv.style.position = 'absolute';
708
  gameOverDiv.style.top = '50%';
@@ -728,9 +705,12 @@ createBuildings() {
728
  }
729
 
730
  animate() {
731
- if (this.isGameOver) return;
 
 
 
732
 
733
- requestAnimationFrame(() => this.animate());
734
 
735
  const currentTime = performance.now();
736
  const deltaTime = (currentTime - this.lastTime) / 1000;
@@ -740,8 +720,7 @@ createBuildings() {
740
  this.renderer.render(this.scene, this.camera);
741
  return;
742
  }
743
-
744
- this.handleMovement();
745
  this.tank.update(this.mouse.x, this.mouse.y);
746
 
747
  const tankPosition = this.tank.getPosition();
 
12
  const ENEMY_MOVE_SPEED = 0.1;
13
  const ENEMY_COUNT_MAX = 5;
14
  const PARTICLE_COUNT = 15;
15
+ const BUILDING_COUNT = 30;
16
  const ENEMY_CONFIG = {
17
  ATTACK_RANGE: 100,
18
  ATTACK_INTERVAL: 2000,
 
73
  this.isLoaded = false;
74
  }
75
  }
76
+ shoot(scene) {
77
+ const currentTime = Date.now();
78
+ if (currentTime - this.lastShootTime < this.shootInterval || this.ammo <= 0) return null;
79
 
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);
84
+
85
+ // ์ด์•Œ ์‹œ์ž‘ ์œ„์น˜ (ํฌํƒ‘ ๋)
86
+ const bulletOffset = new THREE.Vector3(0, 0.5, 2);
87
+ // ํฌํƒ‘์˜ ํšŒ์ „์„ ์ ์šฉ
88
+ bulletOffset.applyQuaternion(this.turretGroup.quaternion);
89
+ bulletOffset.applyQuaternion(this.body.quaternion);
90
+ bullet.position.copy(this.body.position).add(bulletOffset);
91
+
92
+ // ์ด์•Œ ์†๋„ (ํฌํƒ‘ ๋ฐฉํ–ฅ)
93
+ const direction = new THREE.Vector3(0, 0, 1);
94
+ direction.applyQuaternion(this.turretGroup.quaternion);
95
+ direction.applyQuaternion(this.body.quaternion);
96
+ bullet.velocity = direction.multiplyScalar(2);
97
+
98
+ scene.add(bullet);
99
+ this.bullets.push(bullet);
100
+ this.ammo--;
101
+ this.lastShootTime = currentTime;
102
+
103
+ document.getElementById('ammo').textContent = `Ammo: ${this.ammo}/10`;
104
+
105
+ return bullet;
106
+ }
 
 
 
 
107
 
108
  update(mouseX, mouseY) {
109
+ if (!this.body || !this.turretGroup) return;
110
+
111
+ // ์ด์•Œ ์—…๋ฐ์ดํŠธ
112
+ for (let i = this.bullets.length - 1; i >= 0; i--) {
113
+ const bullet = this.bullets[i];
114
+ bullet.position.add(bullet.velocity);
115
+
116
+ // ์ด์•Œ์ด ๋งต ๋ฐ–์œผ๋กœ ๋‚˜๊ฐ€๋ฉด ์ œ๊ฑฐ
117
+ if (Math.abs(bullet.position.x) > MAP_SIZE/2 ||
118
+ Math.abs(bullet.position.z) > MAP_SIZE/2) {
119
+ scene.remove(bullet);
120
+ this.bullets.splice(i, 1);
121
+ }
122
+ }
123
+ }
124
 
125
  move(direction) {
126
+ if (!this.body) return;
127
+
128
+ const moveVector = new THREE.Vector3();
129
+ moveVector.x = direction.x * this.moveSpeed;
130
+ moveVector.z = direction.z * this.moveSpeed;
131
+
132
+ this.body.position.add(moveVector);
133
  }
134
 
135
  rotate(angle) {
 
146
  return this.health <= 0;
147
  }
148
  }
149
+ // Enemy ํด๋ž˜์Šค
150
  class Enemy {
151
  constructor(scene, position, type = 'tank') {
152
  this.scene = scene;
153
  this.position = position;
154
  this.mesh = null;
155
+ this.type = type;
156
+ this.health = type === 'tank' ? 100 : 200;
157
  this.lastAttackTime = 0;
158
  this.bullets = [];
159
  this.isLoaded = false;
160
+ this.moveSpeed = type === 'tank' ? ENEMY_MOVE_SPEED : ENEMY_MOVE_SPEED * 0.7;
161
  }
162
 
163
  async initialize(loader) {
164
  try {
 
165
  const modelPath = this.type === 'tank' ? '/models/enemy1.glb' : '/models/enemy4.glb';
166
  const result = await loader.loadAsync(modelPath);
167
  this.mesh = result.scene;
 
186
  update(playerPosition) {
187
  if (!this.mesh || !this.isLoaded) return;
188
 
 
189
  const direction = new THREE.Vector3()
190
  .subVectors(playerPosition, this.mesh.position)
191
  .normalize();
192
 
193
  this.mesh.lookAt(playerPosition);
 
 
194
  this.mesh.position.add(direction.multiplyScalar(this.moveSpeed));
195
 
196
  // ์ด์•Œ ์—…๋ฐ์ดํŠธ
 
198
  const bullet = this.bullets[i];
199
  bullet.position.add(bullet.velocity);
200
 
 
201
  if (Math.abs(bullet.position.x) > MAP_SIZE ||
202
  Math.abs(bullet.position.z) > MAP_SIZE) {
203
  this.scene.remove(bullet);
 
210
  const currentTime = Date.now();
211
  const attackInterval = this.type === 'tank' ?
212
  ENEMY_CONFIG.ATTACK_INTERVAL :
213
+ ENEMY_CONFIG.ATTACK_INTERVAL * 1.5;
214
 
215
  if (currentTime - this.lastAttackTime < attackInterval) return;
216
 
 
241
  this.health -= damage;
242
  return this.health <= 0;
243
  }
244
+ destroy() {
 
245
  if (this.mesh) {
246
  this.scene.remove(this.mesh);
247
  this.bullets.forEach(bullet => this.scene.remove(bullet));
 
251
  }
252
  }
253
 
254
+ // Particle ํด๋ž˜์Šค
255
  class Particle {
256
  constructor(scene, position) {
257
  const geometry = new THREE.SphereGeometry(0.1);
 
283
  scene.remove(this.mesh);
284
  }
285
  }
286
+
287
  // Game ํด๋ž˜์Šค
288
  class Game {
289
  constructor() {
 
290
  this.scene = new THREE.Scene();
291
  this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
292
  this.renderer = new THREE.WebGLRenderer({ antialias: true });
 
294
  this.renderer.shadowMap.enabled = true;
295
  document.getElementById('gameContainer').appendChild(this.renderer.domElement);
296
 
 
297
  this.tank = new TankPlayer();
298
  this.enemies = [];
299
  this.particles = [];
 
306
  this.isLoading = true;
307
  this.previousTankPosition = new THREE.Vector3();
308
  this.lastTime = performance.now();
309
+ this.animationFrameId = null; // ์• ๋‹ˆ๋ฉ”์ด์…˜ ํ”„๋ ˆ์ž„ ID ์ถ”๊ฐ€
310
 
 
311
  this.mouse = { x: 0, y: 0 };
312
  this.keys = {
313
  forward: false,
 
316
  right: false
317
  };
318
 
 
319
  this.setupEventListeners();
320
  this.initialize();
321
  }
322
 
323
  async initialize() {
324
+ try {
325
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
326
+ this.scene.add(ambientLight);
327
+
328
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
329
+ directionalLight.position.set(50, 50, 50);
330
+ directionalLight.castShadow = true;
331
+ directionalLight.shadow.mapSize.width = 2048;
332
+ directionalLight.shadow.mapSize.height = 2048;
333
+ this.scene.add(directionalLight);
334
+
335
+ const ground = new THREE.Mesh(
336
+ new THREE.PlaneGeometry(MAP_SIZE, MAP_SIZE),
337
+ new THREE.MeshStandardMaterial({
338
+ color: 0x333333,
339
+ roughness: 0.9,
340
+ metalness: 0.1
341
+ })
342
+ );
343
+ ground.rotation.x = -Math.PI / 2;
344
+ ground.receiveShadow = true;
345
+ this.scene.add(ground);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
 
347
+ await this.createBuildings();
348
+ await this.tank.initialize(this.scene, this.loader);
349
+
350
+ if (!this.tank.isLoaded) {
351
+ throw new Error('Tank loading failed');
352
+ }
353
+
354
+ const tankPosition = this.tank.getPosition();
355
+ this.camera.position.set(
356
+ tankPosition.x,
357
+ tankPosition.y + 15,
358
+ tankPosition.z - 30
359
+ );
360
+ this.camera.lookAt(new THREE.Vector3(
361
+ tankPosition.x,
362
+ tankPosition.y + 2,
363
+ tankPosition.z
364
+ ));
365
+
366
+ this.isLoading = false;
367
+ document.getElementById('loading').style.display = 'none';
368
+
369
+ this.animate();
370
+ this.spawnEnemies();
371
+ this.startGameTimer();
372
+
373
+ } catch (error) {
374
+ console.error('Game initialization error:', error);
375
+ this.handleLoadingError();
376
  }
377
+ }
378
 
379
+ setupEventListeners() {
380
+ document.addEventListener('keydown', (event) => {
381
+ if (this.isLoading) return;
382
+ switch(event.code) {
383
+ case 'KeyW': this.keys.forward = true; break;
384
+ case 'KeyS': this.keys.backward = true; break;
385
+ case 'KeyA': this.keys.left = true; break;
386
+ case 'KeyD': this.keys.right = true; break;
387
+ }
388
+ });
389
+
390
+ document.addEventListener('keyup', (event) => {
391
+ if (this.isLoading) return;
392
+ switch(event.code) {
393
+ case 'KeyW': this.keys.forward = false; break;
394
+ case 'KeyS': this.keys.backward = false; break;
395
+ case 'KeyA': this.keys.left = false; break;
396
+ case 'KeyD': this.keys.right = false; break;
397
+ }
398
+ });
399
+
400
+ document.addEventListener('mousemove', (event) => {
401
+ if (this.isLoading || !document.pointerLockElement) return;
402
+ this.mouse.x += event.movementX * 0.002;
403
+ this.mouse.y += event.movementY * 0.002;
404
+ });
405
+
406
+ document.addEventListener('click', () => {
407
+ if (!document.pointerLockElement) {
408
+ document.body.requestPointerLock();
409
+ } else {
410
+ const bullet = this.tank.shoot(this.scene);
411
+ if (bullet) {
412
+ // ์ด์•Œ ๋ฐœ์‚ฌ ํšจ๊ณผ ์ถ”๊ฐ€ ๊ฐ€๋Šฅ
413
+ }
414
+ }
415
+ });
416
+
417
+ document.addEventListener('pointerlockchange', () => {
418
+ if (!document.pointerLockElement) {
419
+ this.mouse.x = 0;
420
+ this.mouse.y = 0;
421
  }
422
+ });
423
+
424
+ window.addEventListener('resize', () => {
425
+ this.camera.aspect = window.innerWidth / window.innerHeight;
426
+ this.camera.updateProjectionMatrix();
427
+ this.renderer.setSize(window.innerWidth, window.innerHeight);
428
+ });
429
+ }
430
+ handleMovement() {
431
+ if (!this.tank.isLoaded) return;
432
+
433
+ const direction = new THREE.Vector3();
434
+
435
+ if (this.keys.forward) direction.z += 1;
436
+ if (this.keys.backward) direction.z -= 1;
437
+ if (this.keys.left) direction.x -= 1;
438
+ if (this.keys.right) direction.x += 1;
439
+
440
+ if (direction.length() > 0) {
441
+ direction.normalize();
442
+
443
+ if (this.keys.left) this.tank.rotate(-1);
444
+ if (this.keys.right) this.tank.rotate(1);
445
+
446
+ direction.applyEuler(this.tank.body.rotation);
447
+ this.tank.move(direction);
448
  }
449
+
450
+ const mouseVector = new THREE.Vector2(this.mouse.x, -this.mouse.y);
451
+ const rotationAngle = Math.atan2(mouseVector.x, mouseVector.y);
452
+
453
+ if (this.tank.turretGroup) {
454
+ this.tank.turretGroup.rotation.y = rotationAngle;
 
 
455
  }
 
456
 
457
+ const tankPos = this.tank.getPosition();
458
+ const cameraDistance = 30;
459
+ const cameraHeight = 15;
460
+ const lookAtHeight = 5;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
461
 
462
+ const tankRotation = this.tank.body.rotation.y;
 
 
463
 
464
+ this.camera.position.set(
465
+ tankPos.x - Math.sin(tankRotation) * cameraDistance,
466
+ tankPos.y + cameraHeight,
467
+ tankPos.z - Math.cos(tankRotation) * cameraDistance
468
+ );
469
+
470
+ const lookAtPoint = new THREE.Vector3(
471
+ tankPos.x + Math.sin(tankRotation) * 10,
472
+ tankPos.y + lookAtHeight,
473
+ tankPos.z + Math.cos(tankRotation) * 10
474
+ );
475
+
476
+ this.camera.lookAt(lookAtPoint);
477
+ }
478
+
479
+ createBuildings() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
480
  const buildingTypes = [
481
  { width: 10, height: 30, depth: 10, color: 0x808080 },
482
  { width: 15, height: 40, depth: 15, color: 0x606060 },
 
504
  this.scene.add(building);
505
  }
506
  }
507
+ return Promise.resolve(); // ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด Promise ๋ฐ˜ํ™˜
508
  }
509
 
510
  createBuilding(type) {
 
515
  specular: 0x111111,
516
  shininess: 30
517
  });
518
+ const building = new THREE.Mesh(geometry, material);
519
  building.castShadow = true;
520
  building.receiveShadow = true;
521
  return building;
 
565
  this.enemies.push(enemy);
566
  }
567
  }
568
+ if (!this.isGameOver) {
569
+ setTimeout(spawnEnemy, 3000);
570
+ }
571
  };
572
 
573
  spawnEnemy();
 
608
 
609
  startGameTimer() {
610
  const timer = setInterval(() => {
611
+ if (this.isLoading || this.isGameOver) return;
612
 
613
  this.gameTime--;
614
+ document.getElementById('time').textContent = `Time: ${this.gameTime}s`;
615
+
616
+ if (this.gameTime <= 0) {
617
  clearInterval(timer);
618
  this.endGame();
619
  }
 
629
  }
630
  }
631
  }
632
+ createExplosion(position) {
 
633
  for (let i = 0; i < PARTICLE_COUNT; i++) {
634
  this.particles.push(new Particle(this.scene, position));
635
  }
 
640
 
641
  const tankPosition = this.tank.getPosition();
642
 
643
+ // ์  ์ด์•Œ๊ณผ ํ”Œ๋ ˆ์ด์–ด ํƒฑํฌ ์ถฉ๋Œ ์ฒดํฌ
644
  this.enemies.forEach(enemy => {
645
  if (!enemy.mesh || !enemy.isLoaded) return;
646
 
 
660
  });
661
  });
662
 
663
+ // ํ”Œ๋ ˆ์ด์–ด ํƒฑํฌ์™€ ๊ฑด๋ฌผ ์ถฉ๋Œ ์ฒดํฌ
664
  const tankBoundingBox = new THREE.Box3().setFromObject(this.tank.body);
665
  for (const building of this.buildings) {
666
  const buildingBox = new THREE.Box3().setFromObject(building);
 
670
  }
671
  }
672
 
673
+ // ์ด์ „ ์œ„์น˜ ์ €์žฅ
674
  this.previousTankPosition = this.tank.body.position.clone();
675
  }
676
 
677
  endGame() {
678
  this.isGameOver = true;
679
+ if (this.animationFrameId) {
680
+ cancelAnimationFrame(this.animationFrameId);
681
+ }
682
+
683
  const gameOverDiv = document.createElement('div');
684
  gameOverDiv.style.position = 'absolute';
685
  gameOverDiv.style.top = '50%';
 
705
  }
706
 
707
  animate() {
708
+ if (this.isGameOver) {
709
+ cancelAnimationFrame(this.animationFrameId);
710
+ return;
711
+ }
712
 
713
+ this.animationFrameId = requestAnimationFrame(() => this.animate());
714
 
715
  const currentTime = performance.now();
716
  const deltaTime = (currentTime - this.lastTime) / 1000;
 
720
  this.renderer.render(this.scene, this.camera);
721
  return;
722
  }
723
+ this.handleMovement();
 
724
  this.tank.update(this.mouse.x, this.mouse.y);
725
 
726
  const tankPosition = this.tank.getPosition();