cutechicken commited on
Commit
f027a73
โ€ข
1 Parent(s): 0fd1d06

Update game.js

Browse files
Files changed (1) hide show
  1. game.js +421 -133
game.js CHANGED
@@ -2,7 +2,6 @@ import * as THREE from 'three';
2
  import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
3
  import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';
4
 
5
-
6
  // ๊ฒŒ์ž„ ์ƒ์ˆ˜
7
  const GAME_DURATION = 180;
8
  const MAP_SIZE = 2000;
@@ -15,58 +14,121 @@ const ENEMY_COUNT_MAX = 5;
15
  const PARTICLE_COUNT = 15;
16
  const BUILDING_COUNT = 30; // ๊ฑด๋ฌผ ์ˆ˜ ์ถ”๊ฐ€
17
  const ENEMY_CONFIG = {
18
- TANK: {
19
- MODEL: '/models/enemy1.glb',
20
- HEALTH: 120,
21
- SPEED: 0.1,
22
- ATTACK_RANGE: 100,
23
- ATTACK_INTERVAL: 2000,
24
- BULLET_SPEED: 2
25
- },
26
- HEAVY_TANK: {
27
- MODEL: '/models/enemy4.glb',
28
- HEALTH: 200,
29
- SPEED: 0.05,
30
- ATTACK_RANGE: 150,
31
- ATTACK_INTERVAL: 3000,
32
- BULLET_SPEED: 1.5
33
- }
34
  };
35
 
36
- // Building ํด๋ž˜์Šค ์ถ”๊ฐ€
37
- class Building {
38
- constructor(scene, position, size) {
39
- const geometry = new THREE.BoxGeometry(size.width, size.height, size.depth);
40
- const material = new THREE.MeshStandardMaterial({
41
- color: 0x808080,
42
- roughness: 0.7,
43
- metalness: 0.3
44
- });
45
- this.mesh = new THREE.Mesh(geometry, material);
46
- this.mesh.position.copy(position);
47
- this.mesh.position.y = size.height / 2;
48
- this.mesh.castShadow = true;
49
- this.mesh.receiveShadow = true;
50
- scene.add(this.mesh);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  }
52
- }
53
 
 
 
 
 
 
 
 
 
 
54
  // Enemy ํด๋ž˜์Šค ์ˆ˜์ •
55
  class Enemy {
56
- constructor(scene, position, type) {
57
  this.scene = scene;
58
  this.position = position;
59
- this.type = type;
60
  this.mesh = null;
61
- this.health = type === 'TANK' ? ENEMY_CONFIG.TANK.HEALTH : ENEMY_CONFIG.HEAVY_TANK.HEALTH;
 
62
  this.lastAttackTime = 0;
63
  this.bullets = [];
64
- this.config = ENEMY_CONFIG[type];
 
65
  }
66
 
67
  async initialize(loader) {
68
  try {
69
- const modelPath = this.type === 'TANK' ? ENEMY_CONFIG.TANK.MODEL : ENEMY_CONFIG.HEAVY_TANK.MODEL;
 
70
  const result = await loader.loadAsync(modelPath);
71
  this.mesh = result.scene;
72
  this.mesh.position.copy(this.position);
@@ -80,29 +142,32 @@ class Enemy {
80
  });
81
 
82
  this.scene.add(this.mesh);
 
83
  } catch (error) {
84
  console.error('Error loading enemy model:', error);
 
85
  }
86
  }
87
 
