Files
Site-Web/hkm/jeu1.html
T
2026-05-16 11:10:19 +02:00

426 lines
13 KiB
HTML
Executable File

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Neon Blitz ++ (Canvas)</title>
<style>
html, body { margin:0; height:100%; background:#000; color:#fff; font-family: Courier, monospace; }
#wrap { display:flex; flex-direction:column; align-items:center; gap:8px; padding:12px; }
canvas { background:#000; border:1px solid #0ff; box-shadow: 0 0 12px #0ff; }
.hud { text-align:center; color:#fff; }
.hint { color:#aaa; font-size:12px; }
</style>
</head>
<body>
<div id="wrap">
<canvas id="game" width="960" height="720"></canvas>
<div class="hud" id="hud"></div>
<div class="hint">Flèches pour bouger • Espace: tir • L: laser • R: restart</div>
</div>
<script>
(() => {
// ----- Canvas & timing -----
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const hudEl = document.getElementById('hud');
const W = canvas.width, H = canvas.height;
let last = performance.now();
let accumulator = 0;
const FPS = 60;
const DT = 1 / FPS;
// ----- Audio (WebAudio simple beeps) -----
let audioCtx = null;
function ensureAudio() {
if (!audioCtx) {
try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } catch(e){}
}
}
function beep(freq=600, ms=80, type='square', vol=0.08) {
if (!audioCtx) return;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = type;
osc.frequency.value = freq;
gain.gain.value = vol;
osc.connect(gain);
gain.connect(audioCtx.destination);
const t = audioCtx.currentTime;
osc.start(t);
osc.stop(t + ms/1000);
}
// ----- State -----
const state = {
running: true,
score: 0,
lives: 3,
kills: 0,
startTime: performance.now(),
highscore: Number(localStorage.getItem('neonblitz_highscore')) || 0,
bossAlive: false,
gameOver: false,
mission: { survive: 60, kills: 30 },
shake: 0,
};
// ----- Player -----
const player = {
x: W/2, y: H - 80,
w: 22, h: 28,
speed: 360, // px/s
color: '#00FFFF'
};
// ----- Arrays -----
const bullets = []; // {x,y,vx,vy,kind,life}
const enemies = []; // {x,y,r,hp,speed,color}
let boss = null; // {x,y,w,h,hp,speed,dir}
// ----- Input -----
const keys = { ArrowLeft:false, ArrowRight:false, ArrowUp:false, ArrowDown:false };
let spaceHeld = false;
let lHeld = false;
let fireCd = 0; // seconds
const FIRE_COOLDOWN = 0.18;
const FIRE_COOLDOWN_LASER = 0.28;
window.addEventListener('keydown', (e) => {
if (!audioCtx) ensureAudio();
if (e.code in keys) keys[e.code] = true;
if (e.code === 'Space') spaceHeld = true;
if (e.code === 'KeyL') lHeld = true;
if (e.code === 'KeyR') restart();
});
window.addEventListener('keyup', (e) => {
if (e.code in keys) keys[e.code] = false;
if (e.code === 'Space') spaceHeld = false;
if (e.code === 'KeyL') lHeld = false;
});
// ----- Utils -----
function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
function rand(min, max) { return Math.random()*(max-min)+min; }
function choice(arr) { return arr[(Math.random()*arr.length)|0]; }
function dist(ax, ay, bx, by) { return Math.hypot(ax-bx, ay-by); }
function circleRectColl(cx, cy, r, rx, ry, rw, rh) {
const nx = clamp(cx, rx, rx+rw);
const ny = clamp(cy, ry, ry+rh);
const dx = cx - nx, dy = cy - ny;
return (dx*dx + dy*dy) <= r*r;
}
// ----- Spawns -----
function spawnEnemy() {
const colors = ['#FF00FF', '#39FF14', '#FFD700', '#FF1493'];
const e = {
x: rand(40, W-40),
y: -40,
r: rand(12, 20),
hp: choice([1,1,1,2]),
speed: 120 + Math.min(220, state.score*0.6),
color: choice(colors)
};
enemies.push(e);
}
function spawnBoss() {
boss = {
x: W/2 - 80,
y: -120,
w: 160, h: 100,
hp: 30,
speed: 120,
dir: 1
};
state.bossAlive = true;
}
// ----- Shooting -----
function fire(kind='normal') {
if (fireCd > 0 || state.gameOver) return;
const isLaser = (kind === 'laser');
const b = {
x: player.x, y: player.y - player.h/2,
vx: 0,
vy: isLaser ? -480 : -760,
kind,
life: 1.5
};
bullets.push(b);
fireCd = isLaser ? FIRE_COOLDOWN_LASER : FIRE_COOLDOWN;
beep(isLaser ? 900 : 700, isLaser ? 65 : 45, 'square', 0.06);
state.shake = Math.min(10, state.shake + (isLaser ? 1.2 : 0.8));
}
// ----- Restart -----
function restart() {
enemies.length = 0;
bullets.length = 0;
boss = null;
Object.assign(state, {
running: true,
score: 0,
lives: 3,
kills: 0,
startTime: performance.now(),
bossAlive: false,
gameOver: false,
mission: state.mission, // keep current thresholds
shake: 0
});
player.x = W/2; player.y = H - 80;
}
// ----- Update -----
let enemySpawnTimer = 0;
const ENEMY_SPAWN_INTERVAL = 0.55; // seconds
function update(dt) {
if (!state.running) return;
// cooldowns
fireCd = Math.max(0, fireCd - dt);
// inputs: movement
const ax = (keys.ArrowRight ? 1 : 0) - (keys.ArrowLeft ? 1 : 0);
const ay = (keys.ArrowDown ? 1 : 0) - (keys.ArrowUp ? 1 : 0);
player.x = clamp(player.x + ax * player.speed * dt, 24, W-24);
player.y = clamp(player.y + ay * player.speed * dt, 40, H-40);
// inputs: shooting
if (spaceHeld) fire('normal');
if (lHeld) fire('laser');
// spawn enemies
enemySpawnTimer -= dt;
if (enemySpawnTimer <= 0 && !state.gameOver) {
spawnEnemy();
enemySpawnTimer = ENEMY_SPAWN_INTERVAL * rand(0.6, 1.4);
}
// boss trigger
if (state.score >= 200 && !state.bossAlive) spawnBoss();
// bullets update
for (let i = bullets.length-1; i >= 0; i--) {
const b = bullets[i];
b.x += b.vx * dt;
b.y += b.vy * dt;
b.life -= dt;
if (b.y < -40 || b.life <= 0) bullets.splice(i,1);
}
// enemies update & collisions
for (let i = enemies.length-1; i >= 0; i--) {
const e = enemies[i];
e.y += e.speed * dt;
// collide with player
if (circleRectColl(e.x, e.y, e.r, player.x - player.w/2, player.y - player.h/2, player.w, player.h)) {
enemies.splice(i,1);
if (!state.gameOver) {
state.lives -= 1;
state.shake = Math.min(18, state.shake + 4.0);
beep(280, 180, 'sawtooth', 0.07);
if (state.lives <= 0) {
state.gameOver = true;
if (state.score > state.highscore) {
state.highscore = state.score;
localStorage.setItem('neonblitz_highscore', String(state.highscore));
}
}
}
continue;
}
// collide with bullets
let hit = false;
for (let j = bullets.length-1; j >= 0; j--) {
const b = bullets[j];
if (dist(e.x, e.y, b.x, b.y) <= e.r + (b.kind === 'laser' ? 14 : 10)) {
// laser traverse: keep moving but consume life faster
e.hp -= (b.kind === 'laser' ? 2 : 1);
bullets.splice(j,1);
state.score += (b.kind === 'laser' ? 6 : 4);
state.kills += (e.hp <= 0 ? 1 : 0);
state.shake = Math.min(14, state.shake + 2.0);
beep(500, 80, 'triangle', 0.06);
if (e.hp <= 0) {
enemies.splice(i,1);
state.score += 10;
hit = true;
break;
}
}
}
if (hit) continue;
// off-screen enemy
if (e.y > H + 60) {
enemies.splice(i,1);
if (!state.gameOver) {
state.lives -= 1;
if (state.lives <= 0) {
state.gameOver = true;
if (state.score > state.highscore) {
state.highscore = state.score;
localStorage.setItem('neonblitz_highscore', String(state.highscore));
}
}
}
}
}
// boss update & collisions
if (boss && state.bossAlive && !state.gameOver) {
// move boss down initially then horizontal pacing
if (boss.y < 120) boss.y += 60 * dt;
else {
boss.x += boss.dir * boss.speed * dt;
if (boss.x < 40) { boss.x = 40; boss.dir = 1; }
if (boss.x + boss.w > W-40) { boss.x = W-40 - boss.w; boss.dir = -1; }
}
// bullets vs boss
for (let j = bullets.length-1; j >= 0; j--) {
const b = bullets[j];
if (circleRectColl(b.x, b.y, (b.kind==='laser'?12:8), boss.x, boss.y, boss.w, boss.h)) {
boss.hp -= (b.kind === 'laser' ? 2 : 1);
bullets.splice(j,1);
state.score += (b.kind === 'laser' ? 10 : 6);
state.shake = Math.min(16, state.shake + 3.0);
beep(420, 120, 'triangle', 0.07);
if (boss.hp <= 0) {
state.score += 200;
state.bossAlive = false;
boss = null;
beep(240, 220, 'sawtooth', 0.08);
}
}
}
// boss collide player
if (circleRectColl(player.x, player.y, 18, boss.x, boss.y, boss.w, boss.h)) {
state.lives -= 1;
state.shake = Math.min(20, state.shake + 6.0);
beep(260, 220, 'square', 0.08);
if (state.lives <= 0) {
state.gameOver = true;
if (state.score > state.highscore) {
state.highscore = state.score;
localStorage.setItem('neonblitz_highscore', String(state.highscore));
}
}
}
}
// missions
const elapsed = (performance.now() - state.startTime) / 1000;
if (!state.gameOver && elapsed >= state.mission.survive && state.kills >= state.mission.kills) {
state.score += 500;
state.mission.survive += 60;
state.mission.kills += 30;
beep(1000, 200, 'square', 0.08);
}
// screen shake
state.shake *= 0.88;
if (state.shake < 0.15) state.shake = 0;
}
// ----- Draw -----
function draw() {
const dx = state.shake ? rand(-state.shake*0.3, state.shake*0.3) : 0;
const dy = state.shake ? rand(-state.shake*0.3, state.shake*0.3) : 0;
ctx.save();
ctx.translate(dx, dy);
// background grid neon
ctx.fillStyle = '#000';
ctx.fillRect(0,0,W,H);
ctx.strokeStyle = 'rgba(0,255,255,0.12)';
ctx.lineWidth = 1;
for (let x=0; x<=W; x+=48) { ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,H); ctx.stroke(); }
for (let y=0; y<=H; y+=48) { ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(W,y); ctx.stroke(); }
// player
ctx.fillStyle = player.color;
ctx.beginPath();
// triangle pointing up
ctx.moveTo(player.x, player.y - player.h/2);
ctx.lineTo(player.x - player.w/2, player.y + player.h/2);
ctx.lineTo(player.x + player.w/2, player.y + player.h/2);
ctx.closePath();
ctx.fill();
// bullets
for (const b of bullets) {
ctx.fillStyle = (b.kind === 'laser') ? '#FFD700' : '#FF00FF';
if (b.kind === 'laser') {
ctx.fillRect(b.x - 4, b.y - 14, 8, 28);
} else {
ctx.beginPath();
ctx.arc(b.x, b.y, 5, 0, Math.PI*2);
ctx.fill();
}
}
// enemies
for (const e of enemies) {
ctx.fillStyle = e.color;
ctx.beginPath();
ctx.arc(e.x, e.y, e.r, 0, Math.PI*2);
ctx.fill();
}
// boss
if (boss && state.bossAlive) {
ctx.fillStyle = '#FF4500';
ctx.fillRect(boss.x, boss.y, boss.w, boss.h);
// boss hp bar
ctx.fillStyle = '#222';
ctx.fillRect(40, 40, W-80, 16);
const ratio = Math.max(0, boss.hp/30);
ctx.fillStyle = '#FF4500';
ctx.fillRect(40, 40, (W-80)*ratio, 16);
ctx.strokeStyle = '#fff';
ctx.strokeRect(40, 40, W-80, 16);
}
ctx.restore();
// HUD
const elapsed = ((performance.now()-state.startTime)/1000)|0;
hudEl.innerHTML =
`Score: ${state.score} &nbsp;&nbsp; ` +
`Vies: ${state.lives} &nbsp;&nbsp; ` +
`Kills: ${state.kills} &nbsp;&nbsp; ` +
`Highscore: ${state.highscore} &nbsp;&nbsp; ` +
`Mission: survivre ${state.mission.survive}s (${elapsed}s) / détruire ${state.mission.kills} (${state.kills})` +
(state.gameOver ? `<br/><span style="color:#FF1493;font-weight:bold">GAME OVER — R pour recommencer</span>` : '');
}
// ----- Main loop -----
function frame(t) {
const dtMs = (t - last) / 1000;
last = t;
accumulator += dtMs;
// fixe-step update
while (accumulator >= DT) {
update(DT);
accumulator -= DT;
}
draw();
requestAnimationFrame(frame);
}
// ----- Start -----
restart();
requestAnimationFrame(frame);
})();
</script>
</body>
</html>