animation d'explopsion

This commit is contained in:
Didictateur 2025-11-22 21:31:52 +01:00
parent 9d74b643d2
commit 8f7530f58f
3 changed files with 107 additions and 12 deletions

View file

@ -211,6 +211,25 @@
}catch(e){ console.warn('move:moved handler error', e); }
});
// mine detonation: show explosion animation at the square and remove local mine visual immediately
socket.on('mine:detonated', (evt) => {
try{
log('recv mine:detonated', evt);
// evt.square is algebraic name like 'e4'
if(evt && evt.square) showExplosionAt(evt.square);
// remove any mine image/dot at that square (owner will get updated minesOwn via room:update)
try{
const sqEl = boardEl && boardEl.querySelector && boardEl.querySelector(`.square[data-square="${evt.square}"]`);
if(sqEl){ const img = sqEl.querySelector('.mine-img'); if(img) img.remove(); const dot = sqEl.querySelector('.mine-dot'); if(dot) dot.remove(); }
}catch(_){ }
}catch(e){ console.warn('mine:detonated handler error', e); }
});
// private detonation info for the mine owner (optional handling)
socket.on('mine:detonated:private', (evt) => {
try{ log('recv mine:detonated:private', evt); /* could show owner-specific UI later */ }catch(e){}
});
socket.emit('room:join', { roomId, playerId }, (resp)=>{
if(resp && resp.error){ log('join error', resp); alert(resp.error); return; }
log('Rejoint room', resp);
@ -500,6 +519,53 @@
});
}
// show an explosion animation centered on a square (client-side visual)
function showExplosionAt(square){
if(!boardEl || !square) return;
const sqEl = boardEl.querySelector(`.square[data-square="${square}"]`);
if(!sqEl) return;
// ensure container is positioned so absolute centering works
if(!sqEl.style.position) sqEl.style.position = 'relative';
// Try to play an authored explosion video if available; fallback to CSS explosion if not.
const videoPath = '/assets/media/FireElement01.mov';
const vid = document.createElement('video');
vid.className = 'explosion-video';
vid.src = videoPath;
vid.autoplay = true; vid.muted = true; vid.playsInline = true; vid.loop = false; vid.preload = 'auto';
vid.style.pointerEvents = 'none';
let usedVideo = false;
// Attach event listeners to handle success or failure
const cleanup = ()=>{ try{ if(vid && vid.parentNode) vid.parentNode.removeChild(vid); }catch(_){ } };
vid.addEventListener('canplay', ()=>{
// video is available and can play — use it
try{ sqEl.appendChild(vid); usedVideo = true; vid.play().catch(()=>{}); }
catch(_){ usedVideo = false; }
}, { once: true });
// if video errors (file missing or codec issue), fallback to CSS explosion
vid.addEventListener('error', ()=>{
try{ cleanup(); }catch(_){ }
// fallback animation
const ex = document.createElement('div'); ex.className = 'explosion'; sqEl.appendChild(ex);
requestAnimationFrame(()=> ex.classList.add('play'));
setTimeout(()=>{ try{ ex.remove(); }catch(_){ if(ex && ex.parentNode) ex.parentNode.removeChild(ex); } }, 700);
}, { once: true });
// If video ends, remove it
vid.addEventListener('ended', ()=>{ try{ cleanup(); }catch(_){ } }, { once: true });
// start loading — if canplay won't fire within a short time, fallback to CSS
vid.load();
// fallback timeout: if video hasn't started in 180ms, show CSS explosion instead
setTimeout(()=>{
if(!usedVideo){
try{ vid.pause(); if(vid && vid.parentNode) vid.parentNode.removeChild(vid); }catch(_){ }
const ex = document.createElement('div'); ex.className = 'explosion'; sqEl.appendChild(ex);
requestAnimationFrame(()=> ex.classList.add('play'));
setTimeout(()=>{ try{ ex.remove(); }catch(_){ if(ex && ex.parentNode) ex.parentNode.removeChild(ex); } }, 700);
}
}, 180);
}
// target highlighting helpers
function clearTargetHighlights(){
if(!boardEl) return;

View file

@ -154,6 +154,37 @@ a:hover{text-decoration:underline}
}
.btn-cancel:hover{ background: #fff5f5; }
/* explosion animation used when a mine detonates */
.explosion{
position:absolute;
left:50%; top:50%; transform:translate(-50%,-50%) scale(0.6);
width:64%; height:64%; border-radius:50%; pointer-events:none; z-index:6;
background: radial-gradient(circle at 30% 30%, rgba(255,255,200,0.98) 0%, rgba(255,160,60,0.95) 40%, rgba(220,40,40,0.95) 60%, rgba(120,10,10,0.85) 100%);
box-shadow: 0 6px 18px rgba(0,0,0,0.35), inset 0 6px 18px rgba(255,255,255,0.08);
opacity: 1; transform-origin:50% 50%;
transform: translate(-50%,-50%) scale(0.2);
}
.explosion.play{ animation: explode 520ms ease-out forwards; }
@keyframes explode{
0%{ transform: translate(-50%,-50%) scale(0.2); opacity:1; filter: blur(0px); }
60%{ transform: translate(-50%,-50%) scale(1.15); opacity:0.95; filter: blur(1px); }
100%{ transform: translate(-50%,-50%) scale(1.6); opacity:0; filter: blur(6px); }
}
/* small debris puffs for a subtle effect */
.explosion::after{
content:''; position:absolute; left:50%; top:50%; transform:translate(-50%,-50%);
width:120%; height:120%; border-radius:50%; background: radial-gradient(circle, rgba(255,200,100,0.6) 0%, rgba(255,200,100,0.0) 40%);
opacity:0; animation: debris 520ms ease-out forwards;
}
@keyframes debris{ 0%{ opacity:0.9; transform:translate(-50%,-50%) scale(0.8); } 100%{ opacity:0; transform:translate(-50%,-50%) scale(1.8); } }
/* video-based explosion: used when a .mov/mp4 file is provided in /assets/media/ */
.explosion-video{
position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); z-index:7; pointer-events:none;
width:160%; height:auto; max-width:220px; max-height:220px; display:block; object-fit:contain;
}
/* floating small piece icons */
.floating-piece{position:absolute;opacity:0.06;width:160px;height:160px;pointer-events:none;transform:translate(-10%,-10%);}

