cutechicken commited on
Commit
2523f3b
β€’
1 Parent(s): efd2261

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +244 -749
index.html CHANGED
@@ -1,787 +1,282 @@
1
- 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
- const GAME_DURATION = 180;
7
- const MAP_SIZE = 2000;
8
- const TANK_HEIGHT = 0.5;
9
- const ENEMY_GROUND_HEIGHT = 0;
10
- const ENEMY_SCALE = 10;
11
- const MAX_HEALTH = 1000;
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,
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.ammo = 10;
36
- this.lastShootTime = 0;
37
- this.shootInterval = 1000;
38
- this.bullets = [];
39
- }
40
-
41
- async initialize(scene, loader) {
42
- try {
43
- const bodyResult = await loader.loadAsync('/models/abramsBody.glb');
44
- this.body = bodyResult.scene;
45
- this.body.position.copy(this.position);
46
-
47
- const turretResult = await loader.loadAsync('/models/abramsTurret.glb');
48
- this.turret = turretResult.scene;
49
-
50
- this.turretGroup.position.y = 0.2;
51
- this.turretGroup.add(this.turret);
52
- this.body.add(this.turretGroup);
53
-
54
- this.body.traverse((child) => {
55
- if (child.isMesh) {
56
- child.castShadow = true;
57
- child.receiveShadow = true;
58
- }
59
- });
60
-
61
- this.turret.traverse((child) => {
62
- if (child.isMesh) {
63
- child.castShadow = true;
64
- child.receiveShadow = true;
65
- }
66
- });
67
-
68
- scene.add(this.body);
69
- this.isLoaded = true;
70
-
71
- } catch (error) {
72
- console.error('Error loading tank models:', error);
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) {
137
- if (!this.body) return;
138
- this.body.rotation.y += angle * this.turnSpeed;
139
- }
140
-
141
- getPosition() {
142
- return this.body ? this.body.position : new THREE.Vector3();
143
- }
144
-
145
- takeDamage(damage) {
146
- this.health -= damage;
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;
170
- this.mesh.position.copy(this.position);
171
- this.mesh.scale.set(ENEMY_SCALE, ENEMY_SCALE, ENEMY_SCALE);
172
-
173
- this.mesh.traverse((child) => {
174
- if (child.isMesh) {
175
- child.castShadow = true;
176
- child.receiveShadow = true;
177
- }
178
- });
179
-
180
- this.scene.add(this.mesh);
181
- this.isLoaded = true;
182
- } catch (error) {
183
- console.error('Error loading enemy model:', error);
184
- this.isLoaded = false;
185
  }
186
- }
187
-
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);
210
- this.bullets.splice(i, 1);
211
- }
212
  }
213
- }
214
-
215
- shoot(playerPosition) {
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
 
223
- const bulletGeometry = new THREE.SphereGeometry(this.type === 'tank' ? 0.2 : 0.3);
224
- const bulletMaterial = new THREE.MeshBasicMaterial({
225
- color: this.type === 'tank' ? 0xff0000 : 0xff6600
226
- });
227
- const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
228
-
229
- bullet.position.copy(this.mesh.position);
230
-
231
- const direction = new THREE.Vector3()
232
- .subVectors(playerPosition, this.mesh.position)
233
- .normalize();
234
-
235
- const bulletSpeed = this.type === 'tank' ?
236
- ENEMY_CONFIG.BULLET_SPEED :
237
- ENEMY_CONFIG.BULLET_SPEED * 0.8;
238
-
239
- bullet.velocity = direction.multiplyScalar(bulletSpeed);
240
-
241
- this.scene.add(bullet);
242
- this.bullets.push(bullet);
243
- this.lastAttackTime = currentTime;
244
- }
245
-
246
- takeDamage(damage) {
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));
255
- this.bullets = [];
256
- this.isLoaded = false;
257
  }
