<!DOCTYPE html> <html> <head> <title>Tank Battle</title> <style> body { margin: 0; overflow: hidden; background: #333; font-family: Arial; } #gameCanvas { background-repeat: repeat; } #instructions { position: fixed; top: 10px; right: 10px; color: white; background: rgba(0,0,0,0.7); padding: 10px; border-radius: 5px; z-index: 1000; } #weaponInfo { position: fixed; top: 150px; right: 10px; color: white; background: rgba(0,0,0,0.7); padding: 10px; border-radius: 5px; z-index: 1000; font-size: 18px; } .button { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); padding: 20px 40px; font-size: 24px; background: #4CAF50; color: white; border: none; border-radius: 5px; cursor: pointer; display: none; z-index: 1000; } #nextRound { top: 80% !important; } #restart { top: 80% !important; } #winMessage { top: 30% !important; font-size: 72px; background: none; } #countdown { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 72px; color: white; text-shadow: 2px 2px 4px rgba(0,0,0,0.5); z-index: 1000; display: none; } #titleScreen { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: url('city2.png') no-repeat center center; background-size: cover; z-index: 2000; display: flex; flex-direction: column; justify-content: center; align-items: center; } #titleScreen h1 { font-size: 72px; color: white; text-shadow: 2px 2px 5px black; margin-bottom: 50px; } .stageButton { padding: 15px 30px; font-size: 24px; background: #4CAF50; color: white; border: none; border-radius: 5px; cursor: pointer; margin: 10px; } .stageButton:disabled { background: #666; cursor: not-allowed; } #shop { position: fixed; top: 30% !important; left: 50%; transform: translate(-50%, -50%); background: rgba(0,0,0,0.9); padding: 20px; border-radius: 10px; color: white; z-index: 1000; display: none; } </style> </head> <body> <div id="instructions"> Controls:<br> WASD - Move tank<br> Mouse - Aim<br> Space - Fire<br> C - Switch Weapon<br> R - Toggle Auto-fire </div> <div id="weaponInfo">Current Weapon: Cannon</div> <div id="countdown">3</div> <button id="nextRound" class="button">Next Round</button> <button id="restart" class="button">Restart Game</button> <canvas id="gameCanvas"></canvas> <div id="titleScreen"> <h1>TANK WAR</h1> <div id="stageSelect"> <button class="stageButton" onclick="startStage(1)">Stage 1</button> <button class="stageButton" onclick="startStage(2)">Stage 2</button> <button class="stageButton" disabled>Stage 3</button> <button class="stageButton" disabled>Stage 4</button> </div> </div> <div id="shop" style="display:none; position:fixed; top:50%; left:50%; transform:translate(-50%,-50%); background:rgba(0,0,0,0.9); padding:20px; border-radius:10px; color:white; z-index:1000;"> <h2>Tank Shop</h2> <div style="display:flex; gap:20px;"> <div id="tank1" style="text-align:center;"> <h3>PZ.IV</h3> <img src="player2.png" width="90" height="50"> <p>300 Gold</p> <p style="color: #4CAF50;">+50% HP</p> <button onclick="buyTank('player2.png', 300, 'tank1')">Buy</button> </div> <div id="tank2" style="text-align:center;"> <h3>TIGER</h3> <img src="player3.png" width="110" height="55"> <p>500 Gold</p> <p style="color: #4CAF50;">+100% HP</p> <p style="color: #ff6b6b;">-30% Speed</p> <button onclick="buyTank('player3.png', 500, 'tank2')">Buy</button> </div> <div id="bf109" style="text-align:center;"> <h3>BF-109</h3> <img src="bf109.png" width="100" height="100"> <p>1000 Gold</p> <p style="color: #4CAF50;">Air support from BF-109</p> <button onclick="buyBF109()">Buy</button> </div> <div id="ju87" style="text-align:center;"> <h3>JU-87</h3> <img src="ju87.png" width="100" height="100"> <p>1500 Gold</p> <p style="color: #4CAF50;">Get ju-87 air support</p> <button onclick="buyJU87()">Buy</button> </div> <div id="apcr" style="text-align:center;"> <h3>APCR</h3> <img src="apcr.png" width="80" height="20"> <p>1000 Gold</p> <p style="color: #4CAF50;">+100% Bullet Speed</p> <button onclick="buyAPCR()">Buy</button> </div> </div> </div> <button id="bossButton" class="button">Fight Boss!</button> <div id="winMessage" class="button" style="font-size: 72px; background: none;">You Win!</div> <script> const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); const nextRoundBtn = document.getElementById('nextRound'); const restartBtn = document.getElementById('restart'); const weaponInfo = document.getElementById('weaponInfo'); const countdownEl = document.getElementById('countdown'); const bossButton = document.getElementById('bossButton'); // 게임의 기본 상태 변수들 근처에 추가 let lastFrameTime = 0; // 마지막 프레임이 실행된 시간 const FPS = 60; // 목표 FPS const frameDelay = 1000 / FPS; // 프레임 사이의 간격 (16.67ms) let deltaTime = 0; // 프레임 간의 시간 차이 canvas.width = window.innerWidth; canvas.height = window.innerHeight; // Game state let currentRound = 1; let currentStage = 1; let gameOver = false; let currentWeapon = 'cannon'; let enemies = []; let bullets = []; let items = []; let lastShot = 0; let isCountingDown = true; let countdownTime = 3; let autoFire = false; let gold = 0; let isBossStage = false; let effects = []; let hasAPCR = false; let hasBF109 = false; let hasJU87 = false; let lastJU87Spawn = 0; let supportUnits = []; let allyUnits = []; let lastSupportSpawn = 0; let gameStarted = false; //경고 시스템 계승 let warningLines = []; let spitfires = []; let lastSpitfireSpawn = 0; let currentVoice = null; // Load assets const backgroundImg = new Image(); backgroundImg.src = 'city.png'; const playerImg = new Image(); playerImg.src = 'player.png'; const enemyImg = new Image(); enemyImg.src = 'enemy.png'; const bulletImg = new Image(); bulletImg.src = 'apcr2.png'; // Audio setup const cannonSound = new Audio('firemn.ogg'); const machinegunSound = new Audio('firemg.ogg'); const enemyFireSound = new Audio('fireenemy.ogg'); let bgm = new Audio('title.ogg'); bgm.volume = 0.7; // 볼륨을 70%로 설정 bgm.loop = true; const countSound = new Audio('count.ogg'); const deathSound = new Audio('death.ogg'); let currentHitSound = null; let currentReloadSound = null; const hitSounds = Array.from({length: 6}, (_, i) => new Audio(`hit${i+1}.ogg`)); const reloadSounds = [new Audio('reload1.ogg'), new Audio('reload2.ogg')]; const escapeSound = new Audio('escape.ogg'); bgm.loop = true; enemyFireSound.volume = 0.5; const weapons = { cannon: { fireRate: 1000, damage: 0.25, bulletSize: 5, sound: cannonSound }, machinegun: { fireRate: 200, damage: 0.05, bulletSize: 2, sound: machinegunSound } }; const player = { x: canvas.width/2, y: canvas.height/2, speed: 5, angle: 0, width: 100, height: 45, health: 1000, maxHealth: 1000 }; function startStage(stageNumber) { console.log("Starting stage:", stageNumber); const titleScreen = document.getElementById('titleScreen'); titleScreen.style.display = 'none'; document.getElementById('instructions').style.display = 'block'; document.getElementById('weaponInfo').style.display = 'block'; document.getElementById('gameCanvas').style.display = 'block'; // 기존 BGM 정지 bgm.pause(); bgm.currentTime = 0; if (stageNumber === 1) { backgroundImg.src = 'city.png'; bgm = new Audio('BGM2.ogg'); bgm.volume = 0.7; // 볼륨을 70%로 설정 } else if (stageNumber === 2) { backgroundImg.src = 'city2.png'; bgm = new Audio('BGM3.ogg'); bgm.volume = 0.7; // 볼륨을 70%로 설정 enemyImg.src = 'enemyuk1.png'; } // 게임 상태 초기화 추가 currentRound = 1; currentStage = stageNumber; gameOver = false; gold = 0; hasAPCR = false; hasBF109 = false; hasJU87 = false; allyUnits = []; supportUnits = []; bullets = []; items = []; effects = []; spitfires = []; // 스핏파이어 배열 초기화 추가 warningLines = []; // 경고선 배열 초기화 추가 lastSpitfireSpawn = Date.now(); // 스폰 타이머 초기화 bgm.loop = true; bgm.play().catch(err => console.error("Error playing game music:", err)); gameStarted = true; initRound(); gameLoop(); // 마지막 프레임 시간 초기화 lastFrameTime = performance.now(); // 게임 루프 시작 gameStarted = true; initRound(); requestAnimationFrame(gameLoop); } function startCountdown() { isCountingDown = true; countdownTime = 3; countdownEl.style.display = 'block'; countdownEl.textContent = countdownTime; bgm.pause(); countSound.play(); // 스핏파이어 관련 요소 초기화 spitfires = []; warningLines = []; lastSpitfireSpawn = 0; // 0으로 초기화하여 새 라운드 시작 시 바로 스폰되도록 함 // 스핏파이어가 발사한 총알 제거 bullets = bullets.filter(bullet => !bullet.isSpitfireBullet); const countInterval = setInterval(() => { countdownTime--; if(countdownTime <= 0) { clearInterval(countInterval); countdownEl.style.display = 'none'; isCountingDown = false; bgm.play(); } countdownEl.textContent = countdownTime > 0 ? countdownTime : 'GO!'; }, 1000); } function initRound() { console.log(`Initializing round ${currentRound}`); // 버튼 상태 초기화 nextRoundBtn.style.display = 'none'; document.getElementById('bossButton').style.display = 'none'; document.getElementById('shop').style.display = 'none'; document.getElementById('winMessage').style.display = 'none'; // 적 생성 enemies = []; // 2스테이지에서는 3명부터 시작해서 1명씩 증가 const enemyCount = currentStage === 2 ? currentRound + 2 : currentRound; for(let i = 0; i < enemyCount; i++) { let x, y; const edge = Math.floor(Math.random() * 4); switch(edge) { case 0: x = Math.random() * canvas.width; y = 0; break; case 1: x = canvas.width; y = Math.random() * canvas.height; break; case 2: x = Math.random() * canvas.width; y = canvas.height; break; case 3: x = 0; y = Math.random() * canvas.height; break; } const enemy = new Enemy(); enemy.x = x; enemy.y = y; enemies.push(enemy); } // 게임 상태 초기화 player.health = player.maxHealth; bullets = []; items = []; supportUnits = []; lastSupportSpawn = 0; // 2스테이지에서 3호전차 지원 유닛 추가 if (currentStage === 2 && allyUnits.length < 2) { allyUnits.push(new PanzerIII()); } console.log(`Round ${currentRound} initialized with ${enemies.length} enemies`); // 카운트다운 시작 startCountdown(); // JU87 스폰 설정 if (hasJU87) { setTimeout(() => { supportUnits.push(new JU87()); lastJU87Spawn = Date.now(); }, 3000); } } function checkRoundClear() { if(enemies.length === 0) { console.log(`Checking round clear: Current round ${currentRound}, Boss stage: ${isBossStage}`); // 하나의 랜덤한 음성만 재생 if (!isBossStage) { // 이전 음성이 있다면 정지 if (currentVoice) { currentVoice.pause(); currentVoice.currentTime = 0; } const voiceFiles = ['voice1.ogg', 'voice2.ogg', 'voice3.ogg', 'voice4.ogg', 'voice5.ogg', 'voice6.ogg']; const randomIndex = Math.floor(Math.random() * voiceFiles.length); currentVoice = new Audio(voiceFiles[randomIndex]); currentVoice.volume = 1.0; currentVoice.play(); } if (!isBossStage) { if(currentRound < 10) { console.log('Normal round clear - showing next round button and shop'); nextRoundBtn.style.display = 'block'; document.getElementById('bossButton').style.display = 'none'; showShop(); } else { console.log('Final round clear - showing boss button'); nextRoundBtn.style.display = 'none'; document.getElementById('bossButton').style.display = 'block'; document.getElementById('shop').style.display = 'none'; } } else { console.log('Boss clear - showing victory message'); gameOver = true; document.getElementById('winMessage').style.display = 'block'; document.getElementById('bossButton').style.display = 'none'; nextRoundBtn.style.display = 'none'; document.getElementById('shop').style.display = 'none'; restartBtn.style.display = 'block'; bgm.pause(); const victorySound = new Audio('victory.ogg'); victorySound.play(); } } } function showShop() { document.getElementById('shop').style.display = 'block'; } const defaultPlayerStats = { maxHealth: 1000, speed: 5, width: 100, height: 45 }; class JU87 { constructor() { this.x = canvas.width; this.y = 50; this.speed = 5; this.width = 100; this.height = 100; this.angle = Math.PI; this.img = new Image(); this.img.src = 'ju87.png'; this.target = null; this.lastShot = 0; this.spawnTime = Date.now(); this.hasPlayedSound = false; this.hasPlayedMGSound = false; this.isReturning = false; this.circleAngle = 0; this.returningToCenter = false; this.ignoreCollisions = false; // 충돌 무시 상태 (타겟팅만 영향) } selectTarget() { if (enemies.length === 0) return null; // 중앙으로 이동 중일 때는 타겟팅 하지 않음 if (this.returningToCenter || this.ignoreCollisions) return null; let nearestEnemy = null; let minDist = Infinity; enemies.forEach(enemy => { if (enemy instanceof Spitfire) return; const dist = Math.hypot(enemy.x - this.x, enemy.y - this.y); if (dist < minDist) { minDist = dist; nearestEnemy = enemy; } }); return nearestEnemy; } checkCollision() { if (!this.target || this.ignoreCollisions) return false; const dist = Math.hypot(this.target.x - this.x, this.target.y - this.y); return dist < (this.width + this.target.width) / 2; } moveToCenter() { const centerX = canvas.width / 2; const centerY = canvas.height / 2; this.angle = Math.atan2(centerY - this.y, centerX - this.x); const dist = Math.hypot(centerX - this.x, centerY - this.y); if (dist > 10) { // 중앙으로 이동하는 동안 더 빠른 속도로 이동 const moveSpeed = this.speed * 1.5; this.x += Math.cos(this.angle) * moveSpeed; this.y += Math.sin(this.angle) * moveSpeed; return false; } // 중앙 도달 시 충돌 무시 상태 해제 this.ignoreCollisions = false; this.returningToCenter = false; return true; } shoot() { // 중앙으로 이동 중일 때는 발사하지 않음 if (this.returningToCenter || this.ignoreCollisions) return; if (!this.hasPlayedMGSound && !isCountingDown) { const mgSound = new Audio('ju87mg.ogg'); mgSound.volume = 1.0; mgSound.play(); this.hasPlayedMGSound = true; } [[20, 50], [80, 50]].forEach(([x, y]) => { const offsetX = x - 50; const offsetY = y - 50; const rotatedX = this.x + (Math.cos(this.angle) * offsetX - Math.sin(this.angle) * offsetY); const rotatedY = this.y + (Math.sin(this.angle) * offsetX + Math.cos(this.angle) * offsetY); bullets.push({ x: rotatedX, y: rotatedY, angle: this.angle, speed: 10, isEnemy: false, damage: weapons.machinegun.damage * 2, size: weapons.machinegun.bulletSize }); }); } update() { if (!this.hasPlayedSound) { const sirenSound = new Audio('ju87siren.ogg'); sirenSound.volume = 1.0; sirenSound.play(); this.hasPlayedSound = true; } const timeSinceSpawn = Date.now() - this.spawnTime; if (timeSinceSpawn > 5000) { if (!this.isReturning) { this.isReturning = true; this.target = null; this.returningToCenter = true; } } // 충돌 감지 및 중앙으로 이동 처리 if (this.checkCollision()) { this.returningToCenter = true; this.ignoreCollisions = true; // 충돌 후 타겟팅 무시 상태 활성화 this.target = null; } if (this.isReturning) { if (this.returningToCenter) { if (this.moveToCenter()) { this.returningToCenter = false; } } else { this.angle = Math.PI; this.x -= this.speed; return this.x > 0; } } else { if (!this.target || !enemies.includes(this.target)) { this.target = this.selectTarget(); if (!this.target) { this.moveToCenter(); } } if (this.target && !this.ignoreCollisions) { this.angle = Math.atan2(this.target.y - this.y, this.target.x - this.x); this.x += Math.cos(this.angle) * this.speed; this.y += Math.sin(this.angle) * this.speed; if (Date.now() - this.lastShot > 200) { this.shoot(); this.lastShot = Date.now(); } } } // 화면 경계 체크 this.x = Math.max(this.width/2, Math.min(canvas.width - this.width/2, this.x)); this.y = Math.max(this.height/2, Math.min(canvas.height - this.height/2, this.y)); return true; } } //2스테이지 스핏파이어 class Spitfire { constructor(yPosition) { this.x = canvas.width; this.y = yPosition; this.speed = 5; this.width = 100; this.height = 100; this.lastShot = 0; this.img = new Image(); this.img.src = 'spitfire.png'; } shoot() { // 카운트다운 중에는 발사하지 않음 if (isCountingDown) return; const mgSound = new Audio('firemg.ogg'); mgSound.volume = 0.5; mgSound.play(); bullets.push({ x: this.x, y: this.y, angle: Math.PI, speed: 10, isEnemy: true, damage: 100, size: 2, isSpitfireBullet: true }); } update() { // 카운트다운 중이면 false를 반환하여 스핏파이어 제거 if (isCountingDown) return false; this.x -= this.speed; const now = Date.now(); if (now - this.lastShot > 200) { this.shoot(); this.lastShot = now; } return this.x > 0; } } //스핏파이어 경고시스템 class WarningLine { constructor(y) { this.y = y; this.startTime = Date.now(); this.duration = 2000; // 2초 } draw(ctx) { ctx.beginPath(); ctx.strokeStyle = 'red'; ctx.lineWidth = 5; // 선 두께를 5로 증가 ctx.setLineDash([10, 20]); // 점선 패턴도 더 크게 조정 ctx.moveTo(0, this.y); ctx.lineTo(canvas.width, this.y); ctx.stroke(); ctx.setLineDash([]); ctx.lineWidth = 1; // 다른 그리기에 영향을 주지 않도록 리셋 }//문제생기면 }+ 수정 isExpired() { return Date.now() - this.startTime > this.duration; } } function spawnSpitfires() { // 2스테이지이고, 카운트다운 중이 아니고, 게임이 진행 중일 때만 if (currentStage === 2 && !isCountingDown && !gameOver && gameStarted) { const now = Date.now(); // lastSpitfireSpawn이 0이면 초기화 // 15초마다 스폰 if (now - lastSpitfireSpawn > 15000) { console.log('Spawning Spitfires...'); // 디버깅용 로그 const positions = [ canvas.height * 0.2, canvas.height * 0.5, canvas.height * 0.8 ]; // 경고선 생성 positions.forEach(y => { warningLines.push(new WarningLine(y)); }); // 2초 후 스핏파이어 생성 setTimeout(() => { if (!isCountingDown && !gameOver) { positions.forEach(y => { spitfires.push(new Spitfire(y)); }); console.log('Spitfires spawned:', spitfires.length); // 디버깅용 로그 } }, 2000); lastSpitfireSpawn = now; } } } class SupportUnit { constructor(yPosition) { this.x = 0; this.y = yPosition; this.speed = 5; this.lastShot = 0; this.width = 100; this.height = 100; this.angle = 0; this.img = new Image(); this.img.src = 'bf109.png'; this.hasPlayedSound = false; this.mgSound = null; } update() { this.x += this.speed; if (isCountingDown) { if (this.mgSound) { this.mgSound.pause(); this.mgSound.currentTime = 0; } this.hasPlayedSound = false; } const now = Date.now(); if (now - this.lastShot > 200 && !isCountingDown) { this.shoot(); this.lastShot = now; } return this.x < canvas.width; } shoot() { if (!this.hasPlayedSound) { const firstSound = new Audio('bf109mg.ogg'); firstSound.volume = 0.7; firstSound.play(); this.hasPlayedSound = true; } if (!isCountingDown) { const shootSound = new Audio('bf109mgse.ogg'); shootSound.volume = 0.3; shootSound.play(); } bullets.push({ x: this.x + Math.cos(this.angle) * 30, y: this.y + Math.sin(this.angle) * 30, angle: this.angle, speed: 10, isEnemy: false, damage: weapons.machinegun.damage, size: weapons.machinegun.bulletSize }); } } class PanzerIII { constructor() { this.x = Math.random() * canvas.width; this.y = Math.random() * canvas.height; this.speed = 2; this.health = 500; this.maxHealth = 500; this.angle = 0; this.width = 70; this.height = 40; this.lastShot = 0; this.shootInterval = 2000; this.img = new Image(); this.img.src = 'team.png'; this.isDead = false; } shoot() { enemyFireSound.cloneNode().play(); bullets.push({ x: this.x + Math.cos(this.angle) * 30, y: this.y + Math.sin(this.angle) * 30, angle: this.angle, speed: 5, isEnemy: false, damage: 50, size: 3, color: 'blue' }); effects.push(new Effect( this.x + Math.cos(this.angle) * 30, this.y + Math.sin(this.angle) * 30, 500, 'fire', this.angle, this )); } update() { if(isCountingDown || this.isDead) return; // 가장 가까운 적 찾기 let nearestEnemy = null; let minDist = Infinity; enemies.forEach(enemy => { const dist = Math.hypot(enemy.x - this.x, enemy.y - this.y); if(dist < minDist) { minDist = dist; nearestEnemy = enemy; } }); // 체력 체크 및 사망 처리 if(this.health <= 0 && !this.isDead) { this.die(); return; } // 적을 향해 이동 및 발사 if(nearestEnemy) { this.angle = Math.atan2(nearestEnemy.y - this.y, nearestEnemy.x - this.x); // 일정 거리 유지 if(minDist > 300) { this.x += Math.cos(this.angle) * this.speed; this.y += Math.sin(this.angle) * this.speed; } const now = Date.now(); if(now - this.lastShot > this.shootInterval) { this.shoot(); this.lastShot = now; } } // 화면 경계 체크 this.x = Math.max(this.width/2, Math.min(canvas.width - this.width/2, this.x)); this.y = Math.max(this.height/2, Math.min(canvas.height - this.height/2, this.y)); } die() { this.isDead = true; // 폭발 이펙트 추가 effects.push(new Effect( this.x, this.y, 1000, 'death' )); // 폭발 사운드 재생 const deathSound = new Audio('death.ogg'); deathSound.volume = 1.0; deathSound.play(); } } function buyTank(tankImg, cost, tankId) { if (gold >= cost) { gold -= cost; playerImg.src = tankImg; document.getElementById(tankId).style.display = 'none'; document.getElementById('shop').style.display = 'none'; if (tankId === 'tank1') { player.maxHealth = 1500; player.speed = defaultPlayerStats.speed; player.width = 90; player.height = 50; } else if (tankId === 'tank2') { player.maxHealth = 2000; player.speed = defaultPlayerStats.speed * 0.7; player.width = 100; player.height = 45; } player.health = player.maxHealth; } } function buyAPCR() { if (gold >= 1000 && !hasAPCR) { gold -= 1000; hasAPCR = true; document.getElementById('apcr').style.display = 'none'; document.getElementById('shop').style.display = 'none'; } } function buyBF109() { if (gold >= 1000 && !hasBF109) { gold -= 1000; hasBF109 = true; document.getElementById('bf109').style.display = 'none'; document.getElementById('shop').style.display = 'none'; } } function buyJU87() { if (gold >= 1500 && !hasJU87) { gold -= 1500; hasJU87 = true; document.getElementById('ju87').style.display = 'none'; document.getElementById('shop').style.display = 'none'; lastJU87Spawn = Date.now(); } } function updateGame() { if(gameOver) return; if(!isCountingDown) { // 플레이어 움직임 if(keys['w']) player.y -= player.speed; if(keys['s']) player.y += player.speed; if(keys['a']) player.x -= player.speed; if(keys['d']) player.x += player.speed; player.x = Math.max(player.width/2, Math.min(canvas.width - player.width/2, player.x)); player.y = Math.max(player.height/2, Math.min(canvas.height - player.height/2, player.y)); fireBullet(); } //플레이어 사망시 소리 재생 부분 - 순서 조정 if(player.health <= 0) { // BGM 정지 bgm.pause(); bgm.currentTime = 0; // escape 효과음 먼저 재생 escapeSound.volume = 1.0; escapeSound.play(); // 약간의 딜레이 후 사망 효과음 재생 setTimeout(() => { deathSound.play(); }, 100); gameOver = true; restartBtn.style.display = 'block'; effects.push(new Effect(player.x, player.y, 1000, 'death')); } // BF109 관련 코드 if (hasBF109 && !isCountingDown) { const now = Date.now(); if (now - lastSupportSpawn > 10000) { // 10초마다 supportUnits.push( new SupportUnit(canvas.height * 0.2), new SupportUnit(canvas.height * 0.5), new SupportUnit(canvas.height * 0.8) ); lastSupportSpawn = now; } } // JU87 관련 코드 if (hasJU87 && !isCountingDown) { const now = Date.now(); if (now - lastJU87Spawn > 15000) { // 15초마다 supportUnits.push(new JU87()); lastJU87Spawn = now; } } // 스핏파이어 스폰 및 업데이트 로직 추가 spawnSpitfires(); // 스핏파이어 업데이트 spitfires = spitfires.filter(spitfire => spitfire.update()); // BF109 데미지 업데이트 추가 updateBF109Damage(); // 경고선 업데이트 warningLines = warningLines.filter(line => !line.isExpired()); // 지원 유닛 업데이트 supportUnits = supportUnits.filter(unit => unit.update()); // 아군 3호전차 업데이트 allyUnits.forEach(unit => unit.update()); allyUnits = allyUnits.filter(unit => unit.health > 0); // 적 업데이트 enemies.forEach(enemy => enemy.update()); if(!isCountingDown) { // 총알 처리 bullets = bullets.filter(bullet => { bullet.x += Math.cos(bullet.angle) * bullet.speed; bullet.y += Math.sin(bullet.angle) * bullet.speed; if(!bullet.isEnemy) { enemies = enemies.filter(enemy => { const dist = Math.hypot(bullet.x - enemy.x, bullet.y - enemy.y); if(dist < 30) { let damage = currentWeapon === 'cannon' ? 250 : 50; enemy.health -= damage; if(enemy.health <= 0) { spawnHealthItem(enemy.x, enemy.y); gold += 100; effects.push(new Effect(enemy.x, enemy.y, 1000, 'death')); deathSound.cloneNode().play(); // 히트 사운드 재생 추가 if (!currentHitSound || currentHitSound.ended) { currentHitSound = hitSounds[Math.floor(Math.random() * hitSounds.length)]; currentHitSound.volume = 1.0; currentHitSound.play(); } return false; } return true; } return true; }); } else { // 상점이 열려있지 않을 때만 플레이어 데미지 처리 if (!document.getElementById('shop').style.display || document.getElementById('shop').style.display === 'none') { const distToPlayer = Math.hypot(bullet.x - player.x, bullet.y - player.y); if(distToPlayer < 30) { player.health -= bullet.damage || 100; return false; } // 아군 3호전차 피격 체크 for(let ally of allyUnits) { const distToAlly = Math.hypot(bullet.x - ally.x, bullet.y - ally.y); if(distToAlly < 30) { ally.health -= bullet.damage || 100; return false; } } } } return bullet.x >= 0 && bullet.x <= canvas.width && bullet.y >= 0 && bullet.y <= canvas.height; }); // 아이템 처리 items = items.filter(item => { const dist = Math.hypot(item.x - player.x, item.y - player.y); if(dist < 30) { player.health = Math.min(player.health + 200, player.maxHealth); return false; } return true; }); // 아군 전차와 적 전차 충돌 체크 allyUnits.forEach(ally => { enemies.forEach(enemy => { const dist = Math.hypot(ally.x - enemy.x, ally.y - enemy.y); if (dist < (ally.width + enemy.width) / 2) { // 충돌 시 서로 밀어내기 const angle = Math.atan2(ally.y - enemy.y, ally.x - enemy.x); const pushDistance = ((ally.width + enemy.width) / 2 - dist) / 2; ally.x += Math.cos(angle) * pushDistance; ally.y += Math.sin(angle) * pushDistance; enemy.x -= Math.cos(angle) * pushDistance; enemy.y -= Math.sin(angle) * pushDistance; } }); }); // 라운드 클리어 체크 checkRoundClear(); } } function drawGame() { ctx.clearRect(0, 0, canvas.width, canvas.height); // 배경 그리기 const pattern = ctx.createPattern(backgroundImg, 'repeat'); ctx.fillStyle = pattern; ctx.fillRect(0, 0, canvas.width, canvas.height); // 플레이어 그리기 ctx.save(); ctx.translate(player.x, player.y); ctx.rotate(player.angle); ctx.drawImage(playerImg, -player.width/2, -player.height/2, player.width, player.height); ctx.restore(); // 체력바 그리기 drawHealthBar(canvas.width/2, 30, player.health, player.maxHealth, 200, 20, 'green'); // 적 그리기 enemies.forEach(enemy => { ctx.save(); ctx.translate(enemy.x, enemy.y); ctx.rotate(enemy.angle); //const img = enemy.isBoss ? (currentStage === 2 ? 'enemyukboss.png' : 'boss.png') : enemy.enemyImg; // 이 부분을 아래와 같이 수정 const img = enemy.enemyImg; // 이미지 객체 직접 사용 ctx.drawImage(img, -enemy.width/2, -enemy.height/2, enemy.width, enemy.height); ctx.restore(); drawHealthBar(enemy.x, enemy.y - 40, enemy.health, enemy.maxHealth, 60, 5, 'red'); }); // 아군 3호전차 그리기 allyUnits.forEach(ally => { ctx.save(); ctx.translate(ally.x, ally.y); ctx.rotate(ally.angle); ctx.drawImage(ally.img, -ally.width/2, -ally.height/2, ally.width, ally.height); ctx.restore(); drawHealthBar(ally.x, ally.y - 40, ally.health, ally.maxHealth, 60, 5, 'blue'); }); // 경고선 그리기 warningLines.forEach(line => line.draw(ctx)); // 스핏파이어 그리기 spitfires.forEach(spitfire => { ctx.save(); ctx.translate(spitfire.x, spitfire.y); ctx.rotate(Math.PI); // 왼쪽으로 비행하므로 180도 회전 ctx.drawImage(spitfire.img, -spitfire.width/2, -spitfire.height/2, spitfire.width, spitfire.height); ctx.restore(); }); // 지원 유닛 그리기 supportUnits.forEach(unit => { ctx.save(); ctx.translate(unit.x, unit.y); ctx.rotate(unit.angle); ctx.drawImage(unit.img, -unit.width/2, -unit.height/2, unit.width, unit.height); ctx.restore(); }); // 총알 그리기 bullets.forEach(bullet => { if (bullet.isEnemy || !bullet.isAPCR) { ctx.beginPath(); ctx.fillStyle = bullet.color || (bullet.isEnemy ? 'red' : 'blue'); ctx.arc(bullet.x, bullet.y, bullet.size, 0, Math.PI * 2); ctx.fill(); } else { ctx.save(); ctx.translate(bullet.x, bullet.y); ctx.rotate(bullet.angle); const width = currentWeapon === 'machinegun' ? 10 : 20; const height = currentWeapon === 'machinegun' ? 5 : 10; ctx.drawImage(bulletImg, -width/2, -height/2, width, height); ctx.restore(); } }); // 아이템 그리기 items.forEach(item => { ctx.beginPath(); ctx.fillStyle = 'green'; ctx.arc(item.x, item.y, 10, 0, Math.PI * 2); ctx.fill(); }); // UI 그리기 ctx.fillStyle = 'white'; ctx.font = '24px Arial'; ctx.fillText(`Stage ${currentStage} - Round ${currentRound}/10`, 10, 30); ctx.fillText(`Gold: ${gold}`, 10, 60); // 이펙트 그리기 effects = effects.filter(effect => !effect.isExpired()); effects.forEach(effect => { effect.update(); ctx.save(); ctx.translate(effect.x, effect.y); if (effect.type === 'fire') ctx.rotate(effect.angle); const size = effect.type === 'death' ? 75 : 42; ctx.drawImage(effect.img, -size/2, -size/2, size, size); ctx.restore(); }); // 카운트다운 오버레이 if (isCountingDown) { ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; ctx.fillRect(0, 0, canvas.width, canvas.height); } } //bf109 히트 시스템 function updateBF109Damage() { supportUnits = supportUnits.filter(unit => { if (unit instanceof SupportUnit) { // BF109인 경우 let hitCount = 0; bullets = bullets.filter(bullet => { if (bullet.isSpitfireBullet) { const dist = Math.hypot(bullet.x - unit.x, bullet.y - unit.y); if (dist < 30) { hitCount++; return false; } } return true; }); if (hitCount >= 5) { effects.push(new Effect(unit.x, unit.y, 1000, 'death')); deathSound.cloneNode().play(); return false; } } return true; }); } function gameLoop(timestamp) { if (!gameOver && gameStarted) { // 첫 프레임일 경우 시간 초기화 if (!lastFrameTime) { lastFrameTime = timestamp; } // 현재 프레임과 이전 프레임의 시간 차이 계산 deltaTime = timestamp - lastFrameTime; // 16.67ms(60FPS)가 지났는지 확인 if (deltaTime >= frameDelay) { // 업데이트할 프레임 수 계산 const numUpdates = Math.floor(deltaTime / frameDelay); // 한번에 너무 많은 업데이트 방지 (최대 3프레임) const maxUpdates = 3; // 게임 상태 업데이트 for (let i = 0; i < Math.min(numUpdates, maxUpdates); i++) { updateGame(frameDelay / 1000); } // 화면 그리기 drawGame(); // 마지막 프레임 시간 업데이트 lastFrameTime = timestamp - (deltaTime % frameDelay); } // 다음 프레임 요청 requestAnimationFrame(gameLoop); } } class Enemy { constructor(isBoss = false) { this.x = Math.random() * canvas.width; this.y = Math.random() * canvas.height; this.health = currentStage === 2 ? (isBoss ? 25000 : 1500) : (isBoss ? 20000 : 1000); this.maxHealth = this.health; this.speed = isBoss ? 1 : 2; this.lastShot = 0; this.shootInterval = isBoss ? 1000 : 1000; this.angle = 0; this.width = 100; this.height = 45; this.moveTimer = 0; this.moveInterval = Math.random() * 2000 + 1000; this.moveAngle = Math.random() * Math.PI * 2; this.isBoss = isBoss; if (currentStage === 2) { if (isBoss) { this.enemyImg = new Image(); this.enemyImg.src = 'enemyukboss.png'; } else if (currentRound >= 7) { this.enemyImg = new Image(); this.enemyImg.src = 'enemyuk3.png'; } else if (currentRound >= 4) { this.enemyImg = new Image(); this.enemyImg.src = 'enemyuk2.png'; } else { this.enemyImg = new Image(); this.enemyImg.src = 'enemyuk1.png'; } } else { if (isBoss) { this.enemyImg = new Image(); this.enemyImg.src = 'boss.png'; } else if (currentRound >= 7) { this.enemyImg = new Image(); this.enemyImg.src = 'enemy3.png'; } else if (currentRound >= 4) { this.enemyImg = new Image(); this.enemyImg.src = 'enemy2.png'; } else { this.enemyImg = new Image(); this.enemyImg.src = 'enemy.png'; // 기본 이미지 추가 } } } update() { if(isCountingDown) return; const now = Date.now(); if (now - this.moveTimer > this.moveInterval) { this.moveAngle = Math.random() * Math.PI * 2; this.moveTimer = now; } this.x += Math.cos(this.moveAngle) * this.speed; this.y += Math.sin(this.moveAngle) * this.speed; this.x = Math.max(this.width/2, Math.min(canvas.width - this.width/2, this.x)); this.y = Math.max(this.height/2, Math.min(canvas.height - this.height/2, this.y)); this.angle = Math.atan2(player.y - this.y, player.x - this.x); if (now - this.lastShot > this.shootInterval && !isCountingDown) { this.shoot(); this.lastShot = now; } } shoot() { const sound = this.isBoss ? new Audio('firemn.ogg') : enemyFireSound.cloneNode(); sound.play(); effects.push(new Effect( this.x + Math.cos(this.angle) * 30, this.y + Math.sin(this.angle) * 30, 500, 'fire', this.angle, this )); bullets.push({ x: this.x + Math.cos(this.angle) * 30, y: this.y + Math.sin(this.angle) * 30, angle: this.angle, speed: this.isBoss ? 10 : 5, isEnemy: true, size: this.isBoss ? 5 : 3, damage: this.isBoss ? 300 : 150 }); } } // 보스 스테이지 시작 함수 수정 function startBossStage() { isBossStage = true; enemies = []; enemies.push(new Enemy(true)); player.health = player.maxHealth; bullets = []; items = []; allyUnits = []; // 3호전차 초기화 if (currentStage === 2 && allyUnits.length < 2) { allyUnits.push(new PanzerIII()); } document.getElementById('bossButton').style.display = 'none'; bgm.src = 'BGM.ogg'; bgm.loop = true; bgm.play(); startCountdown(); } // 이벤트 리스너 document.addEventListener('DOMContentLoaded', () => { const titleScreen = document.getElementById('titleScreen'); const instructions = document.getElementById('instructions'); const weaponInfo = document.getElementById('weaponInfo'); const gameCanvas = document.getElementById('gameCanvas'); instructions.style.display = 'none'; weaponInfo.style.display = 'none'; gameCanvas.style.display = 'none'; bgm.play().catch(err => console.error("Error playing title music:", err)); // 다음 라운드 버튼 클릭 이벤트 nextRoundBtn.addEventListener('click', () => { currentRound++; nextRoundBtn.style.display = 'none'; document.getElementById('shop').style.display = 'none'; initRound(); }); // 재시작 버튼 클릭 이벤트 restartBtn.addEventListener('click', () => { location.reload(); }); // 보스 버튼 클릭 이벤트 document.getElementById('bossButton').addEventListener('click', () => { startBossStage(); }); }); // 키보드 및 마우스 이벤트 const keys = {}; document.addEventListener('keydown', e => { keys[e.key] = true; if(e.key.toLowerCase() === 'c') { currentWeapon = currentWeapon === 'cannon' ? 'machinegun' : 'cannon'; weaponInfo.textContent = `Current Weapon: ${currentWeapon.charAt(0).toUpperCase() + currentWeapon.slice(1)}`; } else if(e.key.toLowerCase() === 'r') { autoFire = !autoFire; } }); document.addEventListener('keyup', e => keys[e.key] = false); canvas.addEventListener('mousemove', (e) => { player.angle = Math.atan2(e.clientY - player.y, e.clientX - player.x); }); window.addEventListener('resize', () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight; }); function spawnHealthItem(x, y) { items.push({x, y}); } function drawHealthBar(x, y, health, maxHealth, width, height, color) { ctx.fillStyle = '#333'; ctx.fillRect(x - width/2, y - height/2, width, height); ctx.fillStyle = color; ctx.fillRect(x - width/2, y - height/2, width * (health/maxHealth), height); } function fireBullet() { if(isCountingDown) return; const weapon = weapons[currentWeapon]; const now = Date.now(); if ((keys[' '] || autoFire) && now - lastShot > weapon.fireRate) { weapon.sound.cloneNode().play(); effects.push(new Effect( player.x + Math.cos(player.angle) * 30, player.y + Math.sin(player.angle) * 30, 500, 'fire', player.angle, player )); bullets.push({ x: player.x + Math.cos(player.angle) * 30, y: player.y + Math.sin(player.angle) * 30, angle: player.angle, speed: hasAPCR ? 20 : 10, isEnemy: false, damage: weapon.damage, size: weapon.bulletSize, isAPCR: hasAPCR }); lastShot = now; } } // Effect 클래스 class Effect { constructor(x, y, duration, type, angle = 0, parent = null) { this.x = x; this.y = y; this.startTime = Date.now(); this.duration = duration; this.type = type; this.angle = angle; this.parent = parent; this.offset = { x: Math.cos(angle) * 30, y: Math.sin(angle) * 30 }; this.img = new Image(); this.img.src = type === 'death' ? 'bang.png' : 'fire2.png'; } update() { if(this.parent && this.type === 'fire') { this.x = this.parent.x + this.offset.x; this.y = this.parent.y + this.offset.y; this.angle = this.parent.angle; } } isExpired() { return Date.now() - this.startTime > this.duration; } } </script> </body> </html>