cutechicken commited on
Commit
4b521cc
โ€ข
1 Parent(s): 2523f3b

Update game.js

Browse files
Files changed (1) hide show
  1. game.js +494 -104
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,
@@ -75,56 +75,62 @@ 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);
84
-
85
- const bulletOffset = new THREE.Vector3(0, 0.5, 2);
86
- bulletOffset.applyQuaternion(this.turretGroup.quaternion);
87
- bulletOffset.applyQuaternion(this.body.quaternion);
88
- bullet.position.copy(this.body.position).add(bulletOffset);
89
-
90
- const direction = new THREE.Vector3(0, 0, 1);
91
- direction.applyQuaternion(this.turretGroup.quaternion);
92
- direction.applyQuaternion(this.body.quaternion);
93
- bullet.velocity = direction.multiplyScalar(2);
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
  update(mouseX, mouseY) {
106
- if (!this.body || !this.turretGroup) return;
107
-
108
- for (let i = this.bullets.length - 1; i >= 0; i--) {
109
- const bullet = this.bullets[i];
110
- bullet.position.add(bullet.velocity);
111
-
112
- if (Math.abs(bullet.position.x) > MAP_SIZE/2 ||
113
- Math.abs(bullet.position.z) > MAP_SIZE/2) {
114
- scene.remove(bullet);
115
- this.bullets.splice(i, 1);
116
- }
117
- }
118
- }
 
 
119
 
120
  move(direction) {
121
- if (!this.body) return;
122
-
123
- const moveVector = new THREE.Vector3();
124
- moveVector.x = direction.x * this.moveSpeed;
125
- moveVector.z = direction.z * this.moveSpeed;
126
-
127
- this.body.position.add(moveVector);
128
  }
129
 
130
  rotate(angle) {
@@ -141,22 +147,23 @@ class TankPlayer {
141
  return this.health <= 0;
142
  }
143
  }