258
- }
259
- }
260
-
261
- // Particle ν΄λž˜μŠ€λŠ” κ·ΈλŒ€λ‘œ μœ μ§€
262
- class Particle {
263
- constructor(scene, position) {
264
- const geometry = new THREE.SphereGeometry(0.1);
265
- const material = new THREE.MeshBasicMaterial({ color: 0xff0000 });
266
- this.mesh = new THREE.Mesh(geometry, material);
267
- this.mesh.position.copy(position);
268
-
269
- this.velocity = new THREE.Vector3(
270
- (Math.random() - 0.5) * 0.3,
271
- Math.random() * 0.2,
272
- (Math.random() - 0.5) * 0.3
273
- );
274
-
275
- this.gravity = -0.01;
276
- this.lifetime = 60;
277
- this.age = 0;
278
-
279
- scene.add(this.mesh);
280
- }
281
-
282
- update() {
283
- this.velocity.y += this.gravity;
284
- this.mesh.position.add(this.velocity);
285
- this.age++;
286
- return this.age < this.lifetime;
287
- }
288
-
289
- destroy(scene) {
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 });
300
- this.renderer.setSize(window.innerWidth, window.innerHeight);
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 = [];
308
- this.buildings = [];
309
- this.loader = new GLTFLoader();
310
- this.controls = null;
311
- this.gameTime = GAME_DURATION;
312
- this.score = 0;
313
- this.isGameOver = false;
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,
322
- backward: false,
323
- left: 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();
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Tank Combat Simulator - FPS Mode</title>
7
+ <style>
8
+ body {
9
+ margin: 0;
10
+ overflow: hidden;
11
+ background: #000;
12
+ font-family: 'Courier New', monospace;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
+ #loading {
16
+ position: fixed;
17
+ top: 50%;
18
+ left: 50%;
19
+ transform: translate(-50%, -50%);
20
+ background: rgba(0,0,0,0.8);
21
+ padding: 20px;
22
+ border-radius: 10px;
23
+ z-index: 2000;
24
+ text-align: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  }
 
 
 
 
26
 
27
+ .loading-spinner {
28
+ width: 50px;
29
+ height: 50px;
30
+ border: 5px solid #0f0;
31
+ border-top: 5px solid transparent;
32
+ border-radius: 50%;
33
+ animation: spin 1s linear infinite;
34
+ margin: 0 auto 20px;
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  }
 
 
 
 
 
 
 
 
 
36
 
37
+ @keyframes spin {
38
+ 0% { transform: rotate(0deg); }
39
+ 100% { transform: rotate(360deg); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
+ .loading-text {
43
+ color: #0f0;
44
+ font-size: 24px;
45
+ text-align: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  }
 
47
 
48
+ #gameContainer {
49
+ position: relative;
50
+ width: 100vw;
51
+ height: 100vh;
52
+ cursor: crosshair;
 
 
53
  }
 
54
 
55
+ #info {
56
+ position: absolute;
57
+ top: 10px;
58
+ left: 10px;
59
+ color: #0f0;
60
+ background: rgba(0,20,0,0.7);
61
+ padding: 10px;
62
+ font-size: 14px;
63
+ z-index: 1001;
64
+ border: 1px solid #0f0;
65
+ border-radius: 5px;
66
+ user-select: none;
 
 
 
 
 
 
67
  }
 
68
 
69
+ #crosshair {
70
+ position: fixed;
71
+ top: 50%;
72
+ left: 50%;
73
+ transform: translate(-50%, -50%);
74
+ width: 40px;
75
+ height: 40px;
76
+ border: 2px solid rgba(255,0,0,0.7);
77
+ border-radius: 50%;
78
+ z-index: 1001;
79
+ pointer-events: none;
80
+ mix-blend-mode: difference;
81
  }
 
82
 
83
+ #crosshair::before,
84
+ #crosshair::after {
85
+ content: '';
86
+ position: absolute;
87
+ background: rgba(255,0,0,0.7);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
 
90
+ #crosshair::before {
91
+ top: 50%;
92
+ left: -10px;
93
+ right: -10px;
94
+ height: 2px;
95
+ transform: translateY(-50%);
 
 
 
 
 
 
 
 
 
96
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
+ #crosshair::after {
99
+ left: 50%;
100
+ top: -10px;
101
+ bottom: -10px;
102
+ width: 2px;
103
+ transform: translateX(-50%);
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  }
 
105
 
106
+ #healthBar {
107
+ position: absolute;
108
+ bottom: 20px;
109
+ left: 20px;
110
+ width: 200px;
111
+ height: 20px;
112
+ background: rgba(0,20,0,0.7);
113
+ border: 2px solid #0f0;
114
+ z-index: 1001;
115
+ border-radius: 10px;
116
+ overflow: hidden;
117
  }
 
 
 
 
118
 
119
+ #health {
120
+ width: 100%;
121
+ height: 100%;
122
+ background: linear-gradient(90deg, #0f0, #00ff00);
123
+ transition: width 0.3s;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
+ #ammo {
127
+ position: absolute;
128
+ bottom: 20px;
129
+ right: 20px;
130
+ color: #0f0;
131
+ background: rgba(0,20,0,0.7);
132
+ padding: 10px;
133
+ font-size: 20px;
134
+ z-index: 1001;
135
+ border: 1px solid #0f0;
136
+ border-radius: 5px;
137
+ }
138
 
139
+ #turretInfo {
140
+ position: absolute;
141
+ top: 50px;
142
+ right: 20px;
143
+ color: #0f0;
144
+ background: rgba(0,20,0,0.7);
145
+ padding: 10px;
146
+ font-size: 16px;
147
+ z-index: 1001;
148
+ border: 1px solid #0f0;
149
+ border-radius: 5px;
150
  }
