|
<!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; |
|
const frameDelay = 1000 / FPS; |
|
let deltaTime = 0; |
|
canvas.width = window.innerWidth; |
|
canvas.height = window.innerHeight; |
|
|
|
|
|
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; |
|
|
|
|
|
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'; |
|
|
|
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; |
|
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.pause(); |
|
bgm.currentTime = 0; |
|
|
|
if (stageNumber === 1) { |
|
backgroundImg.src = 'city.png'; |
|
bgm = new Audio('BGM2.ogg'); |
|
bgm.volume = 0.7; |
|
} else if (stageNumber === 2) { |
|
backgroundImg.src = 'city2.png'; |
|
bgm = new Audio('BGM3.ogg'); |
|
bgm.volume = 0.7; |
|
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; |
|
|
|
|
|
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 = []; |
|
|
|
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; |
|
|
|
|
|
if (currentStage === 2 && allyUnits.length < 2) { |
|
allyUnits.push(new PanzerIII()); |
|
} |
|
|
|
console.log(`Round ${currentRound} initialized with ${enemies.length} enemies`); |
|
|
|
|
|
startCountdown(); |
|
|
|
|
|
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; |
|
} |
|
} |
|
|
|
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() { |
|
|
|
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; |
|
} |
|
|
|
draw(ctx) { |
|
ctx.beginPath(); |
|
ctx.strokeStyle = 'red'; |
|
ctx.lineWidth = 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 && !gameOver && gameStarted) { |
|
const now = Date.now(); |
|
|
|
|
|
|
|
|
|
|
|
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)); |
|
}); |
|
|
|
|
|
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.pause(); |
|
bgm.currentTime = 0; |
|
|
|
|
|
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')); |
|
} |
|
|
|
|
|
if (hasBF109 && !isCountingDown) { |
|
const now = Date.now(); |
|
if (now - lastSupportSpawn > 10000) { |
|
supportUnits.push( |
|
new SupportUnit(canvas.height * 0.2), |
|
new SupportUnit(canvas.height * 0.5), |
|
new SupportUnit(canvas.height * 0.8) |
|
); |
|
lastSupportSpawn = now; |
|
} |
|
} |
|
|
|
|
|
if (hasJU87 && !isCountingDown) { |
|
const now = Date.now(); |
|
if (now - lastJU87Spawn > 15000) { |
|
supportUnits.push(new JU87()); |
|
lastJU87Spawn = now; |
|
} |
|
} |
|
|
|
|
|
spawnSpitfires(); |
|
|
|
|
|
spitfires = spitfires.filter(spitfire => spitfire.update()); |
|
|
|
|
|
updateBF109Damage(); |
|
|
|
|
|
warningLines = warningLines.filter(line => !line.isExpired()); |
|
|
|
|
|
supportUnits = supportUnits.filter(unit => unit.update()); |
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
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.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'); |
|
}); |
|
|
|
|
|
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); |
|
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(); |
|
}); |
|
|
|
|
|
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); |
|
} |
|
} |
|
|
|
function updateBF109Damage() { |
|
supportUnits = supportUnits.filter(unit => { |
|
if (unit instanceof SupportUnit) { |
|
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; |
|
|
|
|
|
if (deltaTime >= frameDelay) { |
|
|
|
const numUpdates = Math.floor(deltaTime / frameDelay); |
|
|
|
|
|
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 = []; |
|
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; |
|
} |
|
} |
|
|
|
|
|
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> |