Spaces:
Running
Running
<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% ; | |
} | |
#restart { | |
top: 80% ; | |
} | |
#winMessage { | |
top: 30% ; | |
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% ; | |
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'); | |
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; | |
// 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'); | |
const countSound = new Audio('count.ogg'); | |
const deathSound = new Audio('death.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'); | |
} else if (stageNumber === 2) { | |
backgroundImg.src = 'city2.png'; | |
bgm = new Audio('BGM3.ogg'); | |
enemyImg.src = 'enemyuk1.png'; | |
} | |
// 게임 상태 초기화 추가 | |
currentRound = 1; | |
currentStage = stageNumber; | |
gameOver = false; | |
gold = 0; | |
hasAPCR = false; | |
hasBF109 = false; | |
hasJU87 = false; | |
allyUnits = []; | |
supportUnits = []; | |
bullets = []; | |
items = []; | |
effects = []; | |
bgm.loop = true; | |
bgm.play().catch(err => console.error("Error playing game music:", err)); | |
gameStarted = true; | |
initRound(); | |
gameLoop(); | |
} | |
function startCountdown() { | |
isCountingDown = true; | |
countdownTime = 3; | |
countdownEl.style.display = 'block'; | |
countdownEl.textContent = countdownTime; | |
bgm.pause(); | |
countSound.play(); | |
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(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; | |
} | |
selectTarget() { | |
if (enemies.length === 0) 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) return false; | |
const dist = Math.hypot(this.target.x - this.x, this.target.y - this.y); | |
return dist < (this.width + this.target.width) / 2; | |
} | |
shoot() { | |
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 | |
}); | |
}); | |
} | |
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) { | |
this.x += Math.cos(this.angle) * this.speed; | |
this.y += Math.sin(this.angle) * this.speed; | |
return false; | |
} | |
return true; | |
} | |
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.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.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() { | |
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() { | |
this.x -= this.speed; | |
const now = Date.now(); | |
if (now - this.lastShot > 200) { // 1초에 5발 | |
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() { | |
if (currentStage === 2 && !isCountingDown) { | |
const now = Date.now(); | |
if (now - lastSpitfireSpawn > 15000) { // 15초마다 | |
const positions = [ | |
canvas.height * 0.2, | |
canvas.height * 0.5, | |
canvas.height * 0.8 | |
]; | |
// 경고선 생성 | |
positions.forEach(y => { | |
warningLines.push(new WarningLine(y)); | |
}); | |
// 2초 후 스핏파이어 생성 | |
setTimeout(() => { | |
positions.forEach(y => { | |
spitfires.push(new Spitfire(y)); | |
}); | |
}, 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(); | |
} | |
// 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()); | |
// 경고선 업데이트 | |
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(); | |
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; | |
if(player.health <= 0) { | |
gameOver = true; | |
restartBtn.style.display = 'block'; | |
effects.push(new Effect(player.x, player.y, 1000, 'death')); | |
deathSound.cloneNode().play(); | |
} | |
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() { | |
if (!gameOver && gameStarted) { | |
updateGame(); | |
drawGame(); | |
requestAnimationFrame(gameLoop); | |
} | |
} | |
class Enemy { | |
constructor(isBoss = false) { | |
this.x = Math.random() * canvas.width; | |
this.y = Math.random() * canvas.height; | |
this.health = currentStage === 2 ? (isBoss ? 15000 : 1500) : (isBoss ? 15000 : 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> |