Spaces:
Running
Running
Download index.html from thomassimplineers/test: direct link, hf CLI and curl.
- Browser
- Download file 67.4 kB
-
https://huggingface.co/spaces/thomassimplineers/test/resolve/main/index.html
- Command line
-
hf download hf://spaces/thomassimplineers/test/index.html
-
curl -L -o index.html https://huggingface.co/spaces/thomassimplineers/test/resolve/main/index.html
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>
<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;