test / index.html
thomassimplineers's picture
Add 2 files
da646ad verified
Raw History Blame Contribute Delete
67.4 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Desert Strike Clone</title> <script src="https://cdn.tailwindcss.com"></script> <style> body { margin: 0; padding: 0; overflow: hidden; background-color: #111; font-family: 'Arial', sans-serif; } canvas { display: block; } .game-container { position: relative; width: 100vw; height: 100vh; } .modal { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: rgba(0, 0, 0, 0.8); color: white; padding: 2rem; border-radius: 0.5rem; border: 2px solid #f59e0b; max-width: 600px; text-align: center; z-index: 100; } .modal button { background-color: #f59e0b; color: black; border: none; padding: 0.5rem 1.5rem; margin-top: 1rem; border-radius: 0.25rem; cursor: pointer; font-weight: bold; font-size: 1rem; transition: all 0.2s; } .modal button:hover { background-color: #fbbf24; transform: scale(1.05); } .modal button:active { transform: scale(0.98); } .health-bar { height: 8px; background-color: #ef4444; transition: width 0.3s; border-radius: 4px; } .fuel-bar { height: 8px; background-color: #3b82f6; transition: width 0.3s; border-radius: 4px; } .ammo-bar { height: 8px; background-color: #f59e0b; transition: width 0.3s; border-radius: 4px; } .minimap { position: absolute; bottom: 20px; right: 20px; width: 180px; height: 180px; background-color: rgba(0, 0, 0, 0.5); border: 2px solid #f59e0b; z-index: 10; border-radius: 8px; overflow: hidden; } .minimap-canvas { width: 100%; height: 100%; } .stats-container { background-color: rgba(0, 0, 0, 0.5); padding: 0.5rem 1rem; border-radius: 0.5rem; border: 1px solid #f59e0b; } .weapon-selector { position: absolute; bottom: 20px; left: 20px; display: flex; gap: 10px; } .weapon-btn { background-color: rgba(0, 0, 0, 0.7); color: white; border: 1px solid #f59e0b; border-radius: 4px; padding: 0.5rem 1rem; cursor: pointer; transition: all 0.2s; } .weapon-btn.active { background-color: #f59e0b; color: black; font-weight: bold; } .weapon-btn:hover { background-color: rgba(245, 158, 11, 0.3); } .mission-marker { position: absolute; color: white; font-size: 12px; background-color: rgba(75, 85, 99, 0.7); padding: 2px 6px; border-radius: 3px; pointer-events: none; } </style> </head> <body> <div class="game-container"> <canvas id="gameCanvas"></canvas> <!-- Main Game UI --> <div id="ui" class="absolute top-0 left-0 w-full p-4 text-white"> <div class="flex justify-between items-center"> <div class="flex space-x-4"> <div class="stats-container"> <div class="text-sm font-medium">HEALTH</div> <div class="w-40 h-2 bg-gray-700 rounded-full"> <div id="healthBar" class="health-bar" style="width: 100%"></div> </div> </div> <div class="stats-container"> <div class="text-sm font-medium">FUEL</div> <div class="w-40 h-2 bg-gray-700 rounded-full"> <div id="fuelBar" class="fuel-bar" style="width: 100%"></div> </div> </div> <div class="stats-container"> <div class="text-sm font-medium">AMMO</div> <div class="w-40 h-2 bg-gray-700 rounded-full"> <div id="ammoBar" class="ammo-bar" style="width: 100%"></div> </div> </div> </div> <div class="stats-container text-right"> <div class="font-bold">SCORE: <span id="score">0</span></div> <div id="missionText" class="text-yellow-400 font-medium">MISSION: DESTROY RADAR</div> <div id="objectiveText" class="text-sm">Objectives: 0/3</div> </div> </div> </div> <!-- Weapon Selector --> <div class="weapon-selector"> <button id="machinegunBtn" class="weapon-btn active">MACHINE GUN (1)</button> <button id="missileBtn" class="weapon-btn">MISSILES (2)</button> <button id="rocketBtn" class="weapon-btn">ROCKETS (3)</button> </div> <!-- Minimap --> <div class="minimap"> <canvas id="minimapCanvas" class="minimap-canvas"></canvas> </div> <!-- Mission Briefing Modal --> <div id="missionBriefing" class="modal"> <h2 class="text-2xl font-bold mb-4 text-yellow-400">MISSION BRIEFING: OPERATION SAND STORM</h2> <div id="briefingText" class="mb-4 text-left"> <p class="mb-2">Location: Enemy controlled territory</p> <p class="mb-2">Primary Objectives:</p> <ul class="list-disc list-inside mb-4"> <li>Destroy radar installation in sector 7</li> <li>Neutralize anti-air defenses</li> <li>Escort POWs to extraction point</li> </ul> <p class="mb-2">Rules of Engagement: Hostile forces will attack on sight</p> <p class="mb-2">Return to base for resupply when needed</p> </div> <p class="text-sm mb-4 text-yellow-300">Controls: WASD to move, Mouse to aim, Left Click to fire, 1-3 to switch weapons</p> <button id="startMissionBtn" class="px-8 py-2 text-lg">BEGIN MISSION</button> </div> <!-- Game Over Modal --> <div id="gameOverModal" class="modal hidden"> <h2 class="text-2xl font-bold mb-4 text-red-500">MISSION FAILED</h2> <p id="gameOverText" class="mb-4">Your helicopter has been destroyed.</p> <p class="mb-4">Final Score: <span id="finalScore" class="text-xl">0</span></p> <button id="restartBtn" class="px-8 py-2">TRY AGAIN</button> </div> <!-- Mission Complete Modal --> <div id="missionCompleteModal" class="modal hidden"> <h2 class="text-2xl font-bold mb-4 text-green-500">MISSION COMPLETE</h2> <p id="missionCompleteText" class="mb-4">You have successfully completed all objectives!</p> <div class="mb-4"> <p>Objectives completed: <span id="completedObjectives">3/3</span></p> <p>Enemies destroyed: <span id="enemiesDestroyed">0</span></p> <p>Bonus score: <span id="bonusScore">0</span></p> </div> <p class="text-xl mb-4">Total Score: <span id="missionScore">0</span></p> <button id="nextMissionBtn" class="px-8 py-2">NEXT MISSION</button> </div> </div> <script> // ============================================= // GAME CONSTANTS AND CONFIGURATION // ============================================= const GAME_WIDTH = 1280; const GAME_HEIGHT = 720; const WORLD_WIDTH = 3000; const WORLD_HEIGHT = 3000; const MINIMAP_SCALE = 0.05; // Game states const GAME_STATE = { READY: 0, PLAYING: 1, GAME_OVER: 2, MISSION_COMPLETE: 3 }; // Weapon types const WEAPON_TYPE = { MACHINE_GUN: 0, MISSILE: 1, ROCKET: 2 }; // Entity types const ENTITY_TYPE = { HELICOPTER: 0, TANK: 1, SOLDIER: 2, RADAR: 3, AA_GUN: 4, BASE: 5, POW: 6, EXPLOSION: 7, PROJECTILE: 8 }; // Mission types const MISSION_TYPE = { DESTROY_RADAR: 0, RESCUE_POWS: 1, DESTROY_HQ: 2 }; // Mission data const MISSIONS = [ { id: 0, type: MISSION_TYPE.DESTROY_RADAR, title: "Destroy Radar Installation", briefing: "Locate and destroy the enemy radar installation in sector 7. This will cripple their air defense capabilities in the region.", objectives: [ { type: ENTITY_TYPE.RADAR, count: 2, completed: 0, description: "Destroy radar installations (0/2)" }, { type: ENTITY_TYPE.AA_GUN, count: 3, completed: 0, description: "Destroy AA guns (0/3)" } ], reward: 2000 }, { id: 1, type: MISSION_TYPE.RESCUE_POWS, title: "Rescue POWs", briefing: "Find and escort the POWs to the extraction point. Watch out for enemy patrols in the area.", objectives: [ { type: ENTITY_TYPE.POW, count: 3, completed: 0, description: "Rescue POWs (0/3)" }, { type: ENTITY_TYPE.TANK, count: 4, completed: 0, description: "Destroy enemy tanks (0/4)" } ], reward: 3000 }, { id: 2, type: MISSION_TYPE.DESTROY_HQ, title: "Destroy Enemy HQ", briefing: "The enemy headquarters contains valuable intelligence and coordinates their forces in the region. Destroy it to cripple their command structure.", objectives: [ { type: 99, count: 1, completed: 0, description: "Destroy enemy HQ (0/1)" }, // Special type for HQ { type: ENTITY_TYPE.SOLDIER, count: 10, completed: 0, description: "Eliminate enemy soldiers (0/10)" } ], reward: 5000 } ]; // Weapon configurations const WEAPONS = [ { // Machine Gun type: WEAPON_TYPE.MACHINE_GUN, name: "Machine Gun", damage: 10, speed: 15, cooldown: 100, range: 400, ammo: 500, maxAmmo: 500, ammoCost: 1, color: "#f59e0b" }, { // Missile type: WEAPON_TYPE.MISSILE, name: "Missile", damage: 50, speed: 10, cooldown: 1000, range: 600, ammo: 20, maxAmmo: 20, ammoCost: 1, color: "#3b82f6", homing: true }, { // Rocket type: WEAPON_TYPE.ROCKET, name: "Rocket", damage: 100, speed: 8, cooldown: 1500, range: 500, ammo: 10, maxAmmo: 10, ammoCost: 1, color: "#ef4444", splashRadius: 80 } ]; // ============================================= // GAME CLASSES // ============================================= class Vector2 { constructor(x = 0, y = 0) { this.x = x; this.y = y; } add(v) { return new Vector2(this.x + v.x, this.y + v.y); } subtract(v) { return new Vector2(this.x - v.x, this.y - v.y); } multiply(scalar) { return new Vector2(this.x * scalar, this.y * scalar); } distanceTo(v) { return Math.sqrt(Math.pow(this.x - v.x, 2) + Math.pow(this.y - v.y, 2)); } length() { return Math.sqrt(this.x * this.x + this.y * this.y); } normalize() { const len = this.length(); if (len === 0) return new Vector2(); return new Vector2(this.x / len, this.y / len); } lerp(v, t) { return new Vector2(this.x + (v.x - this.x) * t, this.y + (v.y - this.y) * t); } clone() { return new Vector2(this.x, this.y); } equals(v) { return this.x === v.x && this.y === v.y; } } class Entity { constructor(type, position, width, height) { this.type = type; this.position = position.clone(); this.lastPosition = position.clone(); this.width = width; this.height = height; this.velocity = new Vector2(); this.removed = false; this.angle = 0; this.health = 100; this.maxHealth = 100; this.team = 0; // 0 = player, 1 = enemy this.collisionRadius = Math.max(width, height) / 2; } get center() { return new Vector2( this.position.x + this.width / 2, this.position.y + this.height / 2 ); } update(deltaTime, game) { // Store last position for collision detection this.lastPosition = this.position.clone(); // Update position based on velocity this.position.x += this.velocity.x * deltaTime; this.position.y += this.velocity.y * deltaTime; // Keep entity within world bounds this.position.x = Math.max(0, Math.min(WORLD_WIDTH - this.width, this.position.x)); this.position.y = Math.max(0, Math.min(WORLD_HEIGHT - this.height, this.position.y)); } intersects(other) { return this.position.x < other.position.x + other.width && this.position.x + this.width > other.position.x && this.position.y < other.position.y + other.height && this.position.y + this.height > other.position.y; } takeDamage(amount) { this.health -= amount; if (this.health <= 0) { this.health = 0; return true; // entity is dead } return false; // entity still alive } draw(ctx, camera) { // Base entity just draws a red rectangle (override in subclasses) ctx.save(); ctx.translate(this.position.x - camera.x, this.position.y - camera.y); ctx.fillStyle = 'red'; ctx.fillRect(0, 0, this.width, this.height); ctx.restore(); } drawHealthBar(ctx, camera) { if (this.health < this.maxHealth) { const barWidth = this.width; const barHeight = 5; const barX = this.position.x - camera.x; const barY = this.position.y - camera.y - 10; ctx.save(); ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; ctx.fillRect(barX, barY, barWidth, barHeight); const healthWidth = (this.health / this.maxHealth) * barWidth; ctx.fillStyle = this.team === 0 ? '#10b981' : '#ef4444'; ctx.fillRect(barX, barY, healthWidth, barHeight); ctx.restore(); } } } class Helicopter extends Entity { constructor(position) { super(ENTITY_TYPE.HELICOPTER, position, 60, 30); this.health = 100; this.maxHealth = 100; this.speed = 0; this.maxSpeed = 200; this.acceleration = 100; this.rotationSpeed = 3; this.fuel = 100; this.maxFuel = 100; this.fuelConsumption = 0.1; // per second this.weapons = WEAPONS.map(w => ({...w})); // Create copies of weapons this.currentWeapon = 0; this.lastFireTime = 0; this.targetAngle = 0; this.rotorSpeed = 0; this.team = 0; // player team } static createPlayer() { return new Helicopter(new Vector2(WORLD_WIDTH / 2, WORLD_HEIGHT / 2)); } update(deltaTime, game) { // Save last position for collision detection super.update(deltaTime, game); // Handle movement based on input let moveX = 0; let moveY = 0; if (game.keys['ArrowUp'] || game.keys['w'] || game.keys['W']) { moveY -= 1; } if (game.keys['ArrowDown'] || game.keys['s'] || game.keys['S']) { moveY += 1; } if (game.keys['ArrowLeft'] || game.keys['a'] || game.keys['A']) { moveX -= 1; } if (game.keys['ArrowRight'] || game.keys['d'] || game.keys['D']) { moveX += 1; } // Calculate movement vector const moveVec = new Vector2(moveX, moveY); if (moveVec.length() > 0) { const normalized = moveVec.normalize(); // Accelerate if (this.speed < this.maxSpeed) { this.speed += this.acceleration * deltaTime; if (this.speed > this.maxSpeed) this.speed = this.maxSpeed; } // Apply movement this.velocity.x = normalized.x * this.speed; this.velocity.y = normalized.y * this.speed; // Calculate target angle for smooth rotation this.targetAngle = Math.atan2(normalized.y, normalized.x); // Consume fuel this.fuel -= this.fuelConsumption * deltaTime; if (this.fuel <= 0) { this.fuel = 0; this.speed = 0; this.velocity.x = 0; this.velocity.y = 0; } } else { // Decelerate if (this.speed > 0) { this.speed -= this.acceleration * 2 * deltaTime; if (this.speed < 0) this.speed = 0; } this.velocity.x = 0; this.velocity.y = 0; } // Smooth rotation this.angle = lerpAngle(this.angle, this.targetAngle, this.rotationSpeed * deltaTime); // Increase rotor speed when moving if (this.speed > 0) { this.rotorSpeed += 10 * deltaTime; } else { this.rotorSpeed -= 5 * deltaTime; } this.rotorSpeed = Math.max(0, Math.min(30, this.rotorSpeed)); // Check for weapon switch if (game.keys['1']) { this.currentWeapon = 0; // Machine gun game.keys['1'] = false; } else if (game.keys['2']) { this.currentWeapon = 1; // Missile game.keys['2'] = false; } else if (game.keys['3']) { this.currentWeapon = 2; // Rocket game.keys['3'] = false; } } fireAt(target, game) { const now = Date.now(); const weapon = this.weapons[this.currentWeapon]; if (now - this.lastFireTime >= weapon.cooldown && weapon.ammo >= weapon.ammoCost) { weapon.ammo -= weapon.ammoCost; this.lastFireTime = now; const direction = target.subtract(this.center).normalize(); const spawnPos = this.center.add(direction.multiply(20)); let projectile; if (weapon.type === WEAPON_TYPE.MACHINE_GUN) { projectile = new Projectile( ENTITY_TYPE.PROJECTILE, spawnPos, direction, weapon.speed, weapon.damage, weapon.range, weapon.color, this ); } else if (weapon.type === WEAPON_TYPE.MISSILE) { projectile = new Missile( spawnPos, direction, weapon.speed, weapon.damage, weapon.range, weapon.color, this ); } else if (weapon.type === WEAPON_TYPE.ROCKET) { projectile = new Rocket( spawnPos, direction, weapon.speed, weapon.damage, weapon.range, weapon.color, this, weapon.splashRadius ); } game.addEntity(projectile); return true; // successfully fired } return false; // couldn't fire (cooldown or no ammo) } draw(ctx, camera) { ctx.save(); ctx.translate(this.position.x - camera.x + this.width / 2, this.position.y - camera.y + this.height / 2); ctx.rotate(this.angle); // Helicopter body ctx.fillStyle = '#4b5563'; ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height); // Cockpit ctx.fillStyle = '#1e40af'; ctx.beginPath(); ctx.arc(this.width / 4, 0, 10, 0, Math.PI * 2); ctx.fill(); // Rotor ctx.strokeStyle = '#6b7280'; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(-this.width / 2, -this.height / 2 - 5); ctx.lineTo(this.width / 2, -this.height / 2 - 5); ctx.stroke(); // Rotor blades (animated) ctx.save(); ctx.translate(0, -this.height / 2 - 5); ctx.rotate(Date.now() / 100 * this.rotorSpeed); for (let i = 0; i < 2; i++) { ctx.rotate(Math.PI * i); ctx.fillStyle = '#9ca3af'; ctx.fillRect(-1, -30, 2, 60); } ctx.restore(); // Weapon mount ctx.fillStyle = '#374151'; ctx.fillRect(this.width / 2 - 5, -3, 10, 6); ctx.restore(); // Draw health bar this.drawHealthBar(ctx, camera); } } class Enemy extends Entity { constructor(type, position) { let width, height; switch (type) { case ENTITY_TYPE.TANK: width = 40; height = 30; break; case ENTITY_TYPE.SOLDIER: width = 20; height = 30; break; case ENTITY_TYPE.AA_GUN: width = 40; height = 40; break; case ENTITY_TYPE.RADAR: width = 60; height = 60; break; default: width = 30; height = 30; } super(type, position, width, height); this.speed = type === ENTITY_TYPE.SOLDIER ? 40 : 20; this.attackRange = 200; this.lastAttackTime = 0; this.attackCooldown = 1000; this.damage = 10; this.health = type === ENTITY_TYPE.RADAR ? 150 : type === ENTITY_TYPE.AA_GUN ? 80 : type === ENTITY_TYPE.TANK ? 60 : 30; this.maxHealth = this.health; this.team = 1; // enemy team // For pathfinding/movement this.targetPosition = position.clone(); this.path = []; this.moveTimer = 0; } update(deltaTime, game) { super.update(deltaTime, game); // Simple AI behavior const player = game.player; if (!player) return; const distToPlayer = this.center.distanceTo(player.center); // Movement logic if (this.type !== ENTITY_TYPE.RADAR) { // Non-stationary enemies move toward player but keep distance if (distToPlayer > this.attackRange * 0.8) { const direction = player.center.subtract(this.center).normalize(); this.velocity = direction.multiply(this.speed); // Random movement when player is far away this.moveTimer -= deltaTime; if (this.moveTimer <= 0) { this.moveTimer = Math.random() * 2000 + 1000; this.targetPosition = this.position.add( new Vector2((Math.random() * 2 - 1) * 200, (Math.random() * 2 - 1) * 200) ); } // Some randomness in movement const targetDir = this.targetPosition.subtract(this.position).normalize(); this.velocity = this.velocity.add(targetDir.multiply(0.3)).normalize().multiply(this.speed); } else if (distToPlayer < this.attackRange * 0.5) { // Move away if too close const direction = this.center.subtract(player.center).normalize(); this.velocity = direction.multiply(this.speed * 0.7); } else { this.velocity = new Vector2(); } // Update angle based on movement if (this.velocity.length() > 0) { this.angle = Math.atan2(this.velocity.y, this.velocity.x); } } else { // Radar doesn't move this.velocity = new Vector2(); this.angle += 0.01 * deltaTime; } // Attack logic if (distToPlayer <= this.attackRange) { const now = Date.now(); if (now - this.lastAttackTime > this.attackCooldown) { this.lastAttackTime = now; // Fire at player if (this.type === ENTITY_TYPE.AA_GUN || this.type === ENTITY_TYPE.TANK) { const direction = player.center.subtract(this.center).normalize(); const spawnPos = this.center.add(direction.multiply(20)); const projectile = new Projectile( ENTITY_TYPE.PROJECTILE, spawnPos, direction, 8, this.damage, 300, '#ef4444', this ); game.addEntity(projectile); } else if (this.type === ENTITY_TYPE.SOLDIER) { // Soldiers don't shoot projectiles, just deal damage if very close if (distToPlayer < 30) { player.takeDamage(this.damage); } } } } } draw(ctx, camera) { ctx.save(); ctx.translate(this.position.x - camera.x + this.width / 2, this.position.y - camera.y + this.height / 2); ctx.rotate(this.angle); switch (this.type) { case ENTITY_TYPE.TANK: // Tank body ctx.fillStyle = '#3a0ca3'; ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height); // Tank turret ctx.fillStyle = '#1e40af'; ctx.fillRect(-10, -15, 20, 10); break; case ENTITY_TYPE.SOLDIER: // Soldier body ctx.fillStyle = '#4338ca'; ctx.fillRect(-this.width / 2, -this.height / 3, this.width, this.height * 0.6); // Soldier head ctx.fillStyle = '#f0abfc'; ctx.beginPath(); ctx.arc(0, -this.height / 3 - 8, 8, 0, Math.PI * 2); ctx.fill(); break; case ENTITY_TYPE.AA_GUN: // Base ctx.fillStyle = '#6b7280'; ctx.fillRect(-this.width / 2, -5, this.width, 10); // Gun ctx.fillStyle = '#9ca3af'; ctx.fillRect(-5, -15, 10, 30); // Barrel ctx.fillStyle = '#4b5563'; ctx.fillRect(0, -20, 30, 5); break; case ENTITY_TYPE.RADAR: // Base ctx.fillStyle = '#6b7280'; ctx.fillRect(-this.width / 2, -this.height / 2, this.width, 10); // Radar dish ctx.fillStyle = '#9ca3af'; ctx.beginPath(); ctx.arc(0, 0, 25, 0, Math.PI * 2); ctx.fill(); // Radar sweep (animated) ctx.strokeStyle = '#3b82f6'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(Math.cos(Date.now() / 500) * 20, Math.sin(Date.now() / 500) * 20); ctx.stroke(); break; } ctx.restore(); // Draw health bar this.drawHealthBar(ctx, camera); } } class Base extends Entity { constructor(position) { super(ENTITY_TYPE.BASE, position, 100, 100); this.team = 0; this.healRate = 10; // per second this.refuelRate = 15; // per second this.rearmRate = 20; // per second } update(deltaTime, game) { super.update(deltaTime, game); // Heal, refuel and rearm player when landed const player = game.player; if (player && this.intersects(player) && player.speed < 10) { // Heal player.health = Math.min(player.maxHealth, player.health + this.healRate * deltaTime / 1000); // Refuel player.fuel = Math.min(player.maxFuel, player.fuel + this.refuelRate * deltaTime / 1000); // Rearm for (const weapon of player.weapons) { weapon.ammo = Math.min(weapon.maxAmmo, weapon.ammo + this.rearmRate * deltaTime / 1000); } } } draw(ctx, camera) { ctx.save(); ctx.translate(this.position.x - camera.x, this.position.y - camera.y); // Landing pad ctx.fillStyle = 'rgba(75, 85, 99, 0.8)'; ctx.fillRect(0, 0, this.width, this.height); // "H" marking ctx.fillStyle = '#fff'; ctx.fillRect(this.width / 2 - 25, this.height / 2 - 5, 50, 10); ctx.fillRect(this.width / 2 - 5, this.height / 2 - 25, 10, 50); // Border ctx.strokeStyle = '#f59e0b'; ctx.lineWidth = 3; ctx.strokeRect(0, 0, this.width, this.height); ctx.restore(); } } class POW extends Entity { constructor(position) { super(ENTITY_TYPE.POW, position, 20, 25); this.rescued = false; } update(deltaTime, game) { super.update(deltaTime, game); // Check if player picked up the POW const player = game.player; if (player && this.intersects(player) && !this.rescued) { this.rescued = true; // Check for mission progress if (game.currentMission && game.currentMission.type === MISSION_TYPE.RESCUE_POWS) { for (const obj of game.currentMission.objectives) { if (obj.type === ENTITY_TYPE.POW) { obj.completed++; game.updateObjectiveUI(); break; } } } } if (this.rescued) { // Follow player if rescued if (player) { const offsetX = player.width * 0.6 * Math.cos(player.angle + Math.PI / 2); const offsetY = player.height * 0.6 * Math.sin(player.angle + Math.PI / 2); const targetPos = new Vector2( player.position.x + player.width / 2 - this.width / 2 + offsetX, player.position.y + player.height / 2 - this.height / 2 + offsetY ); this.position = this.position.lerp(targetPos, 0.1); } } } draw(ctx, camera) { ctx.save(); ctx.translate(this.position.x - camera.x, this.position.y - camera.y); // Body ctx.fillStyle = this.rescued ? '#10b981' : '#f59e0b'; ctx.fillRect(10, 5, 10, 20); // Head ctx.fillStyle = '#f0abfc'; ctx.beginPath(); ctx.arc(15, 5, 6, 0, Math.PI * 2); ctx.fill(); if (!this.rescued) { // Arms (waving for help) ctx.strokeStyle = '#f0abfc'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(10, 10); ctx.lineTo(20 * Math.sin(Date.now() / 300), 5); ctx.lineTo(20, 10); ctx.stroke(); } ctx.restore(); } } class Projectile extends Entity { constructor(type, position, direction, speed, damage, range, color, owner) { super(type, position, 5, 5); this.direction = direction.clone(); this.speed = speed; this.damage = damage; this.range = range; this.color = color; this.owner = owner; this.distanceTraveled = 0; this.velocity = direction.multiply(speed); } update(deltaTime, game) { super.update(deltaTime, game); this.distanceTraveled += this.speed * deltaTime / 1000; if (this.distanceTraveled >= this.range) { this.removed = true; return; } // Check for collisions for (const entity of game.entities) { if (entity === this.owner || entity === game.player) continue; if (this.intersects(entity) && entity.team !== this.owner.team) { entity.takeDamage(this.damage); // Check if entity was destroyed if (entity.health <= 0) { // Create explosion const explosion = new Explosion(entity.center); game.addEntity(explosion); // Add score if player killed it if (this.owner.type === ENTITY_TYPE.HELICOPTER) { game.score += entity.type === ENTITY_TYPE.TANK ? 50 : entity.type === ENTITY_TYPE.SOLDIER ? 20 : entity.type === ENTITY_TYPE.AA_GUN ? 30 : entity.type === ENTITY_TYPE.RADAR ? 100 : entity.type === 99 ? 200 : // HQ 10; // Check mission objectives if (game.currentMission) { for (const obj of game.currentMission.objectives) { if (obj.type === entity.type) { obj.completed++; game.updateObjectiveUI(); break; } } // Check for HQ destruction (special type 99) if (entity.type === 99) { for (const obj of game.currentMission.objectives) { if (obj.type === 99) { obj.completed++; game.updateObjectiveUI(); break; } } } } } entity.removed = true; } this.removed = true; break; } } } draw(ctx, camera) { ctx.save(); ctx.translate(this.position.x - camera.x + this.width / 2, this.position.y - camera.y + this.height / 2); ctx.fillStyle = this.color; ctx.beginPath(); ctx.arc(0, 0, this.width / 2, 0, Math.PI * 2); ctx.fill(); // Trail effect ctx.strokeStyle = this.color; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(-this.direction.x * 10, -this.direction.y * 10); ctx.stroke(); ctx.restore(); } } class Missile extends Projectile { constructor(position, direction, speed, damage, range, color, owner) { super(ENTITY_TYPE.PROJECTILE, position, direction, speed, damage, range, color, owner); this.width = 10; this.height = 5; this.homing = true; this.homingStrength = 0.05; this.target = null; } update(deltaTime, game) { // Find target if we don't have one if (this.homing && !this.target) { let closestDist = Infinity; for (const entity of game.entities) { if (entity.team === this.owner.team || entity.type === ENTITY_TYPE.POW || entity.type === ENTITY_TYPE.BASE) { continue; } const dist = this.center.distanceTo(entity.center); if (dist < closestDist && dist < 300) { closestDist = dist; this.target = entity; } } } if (this.target && !this.target.removed && this.distanceTraveled < this.range * 0.8) { // Home in on target const targetDir = this.target.center.subtract(this.center).normalize(); this.direction = this.direction.lerp(targetDir, this.homingStrength); this.velocity = this.direction.multiply(this.speed); } super.update(deltaTime, game); } draw(ctx, camera) { ctx.save(); ctx.translate(this.position.x - camera.x + this.width / 2, this.position.y - camera.y + this.height / 2); ctx.rotate(Math.atan2(this.direction.y, this.direction.x)); // Missile body ctx.fillStyle = this.color; ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height); // Tail fins ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(-this.width / 2, -this.height / 2); ctx.lineTo(-this.width / 2 - 5, 0); ctx.lineTo(-this.width / 2, this.height / 2); ctx.stroke(); // Engine flame (animated) ctx.fillStyle = '#f59e0b'; ctx.beginPath(); ctx.moveTo(this.width / 2, 0); ctx.lineTo(this.width / 2 + 5 + 5 * Math.sin(Date.now() / 100), -2); ctx.lineTo(this.width / 2 + 5 + 5 * Math.sin(Date.now() / 100 + 1), 2); ctx.closePath(); ctx.fill(); ctx.restore(); } } class Rocket extends Projectile { constructor(position, direction, speed, damage, range, color, owner, splashRadius) { super(ENTITY_TYPE.PROJECTILE, position, direction, speed, damage, range, color, owner); this.width = 12; this.height = 6; this.splashRadius = splashRadius; this.rotationSpeed = 0.05; } update(deltaTime, game) { super.update(deltaTime, game); // Rocket moves with a slight spiral effect this.angle += this.rotationSpeed * deltaTime * (this.speed / 100); } onHit(game, entity) { // Create splash damage const explosion = new Explosion(this.center, this.splashRadius); game.addEntity(explosion); // Damage all entities in splash radius for (const e of game.entities) { if (e !== this.owner && e.team !== this.owner.team) { const dist = this.center.distanceTo(e.center); if (dist < this.splashRadius) { const damage = Math.max(1, this.damage * (1 - dist / this.splashRadius)); if (e.takeDamage(damage)) { // Entity was destroyed game.addEntity(new Explosion(e.center)); // Add score if player killed it if (this.owner.type === ENTITY_TYPE.HELICOPTER) { game.score += e.type === ENTITY_TYPE.TANK ? 50 : e.type === ENTITY_TYPE.SOLDIER ? 20 : e.type === ENTITY_TYPE.AA_GUN ? 30 : e.type === ENTITY_TYPE.RADAR ? 100 : 10; // Check mission objectives if (game.currentMission) { for (const obj of game.currentMission.objectives) { if (obj.type === e.type) { obj.completed++; game.updateObjectiveUI(); break; } } } } e.removed = true; } } } } } draw(ctx, camera) { ctx.save(); ctx.translate(this.position.x - camera.x + this.width / 2, this.position.y - camera.y + this.height / 2); ctx.rotate(Math.atan2(this.direction.y, this.direction.x) + this.angle); // Rocket body ctx.fillStyle = this.color; ctx.fillRect(-this.width / 2, -this.height / 2, this.width, this.height); // Warhead ctx.fillStyle = '#ffffff'; ctx.beginPath(); ctx.moveTo(this.width / 2, -this.height / 2); ctx.lineTo(this.width / 2 + 5, 0); ctx.lineTo(this.width / 2, this.height / 2); ctx.fill(); // Fins ctx.strokeStyle = '#6b7280'; ctx.lineWidth = 1; for (let i = 0; i < 2; i++) { ctx.beginPath(); ctx.moveTo(-this.width / 2 + 3, -this.height / 2); ctx.lineTo(-this.width / 2, -this.height / 2 - 5); ctx.lineTo(-this.width / 2 + 3, -this.height / 2 - 2); ctx.stroke(); // Rotate for next fin ctx.rotate(Math.PI); } // Engine flame (animated) ctx.fillStyle = '#f97316'; ctx.beginPath(); ctx.moveTo(-this.width / 2, 0); ctx.lineTo(-this.width / 2 - 8 - 3 * Math.sin(Date.now() / 100), -2); ctx.lineTo(-this.width / 2 - 8 - 3 * Math.sin(Date.now() / 100 + 1), 2); ctx.closePath(); ctx.fill(); ctx.restore(); } } class Explosion extends Entity { constructor(position, radius = 30) { super(ENTITY_TYPE.EXPLOSION, position, radius * 2, radius * 2); this.radius = radius; this.lifetime = 500; this.startTime = Date.now(); this.particles = []; // Create particles (just for visual effect) for (let i = 0; i < 20; i++) { this.particles.push({ x: 0, y: 0, speed: Math.random() * 3 + 1, angle: Math.random() * Math.PI * 2, lifetime: Math.random() * 300 + 200, startTime: Date.now() }); } } update(deltaTime, game) { if (Date.now() - this.startTime > this.lifetime) { this.removed = true; } } draw(ctx, camera) { const progress = (Date.now() - this.startTime) / this.lifetime; const currentRadius = this.radius * easeOutCubic(progress); ctx.save(); ctx.translate(this.position.x - camera.x + this.width / 2, this.position.y - camera.y + this.height / 2); // Main explosion (grows then fades) const gradient = ctx.createRadialGradient(0, 0, 0, 0, 0, currentRadius); gradient.addColorStop(0, 'rgba(245, 158, 11, 0.8)'); gradient.addColorStop(0.7, 'rgba(239, 68, 68, 0.4)'); gradient.addColorStop(1, 'rgba(220, 38, 38, 0)'); ctx.fillStyle = gradient; ctx.beginPath(); ctx.arc(0, 0, currentRadius, 0, Math.PI * 2); ctx.fill(); // Draw particles for (const particle of this.particles) { const particleProgress = (Date.now() - particle.startTime) / particle.lifetime; if (particleProgress > 1) continue; const dist = particle.speed * particleProgress * currentRadius * 0.8; const x = Math.cos(particle.angle) * dist; const y = Math.sin(particle.angle) * dist; ctx.fillStyle = `rgba(249, 115, 22, ${1 - particleProgress})`; ctx.fillRect(x, y, 2, 2); } ctx.restore(); } } class HQ extends Entity { constructor(position) { super(99, position, 120, 100); this.health = 300; this.maxHealth = 300; this.team = 1; // enemy } draw(ctx, camera) { ctx.save(); ctx.translate(this.position.x - camera.x, this.position.y - camera.y); // Building base ctx.fillStyle = '#6b7280'; ctx.fillRect(0, 40, this.width, 60); // Main structure ctx.fillStyle = '#374151'; ctx.fillRect(20, 10, 80, 80); // Windows ctx.fillStyle = '#1e40af'; for (let i = 0; i < 3; i++) { for (let j = 0; j < 3; j++) { ctx.fillRect(30 + i * 25, 20 + j * 20, 10, 12); } } // Flag ctx.fillStyle = '#ef4444'; ctx.fillRect(105, 15, 15, 15); ctx.restore(); // Draw health bar this.drawHealthBar(ctx, camera); } } // ============================================= // GAME ENGINE AND MAIN LOOP // ============================================= class GameEngine { constructor() { this.canvas = document.getElementById('gameCanvas'); this.ctx = this.canvas.getContext('2d'); this.minimapCanvas = document.getElementById('minimapCanvas'); this.minimapCtx = this.minimapCanvas.getContext('2d'); // Set canvas dimensions this.canvas.width = GAME_WIDTH; this.canvas.height = GAME_HEIGHT; this.minimapCanvas.width = this.minimapCanvas.offsetWidth; this.minimapCanvas.height = this.minimapCanvas.offsetHeight; // Game state this.state = GAME_STATE.READY; this.score = 0; this.currentMission = null; this.currentMissionIndex = 0; this.lastFrameTime = 0; this.keys = {}; this.mouse = { x: 0, y: 0, clicked: false }; // Game entities this.entities = []; this.player = null; this.camera = { x: 0, y: 0 }; // UI elements this.missionBriefing = document.getElementById('missionBriefing'); this.gameOverModal = document.getElementById('gameOverModal'); this.missionCompleteModal = document.getElementById('missionCompleteModal'); // Set up event listeners this.setupEventListeners(); // Show mission briefing this.showMissionBriefing(); } setupEventListeners() { // Keyboard input window.addEventListener('keydown', (e) => { this.keys[e.key] = true; }); window.addEventListener('keyup', (e) => { this.keys[e.key] = false; }); // Mouse input this.canvas.addEventListener('mousemove', (e) => { const rect = this.canvas.getBoundingClientRect(); this.mouse.x = (e.clientX - rect.left) + this.camera.x; this.mouse.y = (e.clientY - rect.top) + this.camera.y; }); this.canvas.addEventListener('mousedown', (e) => { if (e.button === 0) { // Left click this.mouse.clicked = true; } }); this.canvas.addEventListener('mouseup', (e) => { if (e.button === 0) { // Left click this.mouse.clicked = false; } }); // Touch input for mobile this.canvas.addEventListener('touchstart', (e) => { e.preventDefault(); const rect = this.canvas.getBoundingClientRect(); const touch = e.touches[0]; this.mouse.x = (touch.clientX - rect.left) + this.camera.x; this.mouse.y = (touch.clientY - rect.top) + this.camera.y; this.mouse.clicked = true; }); this.canvas.addEventListener('touchmove', (e) => { e.preventDefault(); const rect = this.canvas.getBoundingClientRect(); const touch = e.touches[0]; this.mouse.x = (touch.clientX - rect.left) + this.camera.x; this.mouse.y = (touch.clientY - rect.top) + this.camera.y; }); this.canvas.addEventListener('touchend', (e) => { e.preventDefault(); this.mouse.clicked = false; }); // UI buttons document.getElementById('startMissionBtn').addEventListener('click', () => { this.startMission(); }); document.getElementById('restartBtn').addEventListener('click', () => { this.restartGame(); }); document.getElementById('nextMissionBtn').addEventListener('click', () => { this.nextMission(); }); // Weapon selection buttons document.getElementById('machinegunBtn').addEventListener('click', () => { if (this.player) this.player.currentWeapon = 0; this.updateWeaponUI(); }); document.getElementById('missileBtn').addEventListener('click', () => { if (this.player) this.player.currentWeapon = 1; this.updateWeaponUI(); }); document.getElementById('rocketBtn').addEventListener('click', () => { if (this.player) this.player.currentWeapon = 2; this.updateWeaponUI(); }); } showMissionBriefing() { if (this.currentMissionIndex < MISSIONS.length) { this.currentMission = {...MISSIONS[this.currentMissionIndex]}; document.getElementById('missionText').textContent = `MISSION: ${this.currentMission.title.toUpperCase()}`; document.getElementById('briefingText').innerHTML = ` <p class="mb-2">${this.currentMission.briefing}</p> <p class="mb-2">Primary Objectives:</p> <ul class="list-disc list-inside mb-4"> ${this.currentMission.objectives.map(obj => `<li>${obj.description}</li>`).join('')} </ul> `; this.missionBriefing.classList.remove('hidden'); } else { // All missions completed this.showGameComplete(); } } startMission() { this.missionBriefing.classList.add('hidden'); // Reset game state this.score = 0; this.entities = []; this.state = GAME_STATE.PLAYING; // Create player this.player = Helicopter.createPlayer(); this.addEntity(this.player); // Create base const base = new Base(new Vector2(WORLD_WIDTH / 2, WORLD_HEIGHT / 2)); this.addEntity(base); // Generate mission entities based on current mission this.generateMissionEntities(); // Center camera on player this.centerCameraOnPlayer(); // Start game loop this.lastFrameTime = performance.now(); requestAnimationFrame(this.gameLoop.bind(this)); } generateMissionEntities() { if (!this.currentMission) return; const mission = this.currentMission; switch (mission.type) { case MISSION_TYPE.DESTROY_RADAR: // Create radar installations for (let i = 0; i < 2; i++) { const x = this.player.position.x + (Math.random() * 1000 - 500); const y = this.player.position.y + (Math.random() * 1000 - 500); const radar = new Enemy(ENTITY_TYPE.RADAR, new Vector2( Math.max(0, Math.min(WORLD_WIDTH - 60, x)), Math.max(0, Math.min(WORLD_HEIGHT - 60, y)) )); this.addEntity(radar); } // Create AA guns for (let i = 0; i < 5; i++) { const x = this.player.position.x + (Math.random() * 800 - 400); const y = this.player.position.y + (Math.random() * 800 - 400); const aa = new Enemy(ENTITY_TYPE.AA_GUN, new Vector2( Math.max(0, Math.min(WORLD_WIDTH - 40, x)), Math.max(0, Math.min(WORLD_HEIGHT - 40, y)) )); this.addEntity(aa); } // Create enemy patrols for (let i = 0; i < 3; i++) { const x = this.player.position.x + (Math.random() * 600 - 300); const y = this.player.position.y + (Math.random() * 600 - 300); const tank = new Enemy(ENTITY_TYPE.TANK, new Vector2( Math.max(0, Math.min(WORLD_WIDTH - 40, x)), Math.max(0, Math.min(WORLD_HEIGHT - 30, y)) )); this.addEntity(tank); for (let j = 0; j < 3; j++) { const soldier = new Enemy(ENTITY_TYPE.SOLDIER, new Vector2( x + (Math.random() * 100 - 50), y + (Math.random() * 100 - 50) )); this.addEntity(soldier); } } break; case MISSION_TYPE.RESCUE_POWS: // Create POWs for (let i = 0; i < 3; i++) { const x = this.player.position.x + 300 + (Math.random() * 500 - 250); const y = this.player.position.y + 300 + (Math.random() * 500 - 250); const pow = new POW(new Vector2( Math.max(0, Math.min(WORLD_WIDTH - 20, x)), Math.max(0, Math.min(WORLD_HEIGHT - 25, y)) )); this.addEntity(pow); } // Create enemy patrols for (let i = 0; i < 5; i++) { const x = this.player.position.x + (Math.random() * 700 - 350); const y = this.player.position.y + (Math.random() * 700 - 350); const tank = new Enemy(ENTITY_TYPE.TANK, new Vector2( Math.max(0, Math.min(WORLD_WIDTH - 40, x)), Math.max(0, Math.min(WORLD_HEIGHT - 30, y)) )); this.addEntity(tank); for (let j = 0; j < 3; j++) { const soldier = new Enemy(ENTITY_TYPE.SOLDIER, new Vector2( x + (Math.random() * 100 - </html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Desert Strike Clone</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
background-color: #111;
font-family: 'Arial', sans-serif;
}
canvas {
display: block;
}
.game-container {
position: relative;
width: 100vw;
height: 100vh;
}
.modal {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 0, 0.8);
color: white;
padding: 2rem;
border-radius: 0.5rem;
border: 2px solid #f59e0b;
max-width: 600px;
text-align: center;
z-index: 100;
}
.modal button {
background-color: #f59e0b;
color: black;
border: none;
padding: 0.5rem 1.5rem;
margin-top: 1rem;
border-radius: 0.25rem;
cursor: pointer;
font-weight: bold;
font-size: 1rem;
transition: all 0.2s;
}
.modal button:hover {
background-color: #fbbf24;
transform: scale(1.05);
}
.modal button:active {
transform: scale(0.98);
}
.health-bar {
height: 8px;
background-color: #ef4444;
transition: width 0.3s;
border-radius: 4px;
}
.fuel-bar {
height: 8px;
background-color: #3b82f6;
transition: width 0.3s;
border-radius: 4px;
}
.ammo-bar {
height: 8px;
background-color: #f59e0b;
transition: width 0.3s;
border-radius: 4px;
}
.minimap {
position: absolute;
bottom: 20px;
right: 20px;
width: 180px;
height: 180px;
background-color: rgba(0, 0, 0, 0.5);
border: 2px solid #f59e0b;
z-index: 10;
border-radius: 8px;
overflow: hidden;
}
.minimap-canvas {
width: 100%;
height: 100%;
}
.stats-container {
background-color: rgba(0, 0, 0, 0.5);
padding: 0.5rem 1rem;
border-radius: 0.5rem;
border: 1px solid #f59e0b;
}
.weapon-selector {
position: absolute;
bottom: 20px;
left: 20px;