SHADOW DUNGEON
by PhantomMeteor46588 lines21.7 KB
<!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.