144
-
145
  class Enemy {
146
  constructor(scene, position, type = 'tank') {
147
  this.scene = scene;
148
  this.position = position;
149
  this.mesh = null;
150
- this.type = type;
151
- this.health = type === 'tank' ? 100 : 200;
152
  this.lastAttackTime = 0;
153
  this.bullets = [];
154
  this.isLoaded = false;
155
- this.moveSpeed = type === 'tank' ? ENEMY_MOVE_SPEED : ENEMY_MOVE_SPEED * 0.7;
156
  }
157
 
158
  async initialize(loader) {
159
  try {
 
160
  const modelPath = this.type === 'tank' ? '/models/enemy1.glb' : '/models/enemy4.glb';
161
  const result = await loader.loadAsync(modelPath);
162
  this.mesh = result.scene;
@@ -181,17 +188,22 @@ class Enemy {
181
  update(playerPosition) {
182
  if (!this.mesh || !this.isLoaded) return;
183
 
 
184
  const direction = new THREE.Vector3()
185
  .subVectors(playerPosition, this.mesh.position)
186
  .normalize();
187
 
188
  this.mesh.lookAt(playerPosition);
 
 
189
  this.mesh.position.add(direction.multiplyScalar(this.moveSpeed));
190
 
 
191
  for (let i = this.bullets.length - 1; i >= 0; i--) {
192
  const bullet = this.bullets[i];
193
  bullet.position.add(bullet.velocity);
194
 
 
195
  if (Math.abs(bullet.position.x) > MAP_SIZE ||
196
  Math.abs(bullet.position.z) > MAP_SIZE) {
197
  this.scene.remove(bullet);
@@ -204,7 +216,7 @@ class Enemy {
204
  const currentTime = Date.now();
205
  const attackInterval = this.type === 'tank' ?
206
  ENEMY_CONFIG.ATTACK_INTERVAL :
207
- ENEMY_CONFIG.ATTACK_INTERVAL * 1.5;
208
 
209
  if (currentTime - this.lastAttackTime < attackInterval) return;
210
 
@@ -246,6 +258,7 @@ class Enemy {
246
  }
247
  }
248
 
 
249
  class Particle {
250
  constructor(scene, position) {
251
  const geometry = new THREE.SphereGeometry(0.1);
@@ -277,9 +290,10 @@ class Particle {
277
  scene.remove(this.mesh);
278
  }
279
  }
280
-
281
  class Game {
282
  constructor() {
 
283
  this.scene = new THREE.Scene();
284
  this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
285
  this.renderer = new THREE.WebGLRenderer({ antialias: true });
@@ -287,6 +301,7 @@ class Game {
287
  this.renderer.shadowMap.enabled = true;
288
  document.getElementById('gameContainer').appendChild(this.renderer.domElement);
289
 
 
290
  this.tank = new TankPlayer();
291
  this.enemies = [];
292
  this.particles = [];
@@ -300,6 +315,7 @@ class Game {
300
  this.previousTankPosition = new THREE.Vector3();
301
  this.lastTime = performance.now();
302
 
 
303
  this.mouse = { x: 0, y: 0 };
304
  this.keys = {
305
  forward: false,
@@ -308,90 +324,464 @@ class Game {
308
  right: false
309
  };
310
 
 
311
  this.setupEventListeners();
312
  this.initialize();
313
- this.disposedObjects = new Set();
314
  }
315
 
316
- // ์—ฌ๊ธฐ์— Game ํด๋ž˜์Šค์˜ ๋‚˜๋จธ์ง€ ๋ฉ”์„œ๋“œ๋“ค์ด ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค...
317
- // (initialize, setupEventListeners, handleMovement, animate, checkCollisions, updateUI, endGame ๋“ฑ)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
 
319
- createExplosion(position) {
320
- for (let i = 0; i < PARTICLE_COUNT; i++) {
321
- this.particles.push(new Particle(this.scene, position));
 
 
 
 
 
 
322
  }
323
- }
 
 
 
 
 
 
 
 
 
 
324
 
325
- updateParticles() {
326
- for (let i = this.particles.length - 1; i >= 0; i--) {
327
- const particle = this.particles[i];
328
- if (!particle.update()) {
329
- particle.destroy(this.scene);
330
- this.particles.splice(i, 1);
 
 
 
 
 
 
 
 
 
 
 
331
  }
332
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  }
334
 
335
- async createBuildings() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
  for (let i = 0; i < BUILDING_COUNT; i++) {
337
- const x = (Math.random() - 0.5) * MAP_SIZE;
338
- const z = (Math.random() - 0.5) * MAP_SIZE;
339
-
340
- const geometry = new THREE.BoxGeometry(10, 20, 10);
341
- const material = new THREE.MeshStandardMaterial({ color: 0x808080 });
342
- const building = new THREE.Mesh(geometry, material);
343
 
344
- building.position.set(x, 10, z);
345
- building.castShadow = true;
346
- building.receiveShadow = true;
347
-
348
- this.scene.add(building);
349
- this.buildings.push(building);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350
  }
351
  }
352
 
353
  spawnEnemies() {
354
- const spawnInterval = setInterval(() => {
355
- if (this.isGameOver || this.enemies.length >= ENEMY_COUNT_MAX) return;
356
-
357
- const angle = Math.random() * Math.PI * 2;
358
- const distance = 100 + Math.random() * 50;
359
- const x = Math.cos(angle) * distance;
360
- const z = Math.sin(angle) * distance;
361
-
362
- const enemyType = Math.random() > 0.7 ? 'heavy' : 'tank';
363
- const enemy = new Enemy(this.scene, new THREE.Vector3(x, ENEMY_GROUND_HEIGHT, z), enemyType);
364
- enemy.initialize(this.loader);
365
- this.enemies.push(enemy);
366
- }, 3000);
 
367
  }
368
 
369
- startGameTimer() {
370
- const timerInterval = setInterval(() => {
371
- if (this.isGameOver) {
372
- clearInterval(timerInterval);
373
- return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
  }
375
 
 
 
 
 
 
 
 
 
 
 
 
 
376
  this.gameTime--;
377
- if (this.gameTime <= 0) {
 
378
  this.endGame();
379
- clearInterval(timerInterval);
380
  }
381
  }, 1000);
382
  }
383
 
384
- handleLoadingError() {
385
- this.isLoading = false;
386
- document.getElementById('loading').style.display = 'none';
387
- document.getElementById('error').style.display = 'block';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
  }
389
  }
390
 
391
- // ๊ฒŒ์ž„ ์‹œ์ž‘
392
  window.startGame = function() {
393
  document.getElementById('startScreen').style.display = 'none';
394
  document.body.requestPointerLock();
395
  };
396
 
 
397
  const game = new Game();
 
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,
 
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
  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
  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
+ // ์ด์•Œ ์—…๋ฐ์ดํŠธ
202
  for (let i = this.bullets.length - 1; i >= 0; i--) {
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
  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
 
 
258
  }
259
  }
260
 
261
+ // Particle ํด๋ž˜์Šค๋Š” ๊ทธ๋Œ€๋กœ ์œ ์ง€
262
  class Particle {
263
  constructor(scene, position) {
264
  const geometry = new THREE.SphereGeometry(0.1);
 
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
  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 = [];
 
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
  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 },
517
+ { width: 20, height: 50, depth: 20, color: 0x404040 }
518
+ ];
519
+
520
  for (let i = 0; i < BUILDING_COUNT; i++) {
521
+ const type = buildingTypes[Math.floor(Math.random() * buildingTypes.length)];
522
+ const building = this.createBuilding(type);
 
 
 
 
523
 
524
+ let position;
525
+ let attempts = 0;
526
+ do {
527
+ position = new THREE.Vector3(
528
+ (Math.random() - 0.5) * (MAP_SIZE - type.width),
529
+ type.height / 2,
530
+ (Math.random() - 0.5) * (MAP_SIZE - type.depth)
531
+ );
532
+ attempts++;
533
+ } while (this.checkBuildingCollision(position, type) && attempts < 50);
534
+
535
+ if (attempts < 50) {
536
+ building.position.copy(position);
537
+ this.buildings.push(building);
538
+ this.scene.add(building);
539
+ }
540
+ }
541
+ }
542
+
543
+ createBuilding(type) {
544
+ const geometry = new THREE.BoxGeometry(type.width, type.height, type.depth);
545
+ const material = new THREE.MeshPhongMaterial({
546
+ color: type.color,
547
+ emissive: 0x222222,
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;
555
+ }
556
+
557
+ checkBuildingCollision(position, type) {
558
+ const margin = 5;
559
+ const bbox = new THREE.Box3(
560
+ new THREE.Vector3(
561
+ position.x - (type.width / 2 + margin),
562
+ 0,
563
+ position.z - (type.depth / 2 + margin)
564
+ ),
565
+ new THREE.Vector3(
566
+ position.x + (type.width / 2 + margin),
567
+ type.height,
568
+ position.z + (type.depth / 2 + margin)
569
+ )
570
+ );
571
+
572
+ return this.buildings.some(building => {
573
+ const buildingBox = new THREE.Box3().setFromObject(building);
574
+ return bbox.intersectsBox(buildingBox);
575
+ });
576
+ }
577
+
578
+ handleLoadingError() {
579
+ this.isLoading = false;
580
+ const loadingElement = document.getElementById('loading');
581
+ if (loadingElement) {
582
+ loadingElement.innerHTML = `
583
+ <div class="loading-text" style="color: red;">
584
+ Loading failed. Please refresh the page.
585
+ </div>
586
+ `;
587
  }
588
  }
589
 
590
  spawnEnemies() {
591
+ const spawnEnemy = () => {
592
+ if (this.enemies.length < ENEMY_COUNT_MAX && !this.isGameOver) {
593
+ const position = this.getValidEnemySpawnPosition();
594
+ if (position) {
595
+ const type = Math.random() < 0.7 ? 'tank' : 'heavy';
596
+ const enemy = new Enemy(this.scene, position, type);
597
+ enemy.initialize(this.loader);
598
+ this.enemies.push(enemy);
599
+ }
600
+ }
601
+ setTimeout(spawnEnemy, 3000);
602
+ };
603
+
604
+ spawnEnemy();
605
  }
606
 
607
+ getValidEnemySpawnPosition() {
608
+ const margin = 20;
609
+ let position;
610
+ let attempts = 0;
611
+ const maxAttempts = 50;
612
+
613
+ do {
614
+ position = new THREE.Vector3(
615
+ (Math.random() - 0.5) * (MAP_SIZE - margin * 2),
616
+ ENEMY_GROUND_HEIGHT,
617
+ (Math.random() - 0.5) * (MAP_SIZE - margin * 2)
618
+ );
619
+
620
+ const distanceToPlayer = position.distanceTo(this.tank.getPosition());
621
+ if (distanceToPlayer < 100) continue;
622
+
623
+ let collisionFound = false;
624
+ for (const building of this.buildings) {
625
+ const buildingBox = new THREE.Box3().setFromObject(building);
626
+ if (buildingBox.containsPoint(position)) {
627
+ collisionFound = true;
628
+ break;
629
+ }
630
  }
631
 
632
+ if (!collisionFound) return position;
633
+
634
+ attempts++;
635
+ } while (attempts < maxAttempts);
636
+
637
+ return null;
638
+ }
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
  }
649
  }, 1000);
650
  }
651
 
652
+ updateParticles() {
653
+ for (let i = this.particles.length - 1; i >= 0; i--) {
654
+ const particle = this.particles[i];
655
+ if (!particle.update()) {
656
+ particle.destroy(this.scene);
657
+ this.particles.splice(i, 1);
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
+ }
666
+ }
667
+
668
+ checkCollisions() {
669
+ if (this.isLoading || !this.tank.isLoaded) return;
670
+
671
+ const tankPosition = this.tank.getPosition();
672
+
673
+ this.enemies.forEach(enemy => {
674
+ if (!enemy.mesh || !enemy.isLoaded) return;
675
+
676
+ enemy.bullets.forEach(bullet => {
677
+ const distance = bullet.position.distanceTo(tankPosition);
678
+ if (distance < 1) {
679
+ if (this.tank.takeDamage(10)) {
680
+ this.endGame();
681
+ }
682
+ this.scene.remove(bullet);
683
+ enemy.bullets = enemy.bullets.filter(b => b !== bullet);
684
+
685
+ this.createExplosion(bullet.position);
686
+ document.getElementById('health').style.width =
687
+ `${(this.tank.health / MAX_HEALTH) * 100}%`;
688
+ }
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);
695
+ if (tankBoundingBox.intersectsBox(buildingBox)) {
696
+ this.tank.body.position.copy(this.previousTankPosition);
697
+ break;
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%';
709
+ gameOverDiv.style.left = '50%';
710
+ gameOverDiv.style.transform = 'translate(-50%, -50%)';
711
+ gameOverDiv.style.color = 'white';
712
+ gameOverDiv.style.fontSize = '48px';
713
+ gameOverDiv.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
714
+ gameOverDiv.style.padding = '20px';
715
+ gameOverDiv.style.borderRadius = '10px';
716
+ gameOverDiv.innerHTML = `
717
+ Game Over<br>
718
+ Score: ${this.score}<br>
719
+ Time Survived: ${GAME_DURATION - this.gameTime}s<br>
720
+ <button onclick="location.reload()"
721
+ style="font-size: 24px; padding: 10px; margin-top: 20px;
722
+ cursor: pointer; background: #4CAF50; border: none;
723
+ color: white; border-radius: 5px;">
724
+ Play Again
725
+ </button>
726
+ `;
727
+ document.body.appendChild(gameOverDiv);
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;
737
+ this.lastTime = currentTime;
738
+
739
+ if (this.isLoading) {
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();
748
+ this.enemies.forEach(enemy => {
749
+ enemy.update(tankPosition);
750
+
751
+ if (enemy.isLoaded && enemy.mesh.position.distanceTo(tankPosition) < ENEMY_CONFIG.ATTACK_RANGE) {
752
+ enemy.shoot(tankPosition);
753
+ }
754
+ });
755
+
756
+ this.updateParticles();
757
+ this.checkCollisions();
758
+ this.updateUI();
759
+ this.renderer.render(this.scene, this.camera);
760
+ }
761
+
762
+ updateUI() {
763
+ const healthBar = document.getElementById('health');
764
+ if (healthBar) {
765
+ healthBar.style.width = `${(this.tank.health / MAX_HEALTH) * 100}%`;
766
+ }
767
+
768
+ const timeElement = document.getElementById('time');
769
+ if (timeElement) {
770
+ timeElement.textContent = `Time: ${this.gameTime}s`;
771
+ }
772
+
773
+ const scoreElement = document.getElementById('score');
774
+ if (scoreElement) {
775
+ scoreElement.textContent = `Score: ${this.score}`;
776
+ }
777
  }
778
  }
779
 
780
+ // HTML์˜ startGame ํ•จ์ˆ˜์™€ ์—ฐ๊ฒฐ
781
  window.startGame = function() {
782
  document.getElementById('startScreen').style.display = 'none';
783
  document.body.requestPointerLock();
784
  };
785
 
786
+ // ๊ฒŒ์ž„ ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ
787
  const game = new Game();