Files
Gecofist/gecofist.html
2026-03-24 09:48:28 +01:00

2562 lines
90 KiB
HTML

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GECOFIST v1.5</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #000; overflow: hidden; height: 100vh; font-family: 'Segoe UI', Tahoma, sans-serif; }
canvas { display: block; cursor: none; }
#titleScreen {
position: fixed; inset: 0; z-index: 100;
display: flex; flex-direction: column; align-items: center; justify-content: center;
background: linear-gradient(180deg, #0a0a1a 0%, #1a0505 100%);
}
#titleScreen h1 {
font-size: 6rem; font-weight: 900; letter-spacing: 8px;
background: linear-gradient(180deg, #ff4444, #ff0000, #880000);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
text-shadow: none; filter: drop-shadow(0 0 30px rgba(255,0,0,0.6));
margin-bottom: 5px;
}
#titleScreen .subtitle {
color: #ff6b35; font-size: 1.3rem; margin-bottom: 40px; letter-spacing: 3px;
}
#titleScreen .instructions {
color: #888; font-size: 1rem; margin-bottom: 30px; text-align: center; line-height: 1.8;
}
#titleScreen button {
padding: 18px 60px; font-size: 1.5rem; font-weight: 800;
background: linear-gradient(135deg, #ff4444, #cc0000);
color: #fff; border: 3px solid #ff6666; border-radius: 15px;
cursor: pointer; transition: .3s; text-transform: uppercase; letter-spacing: 4px;
}
#titleScreen button:hover { transform: scale(1.08); box-shadow: 0 0 40px rgba(255,0,0,0.6); }
.gecko-preview { font-size: 1rem; color: #ff6b35; margin-bottom: 20px; }
#hud {
position: fixed; top: 0; left: 0; right: 0; z-index: 50;
display: none; justify-content: space-between; align-items: center;
padding: 10px 25px;
background: rgba(0,0,0,0.7); border-bottom: 2px solid #cc0000;
}
#hud .score { color: #ff4444; font-size: 2rem; font-weight: 900; }
#hud .combo { color: #ffd700; font-size: 1.2rem; font-weight: 700; }
#hud .lives { color: #ff6b35; font-size: 1.5rem; }
#hud .distance { color: #888; font-size: 1rem; }
#gameOver {
position: fixed; inset: 0; z-index: 200;
display: none; flex-direction: column; align-items: center; justify-content: center;
background: rgba(20,0,0,0.9); backdrop-filter: blur(10px);
}
#gameOver.show { display: flex; }
#gameOver h2 { font-size: 3.5rem; color: #ff0000; font-weight: 900; margin-bottom: 10px; }
#gameOver .final { font-size: 4rem; color: #ffd700; font-weight: 900; }
#gameOver .stats { color: #aaa; font-size: 1.1rem; margin: 20px 0; }
#gameOver button {
padding: 15px 50px; font-size: 1.3rem; font-weight: 700;
background: linear-gradient(135deg, #ff4444, #cc0000);
color: #fff; border: 2px solid #ff6666; border-radius: 12px;
cursor: pointer; text-transform: uppercase; letter-spacing: 3px;
}
#gameOver button:hover { transform: scale(1.05); }
</style>
</head>
<body>
<div id="titleScreen">
<div class="gecko-preview">&#128038;</div>
<h1>GECOFIST</h1>
<p class="subtitle">LA MASCOTTE EN FURIE</p>
<p style="color:#555; font-size:0.8rem; margin-bottom: 15px; letter-spacing: 2px;">v1.5</p>
<div class="instructions">
<strong style="color:#ff6b35;">SOURIS</strong> pour diriger le Gecko<br>
<strong style="color:#ff4444;">CLIC</strong> pour lancer des BOULES DE FEU<br>
<strong style="color:#ffaa00;">ESPACE</strong> = SUPER ATTAQUE (tous les 250 pts)<br>
<strong style="color:#00ccff;">POWER-UP x2</strong> = tire la boule sur l'ic&ocirc;ne pour 2 boules de feu !<br>
Cr&eacute;me les petits vieux. Pas de piti&eacute;.
</div>
<button onclick="startGame()">FRAPPER</button>
</div>
<div id="hud">
<div class="score">SCORE: <span id="score">0</span></div>
<div class="combo" id="combo"></div>
<div class="super" id="superHUD" style="color:#ffaa00;font-size:1.3rem;font-weight:700;"></div>
<div class="distance" id="distance">0m</div>
<div class="lives" id="lives"></div>
</div>
<canvas id="c"></canvas>
<div id="gameOver">
<h2>K.O. TOTAL</h2>
<div class="final" id="finalScore">0</div>
<div class="stats" id="goStats"></div>
<button onclick="restart()">ENCORE</button>
</div>
<script>
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let W, H;
let vignetteCanvas = null;
function createVignette() {
vignetteCanvas = document.createElement('canvas');
vignetteCanvas.width = W; vignetteCanvas.height = H;
const vctx = vignetteCanvas.getContext('2d');
const vigGrad = vctx.createRadialGradient(W/2, H/2, H*0.3, W/2, H/2, H*0.9);
vigGrad.addColorStop(0, 'rgba(0,0,0,0)');
vigGrad.addColorStop(1, 'rgba(0,0,0,0.5)');
vctx.fillStyle = vigGrad;
vctx.fillRect(0, 0, W, H);
}
function resize() { W = canvas.width = innerWidth; H = canvas.height = innerHeight; createVignette(); }
addEventListener('resize', resize); resize();
// ========== STATE ==========
let gameRunning = false;
let score = 0, kills = 0, combo = 0, maxCombo = 0, comboTimer = 0;
let lives = 5;
let superAttackCharges = 0;
let lastSuperThreshold = 0; // last score milestone that gave a charge
let superAttackAnim = 0; // animation timer
let distance = 0;
let speed = 1;
let shakeX = 0, shakeY = 0, shakeTime = 0;
let flashAlpha = 0;
let mouseX = W/2, mouseY = H/2;
let punching = false, punchTimer = 0;
// Ground
let groundLines = [];
for (let i = 0; i < 30; i++) groundLines.push({ z: i * 40 });
// Enemies
let enemies = [];
let spawnTimer = 0;
let spawnRate = 80;
// Blood
let bloods = [];
let bloodPools = [];
// Particles (misc)
let particles = [];
// Fireballs
let fireballs = [];
let doubleFireball = false;
let doubleFireballTimer = 0;
const DOUBLE_FB_DURATION = 600; // ~10 seconds at 60fps
// Weapon power-ups
let powerups = [];
let powerupSpawnTimer = 0;
// Boss
const bossImg = new Image();
bossImg.src = 'img/boss.png';
let bossLoaded = false;
bossImg.onload = () => { bossLoaded = true; };
let bossActive = false;
let bossHP = 10;
let bossMaxHP = 10;
let bossX = 0; // sway position
let bossHitFlash = 0;
let bossLasers = []; // active laser beams
let bossLaserTimer = 0;
let bossYellTimer = 0; // "T'ES VIRE" text
let bossYells = [];
let bossDefeated = false;
let bossExplodeTimer = 0;
let nextBossDistance = Infinity;
const BOSS_INTERVAL = 150;
// Mini-Boss
const minibossImg = new Image();
minibossImg.src = 'img/miniboss.png';
let minibossLoaded = false;
minibossImg.onload = () => { minibossLoaded = true; };
let minibossActive = false;
let minibossHP = 5;
let minibossMaxHP = 5;
let minibossX = 0;
let minibossHitFlash = 0;
let minibossLasers = [];
let minibossLaserTimer = 0;
let minibossYells = [];
let minibossDefeated = false;
let minibossExplodeTimer = 0;
let nextMinibossDistance = 150;
const MINIBOSS_INTERVAL = 150;
// Mini-Boss 2
const miniboss2Img = new Image();
miniboss2Img.src = 'img/miniboss2.png';
let miniboss2Loaded = false;
miniboss2Img.onload = () => { miniboss2Loaded = true; };
let miniboss2Active = false;
let miniboss2HP = 7;
let miniboss2MaxHP = 7;
let miniboss2X = 0;
let miniboss2HitFlash = 0;
let miniboss2Lasers = [];
let miniboss2LaserTimer = 0;
let miniboss2Yells = [];
let miniboss2Defeated = false;
let miniboss2ExplodeTimer = 0;
let nextMiniboss2Distance = Infinity;
const MINIBOSS2_INTERVAL = 100;
// Mega fight (all 3 bosses at 500m)
let megaFightTriggered = false;
let nextMegaFightDistance = 500;
const MEGAFIGHT_INTERVAL = 500;
// ========== GECKO SPRITE (image PNG) ==========
const geckoImg = new Image();
geckoImg.src = 'img/geco.png';
let geckoLoaded = false;
geckoImg.onload = () => { geckoLoaded = true; };
function drawGecko(x, y, scale, isPunching) {
ctx.save();
ctx.translate(x, y);
const s = scale * (isPunching ? 1.15 : 1);
const w = 160 * s;
const h = 180 * s;
// Slight tilt when punching
if (isPunching) ctx.rotate(-0.15);
// Bob animation
const bob = Math.sin(Date.now() / 300) * 3;
// The fist is the anchor point (at x,y = mouse cursor).
// The body is drawn to the RIGHT of the fist.
// On the original image (facing right), the left fist is at ~15% from left, ~40% from top.
// Flipped: fist is on the left side => we offset the image so fist aligns with (0,0).
const fistOffX = w * 0.95; // body further to the right of the fist
const fistOffY = h * 0.12; // fist higher up
if (geckoLoaded) {
ctx.scale(-1, 1);
ctx.drawImage(geckoImg, -fistOffX, -h * 0.4 + fistOffY + bob, w, h);
ctx.scale(-1, 1);
} else {
ctx.fillStyle = '#e8531e';
ctx.fillRect(0, -h * 0.4 + bob, w, h);
ctx.fillStyle = '#fff';
ctx.font = '16px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('GECO', w / 2, bob);
}
// Impact FX at the fist (which is at 0,0 = mouse cursor)
if (isPunching) {
const t = Date.now();
ctx.strokeStyle = '#ffd700'; ctx.lineWidth = 3;
for (let i = 0; i < 8; i++) {
const a = (i / 8) * Math.PI * 2 + t / 50;
const r1 = 14;
const r2 = 25 + Math.random() * 10;
ctx.beginPath();
ctx.moveTo(Math.cos(a) * r1, bob + Math.sin(a) * r1);
ctx.lineTo(Math.cos(a) * r2, bob + Math.sin(a) * r2);
ctx.stroke();
}
ctx.strokeStyle = 'rgba(255,255,255,0.5)'; ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(0, bob, 30, 0, Math.PI * 2);
ctx.stroke();
}
ctx.restore();
}
// ========== OLD PERSON DRAW (detailed, bigger) ==========
// Type 0: Grandpa with cane + glasses + beret
// Type 1: Grandma with handbag + curly hair + pearl necklace
// Type 2: Old dude with walker, bald comb-over
function drawOldPerson(x, y, scale, type, hitAnim) {
ctx.save();
ctx.translate(x, y);
const s = scale * 1.5;
ctx.scale(s, s);
if (hitAnim > 0) {
ctx.rotate(hitAnim * 0.8);
ctx.globalAlpha = Math.max(0, 1 - hitAnim * 0.7);
}
const skin = type === 0 ? '#f0c8a0' : type === 1 ? '#f5d5b8' : '#d9b896';
const skinShade = type === 0 ? '#d4a87a' : type === 1 ? '#e0b898' : '#c09a72';
const t = Date.now();
// ---- CANE (type 0) / WALKER (type 2) ----
if (type === 0) {
ctx.strokeStyle = '#6B3410'; ctx.lineWidth = 3.5; ctx.lineCap = 'round';
ctx.beginPath(); ctx.moveTo(20, -5); ctx.lineTo(24, 55); ctx.stroke();
ctx.beginPath(); ctx.arc(20, -7, 5, Math.PI, Math.PI*2); ctx.stroke();
ctx.fillStyle = '#333';
ctx.beginPath(); ctx.arc(24, 56, 3, 0, Math.PI*2); ctx.fill();
}
if (type === 2) {
ctx.strokeStyle = '#888'; ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(-20, 0); ctx.lineTo(-22, 55);
ctx.moveTo(20, 0); ctx.lineTo(22, 55);
ctx.moveTo(-22, 55); ctx.lineTo(22, 55);
ctx.moveTo(-22, 20); ctx.lineTo(22, 20);
ctx.stroke();
ctx.fillStyle = '#c8e550';
ctx.beginPath(); ctx.arc(-22, 57, 3, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(22, 57, 3, 0, Math.PI*2); ctx.fill();
}
// ---- LEGS ----
ctx.fillStyle = type === 1 ? '#5a4a6a' : '#4a4a55';
ctx.strokeStyle = '#333'; ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(-8, 30); ctx.lineTo(-10, 52); ctx.lineTo(-3, 52); ctx.lineTo(-3, 30);
ctx.closePath(); ctx.fill(); ctx.stroke();
ctx.beginPath();
ctx.moveTo(3, 30); ctx.lineTo(3, 52); ctx.lineTo(10, 52); ctx.lineTo(8, 30);
ctx.closePath(); ctx.fill(); ctx.stroke();
// ---- SHOES ----
ctx.fillStyle = type === 1 ? '#8B4560' : '#4a3528';
ctx.strokeStyle = '#2a2a2a'; ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(-12, 51); ctx.lineTo(-12, 56); ctx.lineTo(0, 56); ctx.lineTo(0, 53); ctx.lineTo(-3, 51);
ctx.closePath(); ctx.fill(); ctx.stroke();
ctx.beginPath();
ctx.moveTo(2, 51); ctx.lineTo(2, 56); ctx.lineTo(14, 56); ctx.lineTo(14, 53); ctx.lineTo(10, 51);
ctx.closePath(); ctx.fill(); ctx.stroke();
// ---- BODY ----
const bodyColor = type === 0 ? '#5555aa' : type === 1 ? '#b04070' : '#7a8855';
ctx.fillStyle = bodyColor; ctx.strokeStyle = '#333'; ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(-16, -2); ctx.quadraticCurveTo(-18, 15, -14, 32);
ctx.lineTo(14, 32); ctx.quadraticCurveTo(18, 15, 16, -2);
ctx.closePath(); ctx.fill(); ctx.stroke();
// Body details
if (type === 0) {
ctx.strokeStyle = '#444477'; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(0, 30); ctx.stroke();
ctx.fillStyle = '#ddd';
for (let i = 0; i < 4; i++) { ctx.beginPath(); ctx.arc(0, 4+i*7, 2, 0, Math.PI*2); ctx.fill(); }
ctx.fillStyle = '#eee'; ctx.strokeStyle = '#ccc'; ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(-8, -2); ctx.lineTo(0, 4); ctx.lineTo(8, -2);
ctx.lineTo(5, -4); ctx.lineTo(0, 0); ctx.lineTo(-5, -4);
ctx.closePath(); ctx.fill(); ctx.stroke();
}
if (type === 1) {
ctx.fillStyle = 'rgba(255,200,220,0.5)';
for (let i = 0; i < 6; i++) {
ctx.beginPath(); ctx.arc(-8+(i%3)*8, 5+Math.floor(i/3)*12, 3, 0, Math.PI*2); ctx.fill();
}
ctx.fillStyle = '#f0e8d8'; ctx.strokeStyle = '#ccc'; ctx.lineWidth = 0.5;
for (let i = 0; i < 7; i++) {
const a = Math.PI*0.15 + i*Math.PI*0.1;
ctx.beginPath(); ctx.arc(Math.cos(a)*12, -2+Math.sin(a)*6, 2, 0, Math.PI*2); ctx.fill(); ctx.stroke();
}
}
if (type === 2) {
ctx.strokeStyle = '#aa4444'; ctx.lineWidth = 2.5;
ctx.beginPath(); ctx.moveTo(-6, -2); ctx.lineTo(-8, 30); ctx.stroke();
ctx.beginPath(); ctx.moveTo(6, -2); ctx.lineTo(8, 30); ctx.stroke();
ctx.strokeStyle = 'rgba(100,80,60,0.3)'; ctx.lineWidth = 1;
for (let i = 0; i < 5; i++) { ctx.beginPath(); ctx.moveTo(-15, 2+i*6); ctx.lineTo(15, 2+i*6); ctx.stroke(); }
}
// ---- ARMS ----
ctx.fillStyle = skin; ctx.strokeStyle = '#333'; ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(-16, 2); ctx.quadraticCurveTo(-24, 15, -20, 28);
ctx.quadraticCurveTo(-18, 30, -14, 28); ctx.quadraticCurveTo(-16, 15, -14, 2);
ctx.closePath(); ctx.fill(); ctx.stroke();
ctx.beginPath();
ctx.moveTo(16, 2); ctx.quadraticCurveTo(24, 15, 20, 28);
ctx.quadraticCurveTo(18, 30, 14, 28); ctx.quadraticCurveTo(16, 15, 14, 2);
ctx.closePath(); ctx.fill(); ctx.stroke();
// Hands
ctx.beginPath(); ctx.arc(-20, 29, 4, 0, Math.PI*2); ctx.fill(); ctx.stroke();
ctx.beginPath(); ctx.arc(20, 29, 4, 0, Math.PI*2); ctx.fill(); ctx.stroke();
ctx.strokeStyle = skinShade; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(-22, 28); ctx.lineTo(-24, 31); ctx.stroke();
ctx.beginPath(); ctx.moveTo(22, 28); ctx.lineTo(24, 31); ctx.stroke();
// Handbag (grandma)
if (type === 1) {
ctx.fillStyle = '#8B2252'; ctx.strokeStyle = '#5a1535'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.roundRect(18, 15, 14, 12, 3); ctx.fill(); ctx.stroke();
ctx.strokeStyle = '#5a1535'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.arc(25, 12, 6, Math.PI, Math.PI*2); ctx.stroke();
ctx.fillStyle = '#daa520';
ctx.beginPath(); ctx.arc(25, 17, 2, 0, Math.PI*2); ctx.fill();
}
// ---- NECK ----
ctx.fillStyle = skin; ctx.strokeStyle = '#333'; ctx.lineWidth = 1;
ctx.fillRect(-5, -8, 10, 8);
// ---- HEAD ----
ctx.fillStyle = skin; ctx.strokeStyle = '#333'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.ellipse(0, -18, 15, 14, 0, 0, Math.PI*2); ctx.fill(); ctx.stroke();
// Wrinkles
ctx.strokeStyle = skinShade; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(-8, -24); ctx.quadraticCurveTo(-4, -26, 0, -24); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0, -24); ctx.quadraticCurveTo(4, -26, 8, -24); ctx.stroke();
ctx.beginPath(); ctx.moveTo(-6, -27); ctx.quadraticCurveTo(0, -29, 6, -27); ctx.stroke();
ctx.beginPath(); ctx.moveTo(-12, -14); ctx.lineTo(-9, -12); ctx.stroke();
ctx.beginPath(); ctx.moveTo(12, -14); ctx.lineTo(9, -12); ctx.stroke();
ctx.beginPath(); ctx.moveTo(-5, -14); ctx.quadraticCurveTo(-6, -9, -4, -6); ctx.stroke();
ctx.beginPath(); ctx.moveTo(5, -14); ctx.quadraticCurveTo(6, -9, 4, -6); ctx.stroke();
// Nose (big old person nose)
ctx.fillStyle = skinShade; ctx.strokeStyle = '#333'; ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(-1, -18); ctx.quadraticCurveTo(-5, -10, -3, -8);
ctx.quadraticCurveTo(0, -6, 3, -8); ctx.quadraticCurveTo(5, -10, 1, -18);
ctx.closePath(); ctx.fill(); ctx.stroke();
// Ears (big)
ctx.fillStyle = skin; ctx.strokeStyle = '#333'; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.ellipse(-14, -16, 4, 6, 0, 0, Math.PI*2); ctx.fill(); ctx.stroke();
ctx.beginPath(); ctx.ellipse(14, -16, 4, 6, 0, 0, Math.PI*2); ctx.fill(); ctx.stroke();
if (type !== 1) {
ctx.strokeStyle = '#bbb'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(-15, -18); ctx.lineTo(-18, -20); ctx.stroke();
ctx.beginPath(); ctx.moveTo(15, -18); ctx.lineTo(18, -20); ctx.stroke();
}
// ---- HAIR ----
if (type === 0) {
// Beret + gray sides
ctx.fillStyle = '#333'; ctx.strokeStyle = '#222'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.ellipse(0, -30, 16, 6, 0, 0, Math.PI*2); ctx.fill(); ctx.stroke();
ctx.beginPath(); ctx.arc(0, -30, 3, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#bbb';
ctx.beginPath();
ctx.moveTo(-13, -22); ctx.quadraticCurveTo(-15, -18, -14, -12);
ctx.quadraticCurveTo(-13, -16, -11, -22); ctx.closePath(); ctx.fill();
ctx.beginPath();
ctx.moveTo(13, -22); ctx.quadraticCurveTo(15, -18, 14, -12);
ctx.quadraticCurveTo(13, -16, 11, -22); ctx.closePath(); ctx.fill();
}
if (type === 1) {
// Curly grandma hair
ctx.fillStyle = '#c8c0d8'; ctx.strokeStyle = '#a098b8'; ctx.lineWidth = 1;
for (let row = 0; row < 3; row++) {
for (let i = -3; i <= 3; i++) {
ctx.beginPath(); ctx.arc(i*5, -28-row*4, 5-row, 0, Math.PI*2); ctx.fill(); ctx.stroke();
}
}
}
if (type === 2) {
// Bald + liver spots + comb-over
ctx.fillStyle = skinShade;
ctx.beginPath(); ctx.arc(-5, -28, 2, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(7, -26, 1.5, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(2, -30, 1.5, 0, Math.PI*2); ctx.fill();
ctx.strokeStyle = '#aaa'; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(-3, -31); ctx.quadraticCurveTo(5, -38, 12, -28); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0, -32); ctx.quadraticCurveTo(8, -40, 15, -27); ctx.stroke();
ctx.beginPath(); ctx.moveTo(3, -31); ctx.quadraticCurveTo(10, -38, 16, -26); ctx.stroke();
}
// ---- GLASSES ----
if (type === 0 || type === 2) {
ctx.strokeStyle = type === 0 ? '#8B7355' : '#555'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.roundRect(-12, -20, 10, 8, 2); ctx.stroke();
ctx.beginPath(); ctx.roundRect(2, -20, 10, 8, 2); ctx.stroke();
ctx.beginPath(); ctx.moveTo(-2, -16); ctx.lineTo(2, -16); ctx.stroke();
ctx.beginPath(); ctx.moveTo(-12, -17); ctx.lineTo(-14, -16); ctx.stroke();
ctx.beginPath(); ctx.moveTo(12, -17); ctx.lineTo(14, -16); ctx.stroke();
ctx.fillStyle = 'rgba(255,255,255,0.15)';
ctx.fillRect(-10, -19, 3, 3); ctx.fillRect(4, -19, 3, 3);
}
// ---- EYES (terrified!) ----
ctx.fillStyle = '#fff'; ctx.strokeStyle = '#333'; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.ellipse(-7, -17, 4, 5, 0, 0, Math.PI*2); ctx.fill(); ctx.stroke();
ctx.beginPath(); ctx.ellipse(7, -17, 4, 5, 0, 0, Math.PI*2); ctx.fill(); ctx.stroke();
// Pupils (terrified)
ctx.fillStyle = '#222';
ctx.beginPath(); ctx.arc(-7, -16, 2.5, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(7, -16, 2.5, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#fff';
ctx.beginPath(); ctx.arc(-6, -17, 1, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(8, -17, 1, 0, Math.PI*2); ctx.fill();
// Eyebrows (raised in terror)
ctx.strokeStyle = type === 1 ? '#a098b0' : '#888'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(-11, -23); ctx.quadraticCurveTo(-7, -26, -3, -23); ctx.stroke();
ctx.beginPath(); ctx.moveTo(3, -23); ctx.quadraticCurveTo(7, -26, 11, -23); ctx.stroke();
// ---- MOUTH (screaming) ----
ctx.fillStyle = '#4a0000'; ctx.strokeStyle = '#333'; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.ellipse(0, -6, 6, 5+Math.sin(t/100)*1, 0, 0, Math.PI*2); ctx.fill(); ctx.stroke();
ctx.fillStyle = '#cc5555';
ctx.beginPath(); ctx.ellipse(0, -3, 3, 2, 0, 0, Math.PI); ctx.fill();
// Missing teeth
ctx.fillStyle = '#eee';
ctx.fillRect(-4, -10, 2, 3); ctx.fillRect(3, -10, 2, 3);
// ---- SWEAT DROPS ----
ctx.fillStyle = 'rgba(100,180,255,0.6)';
const sw = Math.sin(t/200) * 3;
ctx.beginPath(); ctx.moveTo(-14, -22); ctx.quadraticCurveTo(-16, -18+sw, -14, -16+sw);
ctx.quadraticCurveTo(-12, -18+sw, -14, -22); ctx.fill();
ctx.beginPath(); ctx.moveTo(14, -20); ctx.quadraticCurveTo(16, -16+sw, 14, -14+sw);
ctx.quadraticCurveTo(12, -16+sw, 14, -20); ctx.fill();
ctx.restore();
}
// ========== BLOOD SYSTEM ==========
function spawnBlood(x, y, amount, velScale) {
// Cap to avoid overflow
const actualAmount = Math.min(amount, 20);
for (let i = 0; i < actualAmount; i++) {
bloods.push({
x, y,
vx: (Math.random() - 0.5) * 12 * velScale,
vy: (Math.random() - 0.8) * 10 * velScale,
size: 2 + Math.random() * 5,
life: 1,
gravity: 0.3 + Math.random() * 0.2,
type: Math.random() > 0.3 ? 'drop' : 'splash',
});
}
// Cap bloods
if (bloods.length > 200) bloods.splice(0, bloods.length - 200);
// Blood pool on ground
bloodPools.push({
x: x + (Math.random()-0.5)*40,
z: 5 + Math.random() * 15,
size: 15 + Math.random() * 25,
alpha: 0.7 + Math.random() * 0.3,
});
// Cap blood pools
if (bloodPools.length > 30) bloodPools.splice(0, bloodPools.length - 30);
}
function updateBloods() {
for (let i = bloods.length - 1; i >= 0; i--) {
const b = bloods[i];
b.x += b.vx;
b.y += b.vy;
b.vy += b.gravity;
b.life -= 0.03;
if (b.life <= 0 || b.y > H + 20) bloods.splice(i, 1);
}
}
function drawBloods() {
for (const b of bloods) {
ctx.globalAlpha = b.life;
if (b.type === 'drop') {
ctx.fillStyle = `rgb(${150 + Math.random()*60}, 0, 0)`;
ctx.beginPath();
ctx.arc(b.x, b.y, b.size, 0, Math.PI * 2);
ctx.fill();
} else {
ctx.fillStyle = '#8B0000';
ctx.fillRect(b.x - b.size/2, b.y - 1, b.size, 2 + Math.random()*2);
}
}
ctx.globalAlpha = 1;
}
// ========== FIREBALLS ==========
function spawnFireball() {
if (fireballs.length >= 15) return; // cap fireballs
fireballs.push({
x: mouseX,
y: mouseY,
z: 0,
startX: mouseX,
startY: mouseY,
targetX: W / 2,
life: 1,
trail: [],
});
}
function updateFireballs() {
const horizon = H * 0.3;
for (let i = fireballs.length - 1; i >= 0; i--) {
const fb = fireballs[i];
fb.z += 18 + speed * 4; // speed of the fireball
// Perspective interpolation: as z grows, x/y move toward vanishing point
const progress = Math.min(fb.z / 1000, 1);
const perspX = fb.startX + (fb.targetX - fb.startX) * progress;
const perspY = fb.startY + (horizon - fb.startY) * progress;
const perspScale = Math.max(0.1, 1 - progress);
fb.screenX = perspX;
fb.screenY = perspY;
fb.screenScale = perspScale;
// Trail
fb.trail.push({ x: perspX, y: perspY, s: perspScale, life: 1 });
if (fb.trail.length > 6) fb.trail.shift();
for (const tr of fb.trail) tr.life -= 0.12;
// Fire particles along the way
if (Math.random() < 0.3) {
particles.push({
x: perspX + (Math.random() - 0.5) * 15 * perspScale,
y: perspY + (Math.random() - 0.5) * 15 * perspScale,
vx: (Math.random() - 0.5) * 3,
vy: (Math.random() - 0.5) * 3 - 1,
size: 2 + Math.random() * 4 * perspScale,
life: 0.5 + Math.random() * 0.3,
color: Math.random() > 0.5 ? '#ff6600' : '#ffcc00',
gravity: -0.1,
});
}
// Check collision with enemies
for (const e of enemies) {
if (e.dead) continue;
const zDiff = Math.abs(fb.z - e.z);
if (zDiff < 80) {
const { sx, sy, size } = getEnemyScreen(e);
const dx = fb.screenX - sx;
const dy = fb.screenY - sy;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 80 * size + 30) {
// HIT!
e.dead = true;
e.hitAnim = 0;
kills++;
combo++;
comboTimer = 120;
if (combo > maxCombo) maxCombo = combo;
let pts = 10 * combo;
score += pts;
// FIRE + BLOOD explosion
spawnBlood(sx, sy, 25 + combo * 5, 1 + combo * 0.2);
spawnBlood(sx, sy, 15, 1.5);
// Extra fire explosion
for (let j = 0; j < 8; j++) {
particles.push({
x: sx, y: sy,
vx: (Math.random() - 0.5) * 14,
vy: (Math.random() - 0.5) * 14,
size: 3 + Math.random() * 6,
life: 0.8 + Math.random() * 0.4,
color: ['#ff4400', '#ff8800', '#ffcc00', '#fff'][Math.floor(Math.random() * 4)],
gravity: -0.05,
});
}
shakeTime = 8 + Math.min(combo * 2, 15);
flashAlpha = 0.15;
spawnHitText(sx, sy - 30, pts, combo);
// Teeth / bone particles
for (let j = 0; j < 3 + combo; j++) {
particles.push({
x: sx, y: sy,
vx: (Math.random() - 0.5) * 10,
vy: -3 - Math.random() * 8,
size: 2 + Math.random() * 3,
life: 1,
color: Math.random() > 0.5 ? '#fff' : '#ddd',
gravity: 0.4,
});
}
// Remove fireball on hit
fireballs.splice(i, 1);
i--;
break;
}
}
}
// Remove if past horizon
if (fb.z > 1050) {
fireballs.splice(i, 1);
}
}
}
function drawFireballs() {
for (const fb of fireballs) {
// Draw trail (simple circles, no gradients)
for (const tr of fb.trail) {
if (tr.life <= 0) continue;
ctx.globalAlpha = tr.life * 0.5;
const r = 6 * tr.s;
ctx.fillStyle = '#ff6600';
ctx.beginPath();
ctx.arc(tr.x, tr.y, r, 0, Math.PI * 2);
ctx.fill();
}
// Draw main fireball (simple layered circles)
ctx.globalAlpha = 0.3;
const r = 14 * fb.screenScale;
ctx.fillStyle = '#ff4400';
ctx.beginPath();
ctx.arc(fb.screenX, fb.screenY, r * 2, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
ctx.fillStyle = '#ff8800';
ctx.beginPath();
ctx.arc(fb.screenX, fb.screenY, r, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#ffee55';
ctx.beginPath();
ctx.arc(fb.screenX, fb.screenY, r * 0.5, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
}
// ========== WEAPON POWER-UPS ==========
function spawnPowerup() {
const lane = (Math.random() - 0.5) * W * 0.6;
powerups.push({
x: W/2 + lane,
z: 1000,
yOff: (Math.random() - 0.5) * 20,
bobPhase: Math.random() * Math.PI * 2,
collected: false,
});
}
function updatePowerups() {
// Don't spawn during boss fights
const anyBossUp = (bossActive && !bossDefeated) || (minibossActive && !minibossDefeated) || (miniboss2Active && !miniboss2Defeated);
if (!anyBossUp && !doubleFireball) {
powerupSpawnTimer++;
if (powerupSpawnTimer >= 400 + Math.random() * 200) { // every ~7-10 seconds
powerupSpawnTimer = 0;
spawnPowerup();
}
}
const baseSpeed = 3 + speed * 1.5;
for (let i = powerups.length - 1; i >= 0; i--) {
const pu = powerups[i];
pu.z -= baseSpeed;
// Check collection by fireball
const { sx, sy, size } = getPowerupScreen(pu);
for (let j = fireballs.length - 1; j >= 0; j--) {
const fb = fireballs[j];
const zDiff = Math.abs(fb.z - pu.z);
if (zDiff < 80) {
const dx = fb.screenX - sx;
const dy = fb.screenY - sy;
if (Math.sqrt(dx*dx + dy*dy) < 70 * size + 30) {
collectPowerup(sx, sy);
powerups.splice(i, 1);
fireballs.splice(j, 1);
i = Math.min(i, powerups.length);
break;
}
}
}
// Passed the player without collection
if (pu.z < -50) {
powerups.splice(i, 1);
}
}
// Double fireball timer
if (doubleFireball) {
doubleFireballTimer--;
if (doubleFireballTimer <= 0) {
doubleFireball = false;
}
}
}
function collectPowerup(sx, sy) {
doubleFireball = true;
doubleFireballTimer = DOUBLE_FB_DURATION;
// Flashy pickup effect
shakeTime = 5;
for (let i = 0; i < 20; i++) {
particles.push({
x: sx, y: sy,
vx: (Math.random()-0.5) * 12,
vy: (Math.random()-0.5) * 12,
size: 3 + Math.random() * 5,
life: 0.8 + Math.random() * 0.3,
color: ['#ff4400','#ff8800','#ffcc00','#fff','#00aaff'][Math.floor(Math.random()*5)],
gravity: -0.05,
});
}
spawnHitText(sx, sy - 30, 0, 0);
hitTexts[hitTexts.length - 1].text = 'DOUBLE FEU!';
hitTexts[hitTexts.length - 1].color = '#00ccff';
}
function getPowerupScreen(pu) {
const horizon = H * 0.3;
const perspScale = Math.max(0.05, 1 - pu.z / 1000);
const screenX = W/2 + (pu.x - W/2) * perspScale;
const screenY = horizon + (1 - perspScale) * 0.1 * H + perspScale * (H * 0.35) + pu.yOff * perspScale;
return { sx: screenX, sy: screenY, size: perspScale };
}
function drawPowerups() {
const t = Date.now();
for (const pu of powerups) {
const { sx, sy, size } = getPowerupScreen(pu);
if (size < 0.05) continue;
const bob = Math.sin(t / 200 + pu.bobPhase) * 5 * size;
const s = size * 25;
ctx.save();
ctx.translate(sx, sy + bob);
// Glow (simple)
ctx.globalAlpha = 0.25;
ctx.fillStyle = '#ff8800';
ctx.beginPath(); ctx.arc(0, 0, s * 1.8, 0, Math.PI * 2); ctx.fill();
ctx.globalAlpha = 1;
// Ring
ctx.strokeStyle = '#ffaa00';
ctx.lineWidth = 2 * size;
ctx.beginPath();
ctx.ellipse(0, 0, s * 1.2, s * 0.6, t / 500, 0, Math.PI * 2);
ctx.stroke();
// Core
ctx.fillStyle = '#ff8800';
ctx.beginPath(); ctx.arc(0, 0, s, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#ffcc00';
ctx.beginPath(); ctx.arc(0, 0, s * 0.6, 0, Math.PI * 2); ctx.fill();
// "x2" text
ctx.fillStyle = '#fff'; ctx.strokeStyle = '#000';
ctx.lineWidth = 2 * size;
ctx.font = `bold ${14 * size}px sans-serif`;
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.strokeText('x2', 0, 0);
ctx.fillText('x2', 0, 0);
ctx.textBaseline = 'alphabetic';
ctx.restore();
}
}
// ========== MINI-BOSS SYSTEM ==========
function activateMiniboss() {
minibossActive = true;
minibossHP = minibossMaxHP;
minibossX = 0;
minibossHitFlash = 0;
minibossLasers = [];
minibossLaserTimer = 0;
minibossYells = [];
minibossDefeated = false;
minibossExplodeTimer = 0;
enemies = [];
}
function updateMiniboss() {
if (!minibossActive || minibossDefeated) return;
const t = Date.now();
minibossX = Math.sin(t / 1200) * W * 0.25;
if (minibossHitFlash > 0) minibossHitFlash -= 0.05;
minibossLaserTimer++;
if (minibossLaserTimer >= 70) {
minibossLaserTimer = 0;
fireMinibossLaser();
}
for (let i = minibossLasers.length - 1; i >= 0; i--) {
const las = minibossLasers[i];
las.progress += 0.04;
las.life -= 0.015;
if (las.progress > 0.85 && !las.hit) {
const dx = mouseX - las.targetX;
const dy = mouseY - las.targetY;
if (Math.sqrt(dx*dx + dy*dy) < 80) {
las.hit = true;
lives--;
combo = 0;
shakeTime = 12;
flashAlpha = 0.35;
document.getElementById('lives').textContent = '\u2764\ufe0f'.repeat(Math.max(0, lives));
if (lives <= 0) endGame();
}
}
if (las.life <= 0) minibossLasers.splice(i, 1);
}
for (let i = minibossYells.length - 1; i >= 0; i--) {
minibossYells[i].life -= 0.012;
minibossYells[i].y += minibossYells[i].vy;
minibossYells[i].scale += 0.01;
if (minibossYells[i].life <= 0) minibossYells.splice(i, 1);
}
// Check fireball hits
const horizon = H * 0.3;
const mbOff1 = megaFightTriggered && bossActive ? -W * 0.25 : 0;
const mbCX = W/2 + minibossX + mbOff1;
const mbCY = horizon - 5;
for (let i = fireballs.length - 1; i >= 0; i--) {
const fb = fireballs[i];
if (fb.z > 800) {
const dx = fb.screenX - mbCX;
const dy = fb.screenY - mbCY;
if (Math.sqrt(dx*dx + dy*dy) < 90) {
minibossHP--;
minibossHitFlash = 1;
shakeTime = 10;
flashAlpha = 0.15;
score += 50;
spawnHitText(mbCX, mbCY - 50, 50, 0);
for (let j = 0; j < 8; j++) {
particles.push({
x: fb.screenX, y: fb.screenY,
vx: (Math.random()-0.5)*12, vy: (Math.random()-0.5)*12,
size: 3+Math.random()*5, life: 0.6+Math.random()*0.4,
color: ['#00ff44','#44ff00','#88ff00'][Math.floor(Math.random()*3)],
gravity: -0.05,
});
}
fireballs.splice(i, 1);
if (minibossHP <= 0) defeatMiniboss();
break;
}
}
}
}
function fireMinibossLaser() {
if (minibossLasers.length >= 8) return; // cap lasers
const horizon = H * 0.3;
const mbOff1 = megaFightTriggered && bossActive ? -W * 0.25 : 0;
const mbCX = W/2 + minibossX + mbOff1;
const mbCY = horizon - 5;
const tx = mouseX + (Math.random()-0.5)*120;
const ty = mouseY + (Math.random()-0.5)*80;
minibossLasers.push({
startX: mbCX - 20, startY: mbCY + 8,
targetX: tx, targetY: ty,
progress: 0, life: 1, hit: false,
});
minibossLasers.push({
startX: mbCX + 18, startY: mbCY + 8,
targetX: tx + 15, targetY: ty,
progress: 0, life: 1, hit: false,
});
const phrases = [
"ATTAQUE PRODUIT P\u00c9RIM\u00c9 !",
"P\u00c9RIM\u00c9 DEPUIS 2019 !",
"DLC D\u00c9PASS\u00c9E !",
"PRODUIT P\u00c9RIM\u00c9 !!",
"RAPPEL PRODUIT !",
];
minibossYells.push({
x: mbCX + (Math.random()-0.5)*100,
y: mbCY - 70 - Math.random()*30,
vy: -1.2 - Math.random(),
life: 1, scale: 1,
text: phrases[Math.floor(Math.random()*phrases.length)],
});
}
function defeatMiniboss() {
minibossDefeated = true;
minibossExplodeTimer = 120;
score += 2500;
lives = 5;
const horizon = H * 0.3;
spawnBlood(W/2 + minibossX, horizon - 5, 60, 2.5);
// After miniboss1 defeated, miniboss2 spawns 100m later
nextMiniboss2Distance = distance + 100;
nextMinibossDistance = Infinity; // wait for full cycle
}
function updateMinibossExplosion() {
if (!minibossDefeated || minibossExplodeTimer <= 0) return;
minibossExplodeTimer--;
const horizon = H * 0.3;
const mbOff1 = megaFightTriggered && bossActive ? -W * 0.25 : 0;
const mbCX = W/2 + minibossX + mbOff1;
const mbCY = horizon - 5;
if (minibossExplodeTimer % 5 === 0) {
const ex = mbCX + (Math.random()-0.5)*160;
const ey = mbCY + (Math.random()-0.5)*120;
shakeTime = 4;
for (let j = 0; j < 10; j++) {
particles.push({
x: ex, y: ey,
vx: (Math.random()-0.5)*14, vy: (Math.random()-0.5)*14,
size: 4+Math.random()*7, life: 0.5+Math.random()*0.4,
color: ['#00ff44','#88ff00','#ffaa00','#fff','#8B0000'][Math.floor(Math.random()*5)],
gravity: 0.1,
});
}
spawnBlood(ex, ey, 8, 1.8);
}
if (minibossExplodeTimer <= 0) {
minibossActive = false;
minibossDefeated = false;
minibossLasers = [];
minibossYells = [];
spawnRate = Math.max(20, 80 - distance / 5);
}
}
function drawMiniboss() {
if (!minibossActive) return;
const horizon = H * 0.3;
const mbOff1 = megaFightTriggered && bossActive ? -W * 0.25 : 0;
const mbCX = W/2 + minibossX + mbOff1;
const mbCY = horizon - 5;
const t = Date.now();
ctx.save();
if (minibossDefeated) {
ctx.globalAlpha = Math.max(0, minibossExplodeTimer / 120);
ctx.translate((Math.random()-0.5)*8, (Math.random()-0.5)*8);
}
// Green aura (simple circle, no gradient)
ctx.fillStyle = 'rgba(0,100,0,0.12)';
ctx.beginPath(); ctx.arc(mbCX, mbCY, 110, 0, Math.PI*2); ctx.fill();
// Face
const mbSize = 90 + Math.sin(t/500)*6;
if (minibossLoaded) {
if (minibossHitFlash > 0.5) ctx.filter = `brightness(${1+minibossHitFlash*2})`;
ctx.drawImage(minibossImg, mbCX-mbSize/2, mbCY-mbSize/2, mbSize, mbSize);
ctx.filter = 'none';
} else {
ctx.fillStyle = minibossHitFlash > 0.5 ? '#fff' : '#d0c090';
ctx.beginPath(); ctx.arc(mbCX, mbCY, mbSize/2, 0, Math.PI*2); ctx.fill();
ctx.strokeStyle = '#333'; ctx.lineWidth = 2; ctx.stroke();
ctx.fillStyle = '#333'; ctx.font = 'bold 16px sans-serif'; ctx.textAlign = 'center';
ctx.fillText('MINI-BOSS', mbCX, mbCY+5);
}
// Green glowing eyes (simple circles)
const eyeGlow = 0.4 + Math.sin(t/180)*0.2;
const leyX = mbCX - mbSize*0.16, leyY = mbCY - mbSize*0.02;
const reyX = mbCX + mbSize*0.14, reyY = mbCY - mbSize*0.02;
ctx.fillStyle = `rgba(0,255,0,${eyeGlow})`;
ctx.beginPath(); ctx.arc(leyX, leyY, 8, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(reyX, reyY, 8, 0, Math.PI*2); ctx.fill();
// HP Bar
if (!minibossDefeated) {
const barW = 150, barH = 10;
const barX = mbCX-barW/2, barY = mbCY-mbSize/2-22;
ctx.fillStyle = 'rgba(0,0,0,0.7)';
ctx.fillRect(barX-2, barY-2, barW+4, barH+4);
const hpR = minibossHP / minibossMaxHP;
const hpG = ctx.createLinearGradient(barX, 0, barX+barW*hpR, 0);
hpG.addColorStop(0, '#00cc00'); hpG.addColorStop(1, '#44ff00');
ctx.fillStyle = hpG;
ctx.fillRect(barX, barY, barW*hpR, barH);
ctx.strokeStyle = '#fff'; ctx.lineWidth = 1;
ctx.strokeRect(barX, barY, barW, barH);
ctx.fillStyle = '#fff'; ctx.font = 'bold 9px sans-serif'; ctx.textAlign = 'center';
ctx.fillText(`${minibossHP} / ${minibossMaxHP}`, mbCX, barY+barH-1);
ctx.fillStyle = '#44ff00'; ctx.font = 'bold 14px sans-serif';
ctx.fillText('Rouxcmoute', mbCX, barY-6);
}
// Lasers (GREEN) - optimized: no radial gradients
for (const las of minibossLasers) {
const prog = Math.min(las.progress, 1);
const cx = las.startX + (las.targetX-las.startX)*prog;
const cy = las.startY + (las.targetY-las.startY)*prog;
ctx.globalAlpha = las.life;
ctx.strokeStyle = 'rgba(0,255,0,0.3)'; ctx.lineWidth = 10;
ctx.beginPath(); ctx.moveTo(las.startX, las.startY); ctx.lineTo(cx, cy); ctx.stroke();
ctx.strokeStyle = '#00ff00'; ctx.lineWidth = 4;
ctx.beginPath(); ctx.moveTo(las.startX, las.startY); ctx.lineTo(cx, cy); ctx.stroke();
ctx.strokeStyle = '#aaffaa'; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(las.startX, las.startY); ctx.lineTo(cx, cy); ctx.stroke();
ctx.fillStyle = 'rgba(100,255,100,0.6)';
ctx.beginPath(); ctx.arc(cx, cy, 14, 0, Math.PI*2); ctx.fill();
}
ctx.globalAlpha = 1;
// Yells
for (const yell of minibossYells) {
ctx.globalAlpha = yell.life;
ctx.fillStyle = '#00dd00'; ctx.strokeStyle = '#003300'; ctx.lineWidth = 3;
ctx.font = `bold ${16*yell.scale}px sans-serif`; ctx.textAlign = 'center';
ctx.strokeText(yell.text, yell.x, yell.y);
ctx.fillText(yell.text, yell.x, yell.y);
}
ctx.globalAlpha = 1;
ctx.restore();
}
// ========== MINI-BOSS 2 SYSTEM ==========
function activateMiniboss2() {
miniboss2Active = true;
miniboss2HP = miniboss2MaxHP;
miniboss2X = 0;
miniboss2HitFlash = 0;
miniboss2Lasers = [];
miniboss2LaserTimer = 0;
miniboss2Yells = [];
miniboss2Defeated = false;
miniboss2ExplodeTimer = 0;
if (!megaFightTriggered || (!bossActive && !minibossActive)) enemies = [];
}
function updateMiniboss2() {
if (!miniboss2Active || miniboss2Defeated) return;
const t = Date.now();
miniboss2X = Math.sin(t / 1000) * W * 0.22;
if (miniboss2HitFlash > 0) miniboss2HitFlash -= 0.05;
miniboss2LaserTimer++;
if (miniboss2LaserTimer >= 80) {
miniboss2LaserTimer = 0;
fireMiniboss2Laser();
}
for (let i = miniboss2Lasers.length - 1; i >= 0; i--) {
const las = miniboss2Lasers[i];
las.progress += 0.038;
las.life -= 0.015;
if (las.progress > 0.85 && !las.hit) {
const dx = mouseX - las.targetX;
const dy = mouseY - las.targetY;
if (Math.sqrt(dx*dx + dy*dy) < 80) {
las.hit = true;
lives--;
combo = 0;
shakeTime = 12;
flashAlpha = 0.35;
document.getElementById('lives').textContent = '\u2764\ufe0f'.repeat(Math.max(0, lives));
if (lives <= 0) endGame();
}
}
if (las.life <= 0) miniboss2Lasers.splice(i, 1);
}
for (let i = miniboss2Yells.length - 1; i >= 0; i--) {
miniboss2Yells[i].life -= 0.012;
miniboss2Yells[i].y += miniboss2Yells[i].vy;
miniboss2Yells[i].scale += 0.01;
if (miniboss2Yells[i].life <= 0) miniboss2Yells.splice(i, 1);
}
// Fireball hits
const horizon = H * 0.3;
const mb2Offset = megaFightTriggered && bossActive ? W * 0.25 : 0;
const mb2CX = W/2 + miniboss2X + mb2Offset;
const mb2CY = horizon - 5;
for (let i = fireballs.length - 1; i >= 0; i--) {
const fb = fireballs[i];
if (fb.z > 800) {
const dx = fb.screenX - mb2CX;
const dy = fb.screenY - mb2CY;
if (Math.sqrt(dx*dx + dy*dy) < 90) {
miniboss2HP--;
miniboss2HitFlash = 1;
shakeTime = 10;
flashAlpha = 0.15;
score += 50;
spawnHitText(mb2CX, mb2CY - 50, 50, 0);
for (let j = 0; j < 8; j++) {
particles.push({
x: fb.screenX, y: fb.screenY,
vx: (Math.random()-0.5)*12, vy: (Math.random()-0.5)*12,
size: 3+Math.random()*5, life: 0.6+Math.random()*0.4,
color: ['#4488ff','#2266ff','#88bbff'][Math.floor(Math.random()*3)],
gravity: -0.05,
});
}
fireballs.splice(i, 1);
if (miniboss2HP <= 0) defeatMiniboss2();
break;
}
}
}
}
function fireMiniboss2Laser() {
if (miniboss2Lasers.length >= 8) return; // cap lasers
const horizon = H * 0.3;
const mb2Offset = megaFightTriggered && bossActive ? W * 0.25 : 0;
const mb2CX = W/2 + miniboss2X + mb2Offset;
const mb2CY = horizon - 5;
const tx = mouseX + (Math.random()-0.5)*120;
const ty = mouseY + (Math.random()-0.5)*80;
miniboss2Lasers.push({
startX: mb2CX - 18, startY: mb2CY + 8,
targetX: tx, targetY: ty,
progress: 0, life: 1, hit: false,
});
miniboss2Lasers.push({
startX: mb2CX + 16, startY: mb2CY + 8,
targetX: tx + 15, targetY: ty,
progress: 0, life: 1, hit: false,
});
const phrases = [
"SI T'AS BESOIN DE RIEN...",
"QU'EST-CE QUE \u00c7A PEUT TE FOUTRE ?",
"SI T'AS BESOIN DE RIEN !",
"M\u00caLE-TOI DE TES AFFAIRES !",
"\u00c7A PEUT TE FOUTRE !!",
];
miniboss2Yells.push({
x: mb2CX + (Math.random()-0.5)*100,
y: mb2CY - 70 - Math.random()*30,
vy: -1.2 - Math.random(),
life: 1, scale: 1,
text: phrases[Math.floor(Math.random()*phrases.length)],
});
}
function defeatMiniboss2() {
miniboss2Defeated = true;
miniboss2ExplodeTimer = 120;
score += 3000;
lives = 5;
const horizon = H * 0.3;
const mb2Offset = megaFightTriggered && bossActive ? W * 0.25 : 0;
spawnBlood(W/2 + miniboss2X + mb2Offset, horizon - 5, 60, 2.5);
// After miniboss2 defeated, boss spawns 150m later
nextBossDistance = distance + 150;
nextMiniboss2Distance = Infinity; // wait for full cycle
}
function updateMiniboss2Explosion() {
if (!miniboss2Defeated || miniboss2ExplodeTimer <= 0) return;
miniboss2ExplodeTimer--;
const horizon = H * 0.3;
const mb2Offset = megaFightTriggered && bossActive ? W * 0.25 : 0;
const mb2CX = W/2 + miniboss2X + mb2Offset;
const mb2CY = horizon - 5;
if (miniboss2ExplodeTimer % 5 === 0) {
const ex = mb2CX + (Math.random()-0.5)*160;
const ey = mb2CY + (Math.random()-0.5)*120;
shakeTime = 4;
for (let j = 0; j < 10; j++) {
particles.push({
x: ex, y: ey,
vx: (Math.random()-0.5)*14, vy: (Math.random()-0.5)*14,
size: 4+Math.random()*7, life: 0.5+Math.random()*0.4,
color: ['#4488ff','#88bbff','#ffaa00','#fff','#8B0000'][Math.floor(Math.random()*5)],
gravity: 0.1,
});
}
spawnBlood(ex, ey, 8, 1.8);
}
if (miniboss2ExplodeTimer <= 0) {
miniboss2Active = false;
miniboss2Defeated = false;
miniboss2Lasers = [];
miniboss2Yells = [];
if (!bossActive && !minibossActive) {
spawnRate = Math.max(20, 80 - distance / 5);
}
}
}
function drawMiniboss2() {
if (!miniboss2Active) return;
const horizon = H * 0.3;
const mb2Offset = megaFightTriggered && bossActive ? W * 0.25 : 0;
const mb2CX = W/2 + miniboss2X + mb2Offset;
const mb2CY = horizon - 5;
const t = Date.now();
ctx.save();
if (miniboss2Defeated) {
ctx.globalAlpha = Math.max(0, miniboss2ExplodeTimer / 120);
ctx.translate((Math.random()-0.5)*8, (Math.random()-0.5)*8);
}
// Blue aura (simple circle)
ctx.fillStyle = 'rgba(0,50,150,0.12)';
ctx.beginPath(); ctx.arc(mb2CX, mb2CY, 110, 0, Math.PI*2); ctx.fill();
// Face
const mb2Size = 90 + Math.sin(t/450)*6;
if (miniboss2Loaded) {
if (miniboss2HitFlash > 0.5) ctx.filter = `brightness(${1+miniboss2HitFlash*2})`;
ctx.drawImage(miniboss2Img, mb2CX-mb2Size/2, mb2CY-mb2Size/2, mb2Size, mb2Size);
ctx.filter = 'none';
} else {
ctx.fillStyle = miniboss2HitFlash > 0.5 ? '#fff' : '#c0a080';
ctx.beginPath(); ctx.arc(mb2CX, mb2CY, mb2Size/2, 0, Math.PI*2); ctx.fill();
ctx.strokeStyle = '#333'; ctx.lineWidth = 2; ctx.stroke();
ctx.fillStyle = '#333'; ctx.font = 'bold 14px sans-serif'; ctx.textAlign = 'center';
ctx.fillText('MINI-BOSS 2', mb2CX, mb2CY+5);
}
// Blue glowing eyes (simple circles)
const eyeGlow = 0.4 + Math.sin(t/180)*0.2;
const leyX = mb2CX - mb2Size*0.15, leyY = mb2CY - mb2Size*0.01;
const reyX = mb2CX + mb2Size*0.13, reyY = mb2CY - mb2Size*0.01;
ctx.fillStyle = `rgba(50,100,255,${eyeGlow})`;
ctx.beginPath(); ctx.arc(leyX, leyY, 8, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(reyX, reyY, 8, 0, Math.PI*2); ctx.fill();
// HP Bar
if (!miniboss2Defeated) {
const barW = 150, barH = 10;
const barX = mb2CX-barW/2, barY = mb2CY-mb2Size/2-22;
ctx.fillStyle = 'rgba(0,0,0,0.7)';
ctx.fillRect(barX-2, barY-2, barW+4, barH+4);
const hpR = miniboss2HP / miniboss2MaxHP;
const hpG = ctx.createLinearGradient(barX, 0, barX+barW*hpR, 0);
hpG.addColorStop(0, '#2266ff'); hpG.addColorStop(1, '#44aaff');
ctx.fillStyle = hpG;
ctx.fillRect(barX, barY, barW*hpR, barH);
ctx.strokeStyle = '#fff'; ctx.lineWidth = 1;
ctx.strokeRect(barX, barY, barW, barH);
ctx.fillStyle = '#fff'; ctx.font = 'bold 9px sans-serif'; ctx.textAlign = 'center';
ctx.fillText(`${miniboss2HP} / ${miniboss2MaxHP}`, mb2CX, barY+barH-1);
ctx.fillStyle = '#44aaff'; ctx.font = 'bold 14px sans-serif';
ctx.fillText('Gregounet', mb2CX, barY-6);
}
// Lasers (BLUE) - optimized: no radial gradients
for (const las of miniboss2Lasers) {
const prog = Math.min(las.progress, 1);
const cx = las.startX + (las.targetX-las.startX)*prog;
const cy = las.startY + (las.targetY-las.startY)*prog;
ctx.globalAlpha = las.life;
ctx.strokeStyle = 'rgba(50,100,255,0.3)'; ctx.lineWidth = 10;
ctx.beginPath(); ctx.moveTo(las.startX, las.startY); ctx.lineTo(cx, cy); ctx.stroke();
ctx.strokeStyle = '#4488ff'; ctx.lineWidth = 4;
ctx.beginPath(); ctx.moveTo(las.startX, las.startY); ctx.lineTo(cx, cy); ctx.stroke();
ctx.strokeStyle = '#aaccff'; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(las.startX, las.startY); ctx.lineTo(cx, cy); ctx.stroke();
ctx.fillStyle = 'rgba(100,150,255,0.6)';
ctx.beginPath(); ctx.arc(cx, cy, 14, 0, Math.PI*2); ctx.fill();
}
ctx.globalAlpha = 1;
// Yells
for (const yell of miniboss2Yells) {
ctx.globalAlpha = yell.life;
ctx.fillStyle = '#4488ff'; ctx.strokeStyle = '#001144'; ctx.lineWidth = 3;
ctx.font = `bold ${15*yell.scale}px sans-serif`; ctx.textAlign = 'center';
ctx.strokeText(yell.text, yell.x, yell.y);
ctx.fillText(yell.text, yell.x, yell.y);
}
ctx.globalAlpha = 1;
ctx.restore();
}
// ========== BOSS SYSTEM ==========
function activateBoss() {
bossActive = true;
bossHP = bossMaxHP;
bossX = 0;
bossHitFlash = 0;
bossLasers = [];
bossLaserTimer = 0;
bossYellTimer = 0;
bossYells = [];
bossDefeated = false;
bossExplodeTimer = 0;
// Clear remaining enemies
enemies = [];
}
function updateBoss() {
if (!bossActive || bossDefeated) return;
const t = Date.now();
// Boss sways left/right
bossX = Math.sin(t / 1500) * W * 0.2;
if (bossHitFlash > 0) bossHitFlash -= 0.05;
// Boss fires lasers periodically
bossLaserTimer++;
if (bossLaserTimer >= 90) { // every ~1.5s
bossLaserTimer = 0;
fireBossLaser();
}
// Update lasers
for (let i = bossLasers.length - 1; i >= 0; i--) {
const las = bossLasers[i];
las.progress += 0.035;
las.life -= 0.015;
// Check if laser hits player (when progress is near 1 = reached player area)
if (las.progress > 0.85 && !las.hit) {
const dx = mouseX - las.targetX;
const dy = mouseY - las.targetY;
if (Math.sqrt(dx * dx + dy * dy) < 80) {
las.hit = true;
lives--;
combo = 0;
shakeTime = 15;
flashAlpha = 0.4;
document.getElementById('lives').textContent = '\u2764\ufe0f'.repeat(Math.max(0, lives));
if (lives <= 0) endGame();
}
}
if (las.life <= 0) bossLasers.splice(i, 1);
}
// Update yells
for (let i = bossYells.length - 1; i >= 0; i--) {
bossYells[i].life -= 0.012;
bossYells[i].y += bossYells[i].vy;
bossYells[i].scale += 0.01;
if (bossYells[i].life <= 0) bossYells.splice(i, 1);
}
// Check fireball hits on boss
const horizon = H * 0.3;
const bossCX = W / 2 + bossX;
const bossCY = horizon - 10;
for (let i = fireballs.length - 1; i >= 0; i--) {
const fb = fireballs[i];
if (fb.z > 800) {
const dx = fb.screenX - bossCX;
const dy = fb.screenY - bossCY;
if (Math.sqrt(dx * dx + dy * dy) < 100) {
// HIT BOSS
bossHP--;
bossHitFlash = 1;
shakeTime = 12;
flashAlpha = 0.2;
score += 100;
spawnHitText(bossCX, bossCY - 60, 100, 0);
// Fire explosion on boss
for (let j = 0; j < 10; j++) {
particles.push({
x: fb.screenX, y: fb.screenY,
vx: (Math.random() - 0.5) * 12,
vy: (Math.random() - 0.5) * 12,
size: 3 + Math.random() * 5,
life: 0.6 + Math.random() * 0.4,
color: ['#ff4400', '#ff8800', '#ffcc00'][Math.floor(Math.random() * 3)],
gravity: -0.05,
});
}
fireballs.splice(i, 1);
if (bossHP <= 0) {
defeatBoss();
}
break;
}
}
}
}
function fireBossLaser() {
if (bossLasers.length >= 10) return; // cap lasers
const horizon = H * 0.3;
const bossCX = W / 2 + bossX;
const bossCY = horizon - 10;
const tx = mouseX + (Math.random() - 0.5) * 100;
const ty = mouseY + (Math.random() - 0.5) * 60;
bossLasers.push({
startX: bossCX - 25,
startY: bossCY + 10,
targetX: tx,
targetY: ty,
progress: 0,
life: 1,
hit: false,
side: 'left',
});
bossLasers.push({
startX: bossCX + 25,
startY: bossCY + 10,
targetX: tx + 20,
targetY: ty,
progress: 0,
life: 1,
hit: false,
side: 'right',
});
// "T'ES VIRE!" yell
bossYellTimer++;
bossYells.push({
x: bossCX + (Math.random() - 0.5) * 100,
y: bossCY - 80 - Math.random() * 40,
vy: -1 - Math.random(),
life: 1,
scale: 1,
text: ["T'ES VIR\u00c9 !", "D\u00c9GAGE !", "TU ES FINI !", "VIR\u00c9 !!!", "DEHORS !"][Math.floor(Math.random() * 5)],
});
}
function defeatBoss() {
bossDefeated = true;
bossExplodeTimer = 180; // 3 seconds of explosions
score += 5000;
lives = 5;
// Massive blood + fire
const horizon = H * 0.3;
const bossCX = W / 2 + bossX;
spawnBlood(bossCX, horizon + 20, 80, 3);
// After boss defeated, restart cycle: miniboss1 in 150m
nextMinibossDistance = distance + 150;
nextBossDistance = Infinity; // wait for full cycle
}
function updateBossExplosion() {
if (!bossDefeated || bossExplodeTimer <= 0) return;
bossExplodeTimer--;
const horizon = H * 0.3;
const bossCX = W / 2 + bossX;
const bossCY = horizon - 10;
// Random explosions all over the boss area
if (bossExplodeTimer % 4 === 0) {
const ex = bossCX + (Math.random() - 0.5) * 200;
const ey = bossCY + (Math.random() - 0.5) * 150;
shakeTime = 5;
for (let j = 0; j < 8; j++) {
particles.push({
x: ex, y: ey,
vx: (Math.random() - 0.5) * 15,
vy: (Math.random() - 0.5) * 15,
size: 4 + Math.random() * 8,
life: 0.6 + Math.random() * 0.5,
color: ['#ff2200', '#ff6600', '#ffaa00', '#fff', '#8B0000'][Math.floor(Math.random() * 5)],
gravity: 0.1,
});
}
spawnBlood(ex, ey, 10, 2);
}
// Boss defeated, resume normal gameplay
if (bossExplodeTimer <= 0) {
bossActive = false;
bossDefeated = false;
bossLasers = [];
bossYells = [];
// Enemies start spawning again
spawnRate = Math.max(20, 80 - distance / 5);
}
}
function drawBoss() {
if (!bossActive) return;
const horizon = H * 0.3;
const bossCX = W / 2 + bossX;
const bossCY = horizon - 10;
const t = Date.now();
ctx.save();
if (bossDefeated) {
// Shake and fade during explosion
ctx.globalAlpha = Math.max(0, bossExplodeTimer / 180);
ctx.translate((Math.random() - 0.5) * 10, (Math.random() - 0.5) * 10);
}
// Boss dark aura (simple circle)
ctx.fillStyle = 'rgba(100,0,0,0.12)';
ctx.beginPath();
ctx.arc(bossCX, bossCY, 130, 0, Math.PI * 2);
ctx.fill();
// Draw boss face (PNG or fallback)
const bossSize = 110 + Math.sin(t / 500) * 8; // breathing, smaller = further back
if (bossLoaded) {
if (bossHitFlash > 0.5) {
// Flash white when hit
ctx.filter = `brightness(${1 + bossHitFlash * 2})`;
}
ctx.drawImage(bossImg, bossCX - bossSize / 2, bossCY - bossSize / 2, bossSize, bossSize);
ctx.filter = 'none';
} else {
// Fallback
ctx.fillStyle = bossHitFlash > 0.5 ? '#fff' : '#f0c090';
ctx.beginPath();
ctx.arc(bossCX, bossCY, bossSize / 2, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#333'; ctx.lineWidth = 3; ctx.stroke();
ctx.fillStyle = '#333';
ctx.font = 'bold 24px sans-serif'; ctx.textAlign = 'center';
ctx.fillText('BOSS', bossCX, bossCY + 8);
}
// Red glowing eyes (simple circles)
const eyeGlow = 0.4 + Math.sin(t / 200) * 0.2;
const leyX = bossCX - bossSize * 0.16;
const leyY = bossCY - bossSize * 0.02;
const reyX = bossCX + bossSize * 0.14;
const reyY = bossCY - bossSize * 0.02;
ctx.fillStyle = `rgba(255,0,0,${eyeGlow})`;
ctx.beginPath(); ctx.arc(leyX, leyY, 10, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(reyX, reyY, 10, 0, Math.PI*2); ctx.fill();
// HP Bar
if (!bossDefeated) {
const barW = 200;
const barH = 12;
const barX = bossCX - barW / 2;
const barY = bossCY - bossSize / 2 - 25;
// Background
ctx.fillStyle = 'rgba(0,0,0,0.7)';
ctx.fillRect(barX - 2, barY - 2, barW + 4, barH + 4);
// HP fill
const hpRatio = bossHP / bossMaxHP;
const hpGrad = ctx.createLinearGradient(barX, 0, barX + barW * hpRatio, 0);
hpGrad.addColorStop(0, '#ff0000');
hpGrad.addColorStop(1, hpRatio > 0.5 ? '#ff4400' : '#ff0000');
ctx.fillStyle = hpGrad;
ctx.fillRect(barX, barY, barW * hpRatio, barH);
// Border
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1;
ctx.strokeRect(barX, barY, barW, barH);
// Text
ctx.fillStyle = '#fff';
ctx.font = 'bold 10px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(`${bossHP} / ${bossMaxHP}`, bossCX, barY + barH - 2);
// Boss name
ctx.fillStyle = '#ff4444';
ctx.font = 'bold 18px sans-serif';
ctx.fillText('BOSS', bossCX, barY - 8);
}
// Draw lasers - optimized: no radial gradients, no particles in draw
for (const las of bossLasers) {
const prog = Math.min(las.progress, 1);
const currentX = las.startX + (las.targetX - las.startX) * prog;
const currentY = las.startY + (las.targetY - las.startY) * prog;
ctx.globalAlpha = las.life;
ctx.strokeStyle = 'rgba(255,0,0,0.3)';
ctx.lineWidth = 12;
ctx.beginPath();
ctx.moveTo(las.startX, las.startY);
ctx.lineTo(currentX, currentY);
ctx.stroke();
ctx.strokeStyle = '#ff0000';
ctx.lineWidth = 5;
ctx.beginPath();
ctx.moveTo(las.startX, las.startY);
ctx.lineTo(currentX, currentY);
ctx.stroke();
ctx.strokeStyle = '#ff8888';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(las.startX, las.startY);
ctx.lineTo(currentX, currentY);
ctx.stroke();
ctx.fillStyle = 'rgba(255,100,100,0.6)';
ctx.beginPath();
ctx.arc(currentX, currentY, 16, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
// Draw yells
for (const yell of bossYells) {
ctx.globalAlpha = yell.life;
ctx.fillStyle = '#ff0000';
ctx.strokeStyle = '#000';
ctx.lineWidth = 3;
ctx.font = `bold ${20 * yell.scale}px sans-serif`;
ctx.textAlign = 'center';
ctx.strokeText(yell.text, yell.x, yell.y);
ctx.fillText(yell.text, yell.x, yell.y);
}
ctx.globalAlpha = 1;
ctx.restore();
}
// ========== CITY BUILDINGS (pre-rendered to offscreen canvas) ==========
let trees = [];
let buildingsCanvas = null; // offscreen canvas for near buildings
let buildingsFarCanvas = null; // offscreen canvas for far buildings
const CITY_WIDTH = 2000;
function generateCity() {
trees = [];
// Generate trees
for (let i = 0; i < 20; i++) {
trees.push({
xRatio: Math.random(),
side: Math.random() > 0.5 ? 1 : -1,
size: 0.7 + Math.random() * 0.5,
trunkH: 15 + Math.random() * 10,
leafR: 12 + Math.random() * 8,
leafShade: Math.floor(Math.random() * 40),
});
}
// Pre-render buildings to offscreen canvases
const skylineH = 300;
// Near buildings
buildingsCanvas = document.createElement('canvas');
buildingsCanvas.width = CITY_WIDTH;
buildingsCanvas.height = skylineH;
const bctx = buildingsCanvas.getContext('2d');
// Far buildings
buildingsFarCanvas = document.createElement('canvas');
buildingsFarCanvas.width = CITY_WIDTH;
buildingsFarCanvas.height = skylineH;
const fctx = buildingsFarCanvas.getContext('2d');
let bx = 0;
while (bx < CITY_WIDTH) {
const bw = 40 + Math.random() * 80;
const bh = 80 + Math.random() * 200;
const floors = Math.floor(bh / 25);
const windowCols = Math.floor(bw / 18);
const hue = Math.floor(Math.random() * 40) + 200;
const light = 25 + Math.floor(Math.random() * 20);
const color = `hsl(${hue}, ${10 + Math.random()*15}%, ${light}%)`;
const colorDark = `hsl(${hue}, ${8 + Math.random()*10}%, ${light - 8}%)`;
const roofType = Math.floor(Math.random() * 3);
const buildY = skylineH - bh;
// --- Near layer ---
bctx.fillStyle = color;
bctx.fillRect(bx, buildY, bw, bh);
bctx.fillStyle = colorDark;
bctx.fillRect(bx + bw - 4, buildY, 4, bh);
// Windows
const winW = 5, winH = 7;
const padX = (bw - windowCols * (winW + 6)) / 2;
for (let row = 0; row < floors; row++) {
for (let col = 0; col < windowCols; col++) {
const wx = bx + padX + col * (winW + 6) + 4;
const wy = buildY + 8 + row * (winH + 10);
if (wy > skylineH - 5) continue;
if (Math.random() > 0.4) {
bctx.fillStyle = `rgba(255, 220, 100, ${0.5 + Math.random() * 0.3})`;
} else {
bctx.fillStyle = 'rgba(30, 40, 60, 0.8)';
}
bctx.fillRect(wx, wy, winW, winH);
}
}
// Roof
if (roofType === 1) {
bctx.strokeStyle = '#555'; bctx.lineWidth = 1.5;
bctx.beginPath();
bctx.moveTo(bx + bw / 2, buildY);
bctx.lineTo(bx + bw / 2, buildY - 20);
bctx.stroke();
bctx.fillStyle = '#ff0000';
bctx.beginPath(); bctx.arc(bx + bw / 2, buildY - 20, 2, 0, Math.PI * 2); bctx.fill();
} else if (roofType === 2) {
bctx.fillStyle = '#4a4a50';
bctx.fillRect(bx + bw * 0.3, buildY - 12, bw * 0.4, 12);
bctx.fillStyle = '#555';
bctx.fillRect(bx + bw * 0.25, buildY - 14, bw * 0.5, 3);
}
// --- Far layer (simpler silhouette) ---
fctx.fillStyle = '#1a1e28';
fctx.fillRect(bx, skylineH - bh * 0.4, bw, bh * 0.4);
bx += bw + 2 + Math.random() * 5;
}
}
generateCity();
// ========== PSEUDO 3D GROUND (City road - deep perspective) ==========
function drawGround() {
const horizon = H * 0.3; // higher horizon = more depth
const t = Date.now();
const DEPTH = 1200; // deeper z range
// ---- SKY (bigger, more gradient layers) ----
const skyGrad = ctx.createLinearGradient(0, 0, 0, horizon);
skyGrad.addColorStop(0, '#0e1a30');
skyGrad.addColorStop(0.3, '#1a2a44');
skyGrad.addColorStop(0.6, '#2d3e5a');
skyGrad.addColorStop(0.85, '#4a5a72');
skyGrad.addColorStop(1, '#6a7a92');
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, W, horizon);
// Clouds (simple, fast)
ctx.fillStyle = 'rgba(180,190,210,0.12)';
const cloudOff = (distance * 3) % (W * 2);
for (let i = 0; i < 6; i++) {
const cx = ((i * 300 - cloudOff) % (W + 400)) - 100;
const cy = 20 + i * 22 + Math.sin(i * 1.5) * 15;
ctx.beginPath();
ctx.ellipse(cx, cy, 80 + i * 10, 16, 0, 0, Math.PI * 2);
ctx.fill();
}
// ---- BUILDINGS SKYLINE (pre-rendered, just scroll & blit) ----
const skylineH = horizon * 1.2;
// Far layer (parallax slow)
if (buildingsFarCanvas) {
const farScroll = (distance * 6) % CITY_WIDTH;
ctx.globalAlpha = 0.5;
const farH = skylineH * 0.5;
ctx.drawImage(buildingsFarCanvas, farScroll, 0, CITY_WIDTH - farScroll, 300, 0, horizon - farH, (CITY_WIDTH - farScroll) / CITY_WIDTH * W, farH);
ctx.drawImage(buildingsFarCanvas, 0, 0, farScroll, 300, (CITY_WIDTH - farScroll) / CITY_WIDTH * W, horizon - farH, farScroll / CITY_WIDTH * W, farH);
ctx.globalAlpha = 1;
}
// Near layer (parallax faster)
if (buildingsCanvas) {
const nearScroll = (distance * 15) % CITY_WIDTH;
const nearH = skylineH;
ctx.drawImage(buildingsCanvas, nearScroll, 0, CITY_WIDTH - nearScroll, 300, 0, horizon - nearH, (CITY_WIDTH - nearScroll) / CITY_WIDTH * W, nearH);
ctx.drawImage(buildingsCanvas, 0, 0, nearScroll, 300, (CITY_WIDTH - nearScroll) / CITY_WIDTH * W, horizon - nearH, nearScroll / CITY_WIDTH * W, nearH);
}
// Atmospheric haze at horizon (simple)
ctx.fillStyle = 'rgba(75,85,105,0.3)';
ctx.fillRect(0, horizon - 15, W, 30);
// Horizon line
ctx.fillStyle = '#3a3a3a';
ctx.fillRect(0, horizon - 1, W, 3);
// ---- ROAD ----
const roadGrad = ctx.createLinearGradient(0, horizon, 0, H);
roadGrad.addColorStop(0, '#606060');
roadGrad.addColorStop(0.08, '#505050');
roadGrad.addColorStop(0.3, '#444444');
roadGrad.addColorStop(0.6, '#383838');
roadGrad.addColorStop(1, '#2a2a2a');
ctx.fillStyle = roadGrad;
ctx.fillRect(0, horizon, W, H - horizon);
const offset = (distance * 60) % 80;
// White edge lines (road borders)
for (let i = 0; i < 30; i++) {
const z = i * 28 + offset;
const pf = z / DEPTH;
if (pf > 1) continue;
const perspY = horizon + pf * (H - horizon);
const nextPY = horizon + ((z + 28) / DEPTH) * (H - horizon);
const rowH = Math.max(1, nextPY - perspY);
if (perspY < horizon || perspY > H) continue;
// Road edges converge toward vanishing point
const edgeDist = W * 0.48 * pf + W * 0.02;
const lineW = 1.5 + pf * 4;
ctx.fillStyle = `rgba(255,255,255,${0.08 + pf * 0.25})`;
ctx.fillRect(W / 2 - edgeDist - lineW / 2, perspY, lineW, rowH);
ctx.fillRect(W / 2 + edgeDist - lineW / 2, perspY, lineW, rowH);
}
// Center dashed yellow line
for (let i = 0; i < 25; i++) {
const z = (i * 45 + offset * 1.2) % (DEPTH + 100);
const pf = z / DEPTH;
if (pf > 1) continue;
const perspY = horizon + pf * (H - horizon);
if (perspY < horizon || perspY > H) continue;
const dashLen = 8 + pf * 18;
const dashW = 1.5 + pf * 4;
ctx.fillStyle = `rgba(255,220,80,${0.1 + pf * 0.35})`;
ctx.fillRect(W / 2 - dashW / 2, perspY, dashW, dashLen * 0.35);
}
// ---- TREES on road edges ----
for (const tree of trees) {
const z = ((tree.xRatio * DEPTH + offset * 0.6) % DEPTH);
const pf = z / DEPTH;
if (pf < 0.02 || pf > 0.95) continue;
const perspY = horizon + pf * (H - horizon);
const s = tree.size * (0.15 + pf * 1.4);
const edgeDist = W * 0.48 * pf + W * 0.02;
const tx = tree.side > 0 ? W / 2 + edgeDist + 18 * s : W / 2 - edgeDist - 18 * s;
// Depth fog on distant trees
const fogAlpha = pf < 0.15 ? pf / 0.15 : 1;
ctx.globalAlpha = fogAlpha;
ctx.fillStyle = '#5a3a1a';
ctx.fillRect(tx - 2 * s, perspY - tree.trunkH * s, 4 * s, tree.trunkH * s);
const lr = tree.leafR * s;
const lShade = tree.leafShade;
ctx.fillStyle = `rgb(${30 + lShade}, ${80 + lShade}, ${25 + lShade})`;
ctx.beginPath();
ctx.arc(tx, perspY - tree.trunkH * s - lr * 0.5, lr, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = `rgb(${40 + lShade}, ${95 + lShade}, ${30 + lShade})`;
ctx.beginPath();
ctx.arc(tx - lr * 0.4, perspY - tree.trunkH * s - lr * 0.3, lr * 0.7, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(tx + lr * 0.5, perspY - tree.trunkH * s - lr * 0.6, lr * 0.6, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
}
// ---- STREET LAMPS ----
for (let i = 0; i < 12; i++) {
const z = ((i * 100 + offset * 0.7) % DEPTH);
const pf = z / DEPTH;
if (pf < 0.03 || pf > 0.92) continue;
const perspY = horizon + pf * (H - horizon);
const s = 0.15 + pf * 1.2;
const edgeDist = W * 0.48 * pf + W * 0.02;
const side = i % 2 === 0 ? -1 : 1;
const lx = W / 2 + side * (edgeDist + 6 * s);
const fogAlpha = pf < 0.12 ? pf / 0.12 : 1;
ctx.globalAlpha = fogAlpha;
ctx.strokeStyle = '#666';
ctx.lineWidth = 2 * s;
ctx.beginPath();
ctx.moveTo(lx, perspY);
ctx.lineTo(lx, perspY - 35 * s);
ctx.stroke();
const dir = -side;
ctx.beginPath();
ctx.moveTo(lx, perspY - 35 * s);
ctx.quadraticCurveTo(lx + dir * 8 * s, perspY - 38 * s, lx + dir * 12 * s, perspY - 33 * s);
ctx.stroke();
const glowR = 15 * s;
const lg = ctx.createRadialGradient(lx + dir * 12 * s, perspY - 33 * s, 0, lx + dir * 12 * s, perspY - 33 * s, glowR);
lg.addColorStop(0, `rgba(255,230,150,${0.4 * fogAlpha})`);
lg.addColorStop(1, 'rgba(255,200,80,0)');
ctx.fillStyle = lg;
ctx.beginPath();
ctx.arc(lx + dir * 12 * s, perspY - 33 * s, glowR, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
}
// ---- Blood pools on ground ----
for (const pool of bloodPools) {
const pScale = 1 - pool.z / 30;
if (pScale <= 0) continue;
const py = horizon + (1 - pScale) * (H - horizon) * 0.3 + H * 0.4;
ctx.globalAlpha = pool.alpha * pScale;
ctx.fillStyle = '#5a0000';
ctx.beginPath();
ctx.ellipse(pool.x, py, pool.size * (1 / pScale) * 0.5, pool.size * 0.2, 0, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
}
// ========== ENEMIES ==========
function spawnEnemy() {
const type = Math.floor(Math.random() * 3);
const lane = (Math.random() - 0.5) * W * 0.7;
// Speed: some are slow (0.5x), normal (1x), or fast (1.8x)
const speedRoll = Math.random();
const enemySpeed = speedRoll < 0.25 ? 0.5 : speedRoll < 0.6 ? 1 : 1.8;
enemies.push({
x: W/2 + lane,
z: 1000,
type,
hp: 1,
hitAnim: 0,
dead: false,
yOff: (Math.random() - 0.5) * 30,
spd: enemySpeed,
});
}
function updateEnemies() {
const baseSpeed = 3 + speed * 1.5;
for (let i = enemies.length - 1; i >= 0; i--) {
const e = enemies[i];
e.z -= baseSpeed * (e.spd || 1);
if (e.dead) {
e.hitAnim += 0.05;
if (e.hitAnim > 1) {
enemies.splice(i, 1);
continue;
}
}
// Passed the player
if (e.z < -50 && !e.dead) {
enemies.splice(i, 1);
lives--;
combo = 0;
shakeTime = 10;
flashAlpha = 0.3;
document.getElementById('lives').textContent = '\u2764\ufe0f'.repeat(Math.max(0, lives));
if (lives <= 0) endGame();
}
}
// Spawn (cap at 15 enemies max)
spawnTimer++;
if (spawnTimer >= spawnRate && enemies.length < 15) {
spawnTimer = 0;
spawnEnemy();
if (Math.random() < 0.3 && enemies.length < 15) spawnEnemy();
}
}
function getEnemyScreen(e) {
const horizon = H * 0.3;
const perspScale = Math.max(0.05, 1 - e.z / 1000);
const screenX = W/2 + (e.x - W/2) * perspScale;
const screenY = horizon + (1 - perspScale) * 0.05 * H + perspScale * (H * 0.5) + e.yOff * perspScale;
const size = perspScale;
return { sx: screenX, sy: screenY, size };
}
function drawEnemies() {
// Sort by z (far first)
const sorted = [...enemies].sort((a, b) => b.z - a.z);
for (const e of sorted) {
const { sx, sy, size } = getEnemyScreen(e);
if (size < 0.05) continue;
drawOldPerson(sx, sy, size, e.type, e.dead ? e.hitAnim : 0);
// Warning indicator when close
if (e.z < 150 && !e.dead) {
ctx.fillStyle = `rgba(255,0,0,${(1 - e.z/150) * 0.5})`;
ctx.font = `${20 * size}px sans-serif`;
ctx.textAlign = 'center';
ctx.fillText('!', sx, sy - 40 * size);
}
}
}
// ========== PUNCH (fires a fireball) ==========
function doPunch() {
if (punchTimer > 0) return;
punching = true;
punchTimer = 12;
// Launch fireball(s) from the fist
spawnFireball();
if (doubleFireball) {
// Second fireball with slight offset
const savedX = mouseX, savedY = mouseY;
mouseX += 30; mouseY -= 15;
spawnFireball();
mouseX = savedX; mouseY = savedY;
}
// Muzzle flash particles at fist
for (let i = 0; i < 10; i++) {
particles.push({
x: mouseX + (Math.random() - 0.5) * 10,
y: mouseY + (Math.random() - 0.5) * 10,
vx: (Math.random() - 0.5) * 8,
vy: (Math.random() - 0.5) * 8,
size: 3 + Math.random() * 5,
life: 0.4 + Math.random() * 0.3,
color: ['#ff4400', '#ff8800', '#ffcc00', '#fff'][Math.floor(Math.random() * 4)],
gravity: -0.05,
});
}
}
// ========== HIT TEXT ==========
let hitTexts = [];
function spawnHitText(x, y, pts, cmb) {
let text = `+${pts}`;
let color = '#ff4444';
if (cmb >= 10) { text += ' MASSACRE!'; color = '#ff0000'; }
else if (cmb >= 7) { text += ' CARNAGE!'; color = '#ff2200'; }
else if (cmb >= 5) { text += ' BRUTAL!'; color = '#ff4400'; }
else if (cmb >= 3) { text += ' COMBO x' + cmb; color = '#ffd700'; }
hitTexts.push({ x, y, text, color, life: 1, vy: -2 });
if (hitTexts.length > 20) hitTexts.splice(0, hitTexts.length - 20);
}
function updateHitTexts() {
for (let i = hitTexts.length - 1; i >= 0; i--) {
const h = hitTexts[i];
h.y += h.vy;
h.life -= 0.02;
if (h.life <= 0) hitTexts.splice(i, 1);
}
}
function drawHitTexts() {
for (const h of hitTexts) {
ctx.globalAlpha = h.life;
ctx.fillStyle = h.color;
ctx.font = `bold ${22 + (1-h.life)*10}px sans-serif`;
ctx.textAlign = 'center';
ctx.fillText(h.text, h.x, h.y);
}
ctx.globalAlpha = 1;
}
// ========== MISC PARTICLES ==========
function updateParticles() {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.vy += p.gravity;
p.life -= 0.025;
if (p.life <= 0) particles.splice(i, 1);
}
// Cap particles to prevent lag
if (particles.length > 150) particles.splice(0, particles.length - 150);
}
function drawParticles() {
for (const p of particles) {
ctx.globalAlpha = p.life;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI*2);
ctx.fill();
}
ctx.globalAlpha = 1;
}
// ========== HUD ==========
function updateHUD() {
// Check for new super attack charges (every 250 pts)
const currentThreshold = Math.floor(score / 250);
if (currentThreshold > lastSuperThreshold) {
superAttackCharges = 1; // max 1, no stacking
lastSuperThreshold = currentThreshold;
}
document.getElementById('score').textContent = score;
document.getElementById('distance').textContent = Math.floor(distance) + 'm';
const comboEl = document.getElementById('combo');
if (combo >= 2) {
comboEl.textContent = `COMBO x${combo}`;
comboEl.style.fontSize = `${1.2 + combo * 0.05}rem`;
} else {
comboEl.textContent = '';
}
document.getElementById('lives').textContent = '\u2764\ufe0f'.repeat(Math.max(0, lives));
const superEl = document.getElementById('superHUD');
let superText = '';
if (doubleFireball) {
const secs = Math.ceil(doubleFireballTimer / 60);
superText += `\uD83D\uDD25x2 ${secs}s `;
}
if (superAttackCharges > 0) {
superText += '\uD83D\uDD25 [ESPACE]';
}
superEl.textContent = superText;
superEl.style.color = doubleFireball ? '#00ccff' : '#ffaa00';
}
// ========== SUPER ATTACK ==========
function doSuperAttack() {
if (superAttackCharges <= 0) return;
superAttackCharges--;
superAttackAnim = 60; // 1 second of animation
// Kill ALL enemies on screen
for (const e of enemies) {
if (e.dead) continue;
e.dead = true;
e.hitAnim = 0;
kills++;
score += 10;
const { sx, sy } = getEnemyScreen(e);
spawnBlood(sx, sy, 30, 2);
// Fire explosion on each enemy
for (let j = 0; j < 10; j++) {
particles.push({
x: sx, y: sy,
vx: (Math.random()-0.5)*15, vy: (Math.random()-0.5)*15,
size: 4+Math.random()*6, life: 0.7+Math.random()*0.4,
color: ['#ff4400','#ff8800','#ffcc00','#fff'][Math.floor(Math.random()*4)],
gravity: 0.05,
});
}
spawnHitText(sx, sy - 20, 10, 0);
}
// Damage boss -2 HP if active
if (bossActive && !bossDefeated) {
bossHP = Math.max(0, bossHP - 2);
bossHitFlash = 1;
shakeTime = 20;
const horizon = H * 0.3;
const bossCX = W/2 + bossX;
const bossCY = horizon - 10;
spawnHitText(bossCX, bossCY - 60, 200, 0);
for (let j = 0; j < 10; j++) {
particles.push({
x: bossCX + (Math.random()-0.5)*100,
y: bossCY + (Math.random()-0.5)*80,
vx: (Math.random()-0.5)*18, vy: (Math.random()-0.5)*18,
size: 5+Math.random()*8, life: 0.8+Math.random()*0.4,
color: ['#ff2200','#ff6600','#ffaa00','#fff'][Math.floor(Math.random()*4)],
gravity: 0.05,
});
}
if (bossHP <= 0) defeatBoss();
}
// Damage miniboss -2 HP if active
if (minibossActive && !minibossDefeated) {
minibossHP = Math.max(0, minibossHP - 2);
minibossHitFlash = 1;
shakeTime = 20;
const horizon = H * 0.3;
const mbOff1 = megaFightTriggered && bossActive ? -W * 0.25 : 0;
const mbCX = W/2 + minibossX + mbOff1;
const mbCY = horizon - 5;
spawnHitText(mbCX, mbCY - 50, 200, 0);
for (let j = 0; j < 10; j++) {
particles.push({
x: mbCX + (Math.random()-0.5)*80,
y: mbCY + (Math.random()-0.5)*60,
vx: (Math.random()-0.5)*16, vy: (Math.random()-0.5)*16,
size: 5+Math.random()*7, life: 0.8+Math.random()*0.4,
color: ['#00ff44','#88ff00','#ffaa00','#fff'][Math.floor(Math.random()*4)],
gravity: 0.05,
});
}
if (minibossHP <= 0) defeatMiniboss();
}
// Damage miniboss2 -2 HP if active
if (miniboss2Active && !miniboss2Defeated) {
miniboss2HP = Math.max(0, miniboss2HP - 2);
miniboss2HitFlash = 1;
shakeTime = 20;
const horizon = H * 0.3;
const mb2Off = megaFightTriggered && bossActive ? W * 0.25 : 0;
const mb2CX = W/2 + miniboss2X + mb2Off;
const mb2CY = horizon - 5;
spawnHitText(mb2CX, mb2CY - 50, 200, 0);
for (let j = 0; j < 10; j++) {
particles.push({
x: mb2CX + (Math.random()-0.5)*80,
y: mb2CY + (Math.random()-0.5)*60,
vx: (Math.random()-0.5)*16, vy: (Math.random()-0.5)*16,
size: 5+Math.random()*7, life: 0.8+Math.random()*0.4,
color: ['#4488ff','#88bbff','#ffaa00','#fff'][Math.floor(Math.random()*4)],
gravity: 0.05,
});
}
if (miniboss2HP <= 0) defeatMiniboss2();
}
// Big screen flash
flashAlpha = 0.6;
shakeTime = Math.max(shakeTime, 25);
}
// ========== GAME FLOW ==========
function startGame() {
document.getElementById('titleScreen').style.display = 'none';
document.getElementById('hud').style.display = 'flex';
resetGame();
gameRunning = true;
lastTime = 0;
accumulator = 0;
requestAnimationFrame(loop);
}
function resetGame() {
score = 0; kills = 0; combo = 0; maxCombo = 0; comboTimer = 0;
lives = 5; distance = 0; speed = 1; spawnTimer = 0; spawnRate = 80;
superAttackCharges = 0; lastSuperThreshold = 0; superAttackAnim = 0;
enemies = []; bloods = []; bloodPools = []; particles = []; hitTexts = []; fireballs = [];
powerups = []; powerupSpawnTimer = 0; doubleFireball = false; doubleFireballTimer = 0;
minibossActive = false; minibossHP = minibossMaxHP; minibossHitFlash = 0;
minibossLasers = []; minibossLaserTimer = 0; minibossYells = [];
minibossDefeated = false; minibossExplodeTimer = 0; nextMinibossDistance = 150;
bossActive = false; bossHP = bossMaxHP; bossHitFlash = 0; bossLasers = [];
bossLaserTimer = 0; bossYells = []; bossDefeated = false; bossExplodeTimer = 0;
nextBossDistance = Infinity; // boss waits for miniboss2 to be defeated
miniboss2Active = false; miniboss2HP = miniboss2MaxHP; miniboss2HitFlash = 0;
miniboss2Lasers = []; miniboss2LaserTimer = 0; miniboss2Yells = [];
miniboss2Defeated = false; miniboss2ExplodeTimer = 0; nextMiniboss2Distance = Infinity; // waits for miniboss1
megaFightTriggered = false; nextMegaFightDistance = 500;
shakeTime = 0; flashAlpha = 0; punching = false; punchTimer = 0;
updateHUD();
}
function endGame() {
gameRunning = false;
document.getElementById('finalScore').textContent = score;
document.getElementById('goStats').textContent =
`${kills} vieux \u00e9clat\u00e9s | Meilleur combo: x${maxCombo} | Distance: ${Math.floor(distance)}m`;
document.getElementById('gameOver').classList.add('show');
}
function restart() {
document.getElementById('gameOver').classList.remove('show');
document.getElementById('gameOver').querySelector('h2').textContent = 'K.O. TOTAL';
resetGame();
gameRunning = true;
lastTime = 0;
accumulator = 0;
requestAnimationFrame(loop);
}
// ========== MAIN LOOP (fixed 60fps logic) ==========
const TICK_RATE = 1000 / 60; // 16.67ms per logic tick
let lastTime = 0;
let accumulator = 0;
function tick() {
// All game logic runs at exactly 60fps
distance += 0.1 * speed;
speed = Math.min(8, 1 + distance / 200);
spawnRate = Math.max(25, 80 - distance / 5);
if (punchTimer > 0) punchTimer--;
if (punchTimer <= 0) punching = false;
if (comboTimer > 0) comboTimer--;
if (comboTimer <= 0 && combo > 0 && !punching) combo = 0;
// Mega fight: all 3 bosses at once
if (distance >= nextMegaFightDistance && !megaFightTriggered) {
megaFightTriggered = true;
enemies = [];
if (!minibossActive) { activateMiniboss(); }
if (!miniboss2Active) { activateMiniboss2(); }
if (!bossActive) { activateBoss(); }
}
// Normal solo triggers (only if no mega fight)
if (!megaFightTriggered) {
if (distance >= nextMinibossDistance && !minibossActive && !bossActive && !miniboss2Active) {
activateMiniboss();
}
if (distance >= nextMiniboss2Distance && !miniboss2Active && !bossActive && !minibossActive) {
activateMiniboss2();
}
if (distance >= nextBossDistance && !bossActive && !minibossActive && !miniboss2Active) {
activateBoss();
}
}
// Check mega fight over (all 3 defeated)
if (megaFightTriggered && !bossActive && !minibossActive && !miniboss2Active) {
megaFightTriggered = false;
nextMegaFightDistance = distance + MEGAFIGHT_INTERVAL;
spawnRate = Math.max(20, 80 - distance / 5);
}
// If any boss is active, stop spawning normal enemies
const anyBossUp = (bossActive && !bossDefeated) || (minibossActive && !minibossDefeated) || (miniboss2Active && !miniboss2Defeated);
if (anyBossUp) {
spawnRate = 99999;
speed = 0.5;
}
updateEnemies();
updateFireballs();
updatePowerups();
updateBloods();
updateParticles();
updateHitTexts();
updateMiniboss();
updateMinibossExplosion();
updateMiniboss2();
updateMiniboss2Explosion();
updateBoss();
updateBossExplosion();
if (shakeTime > 0) {
shakeX = (Math.random() - 0.5) * shakeTime * 1.5;
shakeY = (Math.random() - 0.5) * shakeTime * 1.5;
shakeTime--;
} else {
shakeX = 0; shakeY = 0;
}
if (superAttackAnim > 0) superAttackAnim--;
if (flashAlpha > 0) flashAlpha -= 0.02;
}
function draw() {
ctx.save();
ctx.translate(shakeX, shakeY);
drawGround();
drawMiniboss();
drawMiniboss2();
drawBoss();
drawPowerups();
drawEnemies();
drawFireballs();
drawBloods();
drawParticles();
drawHitTexts();
// Draw Gecko (player)
drawGecko(mouseX, mouseY, 1.3, punching || punchTimer > 8);
// Super attack animation (expanding fire ring - simple)
if (superAttackAnim > 0) {
const progress = 1 - superAttackAnim / 60;
const radius = progress * Math.max(W, H);
ctx.globalAlpha = (1 - progress) * 0.5;
ctx.strokeStyle = '#ff6600';
ctx.lineWidth = 20 * (1 - progress);
ctx.beginPath();
ctx.arc(mouseX, mouseY, radius, 0, Math.PI * 2);
ctx.stroke();
ctx.strokeStyle = '#ffcc00';
ctx.lineWidth = 6 * (1 - progress);
ctx.beginPath();
ctx.arc(mouseX, mouseY, radius * 0.85, 0, Math.PI * 2);
ctx.stroke();
ctx.globalAlpha = 1;
if (superAttackAnim > 30) {
ctx.fillStyle = '#ffd700'; ctx.strokeStyle = '#000'; ctx.lineWidth = 4;
ctx.font = `bold ${40 + progress * 20}px sans-serif`;
ctx.textAlign = 'center';
ctx.globalAlpha = 1 - progress;
ctx.strokeText('SUPER ATTACK !', W/2, H/2 - 50);
ctx.fillText('SUPER ATTACK !', W/2, H/2 - 50);
ctx.globalAlpha = 1;
}
}
// Red flash overlay
if (flashAlpha > 0) {
ctx.fillStyle = `rgba(180, 0, 0, ${flashAlpha})`;
ctx.fillRect(-20, -20, W + 40, H + 40);
}
ctx.restore();
// Vignette (pre-rendered)
if (vignetteCanvas) {
ctx.drawImage(vignetteCanvas, 0, 0, W, H);
}
// Crosshair
ctx.strokeStyle = `rgba(255, 50, 50, ${punching ? 0.8 : 0.3})`;
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.arc(mouseX, mouseY, 20, 0, Math.PI*2);
ctx.stroke();
updateHUD();
}
function loop(timestamp) {
if (!gameRunning) return;
if (lastTime === 0) lastTime = timestamp;
let delta = timestamp - lastTime;
lastTime = timestamp;
// Clamp delta to avoid spiral of death (e.g. tab was hidden)
if (delta > 200) delta = 200;
accumulator += delta;
// Run logic ticks at fixed 60fps
while (accumulator >= TICK_RATE) {
tick();
accumulator -= TICK_RATE;
}
// Render every frame
draw();
requestAnimationFrame(loop);
}
// ========== INPUT ==========
addEventListener('mousemove', e => {
mouseX = e.clientX;
mouseY = e.clientY;
});
addEventListener('mousedown', e => {
if (gameRunning) doPunch();
});
addEventListener('keydown', e => {
if (e.code === 'Space' && gameRunning) {
e.preventDefault();
doSuperAttack();
}
});
addEventListener('touchmove', e => {
e.preventDefault();
mouseX = e.touches[0].clientX;
mouseY = e.touches[0].clientY;
}, { passive: false });
addEventListener('touchstart', e => {
e.preventDefault();
mouseX = e.touches[0].clientX;
mouseY = e.touches[0].clientY;
if (gameRunning) doPunch();
}, { passive: false });
// Prevent context menu
addEventListener('contextmenu', e => e.preventDefault());
</script>
</body>
</html>