cutechicken commited on
Commit
efd2261
โ€ข
1 Parent(s): 0e6c440

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +749 -244
index.html CHANGED
@@ -1,282 +1,787 @@
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>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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();