🎮ArcadeLab

SHADOW DUNGEON

by PhantomMeteor46
588 lines21.7 KB
▶ Play
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
<title>SHADOW DUNGEON</title>
<style>
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent;user-select:none;-webkit-user-select:none}
html,body{width:100%;height:100%;overflow:hidden;background:#000;font-family:'Courier New',monospace;touch-action:none}
#wrap{position:absolute;inset:0;z-index:1}
canvas{display:block;width:100%;height:100%;image-rendering:pixelated}
#hud{position:absolute;top:4px;left:4px;right:4px;z-index:10;display:flex;justify-content:space-between;gap:4px;pointer-events:none;color:#fff;font-size:10px;font-weight:bold;text-shadow:2px 2px 0 #000}
#hud .box{background:rgba(0,0,0,.75);padding:3px 6px;border-radius:3px;border:1px solid #333}
#hpbar-w{width:90px;height:7px;background:#300;border-radius:3px;overflow:hidden;margin-top:2px;border:1px solid #000}
#hpbar{height:100%;background:linear-gradient(90deg,#ff2266,#ff6688);transition:width .15s}
#xpbar-w{width:90px;height:5px;background:#003;border-radius:3px;overflow:hidden;margin-top:2px;border:1px solid #000}
#xpbar{height:100%;background:linear-gradient(90deg,#00ffff,#66ddff);transition:width .15s}
#minimap{position:absolute;top:4px;right:4px;z-index:10;border:2px solid #444;border-radius:3px;background:rgba(0,0,0,.8);pointer-events:none}
#joy{position:absolute;bottom:12px;left:12px;z-index:20;width:100px;height:100px;background:rgba(255,255,255,.08);border-radius:50%;border:2px solid rgba(255,255,255,.25)}
#joyKnob{position:absolute;top:50%;left:50%;width:40px;height:40px;background:rgba(0,255,255,.5);border-radius:50%;transform:translate(-50%,-50%);border:2px solid #00ffff;pointer-events:none}
.abtn{position:absolute;z-index:20;border-radius:50%;border:2px solid rgba(255,255,255,.4);background:rgba(0,0,0,.55);color:#fff;font-family:'Courier New',monospace;font-weight:bold;display:flex;align-items:center;justify-content:center;text-align:center;line-height:1.05;padding:2px}
.abtn:active{background:rgba(255,255,255,.35);transform:scale(.95)}
#btn-atk{bottom:80px;right:12px;width:76px;height:76px;font-size:13px;border-color:#ff2266;color:#ff2266}
#btn-pot{bottom:12px;right:12px;width:58px;height:58px;font-size:10px;border-color:#00ffcc;color:#00ffcc}
#btn-dash{bottom:12px;right:80px;width:58px;height:58px;font-size:10px;border-color:#ffcc00;color:#ffcc00}
#btn-dash:disabled{opacity:.3}
#notif{position:absolute;top:70px;left:50%;transform:translateX(-50%);z-index:50;color:#ffcc00;font-size:11px;font-weight:bold;text-shadow:2px 2px 0 #000;pointer-events:none;opacity:0;transition:opacity .3s;background:rgba(0,0,0,.75);padding:4px 12px;border-radius:4px;border:1px solid #ffcc00;white-space:nowrap}
#start{position:absolute;inset:0;z-index:200;background:radial-gradient(circle at 50% 40%,#1a1a3a,#050510);display:flex;flex-direction:column;align-items:center;justify-content:center;color:#fff;text-align:center;padding:16px}
#start h1{color:#00ffff;font-size:24px;text-shadow:0 0 18px #00ffff;margin-bottom:8px;letter-spacing:2px}
#start p{font-size:10px;line-height:1.7;max-width:290px;color:#aaccff;margin-bottom:20px}
#start p b{color:#ffcc00}
#go{padding:14px 46px;font-size:17px;font-weight:bold;background:#003344;color:#00ffcc;border:2px solid #00ffcc;border-radius:8px;font-family:'Courier New',monospace}
#over{position:absolute;inset:0;z-index:150;background:rgba(5,5,16,.94);display:none;flex-direction:column;align-items:center;justify-content:center;color:#fff;text-align:center;padding:20px}
#over h2{color:#ff2266;font-size:28px;text-shadow:0 0 20px #ff2266;margin-bottom:12px}
#over p{color:#aaccff;font-size:12px;line-height:1.8;margin-bottom:20px}
#retry{padding:13px 38px;font-size:16px;font-weight:bold;background:#331133;color:#ff66aa;border:2px solid #ff66aa;border-radius:8px;font-family:'Courier New',monospace}
</style>
</head>
<body>
<div id="wrap"><canvas id="game"></canvas></div>
<div id="hud">
  <div class="box"><div>Nv <span id="lv">1</span> · <span id="hpt">20/20</span></div><div id="hpbar-w"><div id="hpbar" style="width:100%"></div></div><div id="xpbar-w"><div id="xpbar" style="width:0%"></div></div></div>
  <div class="box" style="text-align:right"><div>⚔ <span id="atk">5</span></div><div>💎 <span id="xpv">0</span></div><div>🧪 <span id="potv">3</span></div></div>
</div>
<canvas id="minimap" width="80" height="80"></canvas>
<div id="joy"><div id="joyKnob"></div></div>
<button class="abtn" id="btn-atk">ATK</button>
<button class="abtn" id="btn-pot">SOIN</button>
<button class="abtn" id="btn-dash">DASH</button>
<div id="notif"></div>
<div id="start"><h1>SHADOW DUNGEON</h1><p>Un donjon généré à l'infini à chaque partie.<br><br><b>Joystick</b> = bouger<br><b>ATK</b> = frapper (rayon court)<br><b>SOIN</b> = potion (+15 PV)<br><b>DASH</b> = esquive rapide<br><br>Chaque étage te fait descendre plus profond. Tue tout, gagne de l'XP, améliore-toi.</p><button id="go">DESCENDRE</button></div>
<div id="over"><h2>TU ES TOMBÉ</h2><p id="overstats"></p><button id="retry">REJOUER</button></div>

<script>
// ==================== CONFIG ====================
const TILE = 28;
const DW = 40, DH = 40;   // taille du donjon (tuiles)
const VIEW_W = 0, VIEW_H = 0;

const T = { FLOOR:0, WALL:1, STAIRS:2 };

const S = {
  running:false, over:false,
  map:[], floor:1, player:null, monsters:[], particles:[], chests:[],
  camera:{x:0,y:0}, joy:{x:0,y:0,active:false},
  canvas:null, ctx:null, mini:null, mctx:null,
  miniSize:80,
};

// ==================== UTIL ====================
function rand(a,b){ return a + Math.floor(Math.random()*(b-a+1)); }
function clamp(v,a,b){ return v<a?a:v>b?b:v; }
function dist(a,b){ return Math.hypot(a.x-b.x,a.y-b.y); }

// ==================== GÉNÉRATION DONJON ====================
// Algorithme : salles rectangulaires + couloirs (classique roguelike)
function generateDungeon() {
  const map = [];
  for (let y=0;y<DH;y++){ const row=[]; for(let x=0;x<DW;x++) row.push(T.WALL); map.push(row); }

  const rooms = [];
  const nbRooms = 6 + Math.floor(S.floor/2);
  for (let i=0;i<nbRooms;i++){
    const w = rand(4,8), h = rand(3,6);
    const x = rand(1,DW-w-2), y = rand(1,DH-h-2);
    let ok = true;
    for (const r of rooms){
      if (x < r.x+r.w+1 && x+w+1 > r.x && y < r.y+r.h+1 && y+h+1 > r.y) { ok=false; break; }
    }
    if (!ok) continue;
    for (let yy=y;yy<y+h;yy++) for (let xx=x;xx<x+w;xx++) map[yy][xx]=T.FLOOR;
    rooms.push({x,y,w,h});
  }

  // Couloirs en L entre salles consécutives
  for (let i=1;i<rooms.length;i++){
    const a = rooms[i-1], b = rooms[i];
    const ax = Math.floor(a.x+a.w/2), ay = Math.floor(a.y+a.h/2);
    const bx = Math.floor(b.x+b.w/2), by = Math.floor(b.y+b.h/2);
    if (Math.random()<0.5){
      for (let x=Math.min(ax,bx);x<=Math.max(ax,bx);x++) map[ay][x]=T.FLOOR;
      for (let y=Math.min(ay,by);y<=Math.max(ay,by);y++) map[y][bx]=T.FLOOR;
    } else {
      for (let y=Math.min(ay,by);y<=Math.max(ay,by);y++) map[y][ax]=T.FLOOR;
      for (let x=Math.min(ax,bx);x<=Math.max(ax,bx);x++) map[by][x]=T.FLOOR;
    }
  }

  // Escalier dans la salle la plus éloignée du spawn (salle 0 = spawn)
  const spawn = rooms[0];
  let farthest = rooms[rooms.length-1], best = 0;
  const sx = Math.floor(spawn.x+spawn.w/2), sy = Math.floor(spawn.y+spawn.h/2);
  for (const r of rooms){
    const cx = Math.floor(r.x+r.w/2), cy = Math.floor(r.y+r.h/2);
    const d = Math.abs(cx-sx)+Math.abs(cy-sy);
    if (d>best){best=d; farthest=r;}
  }
  const stx = Math.floor(farthest.x+farthest.w/2), sty = Math.floor(farthest.y+farthest.h/2);
  map[sty][stx] = T.STAIRS;

  return { map, spawn:{x:sx,y:sy}, stairs:{x:stx,y:sty}, rooms };
}

// ==================== MONSTRES ====================
function makeMonster(x,y){
  // Types selon l'étage
  const types = [
    {name:'slime', hp:15, atk:3, spd:0.55, xp:8, color:'#ff2266', r:10},
    {name:'bat',   hp:10, atk:4, spd:1.0,  xp:10,color:'#aa44ff', r:9},
    {name:'orc',   hp:35, atk:6, spd:0.5,  xp:20,color:'#ff8800', r:13},
  ];
  let type = types[0];
  if (S.floor>=3 && Math.random()<0.4) type = types[1];
  if (S.floor>=5 && Math.random()<0.3) type = types[2];
  const boost = 1 + (S.floor-1)*0.35;
  return {
    x, y, type:type.name, r:type.r,
    hp: Math.floor(type.hp*boost), maxHp: Math.floor(type.hp*boost),
    atk: Math.floor(type.atk*boost), spd: type.spd,
    xp: Math.floor(type.xp*boost), color: type.color,
    face:1, animT:0, animF:0, cd:0, hitFlash:0, alive:true,
    wander:{dx:0,dy:0,t:0}
  };
}

// ==================== INIT PARTIE ====================
function newGame(){
  S.floor = 1;
  S.running = true; S.over = false;
  S.player = {
    x:0, y:0, hp:20, maxHp:20, atk:5, speed:110,
    level:1, xp:0, xpNext:20, potions:3,
    dashCd:0, dashCdMax:2.5, invuln:0, hitFlash:0, face:1, animT:0, animF:0
  };
  loadFloor();
}

function loadFloor(){
  const g = generateDungeon();
  S.map = g.map;
  S.stairs = g.stairs;
  S.player.x = g.spawn.x * TILE + TILE/2;
  S.player.y = g.spawn.y * TILE + TILE/2;
  S.monsters = [];
  S.particles = [];
  S.chests = [];

  // Monstres = nombre croissant avec l'étage
  const count = Math.min(15, 3 + S.floor);
  let placed = 0, tries = 0;
  while (placed < count && tries < 500){
    tries++;
    const x = rand(1, DW-2), y = rand(1, DH-2);
    if (S.map[y][x] !== T.FLOOR) continue;
    const px = x*TILE+TILE/2, py = y*TILE+TILE/2;
    if (Math.hypot(px-S.player.x, py-S.player.y) < 150) continue;
    S.monsters.push(makeMonster(px, py));
    placed++;
  }

  S.camera.x = S.player.x - S.canvas.width/2;
  S.camera.y = S.player.y - S.canvas.height/2;
  notify('ÉTAGE '+S.floor);
  updateHUD();
}

// ==================== COMBAT / XP ====================
function attack(){
  const p = S.player;
  let hit = false;
  for (const m of S.monsters){
    if (!m.alive) continue;
    if (dist(m,p) < 50){
      m.hp -= p.atk;
      m.hitFlash = 0.2;
      const ang = Math.atan2(m.y-p.y, m.x-p.x);
      m.x += Math.cos(ang)*10; m.y += Math.sin(ang)*10;
      hit = true;
      if (m.hp <= 0){ killMonster(m); }
    }
  }
  for (let i=0;i<6;i++){
    const a = (i/6)*Math.PI*2;
    S.particles.push({x:p.x+Math.cos(a)*25,y:p.y+Math.sin(a)*25,vx:Math.cos(a)*80,vy:Math.sin(a)*80,life:.25,color:'#ffcc00',size:2});
  }
}

function killMonster(m){
  m.alive = false;
  gainXP(m.xp);
  for (let i=0;i<10;i++){
    const a = Math.random()*Math.PI*2, s = 40+Math.random()*80;
    S.particles.push({x:m.x,y:m.y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,life:.5,color:m.color,size:3});
  }
  if (Math.random() < 0.2){
    S.player.potions++;
    notify('+1 Potion');
  }
}

function gainXP(a){
  const p = S.player;
  p.xp += a;
  notify('+'+a+' XP');
  while (p.xp >= p.xpNext){
    p.xp -= p.xpNext;
    p.level++;
    p.xpNext = Math.floor(p.xpNext*1.4);
    p.maxHp += 10; p.hp = p.maxHp;
    p.atk += 2;
    notify('★ NIVEAU '+p.level+' ★');
    for (let i=0;i<20;i++){
      const a2 = Math.random()*Math.PI*2, s2 = 100+Math.random()*100;
      S.particles.push({x:p.x,y:p.y,vx:Math.cos(a2)*s2,vy:Math.sin(a2)*s2,life:.8,color:'#00ffff',size:3});
    }
  }
  updateHUD();
}

// ==================== UPDATES ====================
function canWalk(px,py){
  const r = 10;
  const pts = [[px-r,py-r],[px+r,py-r],[px-r,py+r],[px+r,py+r]];
  for (const [x,y] of pts){
    const tx = Math.floor(x/TILE), ty = Math.floor(y/TILE);
    if (tx<0||ty<0||tx>=DW||ty>=DH) return false;
    if (S.map[ty][tx] === T.WALL) return false;
  }
  return true;
}

function updatePlayer(dt){
  const p = S.player;
  const jx = S.joy.x, jy = S.joy.y;
  if (Math.hypot(jx,jy) > 0.15){
    const sp = p.speed*dt;
    const nx = p.x + jx*sp;
    const ny = p.y + jy*sp;
    if (canWalk(nx,p.y)) p.x = nx;
    if (canWalk(p.x,ny)) p.y = ny;
    p.face = jx>0?1:(jx<0?-1:p.face);
    p.animT += dt;
    if (p.animT > .15){ p.animF = (p.animF+1)%4; p.animT = 0; }
  }
  if (p.dashCd > 0) p.dashCd -= dt;
  if (p.invuln > 0) p.invuln -= dt;
  if (p.hitFlash > 0) p.hitFlash -= dt;

  // Escalier
  const tx = Math.floor(p.x/TILE), ty = Math.floor(p.y/TILE);
  if (S.map[ty] && S.map[ty][tx] === T.STAIRS){
    S.floor++;
    loadFloor();
    return;
  }
}

function updateMonsters(dt){
  const p = S.player;
  for (const m of S.monsters){
    if (!m.alive) continue;
    if (m.hitFlash>0) m.hitFlash -= dt;
    if (m.cd>0) m.cd -= dt;
    m.animT += dt;
    if (m.animT>.2){ m.animF=(m.animF+1)%2; m.animT=0; }

    const d = dist(m,p);
    if (d < 250 && d > 25){
      const ang = Math.atan2(p.y-m.y, p.x-m.x);
      const sp = m.spd*55*dt;
      const nx = m.x+Math.cos(ang)*sp;
      const ny = m.y+Math.sin(ang)*sp;
      if (canWalk(nx,m.y)) m.x = nx;
      if (canWalk(m.x,ny)) m.y = ny;
      m.face = Math.cos(ang)>0?1:-1;
    }

    if (d < 30 && m.cd <= 0 && p.invuln <= 0){
      p.hp -= m.atk;
      p.invuln = .7;
      p.hitFlash = .2;
      m.cd = 1.0;
      notify('-'+m.atk+' PV');
      if (p.hp <= 0){ p.hp = 0; gameOver(); return; }
      updateHUD();
    }
  }
}

function gameOver(){
  S.running = false;
  S.over = true;
  document.getElementById('overstats').innerHTML =
    'Étage atteint : <b>'+S.floor+'</b><br>Niveau : <b>'+S.player.level+'</b><br>Monstres tués : <b>'+(S.player.level-1)+'</b>';
  document.getElementById('over').style.display = 'flex';
}

// ==================== RENDU ====================
function draw(){
  const ctx = S.ctx;
  const W = S.canvas.width, H = S.canvas.height;
  ctx.fillStyle = '#0a0a15';
  ctx.fillRect(0,0,W,H);

  // Camera suit le joueur
  S.camera.x += (S.player.x - W/2 - S.camera.x) * 0.15;
  S.camera.y += (S.player.y - H/2 - S.camera.y) * 0.15;
  const camX = S.camera.x, camY = S.camera.y;

  // Tuiles visibles
  const startX = Math.max(0, Math.floor(camX/TILE));
  const endX = Math.min(DW, Math.ceil((camX+W)/TILE));
  const startY = Math.max(0, Math.floor(camY/TILE));
  const endY = Math.min(DH, Math.ceil((camY+H)/TILE));

  for (let y=startY;y<endY;y++){
    for (let x=startX;x<endX;x++){
      const t = S.map[y][x];
      const sx = x*TILE - camX, sy = y*TILE - camY;
      if (t === T.WALL){
        ctx.fillStyle = '#1a1a2e';
        ctx.fillRect(sx,sy,TILE,TILE);
        ctx.fillStyle = '#252540';
        ctx.fillRect(sx,sy,TILE,3);
      } else if (t === T.FLOOR){
        ctx.fillStyle = '#2a2a40';
        ctx.fillRect(sx,sy,TILE,TILE);
        ctx.fillStyle = '#333350';
        ctx.fillRect(sx+2,sy+2,TILE-4,TILE-4);
      } else if (t === T.STAIRS){
        ctx.fillStyle = '#2a2a40';
        ctx.fillRect(sx,sy,TILE,TILE);
        ctx.fillStyle = '#ffcc00';
        ctx.fillRect(sx+4,sy+4,TILE-8,TILE-8);
        ctx.fillStyle = '#aa8800';
        ctx.fillRect(sx+8,sy+8,TILE-16,TILE-16);
      }
    }
  }

  // Monstres (sprite procédural animé)
  for (const m of S.monsters){
    if (!m.alive) continue;
    if (m.x < camX-50 || m.x > camX+W+50 || m.y < camY-50 || m.y > camY+H+50) continue;
    const sx = m.x - camX, sy = m.y - camY;
    const bob = Math.sin(m.animT*8)*2;
    const flash = m.hitFlash > 0;
    // Corps
    ctx.fillStyle = flash ? '#fff' : m.color;
    ctx.beginPath();
    ctx.arc(sx, sy+bob, m.r, 0, Math.PI*2);
    ctx.fill();
    // Yeux
    ctx.fillStyle = '#000';
    ctx.beginPath(); ctx.arc(sx-4, sy-2+bob, 2.5, 0, Math.PI*2); ctx.fill();
    ctx.beginPath(); ctx.arc(sx+4, sy-2+bob, 2.5, 0, Math.PI*2); ctx.fill();
    // Barre de vie
    if (m.hp < m.maxHp){
      const w = m.r*2;
      ctx.fillStyle = '#000';
      ctx.fillRect(sx-w/2, sy-m.r-8+bob, w, 3);
      ctx.fillStyle = '#ff2266';
      ctx.fillRect(sx-w/2, sy-m.r-8+bob, w*(m.hp/m.maxHp), 3);
    }
  }

  // Particules
  for (const pt of S.particles){
    const a = Math.max(0, pt.life);
    ctx.globalAlpha = a;
    ctx.fillStyle = pt.color;
    ctx.fillRect(pt.x-camX-pt.size/2, pt.y-camY-pt.size/2, pt.size, pt.size);
    ctx.globalAlpha = 1;
  }

  // Joueur
  const px = S.player.x - camX, py = S.player.y - camY;
  const flash = S.player.hitFlash > 0;
  // Ombre
  ctx.fillStyle = 'rgba(0,0,0,0.4)';
  ctx.beginPath(); ctx.ellipse(px, py+10, 10, 4, 0, 0, Math.PI*2); ctx.fill();
  // Corps
  ctx.fillStyle = flash ? '#fff' : '#00ffff';
  ctx.beginPath(); ctx.arc(px, py, 10, 0, Math.PI*2); ctx.fill();
  // Détail
  ctx.fillStyle = flash ? '#ccc' : '#0088aa';
  ctx.beginPath(); ctx.arc(px, py, 6, 0, Math.PI*2); ctx.fill();
  // Face (indicateur direction)
  ctx.fillStyle = '#fff';
  ctx.beginPath(); ctx.arc(px + S.player.face*4, py-2, 2, 0, Math.PI*2); ctx.fill();

  // Indicateur ATK
  if (S.player.invuln > 0){
    ctx.strokeStyle = 'rgba(255,255,255,'+(S.player.invuln/S.player.invulnMax||1)*0.5+')';
    ctx.lineWidth = 2;
    ctx.beginPath(); ctx.arc(px, py, 14, 0, Math.PI*2); ctx.stroke();
  }
}

function drawMinimap(){
  const c = S.mctx, size = S.miniSize;
  c.clearRect(0,0,size,size);
  c.fillStyle = '#000';
  c.fillRect(0,0,size,size);
  const scale = size / DW;
  // Tuiles
  for (let y=0;y<DH;y++){
    for (let x=0;x<DW;x++){
      const t = S.map[y][x];
      if (t === T.WALL) continue;
      if (t === T.STAIRS) c.fillStyle = '#ffcc00';
      else c.fillStyle = '#333355';
      c.fillRect(x*scale, y*scale, scale, scale);
    }
  }
  // Monstres
  c.fillStyle = '#ff2266';
  for (const m of S.monsters){
    if (!m.alive) continue;
    c.fillRect((m.x/TILE)*scale-1, (m.y/TILE)*scale-1, 2, 2);
  }
  // Joueur
  c.fillStyle = '#00ffff';
  c.fillRect((S.player.x/TILE)*scale-1.5, (S.player.y/TILE)*scale-1.5, 3, 3);
}

// ==================== HUD / NOTIF ====================
function updateHUD(){
  const p = S.player;
  document.getElementById('lv').textContent = p.level;
  document.getElementById('hpt').textContent = Math.max(0,Math.floor(p.hp))+'/'+p.maxHp;
  document.getElementById('hpbar').style.width = (Math.max(0,p.hp)/p.maxHp*100)+'%';
  document.getElementById('xpbar').style.width = (p.xp/p.xpNext*100)+'%';
  document.getElementById('atk').textContent = p.atk;
  document.getElementById('xpv').textContent = p.xp;
  document.getElementById('potv').textContent = p.potions;
  document.getElementById('btn-dash').disabled = p.dashCd > 0;
}

let notifT = 0;
function notify(msg){
  const el = document.getElementById('notif');
  el.textContent = msg;
  el.style.opacity = 1;
  notifT = 1.2;
}

// ==================== CONTRÔLES ====================
function setupControls(){
  const joy = document.getElementById('joy');
  const knob = document.getElementById('joyKnob');
  const R = 50;
  let jid = null;

  function setKnob(cx,cy){
    const r = joy.getBoundingClientRect();
    let dx = cx-(r.left+r.width/2), dy = cy-(r.top+r.height/2);
    const d = Math.hypot(dx,dy);
    if (d > R){ dx = dx/d*R; dy = dy/d*R; }
    knob.style.transform = `translate(calc(-50% + ${dx}px), calc(-50% + ${dy}px))`;
    S.joy.x = dx/R; S.joy.y = dy/R;
  }

  joy.addEventListener('touchstart', e=>{
    e.preventDefault();
    jid = e.changedTouches[0].identifier;
    setKnob(e.changedTouches[0].clientX, e.changedTouches[0].clientY);
  },{passive:false});

  joy.addEventListener('touchmove', e=>{
    e.preventDefault();
    for (const t of e.changedTouches) if (t.identifier === jid) setKnob(t.clientX, t.clientY);
  },{passive:false});

  joy.addEventListener('touchend', e=>{
    e.preventDefault();
    for (const t of e.changedTouches) if (t.identifier === jid){
      jid = null; S.joy.x = 0; S.joy.y = 0;
      knob.style.transform = 'translate(-50%, -50%)';
    }
  },{passive:false});

  document.getElementById('btn-atk').addEventListener('touchstart', e=>{
    e.preventDefault(); if (S.running) attack();
  },{passive:false});
  document.getElementById('btn-atk').addEventListener('click', ()=>{ if (S.running) attack(); });

  document.getElementById('btn-pot').addEventListener('touchstart', e=>{
    e.preventDefault();
    if (!S.running) return;
    if (S.player.potions > 0 && S.player.hp < S.player.maxHp){
      S.player.potions--;
      S.player.hp = Math.min(S.player.maxHp, S.player.hp+15);
      notify('+15 PV'); updateHUD();
    } else notify('Impossible');
  },{passive:false});
  document.getElementById('btn-pot').addEventListener('click', ()=>{
    if (!S.running) return;
    if (S.player.potions > 0 && S.player.hp < S.player.maxHp){
      S.player.potions--;
      S.player.hp = Math.min(S.player.maxHp, S.player.hp+15);
      notify('+15 PV'); updateHUD();
    } else notify('Impossible');
  });

  document.getElementById('btn-dash').addEventListener('touchstart', e=>{
    e.preventDefault();
    if (!S.running || S.player.dashCd > 0) return;
    S.player.dashCd = S.player.dashCdMax;
    S.player.invuln = 0.4;
    // Dash dans la direction du joystick
    const jx = S.joy.x, jy = S.joy.y;
    if (Math.hypot(jx,jy) > 0.1){
      const sp = 60;
      const nx = S.player.x + jx*sp;
      const ny = S.player.y + jy*sp;
      if (canWalk(nx,S.player.y)) S.player.x = nx;
      if (canWalk(S.player.x,ny)) S.player.y = ny;
    }
    notify('DASH !');
    updateHUD();
  },{passive:false});
  document.getElementById('btn-dash').addEventListener('click', ()=>{
    if (!S.running || S.player.dashCd > 0) return;
    S.player.dashCd = S.player.dashCdMax;
    S.player.invuln = 0.4;
    const jx = S.joy.x, jy = S.joy.y;
    if (Math.hypot(jx,jy) > 0.1){
      const sp = 60;
      const nx = S.player.x + jx*sp;
      const ny = S.player.y + jy*sp;
      if (canWalk(nx,S.player.y)) S.player.x = nx;
      if (canWalk(S.player.x,ny)) S.player.y = ny;
    }
    notify('DASH !');
    updateHUD();
  });
}

// ==================== BOUCLE ====================
let lastT = 0;
function loop(ts){
  requestAnimationFrame(loop);
  const dt = Math.min(0.05, (ts-lastT)/1000) || 0;
  lastT = ts;

  if (notifT > 0){
    notifT -= dt;
    if (notifT <= 0) document.getElementById('notif').style.opacity = 0;
  }

  if (S.running){
    updatePlayer(dt);
    updateMonsters(dt);
    // Particules
    for (let i=S.particles.length-1;i>=0;i--){
      const pt = S.particles[i];
      pt.x += pt.vx*dt; pt.y += pt.vy*dt;
      pt.life -= dt;
      if (pt.life <= 0) S.particles.splice(i,1);
    }
    updateHUD();
  }

  draw();
  drawMinimap();
}

Game Source: SHADOW DUNGEON

Creator: PhantomMeteor46

Libraries: none

Complexity: complex (588 lines, 21.7 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: shadow-dungeon-phantommeteor46" to link back to the original. Then publish at arcadelab.ai/publish.