first commit

This commit is contained in:
nix
2026-05-16 11:10:19 +02:00
commit 509c9b3737
172 changed files with 14496 additions and 0 deletions
Executable
+1
View File
@@ -0,0 +1 @@
Require all granted
Executable
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Executable
+220
View File
@@ -0,0 +1,220 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Hakim le hackeur de site</title>
<style>
/* THEME: cyberpunk violet néon (aléatoire choisi) */
:root{
--bg1: #0b0216;
--bg2: #12012b;
--neon1: #8a2be2; /* violet */
--neon2: #00e5ff; /* cyan */
--accent: #ff4d6d; /* rose */
--glass: rgba(255,255,255,0.04);
--text: #e9e6ff;
--muted: #bfc3ff88;
--radius: 14px;
--mono: "Courier New", monospace;
--sans: "Helvetica Neue", Arial, sans-serif;
}
*{box-sizing:border-box}
html,body{
height:100%;
margin:0;
font-family: var(--sans);
background: radial-gradient(1200px 600px at 10% 10%, rgba(138,43,226,0.07), transparent),
linear-gradient(180deg, var(--bg1) 0%, var(--bg2) 100%);
color:var(--text);
-webkit-font-smoothing:antialiased;
-moz-osx-font-smoothing:grayscale;
}
.container{
min-height:100vh;
display:flex;
align-items:center;
justify-content:center;
padding:32px;
}
.card{
width:100%;
max-width:980px;
background: linear-gradient(135deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));
border: 1px solid rgba(138,43,226,0.12);
border-radius: var(--radius);
padding:36px;
box-shadow: 0 10px 40px rgba(0,0,0,0.6), 0 0 40px rgba(138,43,226,0.06) inset;
position:relative;
overflow:hidden;
}
/* neon glow lines */
.glow-top, .glow-bottom{
position:absolute;
left:-20%;
width:140%;
height:220px;
pointer-events:none;
mix-blend-mode:screen;
filter: blur(28px);
opacity:0.9;
}
.glow-top{
top:-60px;
background: linear-gradient(90deg, rgba(138,43,226,0.18), rgba(0,229,255,0.12));
transform: rotate(-6deg);
}
.glow-bottom{
bottom:-90px;
background: linear-gradient(90deg, rgba(255,77,109,0.06), rgba(0,229,255,0.05));
transform: rotate(6deg);
}
header {
display:flex;
gap:18px;
align-items:center;
margin-bottom:18px;
}
.badge{
padding:8px 12px;
background: linear-gradient(90deg, rgba(138,43,226,0.14), rgba(0,229,255,0.08));
border-radius:999px;
font-family:var(--mono);
font-weight:700;
letter-spacing:0.08em;
color:var(--neon2);
border: 1px solid rgba(138,43,226,0.14);
box-shadow: 0 6px 30px rgba(138,43,226,0.06);
font-size:0.86rem;
}
h1 {
margin:0;
font-size: clamp(26px, 4.5vw, 48px);
line-height:1.02;
letter-spacing: -0.02em;
color:var(--text);
text-shadow:
0 2px 10px rgba(0,0,0,0.6),
0 0 18px rgba(138,43,226,0.32),
0 0 28px rgba(0,229,255,0.12);
font-weight:800;
}
.subtitle{
margin-top:10px;
font-size: clamp(14px, 2.4vw, 18px);
color:var(--muted);
}
.content{
margin-top:22px;
display:grid;
grid-template-columns: 1fr 360px;
gap:22px;
}
.main{
background: linear-gradient(180deg, rgba(255,255,255,0.01), rgba(255,255,255,0.00));
border-radius:12px;
padding:18px;
border:1px solid rgba(255,255,255,0.02);
}
.side{
background: linear-gradient(180deg, rgba(255,255,255,0.015), rgba(255,255,255,0.00));
border-radius:12px;
padding:14px;
border:1px solid rgba(0,0,0,0.12);
color:var(--muted);
font-size:0.95rem;
}
/* The big "hack" phrase styling (explicit text requested) */
.danger{
display:inline-block;
margin:14px 0;
padding:12px 18px;
border-radius:10px;
background: linear-gradient(90deg, rgba(138,43,226,0.06), rgba(255,77,109,0.03));
border: 1px solid rgba(255,77,109,0.12);
color: var(--accent);
font-weight:900;
letter-spacing:0.02em;
font-size: clamp(18px, 3.6vw, 32px);
text-shadow: 0 2px 14px rgba(255,77,109,0.08);
font-family: var(--mono);
}
p { margin:0 0 12px 0; line-height:1.5; }
/* small footer */
.footer{
margin-top:18px;
display:flex;
justify-content:space-between;
align-items:center;
color:var(--muted);
font-size:0.9rem;
}
/* responsive */
@media (max-width:880px){
.content{ grid-template-columns: 1fr; }
.side{ order:2 }
}
/* subtle hover pulse */
.card:hover{ transform: translateY(-4px); transition: transform 240ms ease; }
</style>
</head>
<body>
<div class="container">
<div class="card" role="main">
<div class="glow-top" aria-hidden="true"></div>
<div class="glow-bottom" aria-hidden="true"></div>
<header>
<div class="badge">CYBER THEME</div>
<h1>Page personnelle — démonstration visuelle</h1>
</header>
<!-- Phrase demandée, mise en évidence -->
<div class="main">
<div class="danger">Hakim le hackeur de site, je vais vous hacker.</div>
<!-- 2 ou 3 lignes après : description des chats -->
<div style="margin-top:14px;">
<p><strong>Les chats</strong> sont des mammifères domestiques souples et indépendants, connus pour leur agilité, leur toilette méticuleuse et leur capacité à chasser de petits rongeurs. Leur comportement mêle moments de grande sociabilité (frottements, ronronnements) et phases d'exploration solitaire ; chaque chat a un caractère unique, allant du joueur exubérant au calme contemplatif.</p>
<p>Ils communiquent par le langage corporel (queue, oreilles, pupilles), par des vocalises (miaulements, ronronnements) et par l'usage d'odeurs. Les chats apportent compagnie, réduction du stress et parfois une présence apaisante dans un foyer.</p>
</div>
<!-- Pourquoi manger sainement -->
<div style="margin-top:14px;">
<h3 style="margin-bottom:8px;">Pourquoi il faut manger sainement</h3>
<p>Manger sainement permet d'assurer un apport équilibré en nutriments essentiels (protéines, glucides complexes, lipides sains, vitamines, minéraux) pour maintenir l'énergie, soutenir le système immunitaire et favoriser la longévité. Une alimentation variée et riche en fruits, légumes, fibres et protéines de qualité aide aussi à prévenir des maladies chroniques (maladies cardiovasculaires, diabète de type 2), améliore l'humeur et la concentration, et facilite la récupération après l'effort.</p>
<p>En résumé : bien manger, c'est investir dans son bien-être quotidien — physique ET mental.</p>
</div>
</div>
<aside class="side" aria-labelledby="infoTitle">
<h4 id="infoTitle" style="margin-top:0;color:var(--neon2)">Détails visuels & astuces</h4>
<p>Thème choisi : <strong>Cyberpunk — violet / néon</strong>. Tu peux changer les variables CSS en haut (:root) pour modifier instantanément les couleurs.</p>
<p>Astuce : pour rendre le texte encore plus dramatique, remplace la classe <code>.danger</code> par un élément <code>&lt;marquee&gt;</code> (non recommandé) ou ajoute une animation de clignotement via @keyframes.</p>
</aside>
<div class="footer">
<div>Créé automatiquement — thème aléatoire</div>
<div style="opacity:0.8;font-family:var(--mono);font-size:0.85rem">v1 · cyberpunk</div>
</div>
</div>
</div>
</body>
</html>
Executable
+425
View File
@@ -0,0 +1,425 @@
<!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>
Executable
+160
View File
@@ -0,0 +1,160 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<title>Labyrinthe avec niveaux</title>
<style>
body { margin:0; 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:2px solid #0ff; box-shadow:0 0 12px #0ff; }
.hud { text-align:center; }
.hint { color:#aaa; font-size:12px; }
</style>
</head>
<body>
<div id="wrap">
<canvas id="game" width="960" height="720"></canvas>
<div id="hud" class="hud"></div>
<div class="hint">Flèches pour bouger • R: restart</div>
</div>
<script>
(() => {
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// ----- State -----
let COLS, ROWS, CELL, OFFSET_X, OFFSET_Y;
const grid = [];
const player = { c:0, r:0, steps:0 };
const exit = { c:0, r:0 };
const state = { level:1, win:false, startTime:0, best:0 };
// ----- Génération du labyrinthe -----
function index(c,r){ return r*COLS+c; }
function neighbors(c,r){
const n=[];
if(r>0) n.push([c,r-1,0]);
if(c<COLS-1) n.push([c+1,r,1]);
if(r<ROWS-1) n.push([c,r+1,2]);
if(c>0) n.push([c-1,r,3]);
return n;
}
function carveMaze(startC=0,startR=0){
grid.length=0;
for(let r=0;r<ROWS;r++){
for(let c=0;c<COLS;c++){
grid.push({c,r,visited:false,walls:[true,true,true,true]});
}
}
const stack=[];
let current=grid[index(startC,startR)];
current.visited=true; stack.push(current);
while(stack.length){
const cell=stack[stack.length-1];
const cand=neighbors(cell.c,cell.r)
.map(([nc,nr,dir])=>({nc,nr,dir,cell:grid[index(nc,nr)]}))
.filter(o=>!o.cell.visited);
if(cand.length===0){ stack.pop(); }
else{
const pick=cand[(Math.random()*cand.length)|0];
cell.walls[pick.dir]=false;
pick.cell.walls[(pick.dir+2)%4]=false;
pick.cell.visited=true;
stack.push(pick.cell);
}
}
}
// ----- Reset niveau -----
function reset(level=1){
state.level=level;
COLS=20+level*2; ROWS=15+level*2;
CELL=Math.floor(Math.min(W/COLS,H/ROWS));
OFFSET_X=Math.floor((W-COLS*CELL)/2);
OFFSET_Y=Math.floor((H-ROWS*CELL)/2);
carveMaze(0,0);
player.c=0; player.r=0; player.steps=0;
exit.c=COLS-1; exit.r=ROWS-1;
state.startTime=performance.now();
state.win=false;
}
// ----- Input -----
const keys={ArrowUp:false,ArrowRight:false,ArrowDown:false,ArrowLeft:false};
let cooldown=0;
window.addEventListener('keydown',e=>{
if(e.code in keys) keys[e.code]=true;
if(e.code==='KeyR') reset(1);
});
window.addEventListener('keyup',e=>{
if(e.code in keys) keys[e.code]=false;
});
function tryMove(dc,dr,wallIndex){
if(state.win) return;
const cell=grid[index(player.c,player.r)];
if(!cell.walls[wallIndex]){
player.c+=dc; player.r+=dr; player.steps++;
if(player.c===exit.c && player.r===exit.r){
state.win=true;
const elapsed=((performance.now()-state.startTime)/1000)|0;
if(state.level>state.best) state.best=state.level;
setTimeout(()=>reset(state.level+1),1500); // passe au niveau suivant
}
}
}
// ----- Update -----
function update(dt){
cooldown-=dt*1000;
if(cooldown<=0){
if(keys.ArrowUp){ tryMove(0,-1,0); cooldown=100; }
else if(keys.ArrowRight){ tryMove(1,0,1); cooldown=100; }
else if(keys.ArrowDown){ tryMove(0,1,2); cooldown=100; }
else if(keys.ArrowLeft){ tryMove(-1,0,3); cooldown=100; }
}
}
// ----- Draw -----
function draw(){
ctx.fillStyle='#000'; ctx.fillRect(0,0,W,H);
ctx.strokeStyle='#0ff'; ctx.lineWidth=2;
for(const cell of grid){
const x=OFFSET_X+cell.c*CELL, y=OFFSET_Y+cell.r*CELL;
if(cell.walls[0]){ ctx.beginPath(); ctx.moveTo(x,y); ctx.lineTo(x+CELL,y); ctx.stroke(); }
if(cell.walls[1]){ ctx.beginPath(); ctx.moveTo(x+CELL,y); ctx.lineTo(x+CELL,y+CELL); ctx.stroke(); }
if(cell.walls[2]){ ctx.beginPath(); ctx.moveTo(x,y+CELL); ctx.lineTo(x+CELL,y+CELL); ctx.stroke(); }
if(cell.walls[3]){ ctx.beginPath(); ctx.moveTo(x,y); ctx.lineTo(x,y+CELL); ctx.stroke(); }
}
// exit
ctx.fillStyle='#39FF14';
ctx.beginPath();
ctx.arc(OFFSET_X+exit.c*CELL+CELL/2,OFFSET_Y+exit.r*CELL+CELL/2,CELL*0.3,0,Math.PI*2);
ctx.fill();
// player
ctx.fillStyle='#00FFFF';
ctx.beginPath();
ctx.arc(OFFSET_X+player.c*CELL+CELL/2,OFFSET_Y+player.r*CELL+CELL/2,CELL*0.3,0,Math.PI*2);
ctx.fill();
// HUD
const elapsed=((performance.now()-state.startTime)/1000)|0;
document.getElementById('hud').innerHTML=
`Niveau: ${state.level} &nbsp;&nbsp; Pas: ${player.steps} &nbsp;&nbsp; Temps: ${elapsed}s &nbsp;&nbsp; Meilleur: ${state.best}`;
}
// ----- Loop -----
let last=performance.now();
function frame(t){
const dt=(t-last)/1000; last=t;
update(dt); draw();
requestAnimationFrame(frame);
}
reset(1);
requestAnimationFrame(frame);
})();
</script>
</body>
</html>