88
  update(playerPosition) {
89
- if (!this.mesh) return;
90
 
 
91
  const direction = new THREE.Vector3()
92
  .subVectors(playerPosition, this.mesh.position)
93
  .normalize();
94
 
95
  this.mesh.lookAt(playerPosition);
96
 
97
- // ํƒ€์ž…๋ณ„ ์ด๋™ ์†๋„ ์ ์šฉ
98
- const moveSpeed = this.type === 'TANK' ? ENEMY_CONFIG.TANK.SPEED : ENEMY_CONFIG.HEAVY_TANK.SPEED;
99
- this.mesh.position.add(direction.multiplyScalar(moveSpeed));
100
 
101
  // ์ด์•Œ ์—…๋ฐ์ดํŠธ
102
  for (let i = this.bullets.length - 1; i >= 0; i--) {
103
  const bullet = this.bullets[i];
104
  bullet.position.add(bullet.velocity);
105
 
 
106
  if (Math.abs(bullet.position.x) > MAP_SIZE ||
107
  Math.abs(bullet.position.z) > MAP_SIZE) {
108
  this.scene.remove(bullet);
@@ -113,15 +178,15 @@ class Enemy {
113
 
114
  shoot(playerPosition) {
115
  const currentTime = Date.now();
116
- const attackInterval = this.type === 'TANK' ?
117
- ENEMY_CONFIG.TANK.ATTACK_INTERVAL :
118
- ENEMY_CONFIG.HEAVY_TANK.ATTACK_INTERVAL;
119
 
120
  if (currentTime - this.lastAttackTime < attackInterval) return;
121
 
122
- const bulletGeometry = new THREE.SphereGeometry(0.2);
123
  const bulletMaterial = new THREE.MeshBasicMaterial({
124
- color: this.type === 'TANK' ? 0xff0000 : 0xff6600
125
  });
126
  const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
127
 
@@ -131,10 +196,10 @@ class Enemy {
131
  .subVectors(playerPosition, this.mesh.position)
132
  .normalize();
133
 
134
- const bulletSpeed = this.type === 'TANK' ?
135
- ENEMY_CONFIG.TANK.BULLET_SPEED :
136
- ENEMY_CONFIG.HEAVY_TANK.BULLET_SPEED;
137
-
138
  bullet.velocity = direction.multiplyScalar(bulletSpeed);
139
 
140
  this.scene.add(bullet);
@@ -152,12 +217,47 @@ class Enemy {
152
  this.scene.remove(this.mesh);
153
  this.bullets.forEach(bullet => this.scene.remove(bullet));
154
  this.bullets = [];
 
155
  }
156
  }
157
  }
158
- // Game ํด๋ž˜์Šค ์ˆ˜์ •
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  class Game {
160
  constructor() {
 
161
  this.scene = new THREE.Scene();
162
  this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
163
  this.renderer = new THREE.WebGLRenderer({ antialias: true });
@@ -165,6 +265,7 @@ class Game {
165
  this.renderer.shadowMap.enabled = true;
166
  document.body.appendChild(this.renderer.domElement);
167
 
 
168
  this.tank = new TankPlayer();
169
  this.enemies = [];
170
  this.particles = [];
@@ -174,12 +275,15 @@ class Game {
174
  this.gameTime = GAME_DURATION;
175
  this.score = 0;
176
  this.isGameOver = false;
 
177
 
 
178
  this.mouse = {
179
  x: 0,
180
  y: 0
181
  };
182
 
 
183
  this.keys = {
184
  forward: false,
185
  backward: false,
@@ -187,91 +291,205 @@ class Game {
187
  right: false
188
  };
189
 
 
190
  this.setupEventListeners();
 
 
191
  this.initialize();
192
  }
193
 
194
  async initialize() {
195
- // ์กฐ๋ช… ์„ค์ •
196
- const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
197
- this.scene.add(ambientLight);
198
-
199
- const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
200
- directionalLight.position.set(50, 50, 50);
201
- directionalLight.castShadow = true;
202
- this.scene.add(directionalLight);
203
-
204
- // ๋„์‹œ ๋ฐ”๋‹ฅ ์ƒ์„ฑ
205
- const groundGeometry = new THREE.PlaneGeometry(MAP_SIZE, MAP_SIZE);
206
- const groundMaterial = new THREE.MeshStandardMaterial({
207
- color: 0x333333, // ์–ด๋‘์šด ํšŒ์ƒ‰ (์•„์ŠคํŒ”ํŠธ)
208
- roughness: 0.9,
209
- metalness: 0.1
210
- });
211
- const ground = new THREE.Mesh(groundGeometry, groundMaterial);
212
- ground.rotation.x = -Math.PI / 2;
213
- ground.receiveShadow = true;
214
- this.scene.add(ground);
 
 
 
 
 
 
 
 
 
 
 
 
215
 
216
- // ๊ฑด๋ฌผ ์ƒ์„ฑ
217
- this.createBuildings();
 
218
 
219
- // ํƒฑํฌ ์ดˆ๊ธฐํ™”
220
- await this.tank.initialize(this.scene, this.loader);
221
 
222
- // ์นด๋ฉ”๋ผ ์„ค์ •
223
- this.camera.position.set(0, 10, -10);
224
- this.camera.lookAt(0, 0, 0);
225
 
226
- this.controls = new PointerLockControls(this.camera, document.body);
 
 
 
227
 
228
- this.animate();
229
- this.spawnEnemies();
230
- this.startGameTimer();
 
231
  }
232
 
233
- createBuildings() {
 
 
 
 
 
 
234
  for (let i = 0; i < BUILDING_COUNT; i++) {
235
- const size = {
236
- width: 10 + Math.random() * 20,
237
- height: 20 + Math.random() * 80,
238
- depth: 10 + Math.random() * 20
239
- };
240
-
241
- const position = new THREE.Vector3(
242
- (Math.random() - 0.5) * (MAP_SIZE - size.width),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  0,
244
- (Math.random() - 0.5) * (MAP_SIZE - size.depth)
245
- );
 
 
 
 
 
 
 
 
 
 
 
 
246
 
247
- const building = new Building(this.scene, position, size);
248
- this.buildings.push(building);
 
 
 
 
 
 
 
249
  }
250
  }
251
 
252
  spawnEnemies() {
253
  const spawnEnemy = () => {
254
  if (this.enemies.length < ENEMY_COUNT_MAX && !this.isGameOver) {
255
- const position = new THREE.Vector3(
256
- (Math.random() - 0.5) * MAP_SIZE,
257
- ENEMY_GROUND_HEIGHT,
258
- (Math.random() - 0.5) * MAP_SIZE
259
- );
260
-
261
- // ๋žœ๋คํ•˜๊ฒŒ ์  ์œ ํ˜• ์„ ํƒ
262
- const type = Math.random() > 0.7 ? 'HEAVY_TANK' : 'TANK';
263
- const enemy = new Enemy(this.scene, position, type);
264
- enemy.initialize(this.loader);
265
- this.enemies.push(enemy);
266
  }
267
  setTimeout(spawnEnemy, 3000);
268
  };
269
 
270
  spawnEnemy();
271
  }
272
- // Game ํด๋ž˜์Šค ๊ณ„์†...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  startGameTimer() {
274
  const timer = setInterval(() => {
 
 
275
  this.gameTime--;
276
  if (this.gameTime <= 0 || this.isGameOver) {
277
  clearInterval(timer);
@@ -281,7 +499,10 @@ class Game {
281
  }
282
 
283
  setupEventListeners() {
 
284
  document.addEventListener('keydown', (event) => {
 
 
285
  switch(event.code) {
286
  case 'KeyW': this.keys.forward = true; break;
287
  case 'KeyS': this.keys.backward = true; break;
@@ -291,6 +512,8 @@ class Game {
291
  });
292
 
293
  document.addEventListener('keyup', (event) => {
 
 
294
  switch(event.code) {
295
  case 'KeyW': this.keys.forward = false; break;
296
  case 'KeyS': this.keys.backward = false; break;
@@ -299,11 +522,15 @@ class Game {
299
  }
300
  });
301
 
 
302
  document.addEventListener('mousemove', (event) => {
 
 
303
  this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
304
  this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
305
  });
306
 
 
307
  window.addEventListener('resize', () => {
308
  this.camera.aspect = window.innerWidth / window.innerHeight;
309
  this.camera.updateProjectionMatrix();
@@ -312,6 +539,8 @@ class Game {
312
  }
313
 
314
  handleMovement() {
 
 
315
  const direction = new THREE.Vector3();
316
 
317
  if (this.keys.forward) direction.z += 1;
@@ -342,40 +571,49 @@ class Game {
342
  }
343
 
344
  checkCollisions() {
 
 
345
  const tankPosition = this.tank.getPosition();
346
 
347
  // ์ ๊ณผ์˜ ์ถฉ๋Œ ์ฒดํฌ
348
  this.enemies.forEach(enemy => {
349
- if (!enemy.mesh) return;
350
 
351
- // ์ด์•Œ ์ถฉ๋Œ ์ฒดํฌ
352
  enemy.bullets.forEach(bullet => {
353
  const distance = bullet.position.distanceTo(tankPosition);
354
  if (distance < 1) {
355
- if (this.tank.takeDamage(enemy.type === 'HEAVY_TANK' ? 15 : 10)) {
356
  this.endGame();
357
  }
358
  this.scene.remove(bullet);
359
  enemy.bullets = enemy.bullets.filter(b => b !== bullet);
360
- }
361
- });
362
-
363
- // ๊ฑด๋ฌผ๊ณผ์˜ ์ถฉ๋Œ ์ฒดํฌ
364
- this.buildings.forEach(building => {
365
- // ๊ฐ„๋‹จํ•œ ์ถฉ๋Œ ์ฒดํฌ (์‹ค์ œ ๊ฒŒ์ž„์—์„œ๋Š” ๋” ์ •๊ตํ•œ ์ถฉ๋Œ ์ฒดํฌ๊ฐ€ ํ•„์š”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค)
366
- const distance = enemy.mesh.position.distanceTo(building.mesh.position);
367
- if (distance < 5) { // ์ž„์˜์˜ ์ถฉ๋Œ ๊ฑฐ๋ฆฌ
368
- const pushDirection = new THREE.Vector3()
369
- .subVectors(enemy.mesh.position, building.mesh.position)
370
- .normalize();
371
- enemy.mesh.position.add(pushDirection);
372
  }
373
  });
374
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
  }
376
 
377
  endGame() {
378
  this.isGameOver = true;
 
379
  const gameOverDiv = document.createElement('div');
380
  gameOverDiv.style.position = 'absolute';
381
  gameOverDiv.style.top = '50%';
@@ -383,33 +621,83 @@ class Game {
383
  gameOverDiv.style.transform = 'translate(-50%, -50%)';
384
  gameOverDiv.style.color = 'white';
385
  gameOverDiv.style.fontSize = '48px';
386
- gameOverDiv.innerHTML = `Game Over<br>Score: ${this.score}`;
387
- document.body.appendChild(gameOverDiv);
388
- }
389
-
390
- animate() {
 
 
 
 
 
 
 
 
 
 
 
 
391
  if (this.isGameOver) return;
392
 
393
  requestAnimationFrame(() => this.animate());
394
 
 
 
 
 
 
 
 
395
  this.tank.update(this.mouse.x, this.mouse.y);
396
  this.handleMovement();
397
 
 
398
  const tankPosition = this.tank.getPosition();
399
- this.enemies.forEach(enemy => {
400
  enemy.update(tankPosition);
401
- const distance = enemy.mesh?.position.distanceTo(tankPosition) || Infinity;
402
- if (distance < enemy.config.ATTACK_RANGE) {
403
- enemy.shoot(tankPosition);
 
 
 
404
  }
405
  });
406
 
 
407
  this.updateParticles();
 
 
408
  this.checkCollisions();
409
 
 
 
 
 
410
  this.renderer.render(this.scene, this.camera);
411
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
  }
413
 
414
- // ๊ฒŒ์ž„ ์‹œ์ž‘
415
  const game = new Game();
 
2
  import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
3
  import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';
4
 
 
5
  // ๊ฒŒ์ž„ ์ƒ์ˆ˜
6
  const GAME_DURATION = 180;
7
  const MAP_SIZE = 2000;
 
14
  const PARTICLE_COUNT = 15;
15
  const BUILDING_COUNT = 30; // ๊ฑด๋ฌผ ์ˆ˜ ์ถ”๊ฐ€
16
  const ENEMY_CONFIG = {
17
+ ATTACK_RANGE: 100,
18
+ ATTACK_INTERVAL: 2000,
19
+ BULLET_SPEED: 2
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  };
21
 
22
+ // TankPlayer ํด๋ž˜์Šค
23
+ class TankPlayer {
24
+ constructor() {
25
+ this.body = null;
26
+ this.turret = null;
27
+ this.position = new THREE.Vector3(0, 0, 0);
28
+ this.rotation = new THREE.Euler(0, 0, 0);
29
+ this.turretRotation = 0;
30
+ this.moveSpeed = 0.5;
31
+ this.turnSpeed = 0.03;
32
+ this.turretGroup = new THREE.Group();
33
+ this.health = MAX_HEALTH;
34
+ this.isLoaded = false; // ๋กœ๋”ฉ ์ƒํƒœ ์ถ”๊ฐ€
35
+ this.previousTankPosition = new THREE.Vector3();
36
+ }
37
+
38
+ async initialize(scene, loader) {
39
+ try {
40
+ const bodyResult = await loader.loadAsync('/models/abramsBody.glb');
41
+ this.body = bodyResult.scene;
42
+ this.body.position.copy(this.position);
43
+
44
+ const turretResult = await loader.loadAsync('/models/abramsTurret.glb');
45
+ this.turret = turretResult.scene;
46
+
47
+ this.turretGroup.position.y = 0.2;
48
+ this.turretGroup.add(this.turret);
49
+ this.body.add(this.turretGroup);
50
+
51
+ this.body.traverse((child) => {
52
+ if (child.isMesh) {
53
+ child.castShadow = true;
54
+ child.receiveShadow = true;
55
+ }
56
+ });
57
+
58
+ this.turret.traverse((child) => {
59
+ if (child.isMesh) {
60
+ child.castShadow = true;
61
+ child.receiveShadow = true;
62
+ }
63
+ });
64
+
65
+ scene.add(this.body);
66
+ this.isLoaded = true; // ๋กœ๋”ฉ ์™„๋ฃŒ ํ‘œ์‹œ
67
+
68
+ } catch (error) {
69
+ console.error('Error loading tank models:', error);
70
+ this.isLoaded = false; // ๋กœ๋”ฉ ์‹คํŒจ ํ‘œ์‹œ
71
+ }
72
+ }
73
+
74
+ // ๋‚˜๋จธ์ง€ TankPlayer ๋ฉ”์„œ๋“œ๋“ค์€ ๋™์ผ...
75
+ update(mouseX, mouseY) {
76
+ if (!this.body || !this.turretGroup) return;
77
+
78
+ const targetAngle = Math.atan2(mouseX, mouseY);
79
+ const currentRotation = this.turretGroup.rotation.y;
80
+ const rotationDiff = targetAngle - currentRotation;
81
+
82
+ let normalizedDiff = rotationDiff;
83
+ while (normalizedDiff > Math.PI) normalizedDiff -= Math.PI * 2;
84
+ while (normalizedDiff < -Math.PI) normalizedDiff += Math.PI * 2;
85
+
86
+ this.turretGroup.rotation.y += normalizedDiff * 0.1;
87
+ }
88
+
89
+ move(direction) {
90
+ if (!this.body) return;
91
+
92
+ const moveVector = new THREE.Vector3();
93
+ moveVector.x = direction.x * this.moveSpeed;
94
+ moveVector.z = direction.z * this.moveSpeed;
95
+
96
+ moveVector.applyEuler(this.body.rotation);
97
+ this.body.position.add(moveVector);
98
+ }
99
+
100
+ rotate(angle) {
101
+ if (!this.body) return;
102
+ this.body.rotation.y += angle * this.turnSpeed;
103
  }
 
104
 
105
+ getPosition() {
106
+ return this.body ? this.body.position : new THREE.Vector3();
107
+ }
108
+
109
+ takeDamage(damage) {
110
+ this.health -= damage;
111
+ return this.health <= 0;
112
+ }
113
+ }
114
  // Enemy ํด๋ž˜์Šค ์ˆ˜์ •
115
  class Enemy {
116
+ constructor(scene, position, type = 'tank') {
117
  this.scene = scene;
118
  this.position = position;
 
119
  this.mesh = null;
120
+ this.type = type; // 'tank' ๋˜๋Š” 'heavy'
121
+ this.health = type === 'tank' ? 100 : 200; // heavy๋Š” ์ฒด๋ ฅ์ด ๋” ๋†’์Œ
122
  this.lastAttackTime = 0;
123
  this.bullets = [];
124
+ this.isLoaded = false;
125
+ this.moveSpeed = type === 'tank' ? ENEMY_MOVE_SPEED : ENEMY_MOVE_SPEED * 0.7; // heavy๋Š” ๋” ๋Š๋ฆผ
126
  }
127
 
128
  async initialize(loader) {
129
  try {
130
+ // ํƒ€์ž…์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ๋ชจ๋ธ ๋กœ๋“œ
131
+ const modelPath = this.type === 'tank' ? '/models/enemy1.glb' : '/models/enemy4.glb';
132
  const result = await loader.loadAsync(modelPath);
133
  this.mesh = result.scene;
134
  this.mesh.position.copy(this.position);
 
142
  });
143
 
144
  this.scene.add(this.mesh);
145
+ this.isLoaded = true;
146
  } catch (error) {
147
  console.error('Error loading enemy model:', error);
148
+ this.isLoaded = false;
149
  }
150
  }
151
 
152
  update(playerPosition) {
153
+ if (!this.mesh || !this.isLoaded) return;
154
 
155
+ // ํ”Œ๋ ˆ์ด์–ด ๋ฐฉํ–ฅ์œผ๋กœ ํšŒ์ „
156
  const direction = new THREE.Vector3()
157
  .subVectors(playerPosition, this.mesh.position)
158
  .normalize();
159
 
160
  this.mesh.lookAt(playerPosition);
161
 
162
+ // ํ”Œ๋ ˆ์ด์–ด ๋ฐฉํ–ฅ์œผ๋กœ ์ด๋™ (ํƒ€์ž…์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ์†๋„)
163
+ this.mesh.position.add(direction.multiplyScalar(this.moveSpeed));
 
164
 
165
  // ์ด์•Œ ์—…๋ฐ์ดํŠธ
166
  for (let i = this.bullets.length - 1; i >= 0; i--) {
167
  const bullet = this.bullets[i];
168
  bullet.position.add(bullet.velocity);
169
 
170
+ // ์ด์•Œ์ด ๋งต ๋ฐ–์œผ๋กœ ๋‚˜๊ฐ€๋ฉด ์ œ๊ฑฐ
171
  if (Math.abs(bullet.position.x) > MAP_SIZE ||
172
  Math.abs(bullet.position.z) > MAP_SIZE) {
173
  this.scene.remove(bullet);
 
178
 
179
  shoot(playerPosition) {
180
  const currentTime = Date.now();
181
+ const attackInterval = this.type === 'tank' ?
182
+ ENEMY_CONFIG.ATTACK_INTERVAL :
183
+ ENEMY_CONFIG.ATTACK_INTERVAL * 1.5; // heavy๋Š” ๋ฐœ์‚ฌ ๊ฐ„๊ฒฉ์ด ๋” ๊น€
184
 
185
  if (currentTime - this.lastAttackTime < attackInterval) return;
186
 
187
+ const bulletGeometry = new THREE.SphereGeometry(this.type === 'tank' ? 0.2 : 0.3);
188
  const bulletMaterial = new THREE.MeshBasicMaterial({
189
+ color: this.type === 'tank' ? 0xff0000 : 0xff6600
190
  });
191
  const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
192
 
 
196
  .subVectors(playerPosition, this.mesh.position)
197
  .normalize();
198
 
199
+ const bulletSpeed = this.type === 'tank' ?
200
+ ENEMY_CONFIG.BULLET_SPEED :
201
+ ENEMY_CONFIG.BULLET_SPEED * 0.8;
202
+
203
  bullet.velocity = direction.multiplyScalar(bulletSpeed);
204
 
205
  this.scene.add(bullet);
 
217
  this.scene.remove(this.mesh);
218
  this.bullets.forEach(bullet => this.scene.remove(bullet));
219
  this.bullets = [];
220
+ this.isLoaded = false;
221
  }
222
  }
223
  }
224
+
225
+ // Particle ํด๋ž˜์Šค๋Š” ๊ทธ๋Œ€๋กœ ์œ ์ง€
226
+ class Particle {
227
+ constructor(scene, position) {
228
+ const geometry = new THREE.SphereGeometry(0.1);
229
+ const material = new THREE.MeshBasicMaterial({ color: 0xff0000 });
230
+ this.mesh = new THREE.Mesh(geometry, material);
231
+ this.mesh.position.copy(position);
232
+
233
+ this.velocity = new THREE.Vector3(
234
+ (Math.random() - 0.5) * 0.3,
235
+ Math.random() * 0.2,
236
+ (Math.random() - 0.5) * 0.3
237
+ );
238
+
239
+ this.gravity = -0.01;
240
+ this.lifetime = 60;
241
+ this.age = 0;
242
+
243
+ scene.add(this.mesh);
244
+ }
245
+
246
+ update() {
247
+ this.velocity.y += this.gravity;
248
+ this.mesh.position.add(this.velocity);
249
+ this.age++;
250
+ return this.age < this.lifetime;
251
+ }
252
+
253
+ destroy(scene) {
254
+ scene.remove(this.mesh);
255
+ }
256
+ }
257
+ // Game ํด๋ž˜์Šค
258
  class Game {
259
  constructor() {
260
+ // ๊ธฐ๋ณธ Three.js ์„ค์ •
261
  this.scene = new THREE.Scene();
262
  this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
263
  this.renderer = new THREE.WebGLRenderer({ antialias: true });
 
265
  this.renderer.shadowMap.enabled = true;
266
  document.body.appendChild(this.renderer.domElement);
267
 
268
+ // ๊ฒŒ์ž„ ์š”์†Œ ์ดˆ๊ธฐํ™”
269
  this.tank = new TankPlayer();
270
  this.enemies = [];
271
  this.particles = [];
 
275
  this.gameTime = GAME_DURATION;
276
  this.score = 0;
277
  this.isGameOver = false;
278
+ this.isLoading = true; // ๋กœ๋”ฉ ์ƒํƒœ ์ถ”๊ฐ€
279
 
280
+ // ๋งˆ์šฐ์Šค ์ƒํƒœ
281
  this.mouse = {
282
  x: 0,
283
  y: 0
284
  };
285
 
286
+ // ํ‚ค๋ณด๋“œ ์ƒํƒœ
287
  this.keys = {
288
  forward: false,
289
  backward: false,
 
291
  right: false
292
  };
293
 
294
+ // ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์„ค์ •
295
  this.setupEventListeners();
296
+
297
+ // ๊ฒŒ์ž„ ์ดˆ๊ธฐํ™”
298
  this.initialize();
299
  }
300
 
301
  async initialize() {
302
+ try {
303
+ // ์กฐ๋ช… ์„ค์ •
304
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
305
+ this.scene.add(ambientLight);
306
+
307
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
308
+ directionalLight.position.set(50, 50, 50);
309
+ directionalLight.castShadow = true;
310
+ directionalLight.shadow.mapSize.width = 2048;
311
+ directionalLight.shadow.mapSize.height = 2048;
312
+ this.scene.add(directionalLight);
313
+
314
+ // ๋„์‹œ ๋ฐ”๋‹ฅ ์ƒ์„ฑ (์•„์ŠคํŒ”ํŠธ ์งˆ๊ฐ)
315
+ const groundGeometry = new THREE.PlaneGeometry(MAP_SIZE, MAP_SIZE);
316
+ const groundMaterial = new THREE.MeshStandardMaterial({
317
+ color: 0x333333, // ์–ด๋‘์šด ํšŒ์ƒ‰ (์•„์ŠคํŒ”ํŠธ ์ƒ‰)
318
+ roughness: 0.9,
319
+ metalness: 0.1
320
+ });
321
+ const ground = new THREE.Mesh(groundGeometry, groundMaterial);
322
+ ground.rotation.x = -Math.PI / 2;
323
+ ground.receiveShadow = true;
324
+ this.scene.add(ground);
325
+
326
+ // ๊ฑด๋ฌผ ์ƒ์„ฑ
327
+ await this.createBuildings();
328
+
329
+ // ํƒฑํฌ ์ดˆ๊ธฐํ™”
330
+ await this.tank.initialize(this.scene, this.loader);
331
+ if (!this.tank.isLoaded) {
332
+ throw new Error('Tank loading failed');
333
+ }
334
 
335
+ // ์นด๋ฉ”๋ผ ์œ„์น˜ ์„ค์ •
336
+ this.camera.position.set(0, 10, -10);
337
+ this.camera.lookAt(0, 0, 0);
338
 
339
+ // ํฌ์ธํ„ฐ ๋ฝ ์ปจํŠธ๋กค ์„ค์ •
340
+ this.controls = new PointerLockControls(this.camera, document.body);
341
 
342
+ // ๋กœ๋”ฉ ์™„๋ฃŒ
343
+ this.isLoading = false;
344
+ document.getElementById('loading').style.display = 'none';
345
 
346
+ // ๊ฒŒ์ž„ ์‹œ์ž‘
347
+ this.animate();
348
+ this.spawnEnemies();
349
+ this.startGameTimer();
350
 
351
+ } catch (error) {
352
+ console.error('Game initialization error:', error);
353
+ this.handleLoadingError();
354
+ }
355
  }
356
 
357
+ async createBuildings() {
358
+ const buildingTypes = [
359
+ { width: 10, height: 30, depth: 10, color: 0x808080 },
360
+ { width: 15, height: 40, depth: 15, color: 0x606060 },
361
+ { width: 20, height: 50, depth: 20, color: 0x404040 }
362
+ ];
363
+
364
  for (let i = 0; i < BUILDING_COUNT; i++) {
365
+ const type = buildingTypes[Math.floor(Math.random() * buildingTypes.length)];
366
+ const building = this.createBuilding(type);
367
+
368
+ // ๊ฑด๋ฌผ ์œ„์น˜ ์„ค์ • (๋‹ค๋ฅธ ๊ฑด๋ฌผ๊ณผ ๊ฒน์น˜์ง€ ์•Š๊ฒŒ)
369
+ let position;
370
+ let attempts = 0;
371
+ do {
372
+ position = new THREE.Vector3(
373
+ (Math.random() - 0.5) * (MAP_SIZE - type.width),
374
+ type.height / 2,
375
+ (Math.random() - 0.5) * (MAP_SIZE - type.depth)
376
+ );
377
+ attempts++;
378
+ } while (this.checkBuildingCollision(position, type) && attempts < 50);
379
+
380
+ if (attempts < 50) {
381
+ building.position.copy(position);
382
+ this.buildings.push(building);
383
+ this.scene.add(building);
384
+ }
385
+ }
386
+ }
387
+
388
+ createBuilding(type) {
389
+ const geometry = new THREE.BoxGeometry(type.width, type.height, type.depth);
390
+ const material = new THREE.MeshPhongMaterial({
391
+ color: type.color,
392
+ // ์ฐฝ๋ฌธ ํšจ๊ณผ๋ฅผ ์œ„ํ•œ ์„ค์ •
393
+ emissive: 0x222222,
394
+ specular: 0x111111,
395
+ shininess: 30
396
+ });
397
+ const building = new THREE.Mesh(geometry, material);
398
+ building.castShadow = true;
399
+ building.receiveShadow = true;
400
+ return building;
401
+ }
402
+
403
+ checkBuildingCollision(position, type) {
404
+ const margin = 5; // ๊ฑด๋ฌผ ๊ฐ„ ์ตœ์†Œ ๊ฑฐ๋ฆฌ
405
+ const bbox = new THREE.Box3(
406
+ new THREE.Vector3(
407
+ position.x - (type.width / 2 + margin),
408
  0,
409
+ position.z - (type.depth / 2 + margin)
410
+ ),
411
+ new THREE.Vector3(
412
+ position.x + (type.width / 2 + margin),
413
+ type.height,
414
+ position.z + (type.depth / 2 + margin)
415
+ )
416
+ );
417
+
418
+ return this.buildings.some(building => {
419
+ const buildingBox = new THREE.Box3().setFromObject(building);
420
+ return bbox.intersectsBox(buildingBox);
421
+ });
422
+ }
423
 
424
+ handleLoadingError() {
425
+ this.isLoading = false;
426
+ const loadingElement = document.getElementById('loading');
427
+ if (loadingElement) {
428
+ loadingElement.innerHTML = `
429
+ <div class="loading-text" style="color: red;">
430
+ Loading failed. Please refresh the page.
431
+ </div>
432
+ `;
433
  }
434
  }
435
 
436
  spawnEnemies() {
437
  const spawnEnemy = () => {
438
  if (this.enemies.length < ENEMY_COUNT_MAX && !this.isGameOver) {
439
+ const position = this.getValidEnemySpawnPosition();
440
+ if (position) {
441
+ // ๋žœ๋คํ•˜๊ฒŒ ์  ์œ ํ˜• ์„ ํƒ (70% tank, 30% heavy)
442
+ const type = Math.random() < 0.7 ? 'tank' : 'heavy';
443
+ const enemy = new Enemy(this.scene, position, type);
444
+ enemy.initialize(this.loader);
445
+ this.enemies.push(enemy);
446
+ }
 
 
 
447
  }
448
  setTimeout(spawnEnemy, 3000);
449
  };
450
 
451
  spawnEnemy();
452
  }
453
+
454
+ getValidEnemySpawnPosition() {
455
+ const margin = 20;
456
+ let position;
457
+ let attempts = 0;
458
+ const maxAttempts = 50;
459
+
460
+ do {
461
+ position = new THREE.Vector3(
462
+ (Math.random() - 0.5) * (MAP_SIZE - margin * 2),
463
+ ENEMY_GROUND_HEIGHT,
464
+ (Math.random() - 0.5) * (MAP_SIZE - margin * 2)
465
+ );
466
+
467
+ // ํ”Œ๋ ˆ์ด์–ด์™€์˜ ๊ฑฐ๋ฆฌ ์ฒดํฌ
468
+ const distanceToPlayer = position.distanceTo(this.tank.getPosition());
469
+ if (distanceToPlayer < 100) continue;
470
+
471
+ // ๊ฑด๋ฌผ๊ณผ์˜ ์ถฉ๋Œ ์ฒดํฌ
472
+ let collisionFound = false;
473
+ for (const building of this.buildings) {
474
+ const buildingBox = new THREE.Box3().setFromObject(building);
475
+ if (buildingBox.containsPoint(position)) {
476
+ collisionFound = true;
477
+ break;
478
+ }
479
+ }
480
+
481
+ if (!collisionFound) return position;
482
+
483
+ attempts++;
484
+ } while (attempts < maxAttempts);
485
+
486
+ return null;
487
+ }
488
+
489
  startGameTimer() {
490
  const timer = setInterval(() => {
491
+ if (this.isLoading) return;
492
+
493
  this.gameTime--;
494
  if (this.gameTime <= 0 || this.isGameOver) {
495
  clearInterval(timer);
 
499
  }
500
 
501
  setupEventListeners() {
502
+ // ํ‚ค๋ณด๋“œ ์ด๋ฒคํŠธ
503
  document.addEventListener('keydown', (event) => {
504
+ if (this.isLoading) return;
505
+
506
  switch(event.code) {
507
  case 'KeyW': this.keys.forward = true; break;
508
  case 'KeyS': this.keys.backward = true; break;
 
512
  });
513
 
514
  document.addEventListener('keyup', (event) => {
515
+ if (this.isLoading) return;
516
+
517
  switch(event.code) {
518
  case 'KeyW': this.keys.forward = false; break;
519
  case 'KeyS': this.keys.backward = false; break;
 
522
  }
523
  });
524
 
525
+ // ๋งˆ์šฐ์Šค ์ด๋ฒคํŠธ
526
  document.addEventListener('mousemove', (event) => {
527
+ if (this.isLoading) return;
528
+
529
  this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
530
  this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
531
  });
532
 
533
+ // ์ฐฝ ํฌ๊ธฐ ๋ณ€๊ฒฝ ์ด๋ฒคํŠธ
534
  window.addEventListener('resize', () => {
535
  this.camera.aspect = window.innerWidth / window.innerHeight;
536
  this.camera.updateProjectionMatrix();
 
539
  }
540
 
541
  handleMovement() {
542
+ if (this.isLoading || !this.tank.isLoaded) return;
543
+
544
  const direction = new THREE.Vector3();
545
 
546
  if (this.keys.forward) direction.z += 1;
 
571
  }
572
 
573
  checkCollisions() {
574
+ if (this.isLoading || !this.tank.isLoaded) return;
575
+
576
  const tankPosition = this.tank.getPosition();
577
 
578
  // ์ ๊ณผ์˜ ์ถฉ๋Œ ์ฒดํฌ
579
  this.enemies.forEach(enemy => {
580
+ if (!enemy.mesh || !enemy.isLoaded) return;
581
 
 
582
  enemy.bullets.forEach(bullet => {
583
  const distance = bullet.position.distanceTo(tankPosition);
584
  if (distance < 1) {
585
+ if (this.tank.takeDamage(10)) {
586
  this.endGame();
587
  }
588
  this.scene.remove(bullet);
589
  enemy.bullets = enemy.bullets.filter(b => b !== bullet);
590
+
591
+ // ํ”ผ๊ฒฉ ํšจ๊ณผ
592
+ this.createExplosion(bullet.position);
593
+ document.getElementById('health').style.width =
594
+ `${(this.tank.health / MAX_HEALTH) * 100}%`;
 
 
 
 
 
 
 
595
  }
596
  });
597
  });
598
+
599
+ // ๊ฑด๋ฌผ๊ณผ์˜ ์ถฉ๋Œ ์ฒดํฌ
600
+ const tankBoundingBox = new THREE.Box3().setFromObject(this.tank.body);
601
+ for (const building of this.buildings) {
602
+ const buildingBox = new THREE.Box3().setFromObject(building);
603
+ if (tankBoundingBox.intersectsBox(buildingBox)) {
604
+ // ์ถฉ๋Œ ์‹œ ํƒฑํฌ๋ฅผ ์ด์ „ ์œ„์น˜๋กœ ๋˜๋Œ๋ฆผ
605
+ this.tank.body.position.copy(this.previousTankPosition);
606
+ break;
607
+ }
608
+ }
609
+
610
+ // ํ˜„์žฌ ์œ„์น˜ ์ €์žฅ
611
+ this.previousTankPosition = this.tank.body.position.clone();
612
  }
613
 
614
  endGame() {
615
  this.isGameOver = true;
616
+ // ๊ฒŒ์ž„ ์˜ค๋ฒ„ UI ํ‘œ์‹œ
617
  const gameOverDiv = document.createElement('div');
618
  gameOverDiv.style.position = 'absolute';
619
  gameOverDiv.style.top = '50%';
 
621
  gameOverDiv.style.transform = 'translate(-50%, -50%)';
622
  gameOverDiv.style.color = 'white';
623
  gameOverDiv.style.fontSize = '48px';
624
+ gameOverDiv.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
625
+ gameOverDiv.style.padding = '20px';
626
+ gameOverDiv.style.borderRadius = '10px';
627
+ gameOverDiv.innerHTML = `
628
+ Game Over<br>
629
+ Score: ${this.score}<br>
630
+ Time Survived: ${GAME_DURATION - this.gameTime}s<br>
631
+ <button onclick="location.reload()"
632
+ style="font-size: 24px; padding: 10px; margin-top: 20px;
633
+ cursor: pointer; background: #4CAF50; border: none;
634
+ color: white; border-radius: 5px;">
635
+ Play Again
636
+ </button>
637
+ `;
638
+ document.body.appendChild(gameOverDiv); // ์ด ์ค„์ด ๋ˆ„๋ฝ๋จ
639
+ }
640
+ animate() {
641
  if (this.isGameOver) return;
642
 
643
  requestAnimationFrame(() => this.animate());
644
 
645
+ // ๋กœ๋”ฉ ์ค‘์ด๋ฉด ๋ Œ๋”๋ง๋งŒ ์ˆ˜ํ–‰
646
+ if (this.isLoading) {
647
+ this.renderer.render(this.scene, this.camera);
648
+ return;
649
+ }
650
+
651
+ // ํƒฑํฌ ์—…๋ฐ์ดํŠธ
652
  this.tank.update(this.mouse.x, this.mouse.y);
653
  this.handleMovement();
654
 
655
+ // ์  ์—…๋ฐ์ดํŠธ
656
  const tankPosition = this.tank.getPosition();
657
+ this.enemies.forEach((enemy, index) => {
658
  enemy.update(tankPosition);
659
+
660
+ if (enemy.isLoaded) { // ๋กœ๋”ฉ๋œ ์ ๋งŒ ๊ณต๊ฒฉ ์ˆ˜ํ–‰
661
+ const distance = enemy.mesh.position.distanceTo(tankPosition);
662
+ if (distance < ENEMY_CONFIG.ATTACK_RANGE) {
663
+ enemy.shoot(tankPosition);
664
+ }
665
  }
666
  });
667
 
668
+ // ํŒŒํ‹ฐํด ์—…๋ฐ์ดํŠธ
669
  this.updateParticles();
670
+
671
+ // ์ถฉ๋Œ ์ฒดํฌ
672
  this.checkCollisions();
673
 
674
+ // UI ์—…๋ฐ์ดํŠธ
675
+ this.updateUI();
676
+
677
+ // ๋ Œ๋”๋ง
678
  this.renderer.render(this.scene, this.camera);
679
  }
680
+
681
+ updateUI() {
682
+ // ์ฒด๋ ฅ๋ฐ” ์—…๋ฐ์ดํŠธ
683
+ const healthBar = document.getElementById('health');
684
+ if (healthBar) {
685
+ healthBar.style.width = `${(this.tank.health / MAX_HEALTH) * 100}%`;
686
+ }
687
+
688
+ // ์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ
689
+ const timeElement = document.getElementById('time');
690
+ if (timeElement) {
691
+ timeElement.textContent = `Time: ${this.gameTime}s`;
692
+ }
693
+
694
+ // ์ ์ˆ˜ ์—…๋ฐ์ดํŠธ
695
+ const scoreElement = document.getElementById('score');
696
+ if (scoreElement) {
697
+ scoreElement.textContent = `Score: ${this.score}`;
698
+ }
699
+ }
700
  }
701
 
702
+ // ๊ฒŒ์ž„ ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ
703
  const game = new Game();