View file

@ -700,18 +700,16 @@ io.on('connection', (socket) => {
for(let i = room.activeCardEffects.length - 1; i >= 0; i--){
const e = room.activeCardEffects[i];
if(e && e.type === 'mine' && e.square === to){
// owner is immune to their own mine (assumption). Only detonate for other players.
if(e.playerId !== senderId){
// remove the moving piece from the board
const rmIdx = pieces.findIndex(p => p.id === moving.id);
if(rmIdx >= 0){ pieces.splice(rmIdx, 1); }
// consume the mine
try{ room.activeCardEffects.splice(i,1); }catch(_){ }
// broadcast detonation to the room (informational)
try{ io.to(roomId).emit('mine:detonated', { roomId: room.id, ownerId: e.playerId, detonatorId: senderId, square: to, piece: moving }); }catch(_){ }
// privately inform the owner as well with effect id and piece details
try{ const owner = (room.players||[]).find(p => p.id === e.playerId); if(owner && owner.socketId) io.to(owner.socketId).emit('mine:detonated:private', { roomId: room.id, effectId: e.id, square: to, piece: moving }); }catch(_){ }
}
// detonate for any piece that lands on the mine (owner included)
// remove the moving piece from the board
const rmIdx = pieces.findIndex(p => p.id === moving.id);
if(rmIdx >= 0){ pieces.splice(rmIdx, 1); }
// consume the mine
try{ room.activeCardEffects.splice(i,1); }catch(_){ }
// broadcast detonation to the room (informational)
try{ io.to(roomId).emit('mine:detonated', { roomId: room.id, ownerId: e.playerId, detonatorId: senderId, square: to, piece: moving }); }catch(_){ }
// privately inform the owner as well with effect id and piece details
try{ const owner = (room.players||[]).find(p => p.id === e.playerId); if(owner && owner.socketId) io.to(owner.socketId).emit('mine:detonated:private', { roomId: room.id, effectId: e.id, square: to, piece: moving }); }catch(_){ }
// a mine was found/handled; break (only one mine per square expected)
break;
}