151
 
152
+ #gameTitle {
153
+ position: absolute;
154
+ top: 10px;
155
+ left: 50%;
156
+ transform: translateX(-50%);
157
+ color: #0f0;
158
+ background: rgba(0,20,0,0.7);
159
+ padding: 10px 20px;
160
+ font-size: 20px;
161
+ z-index: 1001;
162
+ border: 1px solid #0f0;
163
+ border-radius: 5px;
164
+ text-transform: uppercase;
165
+ letter-spacing: 2px;
166
+ }
167
 
168
+ #gameStats {
169
+ position: absolute;
170
+ top: 10px;
171
+ right: 20px;
172
+ color: #0f0;
173
+ background: rgba(0,20,0,0.7);
174
+ padding: 10px;
175
+ font-size: 16px;
176
+ z-index: 1001;
177
+ border: 1px solid #0f0;
178
+ border-radius: 5px;
179
+ text-align: right;
180
+ }
181
 
182
+ .start-screen {
183
+ position: fixed;
184
+ top: 0;
185
+ left: 0;
186
+ width: 100%;
187
+ height: 100%;
188
+ background: rgba(0,0,0,0.8);
189
+ display: flex;
190
+ justify-content: center;
191
+ align-items: center;
192
+ flex-direction: column;
193
+ z-index: 2000;
194
  }
195
 
196
+ .start-button {
197
+ padding: 15px 30px;
198
+ font-size: 24px;
199
+ background: #0f0;
200
+ color: #000;
201
+ border: none;
202
+ border-radius: 5px;
203
+ cursor: pointer;
204
+ margin-top: 20px;
205
+ transition: transform 0.2s;
206
  }
207
 
208
+ .start-button:hover {
209
+ transform: scale(1.1);
 
210
  }
 
 
211
 
212
+ #minimap {
213
+ position: absolute;
214
+ bottom: 20px;
215
+ right: 20px;
216
+ width: 200px;
217
+ height: 200px;
218
+ background: rgba(0,20,0,0.7);
219
+ border: 2px solid #0f0;
220
+ border-radius: 5px;
221
+ z-index: 1001;
222
+ }
223
+ </style>
224
+ </head>
225
+ <body>
226
+ <div id="loading">
227
+ <div class="loading-spinner"></div>
228
+ <div class="loading-text">Loading tank assets...</div>
229
+ </div>
230
+
231
+ <div class="start-screen" id="startScreen">
232
+ <h1 style="color: #0f0; font-size: 48px; margin-bottom: 20px;">Tank Combat Simulator</h1>
233
+ <button class="start-button" onclick="startGame()">Start Game</button>
234
+ <div style="color: #0f0; margin-top: 20px; text-align: center;">
235
+ <p>Controls:</p>
236
+ <p>W,A,S,D - Move Tank</p>
237
+ <p>Mouse - Look Around</p>
238
+ <p>Left Click - Fire</p>
239
+ <p>ESC - Pause</p>
240
+ </div>
241
+ </div>
242
+
243
+ <div id="gameContainer">
244
+ <div id="gameTitle">Tank Combat Simulator</div>
245
+ <div id="gameStats">
246
+ <div id="score">Score: 0</div>
247
+ <div id="time">Time: 180s</div>
248
+ </div>
249
+ <div id="crosshair"></div>
250
+ <div id="healthBar"><div id="health"></div></div>
251
+ <div id="ammo">Ammo: 10/10</div>
252
+ <div id="turretInfo">Turret Angle: 0Β°</div>
253
+ <div id="minimap"></div>
254
+ </div>
255
+
256
+ <script type="importmap">
257
+ {
258
+ "imports": {
259
+ "three": "https://unpkg.com/three@0.157.0/build/three.module.js",
260
+ "three/addons/": "https://unpkg.com/three@0.157.0/examples/jsm/"
261
+ }
262
+ }
263
+ </script>
264
+ <script>
265
+ function startGame() {
266
+ document.getElementById('startScreen').style.display = 'none';
267
+ // 여기에 κ²Œμž„ μ‹œμž‘ 둜직 μΆ”κ°€
268
+ document.body.requestPointerLock();
269
+ }
270
 
271
+ // 포인터 락 이벀트 처리
272
+ document.addEventListener('pointerlockchange', () => {
273
+ if (document.pointerLockElement === document.body) {
274
+ console.log('Pointer locked');
275
+ } else {
276
+ console.log('Pointer unlocked');
277
+ }
278
+ });
279
+ </script>
280
+ <script type="module" src="game.js"></script>
281
+ </body>
282
+ </html>