TankWar3D / game.js
cutechicken's picture
Update game.js
6fd82de verified
raw
history blame
25 kB
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';
// ๊ฒŒ์ž„ ์ƒ์ˆ˜
const GAME_DURATION = 180;
const MAP_SIZE = 2000;
const TANK_HEIGHT = 2.0; // ์ „์ฐจ ๋†’์ด๋กœ ๋ณ€๊ฒฝ
const ENEMY_GROUND_HEIGHT = 0;
const ENEMY_SCALE = 10;
const MAX_HEALTH = 1000;
const ENEMY_MOVE_SPEED = 0.1;
const ENEMY_COUNT_MAX = 5;
const PARTICLE_COUNT = 15;
const OBSTACLE_COUNT = 50;
const ENEMY_CONFIG = {
ATTACK_RANGE: 100,
ATTACK_INTERVAL: 2000,
BULLET_SPEED: 2
};
// TankPlayer ํด๋ž˜์Šค ์ •์˜
class TankPlayer {
constructor() {
this.body = null; // ์ „์ฐจ ๋ชธ์ฒด
this.turret = null; // ์ „์ฐจ ํฌํƒ‘
this.position = new THREE.Vector3(0, 0, 0);
this.rotation = new THREE.Euler(0, 0, 0);
this.turretRotation = 0;
this.moveSpeed = 0.5;
this.turnSpeed = 0.03;
}
async initialize(scene, loader) {
try {
// ๋ชธ์ฒด ๋กœ๋“œ
const bodyResult = await loader.loadAsync('/models/abramsBody.glb');
this.body = bodyResult.scene;
this.body.position.copy(this.position);
// ํฌํƒ‘ ๋กœ๋“œ
const turretResult = await loader.loadAsync('/models/abramsTurret.glb');
this.turret = turretResult.scene;
// ํฌํƒ‘ ์œ„์น˜ ์กฐ์ • (๋ชธ์ฒด ์œ„์— ์˜ฌ๋ฆผ)
this.turret.position.y = TANK_HEIGHT;
// ๊ทธ๋ฆผ์ž ์„ค์ •
this.body.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
this.turret.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
// ์”ฌ์— ์ถ”๊ฐ€
this.body.add(this.turret);
scene.add(this.body);
} catch (error) {
console.error('Error loading tank models:', error);
}
}
update(mouseX, mouseY) {
if (!this.body || !this.turret) return;
// ๋งˆ์šฐ์Šค ์œ„์น˜๋ฅผ ์ด์šฉํ•œ ํฌํƒ‘ ํšŒ์ „ ๊ณ„์‚ฐ
const mousePosition = new THREE.Vector2(mouseX, mouseY);
const targetAngle = Math.atan2(mousePosition.x, mousePosition.y);
// ํฌํƒ‘ ๋ถ€๋“œ๋Ÿฌ์šด ํšŒ์ „
const currentRotation = this.turret.rotation.y;
const rotationDiff = targetAngle - currentRotation;
this.turret.rotation.y += rotationDiff * 0.1;
}
move(direction) {
if (!this.body) return;
const moveVector = new THREE.Vector3();
moveVector.x = direction.x * this.moveSpeed;
moveVector.z = direction.z * this.moveSpeed;
moveVector.applyEuler(this.body.rotation);
this.body.position.add(moveVector);
}
rotate(angle) {
if (!this.body) return;
this.body.rotation.y += angle * this.turnSpeed;
}
}
// ๊ฒŒ์ž„ ๋ณ€์ˆ˜
let scene, camera, renderer, controls;
let player; // ์ „์ฐจ ํ”Œ๋ ˆ์ด์–ด ์ธ์Šคํ„ด์Šค
let enemies = [];
let bullets = [];
let enemyBullets = [];
let playerHealth = MAX_HEALTH;
let ammo = 30;
let currentStage = 1;
let isGameOver = false;
let lastTime = performance.now();
let lastRender = 0;
// ์˜ค์‹ค๋ ˆ์ดํ„ฐ ๊ธฐ๋ฐ˜ ์ด์†Œ๋ฆฌ ์ƒ์„ฑ๊ธฐ
class GunSoundGenerator {
constructor() {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
createGunshot() {
const currentTime = this.audioContext.currentTime;
// ๋ฉ”์ธ ์˜ค์‹ค๋ ˆ์ดํ„ฐ
const osc = this.audioContext.createOscillator();
const gainNode = this.audioContext.createGain();
osc.type = 'square';
osc.frequency.setValueAtTime(200, currentTime);
osc.frequency.exponentialRampToValueAtTime(50, currentTime + 0.1);
gainNode.gain.setValueAtTime(0.5, currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, currentTime + 0.1);
osc.connect(gainNode);
gainNode.connect(this.audioContext.destination);
osc.start(currentTime);
osc.stop(currentTime + 0.1);
// ๋…ธ์ด์ฆˆ ์ถ”๊ฐ€
const bufferSize = this.audioContext.sampleRate * 0.1;
const buffer = this.audioContext.createBuffer(1, bufferSize, this.audioContext.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = Math.random() * 2 - 1;
}
const noise = this.audioContext.createBufferSource();
const noiseGain = this.audioContext.createGain();
noise.buffer = buffer;
noiseGain.gain.setValueAtTime(0.2, currentTime);
noiseGain.gain.exponentialRampToValueAtTime(0.01, currentTime + 0.05);
noise.connect(noiseGain);
noiseGain.connect(this.audioContext.destination);
noise.start(currentTime);
}
resume() {
if (this.audioContext.state === 'suspended') {
this.audioContext.resume();
}
}
}
// ์‚ฌ์šด๋“œ ์‹œ์Šคํ…œ ์ดˆ๊ธฐํ™”
const gunSound = new GunSoundGenerator();
async function init() {
document.getElementById('loading').style.display = 'block';
try {
// Scene ์ดˆ๊ธฐํ™”
scene = new THREE.Scene();
scene.background = new THREE.Color(0x87ceeb);
scene.fog = new THREE.Fog(0x87ceeb, 0, 1000);
// Renderer ์ตœ์ ํ™”
renderer = new THREE.WebGLRenderer({
antialias: false,
powerPreference: "high-performance"
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.BasicShadowMap;
document.body.appendChild(renderer.domElement);
// Camera ์„ค์ •
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, TANK_HEIGHT + 5, -10); // ์นด๋ฉ”๋ผ ์œ„์น˜ ์ „์ฐจ์— ๋งž๊ฒŒ ์กฐ์ •
// ๊ธฐ๋ณธ ์กฐ๋ช…
scene.add(new THREE.AmbientLight(0xffffff, 0.6));
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
dirLight.position.set(100, 100, 50);
dirLight.castShadow = true;
dirLight.shadow.mapSize.width = 1024;
dirLight.shadow.mapSize.height = 1024;
scene.add(dirLight);
// Controls ์„ค์ •
controls = new PointerLockControls(camera, document.body);
// ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
setupEventListeners();
// ํ”Œ๋ ˆ์ด์–ด ์ดˆ๊ธฐํ™”
player = new TankPlayer();
await player.initialize(scene, new GLTFLoader());
// ๊ฒŒ์ž„ ์š”์†Œ ์ดˆ๊ธฐํ™”
await Promise.all([
createTerrain(),
createEnemies()
]);
document.getElementById('loading').style.display = 'none';
console.log('Game initialized successfully');
} catch (error) {
console.error('Initialization error:', error);
document.getElementById('loading').innerHTML = `
<div class="loading-text" style="color: #ff0000;">
Error loading models. Please check console and file paths.
</div>
`;
throw error;
}
}
function setupEventListeners() {
document.addEventListener('click', onClick);
document.addEventListener('keydown', onKeyDown);
document.addEventListener('keyup', onKeyUp);
document.addEventListener('mousemove', onMouseMove);
window.addEventListener('resize', onWindowResize);
}
// ๋งˆ์šฐ์Šค ์ด๋™ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ์ถ”๊ฐ€
function onMouseMove(event) {
if (controls.isLocked && player) {
const mouseX = (event.clientX / window.innerWidth) * 2 - 1;
const mouseY = -(event.clientY / window.innerHeight) * 2 + 1;
player.update(mouseX, mouseY);
}
}
async function testModelLoading() {
const loader = new GLTFLoader();
try {
const modelPath = 'models/enemy1.glb';
console.log('Testing model loading:', modelPath);
const gltf = await loader.loadAsync(modelPath);
console.log('Test model loaded successfully:', gltf);
} catch (error) {
console.error('Test model loading failed:', error);
throw error;
}
}
function createTerrain() {
return new Promise((resolve) => {
const geometry = new THREE.PlaneGeometry(MAP_SIZE, MAP_SIZE, 100, 100);
const material = new THREE.MeshStandardMaterial({
color: 0xD2B48C,
roughness: 0.8,
metalness: 0.2
});
const vertices = geometry.attributes.position.array;
for (let i = 0; i < vertices.length; i += 3) {
vertices[i + 2] = Math.sin(vertices[i] * 0.01) * Math.cos(vertices[i + 1] * 0.01) * 20;
}
geometry.attributes.position.needsUpdate = true;
geometry.computeVertexNormals();
const terrain = new THREE.Mesh(geometry, material);
terrain.rotation.x = -Math.PI / 2;
terrain.receiveShadow = true;
scene.add(terrain);
addObstacles();
resolve();
});
}
function addObstacles() {
const rockGeometry = new THREE.DodecahedronGeometry(10);
const rockMaterial = new THREE.MeshStandardMaterial({
color: 0x8B4513,
roughness: 0.9
});
for (let i = 0; i < OBSTACLE_COUNT; i++) {
const rock = new THREE.Mesh(rockGeometry, rockMaterial);
rock.position.set(
(Math.random() - 0.5) * MAP_SIZE * 0.9,
Math.random() * 10,
(Math.random() - 0.5) * MAP_SIZE * 0.9
);
rock.rotation.set(
Math.random() * Math.PI,
Math.random() * Math.PI,
Math.random() * Math.PI
);
rock.castShadow = true;
rock.receiveShadow = true;
scene.add(rock);
}
}
async function createEnemies() {
console.log('Creating enemies...');
const loader = new GLTFLoader();
const enemyCount = Math.min(3 + currentStage, ENEMY_COUNT_MAX);
for (let i = 0; i < enemyCount; i++) {
const angle = (i / enemyCount) * Math.PI * 2;
const radius = 200;
const position = new THREE.Vector3(
Math.cos(angle) * radius,
ENEMY_GROUND_HEIGHT,
Math.sin(angle) * radius
);
// ์ž„์‹œ ์  ์ƒ์„ฑ
const tempEnemy = createTemporaryEnemy(position);
scene.add(tempEnemy.model);
enemies.push(tempEnemy);
try {
const modelIndex = i % 4 + 1;
const modelPath = `models/enemy${modelIndex}.glb`;
console.log(`Loading model: ${modelPath}`);
const gltf = await loader.loadAsync(modelPath);
const model = gltf.scene;
model.scale.set(ENEMY_SCALE, ENEMY_SCALE, ENEMY_SCALE);
model.position.copy(position);
model.traverse((node) => {
if (node.isMesh) {
node.castShadow = true;
node.receiveShadow = true;
node.material.metalness = 0.2;
node.material.roughness = 0.8;
}
});
scene.remove(tempEnemy.model);
scene.add(model);
enemies[enemies.indexOf(tempEnemy)].model = model;
console.log(`Successfully loaded enemy model ${modelIndex}`);
} catch (error) {
console.error(`Error loading enemy model:`, error);
}
}
}
function createTemporaryEnemy(position) {
const geometry = new THREE.BoxGeometry(5, 10, 5);
const material = new THREE.MeshPhongMaterial({
color: 0xff0000,
transparent: true,
opacity: 0.8
});
const model = new THREE.Mesh(geometry, material);
model.position.copy(position);
model.castShadow = true;
model.receiveShadow = true;
return {
model: model,
health: 100,
speed: ENEMY_MOVE_SPEED,
lastAttackTime: 0
};
}
function createExplosion(position) {
const particles = [];
for (let i = 0; i < PARTICLE_COUNT; i++) {
const particle = new THREE.Mesh(
new THREE.SphereGeometry(0.3),
new THREE.MeshBasicMaterial({
color: 0xff4400,
transparent: true,
opacity: 1
})
);
particle.position.copy(position);
particle.velocity = new THREE.Vector3(
(Math.random() - 0.5) * 2,
Math.random() * 2,
(Math.random() - 0.5) * 2
);
particles.push(particle);
scene.add(particle);
}
// ํญ๋ฐœ ๊ด‘์› ํšจ๊ณผ
const explosionLight = new THREE.PointLight(0xff4400, 2, 20);
explosionLight.position.copy(position);
scene.add(explosionLight);
let opacity = 1;
const animate = () => {
opacity -= 0.05;
if (opacity <= 0) {
particles.forEach(p => scene.remove(p));
scene.remove(explosionLight);
return;
}
particles.forEach(particle => {
particle.position.add(particle.velocity);
particle.material.opacity = opacity;
});
requestAnimationFrame(animate);
};
animate();
}
function onClick() {
if (!controls.isLocked) {
controls.lock();
gunSound.resume();
} else if (ammo > 0) {
shoot();
}
}
function onKeyDown(event) {
if (!player) return;
switch(event.code) {
case 'KeyW':
moveState.forward = true;
const forwardDir = new THREE.Vector3(0, 0, -1);
player.move(forwardDir);
break;
case 'KeyS':
moveState.backward = true;
const backwardDir = new THREE.Vector3(0, 0, 1);
player.move(backwardDir);
break;
case 'KeyA':
moveState.left = true;
player.rotate(1);
break;
case 'KeyD':
moveState.right = true;
player.rotate(-1);
break;
case 'KeyR': reload(); break;
}
}
function onKeyUp(event) {
switch(event.code) {
case 'KeyW': moveState.forward = false; break;
case 'KeyS': moveState.backward = false; break;
case 'KeyA': moveState.left = false; break;
case 'KeyD': moveState.right = false; break;
}
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// ์ด๋™ ์ƒํƒœ
const moveState = {
forward: false,
backward: false,
left: false,
right: false
};
function shoot() {
if (ammo <= 0 || !player || !player.turret) return;
ammo--;
updateAmmoDisplay();
const bullet = createBullet();
bullets.push(bullet);
gunSound.createGunshot();
// ์ด๊ตฌ ํ™”์—ผ ํšจ๊ณผ
const muzzleFlash = new THREE.PointLight(0xffff00, 3, 10);
muzzleFlash.position.copy(player.turret.position);
scene.add(muzzleFlash);
setTimeout(() => scene.remove(muzzleFlash), 50);
}
function createBullet() {
const bullet = new THREE.Mesh(
new THREE.SphereGeometry(0.5),
new THREE.MeshBasicMaterial({
color: 0xffff00,
emissive: 0xffff00,
emissiveIntensity: 1
})
);
// ํฌํƒ‘ ์œ„์น˜์—์„œ ์ด์•Œ ๋ฐœ์‚ฌ
bullet.position.copy(player.turret.position);
const direction = new THREE.Vector3(0, 0, -1);
direction.applyQuaternion(player.turret.quaternion);
bullet.velocity = direction.multiplyScalar(5);
scene.add(bullet);
return bullet;
}
function createEnemyBullet(enemy) {
const bullet = new THREE.Mesh(
new THREE.SphereGeometry(0.5),
new THREE.MeshBasicMaterial({
color: 0xff0000,
emissive: 0xff0000,
emissiveIntensity: 1
})
);
bullet.position.copy(enemy.model.position);
bullet.position.y += 5;
const direction = new THREE.Vector3();
direction.subVectors(player.body.position, enemy.model.position).normalize();
bullet.velocity = direction.multiplyScalar(ENEMY_CONFIG.BULLET_SPEED);
scene.add(bullet);
return bullet;
}
function updateMovement() {
if (controls.isLocked && !isGameOver && player) {
// ์นด๋ฉ”๋ผ ์œ„์น˜ ์—…๋ฐ์ดํŠธ
const cameraOffset = new THREE.Vector3(0, TANK_HEIGHT + 5, -15);
cameraOffset.applyQuaternion(player.body.quaternion);
camera.position.copy(player.body.position).add(cameraOffset);
// ์นด๋ฉ”๋ผ๊ฐ€ ํ”Œ๋ ˆ์ด์–ด๋ฅผ ๋ฐ”๋ผ๋ณด๋„๋ก ์„ค์ •
camera.lookAt(player.body.position);
}
}
function updateBullets() {
for (let i = bullets.length - 1; i >= 0; i--) {
if (!bullets[i]) continue;
bullets[i].position.add(bullets[i].velocity);
// ์ ๊ณผ์˜ ์ถฉ๋Œ ๊ฒ€์‚ฌ
for (let j = enemies.length - 1; j >= 0; j--) {
const enemy = enemies[j];
if (!enemy || !enemy.model) continue;
if (bullets[i] && bullets[i].position.distanceTo(enemy.model.position) < 10) {
scene.remove(bullets[i]);
bullets.splice(i, 1);
enemy.health -= 25;
createExplosion(enemy.model.position.clone());
if (enemy.health <= 0) {
createExplosion(enemy.model.position.clone());
scene.remove(enemy.model);
enemies.splice(j, 1);
}
break;
}
}
// ๋ฒ”์œ„ ๋ฒ—์–ด๋‚œ ์ด์•Œ ์ œ๊ฑฐ
if (bullets[i] && bullets[i].position.distanceTo(player.body.position) > 1000) {
scene.remove(bullets[i]);
bullets.splice(i, 1);
}
}
}
function updateEnemyBullets() {
for (let i = enemyBullets.length - 1; i >= 0; i--) {
if (!enemyBullets[i]) continue;
enemyBullets[i].position.add(enemyBullets[i].velocity);
if (enemyBullets[i].position.distanceTo(player.body.position) < 3) {
playerHealth -= 10;
updateHealthBar();
createExplosion(enemyBullets[i].position.clone());
scene.remove(enemyBullets[i]);
enemyBullets.splice(i, 1);
if (playerHealth <= 0) {
gameOver(false);
}
continue;
}
if (enemyBullets[i].position.distanceTo(player.body.position) > 1000) {
scene.remove(enemyBullets[i]);
enemyBullets.splice(i, 1);
}
}
}
function updateEnemies() {
const currentTime = Date.now();
enemies.forEach(enemy => {
if (!enemy || !enemy.model) return;
// ์  ์ด๋™ ๋กœ์ง
const direction = new THREE.Vector3();
direction.subVectors(player.body.position, enemy.model.position);
direction.y = 0;
direction.normalize();
const newPosition = enemy.model.position.clone()
.add(direction.multiplyScalar(enemy.speed));
newPosition.y = ENEMY_GROUND_HEIGHT;
enemy.model.position.copy(newPosition);
// ์ ์ด ํ”Œ๋ ˆ์ด์–ด๋ฅผ ๋ฐ”๋ผ๋ณด๋„๋ก ์„ค์ •
enemy.model.lookAt(new THREE.Vector3(
player.body.position.x,
enemy.model.position.y,
player.body.position.z
));
// ๊ณต๊ฒฉ ๋กœ์ง
const distanceToPlayer = enemy.model.position.distanceTo(player.body.position);
if (distanceToPlayer < ENEMY_CONFIG.ATTACK_RANGE &&
currentTime - enemy.lastAttackTime > ENEMY_CONFIG.ATTACK_INTERVAL) {
enemyBullets.push(createEnemyBullet(enemy));
enemy.lastAttackTime = currentTime;
// ๊ณต๊ฒฉ ์‹œ ๋ฐœ๊ด‘ ํšจ๊ณผ
const attackFlash = new THREE.PointLight(0xff0000, 2, 20);
attackFlash.position.copy(enemy.model.position);
scene.add(attackFlash);
setTimeout(() => scene.remove(attackFlash), 100);
}
});
}
function reload() {
ammo = 30;
updateAmmoDisplay();
}
function updateAmmoDisplay() {
document.getElementById('ammo').textContent = `Ammo: ${ammo}/30`;
}
function updateHealthBar() {
const healthElement = document.getElementById('health');
const healthPercentage = (playerHealth / MAX_HEALTH) * 100;
healthElement.style.width = `${healthPercentage}%`;
}
function updateTankHUD() {
if (!player || !player.body) return;
document.querySelector('#altitude-indicator span').textContent =
Math.round(player.body.position.y);
const speed = Math.round(
Math.sqrt(
moveState.forward * moveState.forward +
moveState.right * moveState.right
) * 100
);
document.querySelector('#speed-indicator span').textContent = speed;
const heading = Math.round(
(player.body.rotation.y * (180 / Math.PI) + 360) % 360
);
document.querySelector('#compass span').textContent = heading;
updateRadar();
}
function updateRadar() {
if (!player || !player.body) return;
const radarTargets = document.querySelector('.radar-targets');
radarTargets.innerHTML = '';
enemies.forEach(enemy => {
if (!enemy || !enemy.model) return;
const relativePos = enemy.model.position.clone().sub(player.body.position);
const distance = relativePos.length();
if (distance < 500) {
const playerAngle = player.body.rotation.y;
const enemyAngle = Math.atan2(relativePos.x, relativePos.z);
const relativeAngle = enemyAngle - playerAngle;
const normalizedDistance = distance / 500;
const dot = document.createElement('div');
dot.className = 'radar-dot';
dot.style.left = `${50 + Math.sin(relativeAngle) * normalizedDistance * 45}%`;
dot.style.top = `${50 + Math.cos(relativeAngle) * normalizedDistance * 45}%`;
radarTargets.appendChild(dot);
}
});
}
function checkGameStatus() {
if (enemies.length === 0 && currentStage < 5) {
currentStage++;
document.getElementById('stage').style.display = 'block';
document.getElementById('stage').textContent = `Stage ${currentStage}`;
setTimeout(() => {
document.getElementById('stage').style.display = 'none';
createEnemies();
}, 2000);
}
}
function cleanupResources() {
bullets.forEach(bullet => scene.remove(bullet));
bullets = [];
enemyBullets.forEach(bullet => scene.remove(bullet));
enemyBullets = [];
enemies.forEach(enemy => {
if (enemy && enemy.model) {
scene.remove(enemy.model);
}
});
enemies = [];
}
function gameOver(won) {
isGameOver = true;
controls.unlock();
cleanupResources();
setTimeout(() => {
alert(won ? 'Mission Complete!' : 'Game Over!');
location.reload();
}, 100);
}
function gameLoop(timestamp) {
requestAnimationFrame(gameLoop);
if (timestamp - lastRender < 16) {
return;
}
lastRender = timestamp;
if (controls.isLocked && !isGameOver) {
updateMovement();
updateBullets();
updateEnemies();
updateEnemyBullets();
updateTankHUD();
checkGameStatus();
}
renderer.render(scene, camera);
}
// ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง
let lastFpsUpdate = 0;
let frameCount = 0;
function updateFPS(timestamp) {
frameCount++;
if (timestamp - lastFpsUpdate >= 1000) {
const fps = Math.round(frameCount * 1000 / (timestamp - lastFpsUpdate));
console.log('FPS:', fps);
frameCount = 0;
lastFpsUpdate = timestamp;
}
requestAnimationFrame(updateFPS);
}
// ๊ฒŒ์ž„ ์‹œ์ž‘
window.addEventListener('load', async () => {
try {
await init();
console.log('Game started');
console.log('Active enemies:', enemies.length);
gameLoop(performance.now());
updateFPS(performance.now());
} catch (error) {
console.error('Game initialization error:', error);
document.getElementById('loading').innerHTML = `
<div class="loading-text" style="color: #ff0000;">
Error loading game. Please check console and file paths.
</div>
`;
}
});
// ๋””๋ฒ„๊น…์„ ์œ„ํ•œ ์ „์—ญ ์ ‘๊ทผ
window.debugGame = {
scene,
camera,
enemies,
gunSound,
player,
reloadEnemies: createEnemies
};