Certainly! We can set up a basic structure for a wave-based tank game with a shop system using HTML, CSS, and JavaScript.
This provides the basic structure:
script.js):
requestAnimationFrame).To run this, save the files as index.html, style.css, and script.js in the same folder and open index.html in a browser. You can move the tank with WASD or arrow keys and shoot with the spacebar. Enemies will spawn and move towards you. Defeating them gives coins. The shop will open when your score reaches 30, 60, etc., and can also be toggled with 'z'. You can buy power-ups if you have enough coins.
This is a starting point. A full game would require more features like:
Certainly! Let's enhance the game with a tank arm, mouse aiming, bullet effects, coins, health, a shop update, and a proper game over screen.
Here's a summary of the changes:
#healthBarContainer, #healthBar) and the game over screen (#gameOverScreen, #finalScore, #restartBtn). Added .glow class to UI elements..glow class for visual enhancement. Improved general UI and shop styling.script.js):
armLength, armWidth to player. Added mousemove listener to track mousePos. Modified drawPlayer to draw the arm rotated towards the mouse. Modified fireBullet to calculate bullet direction based on the angle to the mouse and start the bullet from the arm tip.bulletSize to 8 and bulletColor to 'black'. Modified drawBullets to draw circles (arc).bouncesLeft property to bullets (starts at 1). Modified moveBullets to check for wall collisions. If bouncesLeft > 0, it reverses the direction and decrements bouncesLeft. If bouncesLeft is 0 on collision, the bullet is removed.particles array, createParticles, updateParticles, and drawParticles functions. Particles are created when a bullet is removed (either by hitting a wall after bouncing or hitting an enemy).updateUI to manage the health bar width and color. CSS handles the glow effects.shopScoreThreshold to 10. Added lastShopScoreMultiple to ensure the shop only opens once when crossing a score multiple.coinsArray, createCoin, and drawCoins functions. createCoin is called when an enemy is defeated. Added logic in checkCollisions to check for player-coin collisions, add coins, and remove the collected coin.health, maxHealth, lastHitTime, and hitCooldown to the player object. Modified player-enemy collision to decrease health only if the hit cooldown has passed. Added a 'Heal' power-up example.updateUI now updates the width and color of the #healthBar element.gameOver function to set gameState to 'gameover', show the #gameOverScreen, update the final score, and hide the UI. Created a resetGame function to reset all variables, hide the game over screen, show the UI, set gameState back to 'playing', and restart the game loop. The restart button calls resetGame. The game starts by calling resetGame.drawCoins, drawParticles, updateParticles). Added checks for gameState to pause movement/spawning/collision logic when in 'shop' or 'gameover' states.Now, when you run the game, you'll see a tank with an arm that follows your mouse, bullets that bounce once, coins dropping from enemies, a health bar, a shop that opens every 10 score, and a proper game over screen with a restart option.*
Okay, let's add some debugging logs to see what's happening with the game over screen visibility and ensure the resetGame function is correctly hiding it at the start.
I've added console logs in the resetGame and gameOver functions to help diagnose why the game over screen might be showing unexpectedly.
When you run the game, open your browser's developer console (usually F12).
resetGame is called and attempts to hide the screen..hidden { display: none; } is being applied correctly (you can inspect the element in the browser's developer tools).Based on the code, resetGame() is called at the very beginning, and it explicitly adds the hidden class to the gameOverScreen element. This should hide it according to the CSS. The game over screen should only become visible when the gameOver() function is called (when player health reaches 0), which removes the hidden class.
Please check the console logs and the applied CSS styles if the issue persists.
Okay, I will remove the dedicated game over screen and make the game freeze when the player's health reaches zero.
I have removed the #gameOverScreen element from index.html and its corresponding styles from style.css.
In script.js:
gameOver function.gameState = 'gameover' when health drops to 0.resetGame to remove game over screen handling and the restart button listener.gameLoop now simply returns early if gameState is 'gameover', effectively freezing the game state updates while keeping the last frame drawn on the canvas.The game will now freeze in place when your health reaches zero.
Okay, let's add the glowing, gravitating coins and the special rainbow coin effect.
Here are the changes implemented:
hasBounced boolean property to bullets. It's set to true when a bullet successfully bounces off a wall.createCoin to accept an optional isRainbow parameter. It sets the size, color ('rainbow' string or 'gold'), and value based on this flag.checkCollisions, when a bullet hits an enemy and the enemy's health drops to 0, createCoin is now called with bullet.hasBounced as the second argument. This means a rainbow coin is created only if the bullet had bounced before hitting the enemy.normalCoinSize, rainbowCoinSize, normalCoinValue, rainbowCoinValue, coinCollectionRadius, and coinCollectSpeed constants. Coin objects now store isCollected, collectSpeed, and collectionRadius.updateCoins):
updateCoins is added.coinsArray.coinCollectionRadius of the player, its isCollected flag is set to true.isCollected is true, the coin's position is updated to move towards the player using a fraction (coinCollectSpeed) of the distance.player.size / 2), its value is added to the player's coins, the UI is updated, and the coin is removed from the array.drawCoins):
drawCoins to handle both normal and rainbow coins.updateCoins() within the gameState === 'playing' block to handle coin movement and collection.checkCollisions as it's now handled by the gravitation logic in updateCoins.coinsArray is cleared in resetGame.Now, enemies will drop normal gold coins by default. If you manage to hit and defeat an enemy with a bullet that has bounced off a wall, they will drop a larger, glowing rainbow coin that gives more coins. All coins will gravitate towards the player when they get close.
Okay, let's integrate these new features to make the game more dynamic and visually engaging.
Here's a breakdown of the changes:
<link> for the Passion One font from Google Fonts.<div id="damageOverlay"> for the screen effect.<div id="enemiesRemainingDisplay"> to the UI.font-family: 'Passion One', sans-serif; on the body.#enemiesRemainingDisplay and adjusted #waveDisplay position.#damageOverlay including the hit class and @keyframes hit-pulse for the animation.low-health class for the constant low health effect (opacity controlled by JS).#healthBarContainer to use filter: drop-shadow() for the glow effect, which looks better than box-shadow for this purpose and can be transitioned.box-shadow from #healthBar.script.js):
damageOverlay.triggerDamageOverlay() function to apply the hit class and force the pulse animation restart.triggerDamageOverlay() in checkCollisions when the player is hit.updateUI, calculated lowHealthOpacity based on health percentage when health is <= 30.low-health class and set the backgroundColor opacity of damageOverlay based on low health status.collectionRadius from coin properties.updateCoins to always calculate the direction towards the player and move the coin.(distance / 50 + 1) to make coins move faster as they get closer, like Minecraft XP orbs.coinCollectSpeed slightly.rainbowColorIndex variable.rainbowColorIndex in drawCoins.hsl() color based on rainbowColorIndex and the coin's position, creating a pulsing/shifting color effect. Used shadowBlur for the glow on canvas.enemiesPerWave array to define wave configurations.totalEnemiesThisWave and enemiesRemainingThisWave variables.startWave(waveNumber) function to initialize wave variables, clear entities, and start spawning enemies for that wave using an interval.checkWaveCompletion() function, called when an enemy is defeated, to check if enemiesRemainingThisWave is 0 and advance the wave after a delay.startWave(1) in resetGame to begin the first wave.spawnEnemy() call from the main game loop as spawning is now managed by the wave interval.toughEnemySize, toughEnemyColor, toughEnemySpeed constants.spawnEnemy to accept a type parameter ('normal' or 'tough') and set enemy properties accordingly (size, color, speed, health, maxHealth, type).checkCollisions to handle enemies with maxHealth > 1.drawEnemies to draw a small health bar above enemies with maxHealth > 1.updateUI to change the filter property of healthBarContainer based on health percentage to change the glow color.ctx.shadowBlur and ctx.shadowColor.resetGame to clear wave variables, clear coins/particles, hide the damage overlay, and call startWave(1).Now you have a more complete wave system, a tougher enemy type in wave 2 with a health bar, coins that gravitate and glow/pulse, a damage/low health screen effect, and the Passion One font!
Okay, I will revert the font back to the previous one.
I have removed the link to the Passion One font in index.html and reverted the font-family and font-size properties in style.css back to their previous values. The script.js file remains unchanged as the font was a styling concern.
Okay, let's address these issues. It sounds like something went wrong with the game loop or entity management after the last change, and the UI text styling needs adjustment now that the font size is smaller.
I'll add some console logs to help diagnose why the entities might not be drawing and adjust the UI text positioning in the CSS.
<jitProject id="wave-tank-game" title="Wave Tank Game"> <jitFile type="file" filePath="index.html"> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Wave Tank Game</title> <link rel="stylesheet" href="style.css"> <!-- Removed Passion One font link --> </head> <body> <canvas id="gameCanvas"></canvas> <div id="gameUI">
<div id="scoreDisplay" class="glow">Score: 0</div>
<div id="coinDisplay" class="glow">Coins: 0</div>
<div id="waveDisplay" class="glow">Wave: 1</div>
<div id="enemiesRemainingDisplay" class="glow">Enemies: 0</div> <!-- New UI element -->
<div id="healthBarContainer">
<div id="healthBar"></div>
</div>
</div>
<div id="shop" class="hidden">
<h2>Shop</h2>
<div id="shopItems">
<!-- Power-up items will be added here by JavaScript -->
</div>
<button id="closeShopBtn">Close Shop (Z)</button>
</div>
<!-- Overlay for damage/low health effects -->
<div id="damageOverlay" class=""></div>
<script src="script.js"></script>
</body>
</html>
<div id="gameUI">
<div id="scoreDisplay" class="glow">Score: 0</div>
<div id="coinDisplay" class="glow">Coins: 0</div>
<div id="waveDisplay" class="glow">Wave: 1</div>
<div id="enemiesRemainingDisplay" class="glow">Enemies: 0</div> <!-- New UI element -->
<div id="healthBarContainer">
<div id="healthBar"></div>
</div>
</div>
<div id="shop" class="hidden">
<h2>Shop</h2>
<div id="shopItems">
<!-- Power-up items will be added here by JavaScript -->
</div>
<button id="closeShopBtn">Close Shop (Z)</button>
</div>
<!-- Overlay for damage/low health effects -->
<div id="damageOverlay" class=""></div>
<script src="script.js"></script>
</body>
</html>
canvas {
border: 2px solid #555;
background-color: #282828; /* Dark canvas */
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); /* Subtle shadow */
}
#gameUI {
position: absolute;
top: 15px;
left: 15px;
display: flex;
flex-direction: column;
gap: 8px; /* Increased gap */
font-size: 1.3em; /* Reverted font size */
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); /* Text shadow for readability */
}
#waveDisplay, #enemiesRemainingDisplay {
position: absolute;
top: 15px;
right: 15px;
font-size: 1.3em; /* Reverted font size */
text-align: right;
width: 200px; /* Give it some width */
}
#enemiesRemainingDisplay {
top: 40px; /* Adjusted position */
}
.glow {
/* Basic glow effect */
text-shadow: 0 0 5px #fff, 0 0 10px #fff, 0 0 15px #007bff, 0 0 20px #007bff, 0 0 25px #007bff, 0 0 30px #007bff, 0 0 35px #007bff;
}
#healthBarContainer {
width: 150px; /* Fixed width */
height: 20px;
background-color: #555;
border: 1px solid #888;
border-radius: 5px;
overflow: hidden;
margin-top: 10px;
box-shadow: inset 0 0 5px rgba(0,0,0,0.3);
/* Player Health Bar Glow */
filter: drop-shadow(0 0 5px #28a745); /* Default green glow */
transition: filter 0.3s ease-in-out;
}
#healthBar {
height: 100%;
width: 100%; /* Will be updated by JS */
background-color: #28a745; /* Green */
transition: width 0.3s ease-in-out, background-color 0.3s ease-in-out; /* Smooth transition */
/* box-shadow handled by container filter */
}
#shop {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(30, 30, 30, 0.95); /* Darker, slightly more opaque */
padding: 30px; /* More padding */
border: 3px solid gold; /* Thicker border */
border-radius: 15px; /* More rounded corners */
text-align: center;
z-index: 10;
box-shadow: 0 0 30px gold; /* Gold glow */
}
#shop h2 {
margin-top: 0;
color: gold;
text-shadow: 0 0 10px gold; /* Glow for title */
}
#shopItems {
display: flex;
flex-direction: column;
gap: 15px; /* Increased gap */
margin-bottom: 20px; /* More margin */
}
.shop-item {
background-color: #444;
padding: 15px; /* More padding */
border-radius: 8px; /* More rounded */
cursor: pointer;
transition: background-color 0.2s ease, transform 0.1s ease;
border: 1px solid #666;
box-shadow: 0 0 5px rgba(255, 255, 255, 0.2); /* Subtle item glow */
}
.shop-item:hover {
background-color: #555;
transform: translateY(-2px); /* Slight lift effect */
}
.shop-item span {
font-size: 1.1em;
}
.shop-item button {
margin-left: 15px; /* More margin */
padding: 8px 15px; /* More padding */
cursor: pointer;
background-color: #007bff; /* Blue button */
color: white;
border: none;
border-radius: 5px;
transition: background-color 0.2s ease;
}
.shop-item button:hover {
background-color: #0056b3; /* Darker blue */
}
#closeShopBtn {
padding: 12px 25px; /* More padding */
cursor: pointer;
background-color: #dc3545; /* Red button */
color: white;
border: none;
border-radius: 5px;
transition: background-color 0.2s ease;
}
#closeShopBtn:hover {
background-color: #c82333; /* Darker red */
}
.hidden {
display: none;
}
#damageOverlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 0, 0, 0); /* Start fully transparent */
pointer-events: none; /* Allow clicks/mouse events to pass through */
z-index: 5; /* Below UI, above canvas */
transition: background-color 0.1s ease-out; /* Smooth fade out */
}
#damageOverlay.hit {
background-color: rgba(255, 0, 0, 0.3); /* Pulse opacity */
animation: hit-pulse 0.5s ease-out;
}
@keyframes hit-pulse {
0% { background-color: rgba(255, 0, 0, 0.3); }
100% { background-color: rgba(255, 0, 0, 0); }
}
#damageOverlay.low-health {
/* Opacity set by JS */
animation: none; /* Disable pulse animation */
}
canvas {
border: 2px solid #555;
background-color: #282828; /* Dark canvas */
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); /* Subtle shadow */
}
#gameUI {
position: absolute;
top: 15px;
left: 15px;
display: flex;
flex-direction: column;
gap: 8px; /* Increased gap */
font-size: 1.3em; /* Reverted font size */
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); /* Text shadow for readability */
}
#waveDisplay, #enemiesRemainingDisplay {
position: absolute;
top: 15px;
right: 15px;
font-size: 1.3em; /* Reverted font size */
text-align: right;
width: 200px; /* Give it some width */
}
#enemiesRemainingDisplay {
top: 40px; /* Adjusted position */
}
.glow {
/* Basic glow effect */
text-shadow: 0 0 5px #fff, 0 0 10px #fff, 0 0 15px #007bff, 0 0 20px #007bff, 0 0 25px #007bff, 0 0 30px #007bff, 0 0 35px #007bff;
}
#healthBarContainer {
width: 150px; /* Fixed width */
height: 20px;
background-color: #555;
border: 1px solid #888;
border-radius: 5px;
overflow: hidden;
margin-top: 10px;
box-shadow: inset 0 0 5px rgba(0,0,0,0.3);
/* Player Health Bar Glow */
filter: drop-shadow(0 0 5px #28a745); /* Default green glow */
transition: filter 0.3s ease-in-out;
}
#healthBar {
height: 100%;
width: 100%; /* Will be updated by JS */
background-color: #28a745; /* Green */
transition: width 0.3s ease-in-out, background-color 0.3s ease-in-out; /* Smooth transition */
/* box-shadow handled by container filter */
}
#shop {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(30, 30, 30, 0.95); /* Darker, slightly more opaque */
padding: 30px; /* More padding */
border: 3px solid gold; /* Thicker border */
border-radius: 15px; /* More rounded corners */
text-align: center;
z-index: 10;
box-shadow: 0 0 30px gold; /* Gold glow */
}
#shop h2 {
margin-top: 0;
color: gold;
text-shadow: 0 0 10px gold; /* Glow for title */
}
#shopItems {
display: flex;
flex-direction: column;
gap: 15px; /* Increased gap */
margin-bottom: 20px; /* More margin */
}
.shop-item {
background-color: #444;
padding: 15px; /* More padding */
border-radius: 8px; /* More rounded */
cursor: pointer;
transition: background-color 0.2s ease, transform 0.1s ease;
border: 1px solid #666;
box-shadow: 0 0 5px rgba(255, 255, 255, 0.2); /* Subtle item glow */
}
.shop-item:hover {
background-color: #555;
transform: translateY(-2px); /* Slight lift effect */
}
.shop-item span {
font-size: 1.1em;
}
.shop-item button {
margin-left: 15px; /* More margin */
padding: 8px 15px; /* More padding */
cursor: pointer;
background-color: #007bff; /* Blue button */
color: white;
border: none;
border-radius: 5px;
transition: background-color 0.2s ease;
}
.shop-item button:hover {
background-color: #0056b3; /* Darker blue */
}
#closeShopBtn {
padding: 12px 25px; /* More padding */
cursor: pointer;
background-color: #dc3545; /* Red button */
color: white;
border: none;
border-radius: 5px;
transition: background-color 0.2s ease;
}
#closeShopBtn:hover {
background-color: #c82333; /* Darker red */
}
.hidden {
display: none;
}
#damageOverlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 0, 0, 0); /* Start fully transparent */
pointer-events: none; /* Allow clicks/mouse events to pass through */
z-index: 5; /* Below UI, above canvas */
transition: background-color 0.1s ease-out; /* Smooth fade out */
}
#damageOverlay.hit {
background-color: rgba(255, 0, 0, 0.3); /* Pulse opacity */
animation: hit-pulse 0.5s ease-out;
}
@keyframes hit-pulse {
0% { background-color: rgba(255, 0, 0, 0.3); }
100% { background-color: rgba(255, 0, 0, 0); }
}
#damageOverlay.low-health {
/* Opacity set by JS */
animation: none; /* Disable pulse animation */
}
canvas.width = 800;
canvas.height = 600;
let gameState = 'playing'; // 'playing', 'shop', 'gameover'
let score = 0;
let coins = 0;
let wave = 1;
let timeElapsed = 0; // in seconds
const scorePerSecond = 1;
const shopScoreThreshold = 10; // Shop opens every 10 score
let lastShopScoreMultiple = 0; // To track when shop was last opened by score
// Wave Management
let totalEnemiesThisWave = 0;
let enemiesRemainingThisWave = 0;
const enemiesPerWave = [
{ normal: 10, tough: 0 }, // Wave 1: 10 normal enemies
{ normal: 15, tough: 5 } // Wave 2: 15 normal, 5 tough enemies
// Add more waves here
];
// Player
const player = {
x: canvas.width / 2,
y: canvas.height / 2,
size: 30,
color: 'green',
speed: 5,
armLength: 40,
armWidth: 10,
bullets: [],
fireRate: 200, // ms between shots
lastShotTime: 0,
health: 12,
maxHealth: 12,
lastHitTime: 0,
hitCooldown: 500, // ms cooldown after being hit
powerups: {} // Active powerups
};
// Mouse position
const mousePos = { x: 0, y: 0 };
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
mousePos.x = e.clientX - rect.left;
mousePos.y = e.clientY - rect.top;
});
// Enemies
let enemies = [];
const normalEnemySize = 25;
const toughEnemySize = normalEnemySize * 1.5; // 1.5x bigger
const normalEnemyColor = 'red';
const toughEnemyColor = '#8B0000'; // Darker red
const normalEnemySpeed = 2;
const toughEnemySpeed = 1.5; // Slightly slower? Or faster? Let's make them a bit slower but tougher
const enemySpawnRate = 1000; // ms
let lastEnemySpawnTime = 0;
let spawnIntervalId = null; // To keep track of the spawn interval
// Bullets
const bulletSize = 8; // Bigger
const bulletColor = 'black'; // Black
const bulletSpeed = 7;
// Particles
let particles = [];
// Coins
let coinsArray = [];
const normalCoinSize = 15;
const rainbowCoinSize = normalCoinSize * 1.5;
const coinColor = 'gold';
const rainbowCoinValue = 50; // Value for rainbow coin
const normalCoinValue = 10; // Value for normal coin
const coinCollectSpeed = 0.05; // Speed at which coins move towards player (adjusted for constant pull)
let rainbowColorIndex = 0; // For rainbow coin pulse animation
// Input handling
const keys = {};
document.addEventListener('keydown', (e) => {
keys[e.key] = true;
if (e.key === 'z' && gameState !== 'gameover') {
toggleShop();
}
});
document.addEventListener('keyup', (e) => {
keys[e.key] = false;
});
// Shop Power-ups (Example)
const powerups = {
speedBoost: {
name: 'Speed Boost',
cost: 50,
duration: 10000, // ms
effect: (p) => { p.speed *= 1.5; },
endEffect: (p) => { p.speed /= 1.5; }
},
fireRateBoost: {
name: 'Fire Rate Boost',
cost: 75,
duration: 15000, // ms
effect: (p) => { p.fireRate *= 0.6; },
endEffect: (p) => { p.fireRate /= 0.6; }
},
healthBoost: {
name: 'Heal (3 HP)',
cost: 30,
effect: (p) => { p.health = Math.min(p.maxHealth, p.health + 3); updateUI(); },
duration: 0 // Instant effect
}
// Add more powerups here
};
function drawPlayer() {
// Draw tank body (square)
ctx.fillStyle = player.color;
ctx.fillRect(player.x - player.size / 2, player.y - player.size / 2, player.size, player.size);
// Draw tank arm (rotated rectangle)
const angle = Math.atan2(mousePos.y - player.y, mousePos.x - player.x);
ctx.save(); // Save current canvas state
ctx.translate(player.x, player.y); // Move origin to player center
ctx.rotate(angle); // Rotate
ctx.fillStyle = 'gray'; // Gray arm
// Draw arm starting from player center, extending outwards
ctx.fillRect(0, -player.armWidth / 2, player.armLength, player.armWidth);
ctx.restore(); // Restore canvas state
}
function movePlayer() {
if (gameState !== 'playing') return;
if (keys['ArrowUp'] || keys['w']) player.y -= player.speed;
if (keys['ArrowDown'] || keys['s']) player.y += player.speed;
if (keys['ArrowLeft'] || keys['a']) player.x -= player.speed;
if (keys['ArrowRight'] || keys['d']) player.x += player.speed;
// Keep player within bounds
player.x = Math.max(player.size / 2, Math.min(canvas.width - player.size / 2, player.x));
player.y = Math.max(player.size / 2, Math.min(canvas.height - player.size / 2, player.y));
}
function fireBullet() {
if (gameState !== 'playing') return;
const now = Date.now();
if (keys[' '] && now - player.lastShotTime > player.fireRate) {
const angle = Math.atan2(mousePos.y - player.y, mousePos.x - player.x);
const directionX = Math.cos(angle);
const directionY = Math.sin(angle);
// Start bullet slightly ahead of the tank arm tip
const startX = player.x + directionX * (player.size / 2 + player.armLength);
const startY = player.y + directionY * (player.size / 2 + player.armLength);
player.bullets.push({
x: startX,
y: startY,
size: bulletSize,
color: bulletColor,
speed: bulletSpeed,
directionX: directionX,
directionY: directionY,
bouncesLeft: 1, // Can bounce once
hasBounced: false // Flag to track if it has bounced
});
player.lastShotTime = now;
}
}
function drawBullets() {
ctx.fillStyle = bulletColor;
player.bullets.forEach(bullet => {
ctx.beginPath();
ctx.arc(bullet.x, bullet.y, bullet.size / 2, 0, Math.PI * 2); // Draw circle
ctx.fill();
});
}
function moveBullets() {
player.bullets = player.bullets.filter(bullet => {
bullet.x += bullet.directionX * bullet.speed;
bullet.y += bullet.directionY * bullet.speed;
let removed = false;
// Check for wall collisions
const hitLeft = bullet.x - bullet.size / 2 < 0;
const hitRight = bullet.x + bullet.size / 2 > canvas.width;
const hitTop = bullet.y - bullet.size / 2 < 0;
const hitBottom = bullet.y + bullet.size / 2 > canvas.height;
if ((hitLeft || hitRight || hitTop || hitBottom)) {
if (bullet.bouncesLeft > 0) {
// Bounce
if (hitLeft || hitRight) {
bullet.directionX *= -1;
// Adjust position slightly to prevent sticking
bullet.x = hitLeft ? bullet.size / 2 : canvas.width - bullet.size / 2;
}
if (hitTop || hitBottom) {
bullet.directionY *= -1;
// Adjust position slightly to prevent sticking
bullet.y = hitTop ? bullet.size / 2 : canvas.height - bullet.size / 2;
}
bullet.bouncesLeft--;
bullet.hasBounced = true; // Mark that it has bounced
} else {
// No bounces left, remove bullet and create particles
createParticles(bullet.x, bullet.y, 15, bullet.color); // 15 particles
removed = true;
}
}
// Remove bullets that are off-screen after bouncing is exhausted
if (!removed && (bullet.x < -bullet.size || bullet.x > canvas.width + bullet.size || bullet.y < -bullet.size || bullet.y > canvas.height + bullet.size)) {
removed = true;
}
return !removed; // Keep bullet if not removed
});
}
function drawEnemies() {
enemies.forEach(enemy => {
ctx.fillStyle = enemy.color;
ctx.fillRect(enemy.x - enemy.size / 2, enemy.y - enemy.size / 2, enemy.size, enemy.size);
// Draw health bar for tough enemies
if (enemy.maxHealth > 1) {
const barWidth = enemy.size * 0.8;
const barHeight = 5;
const barX = enemy.x - barWidth / 2;
const barY = enemy.y - enemy.size / 2 - barHeight - 5; // 5px above enemy
// Background
ctx.fillStyle = '#555';
ctx.fillRect(barX, barY, barWidth, barHeight);
// Health
const healthWidth = (enemy.health / enemy.maxHealth) * barWidth;
ctx.fillStyle = '#28a745'; // Green health
ctx.fillRect(barX, barY, healthWidth, barHeight);
// Health bar glow (drawn on canvas)
ctx.save();
ctx.shadowBlur = 8;
ctx.shadowColor = '#28a745'; // Green glow
ctx.fillStyle = '#28a745';
ctx.fillRect(barX, barY, healthWidth, barHeight); // Redraw to apply glow
ctx.restore();
}
});
}
function moveEnemies() {
if (gameState !== 'playing') return;
enemies.forEach(enemy => {
// Simple AI: Move towards the player
const dx = player.x - enemy.x;
const dy = player.y - enemy.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
enemy.x += (dx / distance) * enemy.speed;
enemy.y += (dy / distance) * enemy.speed;
}
});
}
function spawnEnemy(type = 'normal') {
if (gameState !== 'playing') return;
// Spawn enemy at a random edge of the canvas
let x, y;
const edge = Math.floor(Math.random() * 4); // 0: top, 1: right, 2: bottom, 3: left
const size = type === 'tough' ? toughEnemySize : normalEnemySize;
switch (edge) {
case 0: // Top
x = Math.random() * canvas.width;
y = -size;
break;
case 1: // Right
x = canvas.width + size;
y = Math.random() * canvas.height;
break;
case 2: // Bottom
x = Math.random() * canvas.width;
y = canvas.height + size;
break;
case 3: // Left
x = -size;
y = Math.random() * canvas.height;
break;
}
enemies.push({
x: x,
y: y,
size: size,
color: type === 'tough' ? toughEnemyColor : normalEnemyColor,
speed: type === 'tough' ? toughEnemySpeed : normalEnemySpeed,
health: type === 'tough' ? 2 : 1, // Tough enemies have 2 HP
maxHealth: type === 'tough' ? 2 : 1, // Store max health for bar
type: type // Store enemy type
});
enemiesRemainingThisWave++; // Increment count when spawning
updateUI();
console.log(`Spawned ${type} enemy. Total enemies remaining: ${enemiesRemainingThisWave}`); // Debug log
}
function startWave(waveNumber) {
console.log(`Attempting to start Wave ${waveNumber}`); // Debug log
wave = waveNumber;
enemies = []; // Clear existing enemies
coinsArray = []; // Clear existing coins
particles = []; // Clear existing particles
lastEnemySpawnTime = 0; // Reset spawn timer
// Clear any existing spawn interval
if (spawnIntervalId) {
clearInterval(spawnIntervalId);
}
const waveConfig = enemiesPerWave[waveNumber - 1]; // Get config for current wave (0-indexed)
if (!waveConfig) {
console.log("Game Complete or No more waves defined!");
gameState = 'gameover'; // Or loop waves, or show win screen
return;
}
totalEnemiesThisWave = waveConfig.normal + waveConfig.tough;
enemiesRemainingThisWave = 0; // Will be incremented by spawnEnemy
let spawnedCount = 0;
spawnIntervalId = setInterval(() => {
if (gameState !== 'playing') {
clearInterval(spawnIntervalId);
spawnIntervalId = null;
return;
}
if (spawnedCount < waveConfig.normal) {
spawnEnemy('normal');
spawnedCount++;
} else if (spawnedCount < totalEnemiesThisWave) {
spawnEnemy('tough');
spawnedCount++;
} else {
clearInterval(spawnIntervalId); // Stop spawning when all enemies for the wave are queued
spawnIntervalId = null;
console.log(`All ${totalEnemiesThisWave} enemies for Wave ${wave} queued.`); // Debug log
}
}, enemySpawnRate);
updateUI();
console.log(`Starting Wave ${wave} with ${totalEnemiesThisWave} enemies.`); // Debug log
}
function createParticles(x, y, count, color) {
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = Math.random() * 3 + 1;
particles.push({
x: x,
y: y,
size: Math.random() * 4 + 1,
color: color,
velocityX: Math.cos(angle) * speed,
velocityY: Math.sin(angle) * speed,
lifetime: 60 // frames
});
}
}
function updateParticles() {
particles = particles.filter(particle => {
particle.x += particle.velocityX;
particle.y += particle.velocityY;
particle.lifetime--;
return particle.lifetime > 0;
});
}
function createCoin(x, y, isRainbow = false) {
coinsArray.push({
x: x,
y: y,
size: isRainbow ? rainbowCoinSize : normalCoinSize,
color: isRainbow ? 'rainbow' : coinColor, // Use 'rainbow' string to indicate type
value: isRainbow ? rainbowCoinValue : normalCoinValue,
collectSpeed: coinCollectSpeed,
// No longer need collectionRadius or isCollected flag for constant pull
});
}
function drawCoins() {
// Update rainbow color index for pulsing effect
rainbowColorIndex = (rainbowColorIndex + 1) % 360; // Cycle through hues (0-359)
coinsArray.forEach(coin => {
const radius = coin.size / 2;
// Draw glow effect
ctx.save();
ctx.globalAlpha = 0.5; // Semi-transparent glow
if (coin.color === 'rainbow') {
// Rainbow glow - pulse hue
const pulsedHue = (rainbowColorIndex + (coin.x + coin.y) * 0.1) % 360; // Offset hue slightly based on position
ctx.shadowBlur = 15; // Stronger glow
ctx.shadowColor = `hsl(${pulsedHue}, 100%, 70%)`; // Pulsing color glow
ctx.fillStyle = `hsl(${pulsedHue}, 100%, 70%)`; // Fill with glow color for shadow effect
ctx.beginPath();
ctx.arc(coin.x, coin.y, radius * 1.5, 0, Math.PI * 2); // Larger glow radius
ctx.fill();
} else {
// Gold glow
ctx.shadowBlur = 10; // Gold glow
ctx.shadowColor = 'rgba(255, 215, 0, 0.8)';
ctx.fillStyle = 'rgba(255, 215, 0, 0.8)';
ctx.beginPath();
ctx.arc(coin.x, coin.y, radius * 1.2, 0, Math.PI * 2); // Slightly larger glow
ctx.fill();
}
ctx.restore(); // Restore shadow and alpha
// Draw the coin itself
if (coin.color === 'rainbow') {
// Draw rainbow coin with pulsing color
const pulsedHue = (rainbowColorIndex + (coin.x + coin.y) * 0.1) % 360;
ctx.fillStyle = `hsl(${pulsedHue}, 100%, 50%)`; // Pulsing color
} else {
// Draw normal gold coin
ctx.fillStyle = coin.color;
}
ctx.beginPath();
ctx.arc(coin.x, coin.y, radius, 0, Math.PI * 2);
ctx.fill();
});
}
function updateCoins() {
if (gameState !== 'playing') return;
coinsArray = coinsArray.filter(coin => {
const dx = player.x - coin.x;
const dy = player.y - coin.y;
const distance = Math.sqrt(dx * dx + dy * dy);
// Always move towards player
if (distance > 1) { // Avoid division by zero and jitter when very close
coin.x += (dx / distance) * coin.collectSpeed * (distance / 50 + 1); // Speed up as it gets closer
coin.y += (dy / distance) * coin.collectSpeed * (distance / 50 + 1);
}
// Check if coin has reached the player
if (distance < player.size / 2) { // Or a smaller threshold
coins += coin.value;
updateUI();
return false; // Remove coin
}
return true; // Keep coin if not collected yet
});
}
function checkCollisions() {
if (gameState !== 'playing') return;
const now = Date.now();
// Bullet-Enemy collision
player.bullets = player.bullets.filter(bullet => {
let hit = false;
enemies = enemies.filter(enemy => {
const dx = bullet.x - enemy.x;
const dy = bullet.y - enemy.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < bullet.size / 2 + enemy.size / 2) {
// Collision detected
enemy.health--;
hit = true; // Mark bullet for removal
createParticles(bullet.x, bullet.y, 10, bullet.color); // Particles on hit
if (enemy.health <= 0) {
// Check if bullet bounced before hitting this enemy
createCoin(enemy.x, enemy.y, bullet.hasBounced); // Create coin (rainbow if bounced)
enemiesRemainingThisWave--; // Decrement count when enemy is defeated
checkWaveCompletion(); // Check if wave is complete
return false; // Remove enemy
}
return true; // Keep enemy
}
return true; // Keep enemy
});
return !hit; // Remove bullet if it hit something
});
// Player-Enemy collision
enemies.forEach(enemy => {
const dx = player.x - enemy.x;
const dy = player.y - enemy.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < player.size / 2 + enemy.size / 2) {
// Collision detected
if (now - player.lastHitTime > player.hitCooldown) {
player.health--;
player.lastHitTime = now;
triggerDamageOverlay(); // Trigger damage animation
updateUI();
if (player.health <= 0) {
// Game Over - Freeze the game
gameState = 'gameover';
console.log('Game Over! Player health reached 0.'); // Log game over
// Clear the spawn interval on game over
if (spawnIntervalId) {
clearInterval(spawnIntervalId);
spawnIntervalId = null;
}
}
}
}
});
// Player-Coin collision is now handled by updateCoins gravitation logic
}
function checkWaveCompletion() {
// Check if all enemies that were *supposed* to spawn this wave have been defeated
// This requires tracking total enemies spawned vs total enemies defeated
// A simpler approach for now: check if enemiesRemainingThisWave is 0 AND there are no enemies currently on screen.
// This assumes all enemies for the wave have finished spawning by the time the last one is killed.
// A more robust system would track spawned vs killed counts separately.
// Let's refine this: check if enemiesRemainingThisWave is 0 AND the spawn interval has finished.
if (enemiesRemainingThisWave <= 0 && spawnIntervalId === null && enemies.length === 0) {
// All enemies for the current wave are defeated and no more are spawning
console.log(`Wave ${wave} complete!`);
// Give a short delay before starting next wave or shop
setTimeout(() => {
// Decide whether to open shop or start next wave
// For now, let's just advance wave after a delay
startWave(wave + 1);
}, 3000); // 3 second delay
}
}
function updateScore(deltaTime) {
if (gameState === 'playing') {
timeElapsed += deltaTime / 1000; // Convert ms to seconds
score = timeElapsed * scorePerSecond;
// Check if shop should open based on score threshold
const currentScoreMultiple = Math.floor(score / shopScoreThreshold);
if (currentScoreMultiple > lastShopScoreMultiple && currentScoreMultiple > 0) {
openShop();
lastShopScoreMultiple = currentScoreMultiple;
}
}
updateUI();
}
function updateUI() {
scoreDisplay.textContent = 'Score: ' + Math.floor(score);
coinDisplay.textContent = 'Coins: ' + coins;
waveDisplay.textContent = 'Wave: ' + wave;
enemiesRemainingDisplay.textContent = 'Enemies: ' + enemiesRemainingThisWave; // Update enemies remaining
// Update health bar width and color
const healthPercentage = (player.health / player.maxHealth) * 100;
healthBar.style.width = healthPercentage + '%';
let healthColor = '#28a745'; // Green
let glowColor = '#28a745'; // Green glow
let lowHealthOpacity = 0; // Overlay opacity
if (healthPercentage <= 60 && healthPercentage > 30) {
healthColor = '#ffc107'; // Yellow
glowColor = '#ffc107'; // Yellow glow
} else if (healthPercentage <= 30) {
healthColor = '#dc3545'; // Red
glowColor = '#dc3545'; // Red glow
// Low health overlay opacity increases as health decreases
lowHealthOpacity = 0.1 + (1 - healthPercentage / 30) * 0.4; // From 0.1 to 0.5
lowHealthOpacity = Math.min(lowHealthOpacity, 0.5); // Cap opacity
}
healthBar.style.backgroundColor = healthColor;
healthBarContainer.style.filter = `drop-shadow(0 0 5px ${glowColor})`; // Update container glow
// Manage low health overlay
if (gameState === 'playing' && healthPercentage <= 30) {
damageOverlay.classList.add('low-health');
damageOverlay.style.backgroundColor = `rgba(255, 0, 0, ${lowHealthOpacity})`;
} else {
damageOverlay.classList.remove('low-health');
damageOverlay.style.backgroundColor = 'rgba(255, 0, 0, 0)'; // Ensure transparent when not low health
}
}
function triggerDamageOverlay() {
damageOverlay.classList.remove('hit'); // Reset animation
// Use a timeout to re-add the class after a brief moment, forcing the animation to restart
setTimeout(() => {
damageOverlay.classList.add('hit');
}, 10); // Small delay
}
function populateShop() {
shopItemsElement.innerHTML = ''; // Clear previous items
for (const key in powerups) {
const item = powerups[key];
const itemDiv = document.createElement('div');
itemDiv.classList.add('shop-item');
itemDiv.innerHTML = `
<span>${item.name} - Cost: ${item.cost}</span>
<button data-powerup="${key}">Buy</button>
`;
shopItemsElement.appendChild(itemDiv);
}
// Add event listeners to buy buttons
shopItemsElement.querySelectorAll('.shop-item button').forEach(button => {
button.addEventListener('click', handleBuyPowerup);
});
}
function handleBuyPowerup(event) {
const powerupKey = event.target.dataset.powerup;
const powerup = powerups[powerupKey];
if (coins >= powerup.cost) {
coins -= powerup.cost;
applyPowerup(powerupKey);
updateUI();
// Optionally close shop or update item state
} else {
// Simple feedback
console.log('Not enough coins for ' + powerup.name);
}
}
function applyPowerup(key) {
const powerup = powerups[key];
const now = Date.now();
// If powerup is already active and has duration, clear old timer
if (powerup.duration > 0 && player.powerups[key] && player.powerups[key].timer) {
clearTimeout(player.powerups[key].timer);
// Revert the old effect before applying the new one if it's a stacking boost
if (powerup.endEffect) {
powerup.endEffect(player);
}
}
// Apply the effect
if (powerup.effect) {
powerup.effect(player);
}
// Set a timer to end the effect if it has a duration
if (powerup.duration > 0) {
const timer = setTimeout(() => {
if (powerup.endEffect) {
powerup.endEffect(player);
}
delete player.powerups[key]; // Remove from active powerups
console.log(`${powerup.name} effect ended.`);
}, powerup.duration);
// Store the active powerup info
player.powerups[key] = {
endTime: now + powerup.duration,
timer: timer // Store timer ID to clear if reapplied
};
console.log(`${powerup.name} applied for ${powerup.duration / 1000} seconds.`);
} else {
// Instant powerups don't need a timer or to be stored in player.powerups
console.log(`${powerup.name} applied instantly.`);
}
}
function openShop() {
gameState = 'shop';
populateShop();
shopElement.classList.remove('hidden');
// Pause game elements that shouldn't update in shop
}
function closeShop() {
gameState = 'playing';
shopElement.classList.add('hidden');
// Resume game elements
}
function toggleShop() {
if (gameState === 'playing') {
openShop();
} else if (gameState === 'shop') {
closeShop();
}
}
function resetGame() {
console.log('Resetting game...'); // Debug log
// Reset all game state variables
gameState = 'playing';
score = 0;
coins = 0;
timeElapsed = 0;
lastShopScoreMultiple = 0;
player.x = canvas.width / 2;
player.y = canvas.height / 2;
player.speed = 5; // Reset speed if powerup was active
player.fireRate = 200; // Reset fire rate
player.bullets = [];
player.lastShotTime = 0;
player.health = player.maxHealth;
player.lastHitTime = 0;
player.powerups = {}; // Clear active powerups
enemies = [];
lastEnemySpawnTime = 0;
particles = [];
coinsArray = []; // Clear coins
// Ensure shop is hidden on reset
shopElement.classList.add('hidden');
gameUI.classList.remove('hidden'); // Ensure UI is visible
damageOverlay.classList.remove('hit', 'low-health'); // Hide overlays
damageOverlay.style.backgroundColor = 'rgba(255, 0, 0, 0)';
// Clear any active spawn interval
if (spawnIntervalId) {
clearInterval(spawnIntervalId);
spawnIntervalId = null;
}
updateUI(); // Update UI to show reset values
lastTime = 0; // Reset time for game loop
startWave(1); // Start the first wave
requestAnimationFrame(gameLoop); // Restart the game loop
}
// Close shop button event
closeShopBtn.addEventListener('click', closeShop);
// Game Loop
let lastTime = 0;
function gameLoop(currentTime) {
// console.log('Game Loop running. State:', gameState); // Debug log
// Simple test drawing - remove later
// ctx.fillStyle = 'blue';
// ctx.fillRect(0, 0,
canvas.width = 800;
canvas.height = 600;
let gameState = 'playing'; // 'playing', 'shop', 'gameover'
let score = 0;
let coins = 0;
let wave = 1;
let timeElapsed = 0; // in seconds
const scorePerSecond = 1;
const shopScoreThreshold = 10; // Shop opens every 10 score
let lastShopScoreMultiple = 0; // To track when shop was last opened by score
// Wave Management
let totalEnemiesThisWave = 0;
let enemiesRemainingThisWave = 0;
const enemiesPerWave = [
{ normal: 10, tough: 0 }, // Wave 1: 10 normal enemies
{ normal: 15, tough: 5 } // Wave 2: 15 normal, 5 tough enemies
// Add more waves here
];
// Player
const player = {
x: canvas.width / 2,
y: canvas.height / 2,
size: 30,
color: 'green',
speed: 5,
armLength: 40,
armWidth: 10,
bullets: [],
fireRate: 200, // ms between shots
lastShotTime: 0,
health: 12,
maxHealth: 12,
lastHitTime: 0,
hitCooldown: 500, // ms cooldown after being hit
powerups: {} // Active powerups
};
// Mouse position
const mousePos = { x: 0, y: 0 };
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
mousePos.x = e.clientX - rect.left;
mousePos.y = e.clientY - rect.top;
});
// Enemies
let enemies = [];
const normalEnemySize = 25;
const toughEnemySize = normalEnemySize * 1.5; // 1.5x bigger
const normalEnemyColor = 'red';
const toughEnemyColor = '#8B0000'; // Darker red
const normalEnemySpeed = 2;
const toughEnemySpeed = 1.5; // Slightly slower? Or faster? Let's make them a bit slower but tougher
const enemySpawnRate = 1000; // ms
let lastEnemySpawnTime = 0;
let spawnIntervalId = null; // To keep track of the spawn interval
// Bullets
const bulletSize = 8; // Bigger
const bulletColor = 'black'; // Black
const bulletSpeed = 7;
// Particles
let particles = [];
// Coins
let coinsArray = [];
const normalCoinSize = 15;
const rainbowCoinSize = normalCoinSize * 1.5;
const coinColor = 'gold';
const rainbowCoinValue = 50; // Value for rainbow coin
const normalCoinValue = 10; // Value for normal coin
const coinCollectSpeed = 0.05; // Speed at which coins move towards player (adjusted for constant pull)
let rainbowColorIndex = 0; // For rainbow coin pulse animation
// Input handling
const keys = {};
document.addEventListener('keydown', (e) => {
keys[e.key] = true;
if (e.key === 'z' && gameState !== 'gameover') {
toggleShop();
}
});
document.addEventListener('keyup', (e) => {
keys[e.key] = false;
});
// Shop Power-ups (Example)
const powerups = {
speedBoost: {
name: 'Speed Boost',
cost: 50,
duration: 10000, // ms
effect: (p) => { p.speed *= 1.5; },
endEffect: (p) => { p.speed /= 1.5; }
},
fireRateBoost: {
name: 'Fire Rate Boost',
cost: 75,
duration: 15000, // ms
effect: (p) => { p.fireRate *= 0.6; },
endEffect: (p) => { p.fireRate /= 0.6; }
},
healthBoost: {
name: 'Heal (3 HP)',
cost: 30,
effect: (p) => { p.health = Math.min(p.maxHealth, p.health + 3); updateUI(); },
duration: 0 // Instant effect
}
// Add more powerups here
};
function drawPlayer() {
// Draw tank body (square)
ctx.fillStyle = player.color;
ctx.fillRect(player.x - player.size / 2, player.y - player.size / 2, player.size, player.size);
// Draw tank arm (rotated rectangle)
const angle = Math.atan2(mousePos.y - player.y, mousePos.x - player.x);
ctx.save(); // Save current canvas state
ctx.translate(player.x, player.y); // Move origin to player center
ctx.rotate(angle); // Rotate
ctx.fillStyle = 'gray'; // Gray arm
// Draw arm starting from player center, extending outwards
ctx.fillRect(0, -player.armWidth / 2, player.armLength, player.armWidth);
ctx.restore(); // Restore canvas state
}
function movePlayer() {
if (gameState !== 'playing') return;
if (keys['ArrowUp'] || keys['w']) player.y -= player.speed;
if (keys['ArrowDown'] || keys['s']) player.y += player.speed;
if (keys['ArrowLeft'] || keys['a']) player.x -= player.speed;
if (keys['ArrowRight'] || keys['d']) player.x += player.speed;
// Keep player within bounds
player.x = Math.max(player.size / 2, Math.min(canvas.width - player.size / 2, player.x));
player.y = Math.max(player.size / 2, Math.min(canvas.height - player.size / 2, player.y));
}
function fireBullet() {
if (gameState !== 'playing') return;
const now = Date.now();
if (keys[' '] && now - player.lastShotTime > player.fireRate) {
const angle = Math.atan2(mousePos.y - player.y, mousePos.x - player.x);
const directionX = Math.cos(angle);
const directionY = Math.sin(angle);
// Start bullet slightly ahead of the tank arm tip
const startX = player.x + directionX * (player.size / 2 + player.armLength);
const startY = player.y + directionY * (player.size / 2 + player.armLength);
player.bullets.push({
x: startX,
y: startY,
size: bulletSize,
color: bulletColor,
speed: bulletSpeed,
directionX: directionX,
directionY: directionY,
bouncesLeft: 1, // Can bounce once
hasBounced: false // Flag to track if it has bounced
});
player.lastShotTime = now;
}
}
function drawBullets() {
ctx.fillStyle = bulletColor;
player.bullets.forEach(bullet => {
ctx.beginPath();
ctx.arc(bullet.x, bullet.y, bullet.size / 2, 0, Math.PI * 2); // Draw circle
ctx.fill();
});
}
function moveBullets() {
player.bullets = player.bullets.filter(bullet => {
bullet.x += bullet.directionX * bullet.speed;
bullet.y += bullet.directionY * bullet.speed;
let removed = false;
// Check for wall collisions
const hitLeft = bullet.x - bullet.size / 2 < 0;
const hitRight = bullet.x + bullet.size / 2 > canvas.width;
const hitTop = bullet.y - bullet.size / 2 < 0;
const hitBottom = bullet.y + bullet.size / 2 > canvas.height;
if ((hitLeft || hitRight || hitTop || hitBottom)) {
if (bullet.bouncesLeft > 0) {
// Bounce
if (hitLeft || hitRight) {
bullet.directionX *= -1;
// Adjust position slightly to prevent sticking
bullet.x = hitLeft ? bullet.size / 2 : canvas.width - bullet.size / 2;
}
if (hitTop || hitBottom) {
bullet.directionY *= -1;
// Adjust position slightly to prevent sticking
bullet.y = hitTop ? bullet.size / 2 : canvas.height - bullet.size / 2;
}
bullet.bouncesLeft--;
bullet.hasBounced = true; // Mark that it has bounced
} else {
// No bounces left, remove bullet and create particles
createParticles(bullet.x, bullet.y, 15, bullet.color); // 15 particles
removed = true;
}
}
// Remove bullets that are off-screen after bouncing is exhausted
if (!removed && (bullet.x < -bullet.size || bullet.x > canvas.width + bullet.size || bullet.y < -bullet.size || bullet.y > canvas.height + bullet.size)) {
removed = true;
}
return !removed; // Keep bullet if not removed
});
}
function drawEnemies() {
enemies.forEach(enemy => {
ctx.fillStyle = enemy.color;
ctx.fillRect(enemy.x - enemy.size / 2, enemy.y - enemy.size / 2, enemy.size, enemy.size);
// Draw health bar for tough enemies
if (enemy.maxHealth > 1) {
const barWidth = enemy.size * 0.8;
const barHeight = 5;
const barX = enemy.x - barWidth / 2;
const barY = enemy.y - enemy.size / 2 - barHeight - 5; // 5px above enemy
// Background
ctx.fillStyle = '#555';
ctx.fillRect(barX, barY, barWidth, barHeight);
// Health
const healthWidth = (enemy.health / enemy.maxHealth) * barWidth;
ctx.fillStyle = '#28a745'; // Green health
ctx.fillRect(barX, barY, healthWidth, barHeight);
// Health bar glow (drawn on canvas)
ctx.save();
ctx.shadowBlur = 8;
ctx.shadowColor = '#28a745'; // Green glow
ctx.fillStyle = '#28a745';
ctx.fillRect(barX, barY, healthWidth, barHeight); // Redraw to apply glow
ctx.restore();
}
});
}
function moveEnemies() {
if (gameState !== 'playing') return;
enemies.forEach(enemy => {
// Simple AI: Move towards the player
const dx = player.x - enemy.x;
const dy = player.y - enemy.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 0) {
enemy.x += (dx / distance) * enemy.speed;
enemy.y += (dy / distance) * enemy.speed;
}
});
}
function spawnEnemy(type = 'normal') {
if (gameState !== 'playing') return;
// Spawn enemy at a random edge of the canvas
let x, y;
const edge = Math.floor(Math.random() * 4); // 0: top, 1: right, 2: bottom, 3: left
const size = type === 'tough' ? toughEnemySize : normalEnemySize;
switch (edge) {
case 0: // Top
x = Math.random() * canvas.width;
y = -size;
break;
case 1: // Right
x = canvas.width + size;
y = Math.random() * canvas.height;
break;
case 2: // Bottom
x = Math.random() * canvas.width;
y = canvas.height + size;
break;
case 3: // Left
x = -size;
y = Math.random() * canvas.height;
break;
}
enemies.push({
x: x,
y: y,
size: size,
color: type === 'tough' ? toughEnemyColor : normalEnemyColor,
speed: type === 'tough' ? toughEnemySpeed : normalEnemySpeed,
health: type === 'tough' ? 2 : 1, // Tough enemies have 2 HP
maxHealth: type === 'tough' ? 2 : 1, // Store max health for bar
type: type // Store enemy type
});
enemiesRemainingThisWave++; // Increment count when spawning
updateUI();
console.log(`Spawned ${type} enemy. Total enemies remaining: ${enemiesRemainingThisWave}`); // Debug log
}
function startWave(waveNumber) {
console.log(`Attempting to start Wave ${waveNumber}`); // Debug log
wave = waveNumber;
enemies = []; // Clear existing enemies
coinsArray = []; // Clear existing coins
particles = []; // Clear existing particles
lastEnemySpawnTime = 0; // Reset spawn timer
// Clear any existing spawn interval
if (spawnIntervalId) {
clearInterval(spawnIntervalId);
}
const waveConfig = enemiesPerWave[waveNumber - 1]; // Get config for current wave (0-indexed)
if (!waveConfig) {
console.log("Game Complete or No more waves defined!");
gameState = 'gameover'; // Or loop waves, or show win screen
return;
}
totalEnemiesThisWave = waveConfig.normal + waveConfig.tough;
enemiesRemainingThisWave = 0; // Will be incremented by spawnEnemy
let spawnedCount = 0;
spawnIntervalId = setInterval(() => {
if (gameState !== 'playing') {
clearInterval(spawnIntervalId);
spawnIntervalId = null;
return;
}
if (spawnedCount < waveConfig.normal) {
spawnEnemy('normal');
spawnedCount++;
} else if (spawnedCount < totalEnemiesThisWave) {
spawnEnemy('tough');
spawnedCount++;
} else {
clearInterval(spawnIntervalId); // Stop spawning when all enemies for the wave are queued
spawnIntervalId = null;
console.log(`All ${totalEnemiesThisWave} enemies for Wave ${wave} queued.`); // Debug log
}
}, enemySpawnRate);
updateUI();
console.log(`Starting Wave ${wave} with ${totalEnemiesThisWave} enemies.`); // Debug log
}
function createParticles(x, y, count, color) {
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = Math.random() * 3 + 1;
particles.push({
x: x,
y: y,
size: Math.random() * 4 + 1,
color: color,
velocityX: Math.cos(angle) * speed,
velocityY: Math.sin(angle) * speed,
lifetime: 60 // frames
});
}
}
function updateParticles() {
particles = particles.filter(particle => {
particle.x += particle.velocityX;
particle.y += particle.velocityY;
particle.lifetime--;
return particle.lifetime > 0;
});
}
function createCoin(x, y, isRainbow = false) {
coinsArray.push({
x: x,
y: y,
size: isRainbow ? rainbowCoinSize : normalCoinSize,
color: isRainbow ? 'rainbow' : coinColor, // Use 'rainbow' string to indicate type
value: isRainbow ? rainbowCoinValue : normalCoinValue,
collectSpeed: coinCollectSpeed,
// No longer need collectionRadius or isCollected flag for constant pull
});
}
function drawCoins() {
// Update rainbow color index for pulsing effect
rainbowColorIndex = (rainbowColorIndex + 1) % 360; // Cycle through hues (0-359)
coinsArray.forEach(coin => {
const radius = coin.size / 2;
// Draw glow effect
ctx.save();
ctx.globalAlpha = 0.5; // Semi-transparent glow
if (coin.color === 'rainbow') {
// Rainbow glow - pulse hue
const pulsedHue = (rainbowColorIndex + (coin.x + coin.y) * 0.1) % 360; // Offset hue slightly based on position
ctx.shadowBlur = 15; // Stronger glow
ctx.shadowColor = `hsl(${pulsedHue}, 100%, 70%)`; // Pulsing color glow
ctx.fillStyle = `hsl(${pulsedHue}, 100%, 70%)`; // Fill with glow color for shadow effect
ctx.beginPath();
ctx.arc(coin.x, coin.y, radius * 1.5, 0, Math.PI * 2); // Larger glow radius
ctx.fill();
} else {
// Gold glow
ctx.shadowBlur = 10; // Gold glow
ctx.shadowColor = 'rgba(255, 215, 0, 0.8)';
ctx.fillStyle = 'rgba(255, 215, 0, 0.8)';
ctx.beginPath();
ctx.arc(coin.x, coin.y, radius * 1.2, 0, Math.PI * 2); // Slightly larger glow
ctx.fill();
}
ctx.restore(); // Restore shadow and alpha
// Draw the coin itself
if (coin.color === 'rainbow') {
// Draw rainbow coin with pulsing color
const pulsedHue = (rainbowColorIndex + (coin.x + coin.y) * 0.1) % 360;
ctx.fillStyle = `hsl(${pulsedHue}, 100%, 50%)`; // Pulsing color
} else {
// Draw normal gold coin
ctx.fillStyle = coin.color;
}
ctx.beginPath();
ctx.arc(coin.x, coin.y, radius, 0, Math.PI * 2);
ctx.fill();
});
}
function updateCoins() {
if (gameState !== 'playing') return;
coinsArray = coinsArray.filter(coin => {
const dx = player.x - coin.x;
const dy = player.y - coin.y;
const distance = Math.sqrt(dx * dx + dy * dy);
// Always move towards player
if (distance > 1) { // Avoid division by zero and jitter when very close
coin.x += (dx / distance) * coin.collectSpeed * (distance / 50 + 1); // Speed up as it gets closer
coin.y += (dy / distance) * coin.collectSpeed * (distance / 50 + 1);
}
// Check if coin has reached the player
if (distance < player.size / 2) { // Or a smaller threshold
coins += coin.value;
updateUI();
return false; // Remove coin
}
return true; // Keep coin if not collected yet
});
}
function checkCollisions() {
if (gameState !== 'playing') return;
const now = Date.now();
// Bullet-Enemy collision
player.bullets = player.bullets.filter(bullet => {
let hit = false;
enemies = enemies.filter(enemy => {
const dx = bullet.x - enemy.x;
const dy = bullet.y - enemy.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < bullet.size / 2 + enemy.size / 2) {
// Collision detected
enemy.health--;
hit = true; // Mark bullet for removal
createParticles(bullet.x, bullet.y, 10, bullet.color); // Particles on hit
if (enemy.health <= 0) {
// Check if bullet bounced before hitting this enemy
createCoin(enemy.x, enemy.y, bullet.hasBounced); // Create coin (rainbow if bounced)
enemiesRemainingThisWave--; // Decrement count when enemy is defeated
checkWaveCompletion(); // Check if wave is complete
return false; // Remove enemy
}
return true; // Keep enemy
}
return true; // Keep enemy
});
return !hit; // Remove bullet if it hit something
});
// Player-Enemy collision
enemies.forEach(enemy => {
const dx = player.x - enemy.x;
const dy = player.y - enemy.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < player.size / 2 + enemy.size / 2) {
// Collision detected
if (now - player.lastHitTime > player.hitCooldown) {
player.health--;
player.lastHitTime = now;
triggerDamageOverlay(); // Trigger damage animation
updateUI();
if (player.health <= 0) {
// Game Over - Freeze the game
gameState = 'gameover';
console.log('Game Over! Player health reached 0.'); // Log game over
// Clear the spawn interval on game over
if (spawnIntervalId) {
clearInterval(spawnIntervalId);
spawnIntervalId = null;
}
}
}
}
});
// Player-Coin collision is now handled by updateCoins gravitation logic
}
function checkWaveCompletion() {
// Check if all enemies that were *supposed* to spawn this wave have been defeated
// This requires tracking total enemies spawned vs total enemies defeated
// A simpler approach for now: check if enemiesRemainingThisWave is 0 AND there are no enemies currently on screen.
// This assumes all enemies for the wave have finished spawning by the time the last one is killed.
// A more robust system would track spawned vs killed counts separately.
// Let's refine this: check if enemiesRemainingThisWave is 0 AND the spawn interval has finished.
if (enemiesRemainingThisWave <= 0 && spawnIntervalId === null && enemies.length === 0) {
// All enemies for the current wave are defeated and no more are spawning
console.log(`Wave ${wave} complete!`);
// Give a short delay before starting next wave or shop
setTimeout(() => {
// Decide whether to open shop or start next wave
// For now, let's just advance wave after a delay
startWave(wave + 1);
}, 3000); // 3 second delay
}
}
function updateScore(deltaTime) {
if (gameState === 'playing') {
timeElapsed += deltaTime / 1000; // Convert ms to seconds
score = timeElapsed * scorePerSecond;
// Check if shop should open based on score threshold
const currentScoreMultiple = Math.floor(score / shopScoreThreshold);
if (currentScoreMultiple > lastShopScoreMultiple && currentScoreMultiple > 0) {
openShop();
lastShopScoreMultiple = currentScoreMultiple;
}
}
updateUI();
}
function updateUI() {
scoreDisplay.textContent = 'Score: ' + Math.floor(score);
coinDisplay.textContent = 'Coins: ' + coins;
waveDisplay.textContent = 'Wave: ' + wave;
enemiesRemainingDisplay.textContent = 'Enemies: ' + enemiesRemainingThisWave; // Update enemies remaining
// Update health bar width and color
const healthPercentage = (player.health / player.maxHealth) * 100;
healthBar.style.width = healthPercentage + '%';
let healthColor = '#28a745'; // Green
let glowColor = '#28a745'; // Green glow
let lowHealthOpacity = 0; // Overlay opacity
if (healthPercentage <= 60 && healthPercentage > 30) {
healthColor = '#ffc107'; // Yellow
glowColor = '#ffc107'; // Yellow glow
} else if (healthPercentage <= 30) {
healthColor = '#dc3545'; // Red
glowColor = '#dc3545'; // Red glow
// Low health overlay opacity increases as health decreases
lowHealthOpacity = 0.1 + (1 - healthPercentage / 30) * 0.4; // From 0.1 to 0.5
lowHealthOpacity = Math.min(lowHealthOpacity, 0.5); // Cap opacity
}
healthBar.style.backgroundColor = healthColor;
healthBarContainer.style.filter = `drop-shadow(0 0 5px ${glowColor})`; // Update container glow
// Manage low health overlay
if (gameState === 'playing' && healthPercentage <= 30) {
damageOverlay.classList.add('low-health');
damageOverlay.style.backgroundColor = `rgba(255, 0, 0, ${lowHealthOpacity})`;
} else {
damageOverlay.classList.remove('low-health');
damageOverlay.style.backgroundColor = 'rgba(255, 0, 0, 0)'; // Ensure transparent when not low health
}
}
function triggerDamageOverlay() {
damageOverlay.classList.remove('hit'); // Reset animation
// Use a timeout to re-add the class after a brief moment, forcing the animation to restart
setTimeout(() => {
damageOverlay.classList.add('hit');
}, 10); // Small delay
}
function populateShop() {
shopItemsElement.innerHTML = ''; // Clear previous items
for (const key in powerups) {
const item = powerups[key];
const itemDiv = document.createElement('div');
itemDiv.classList.add('shop-item');
itemDiv.innerHTML = `
<span>${item.name} - Cost: ${item.cost}</span>
<button data-powerup="${key}">Buy</button>
`;
shopItemsElement.appendChild(itemDiv);
}
// Add event listeners to buy buttons
shopItemsElement.querySelectorAll('.shop-item button').forEach(button => {
button.addEventListener('click', handleBuyPowerup);
});
}
function handleBuyPowerup(event) {
const powerupKey = event.target.dataset.powerup;
const powerup = powerups[powerupKey];
if (coins >= powerup.cost) {
coins -= powerup.cost;
applyPowerup(powerupKey);
updateUI();
// Optionally close shop or update item state
} else {
// Simple feedback
console.log('Not enough coins for ' + powerup.name);
}
}
function applyPowerup(key) {
const powerup = powerups[key];
const now = Date.now();
// If powerup is already active and has duration, clear old timer
if (powerup.duration > 0 && player.powerups[key] && player.powerups[key].timer) {
clearTimeout(player.powerups[key].timer);
// Revert the old effect before applying the new one if it's a stacking boost
if (powerup.endEffect) {
powerup.endEffect(player);
}
}
// Apply the effect
if (powerup.effect) {
powerup.effect(player);
}
// Set a timer to end the effect if it has a duration
if (powerup.duration > 0) {
const timer = setTimeout(() => {
if (powerup.endEffect) {
powerup.endEffect(player);
}
delete player.powerups[key]; // Remove from active powerups
console.log(`${powerup.name} effect ended.`);
}, powerup.duration);
// Store the active powerup info
player.powerups[key] = {
endTime: now + powerup.duration,
timer: timer // Store timer ID to clear if reapplied
};
console.log(`${powerup.name} applied for ${powerup.duration / 1000} seconds.`);
} else {
// Instant powerups don't need a timer or to be stored in player.powerups
console.log(`${powerup.name} applied instantly.`);
}
}
function openShop() {
gameState = 'shop';
populateShop();
shopElement.classList.remove('hidden');
// Pause game elements that shouldn't update in shop
}
function closeShop() {
gameState = 'playing';
shopElement.classList.add('hidden');
// Resume game elements
}
function toggleShop() {
if (gameState === 'playing') {
openShop();
} else if (gameState === 'shop') {
closeShop();
}
}
function resetGame() {
console.log('Resetting game...'); // Debug log
// Reset all game state variables
gameState = 'playing';
score = 0;
coins = 0;
timeElapsed = 0;
lastShopScoreMultiple = 0;
player.x = canvas.width / 2;
player.y = canvas.height / 2;
player.speed = 5; // Reset speed if powerup was active
player.fireRate = 200; // Reset fire rate
player.bullets = [];
player.lastShotTime = 0;
player.health = player.maxHealth;
player.lastHitTime = 0;
player.powerups = {}; // Clear active powerups
enemies = [];
lastEnemySpawnTime = 0;
particles = [];
coinsArray = []; // Clear coins
// Ensure shop is hidden on reset
shopElement.classList.add('hidden');
gameUI.classList.remove('hidden'); // Ensure UI is visible
damageOverlay.classList.remove('hit', 'low-health'); // Hide overlays
damageOverlay.style.backgroundColor = 'rgba(255, 0, 0, 0)';
// Clear any active spawn interval
if (spawnIntervalId) {
clearInterval(spawnIntervalId);
spawnIntervalId = null;
}
updateUI(); // Update UI to show reset values
lastTime = 0; // Reset time for game loop
startWave(1); // Start the first wave
requestAnimationFrame(gameLoop); // Restart the game loop
}
// Close shop button event
closeShopBtn.addEventListener('click', closeShop);
// Game Loop
let lastTime = 0;
function gameLoop(currentTime) {
// console.log('Game Loop running. State:', gameState); // Debug log
// Simple test drawing - remove later
// ctx.fillStyle = 'blue';
// ctx.fillRect(0, 0,