animation d'explopsion
This commit is contained in:
parent
9d74b643d2
commit
8f7530f58f
3 changed files with 107 additions and 12 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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%);}
|
||||
|
||||
|
|
|
|||
|
|
@ -700,8 +700,7 @@ 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){
|
||||
// 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); }
|
||||
|
|
@ -711,7 +710,6 @@ io.on('connection', (socket) => {
|
|||
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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue