diff --git a/public/game.html b/public/game.html
index cb1e057..0b1b3c7 100644
--- a/public/game.html
+++ b/public/game.html
@@ -68,6 +68,16 @@
}
}
+ // play a short shuffle animation (shake + fade) when pieces are mixed
+ function playShuffleAnimation(){
+ if(!boardEl) return;
+ try{
+ boardEl.classList.add('shuffle-play');
+ // remove after animation duration
+ setTimeout(()=>{ try{ boardEl.classList.remove('shuffle-play'); }catch(_){ } }, 700);
+ }catch(_){ }
+ }
+
// helper: decide if a card requires a target selected after clicking 'Jouer'
function cardRequiresTarget(cardId){
if(!cardId) return null;
@@ -185,6 +195,10 @@
try{
const effect = data && data.effect ? data.effect : data;
if(!effect) return;
+ // play shuffle animation for melange
+ if(effect.type === 'melange' || effect.type === 'm\u00E9lange'){
+ try{ playShuffleAnimation(); }catch(_){ }
+ }
// if this is a private or public effect relating to this player, show a small note
if(effect.playerId && effect.playerId === myPlayerId){
showEffectNotification(effect);
diff --git a/public/styles/style.css b/public/styles/style.css
index d3fc39e..ef60c81 100644
--- a/public/styles/style.css
+++ b/public/styles/style.css
@@ -225,3 +225,12 @@ button:hover{transform:translateY(-3px);transition:all 180ms ease}
.pokemon-card{flex:0 0 140px;height:220px}
}
+/* shuffle animation: quick jitter + opacity pulse to emphasize randomization */
+#board.shuffle-play{
+ transition: transform 700ms cubic-bezier(.2,.9,.2,1), opacity 700ms ease;
+ transform: translateY(-6px) rotate(-1deg) scale(0.995);
+ opacity: 0.9;
+}
+#board.shuffle-play .square img{ transition: transform 700ms cubic-bezier(.2,.9,.2,1); transform: rotate(2deg) scale(0.995); filter: drop-shadow(0 6px 18px rgba(0,0,0,0.06)); }
+
+
diff --git a/server.js b/server.js
index 1af3850..3979f48 100644
--- a/server.js
+++ b/server.js
@@ -165,7 +165,7 @@ function buildDefaultDeck(){
// ['plus on est de fous','Si le joueur possède deux fous dans la même diagonale, alors toutes les pièces adverses encadrées par ces deux fous sont capturés'],
// ['cluster','Désigne 4 pions formant un rectangle. Tant que ces pions ne bougent pas, aucune pièce ne peut sortir ou rentrer dans ce rectangle.'],
// ['vacances','Choisie une pièce qui sort du plateau pendant deux tours. Ce après quoi elle tente de revenir: si la case est occupée, alors la pièce vacancière est capturée par la pièce occupant la case.'],
- // ['mélange','La position de toutes les pièces sont échangées aléatoirement.'],
+ ['mélange','La position de toutes les pièces sont échangées aléatoirement.'],
// ['la parrure','Une reine est dégradée en pion'],
// ['tricherie','Choisis une carte de la pioche parmis trois'],
// ['tout ou rien','Une pièce choisie ne peut maintenant se déplacer que si elle capture.'],
@@ -178,7 +178,10 @@ function buildDefaultDeck(){
function cap(s){ if(!s) return s; s = String(s).trim(); return s.charAt(0).toUpperCase() + s.slice(1); }
return cards.map(([title,desc])=>{
const normalized = (title||'').toString().trim();
- const cardId = normalized.replace(/[^a-z0-9]+/gi,'_').toLowerCase();
+ // generate an ASCII-friendly slug by removing diacritics before replacing non-alphanumerics
+ let ascii = normalized;
+ try{ ascii = ascii.normalize('NFD').replace(/\p{Diacritic}/gu, ''); }catch(e){ /* ignore if normalize unsupported */ }
+ const cardId = ascii.replace(/[^a-z0-9]+/gi,'_').toLowerCase();
return { id: uuidv4().slice(0,8), cardId, title: cap(normalized), description: desc };
});
}
@@ -1480,6 +1483,76 @@ io.on('connection', (socket) => {
played.payload = Object.assign({}, payload, { applied: 'coincoin', from: source, allowed: destChoices.slice(0) });
}catch(e){ console.error('coincoin effect error', e); }
}
+ // mélange: permute aléatoirement la position de toutes les pièces (échange entre cases occupées)
+ else if((typeof cardId === 'string' && (cardId.indexOf('melange') !== -1 || cardId.indexOf('m\u00E9lange') !== -1 || cardId.indexOf('m\u00E9l') !== -1)) || cardId === 'melange' || cardId === 'm\u00E9lange'){
+ try{
+ const board = room.boardState;
+ if(!board || !Array.isArray(board.pieces) || board.pieces.length === 0){
+ // nothing to do; restore card
+ try{
+ room.hands = room.hands || {};
+ room.hands[senderId] = room.hands[senderId] || [];
+ if(removed) room.hands[senderId].push(removed);
+ room.discard = room.discard || [];
+ for(let i = room.discard.length - 1; i >= 0; i--){ if(room.discard[i] && room.discard[i].id === (removed && removed.id)){ room.discard.splice(i,1); break; } }
+ }catch(e){}
+ return cb && cb({ error: 'no pieces to shuffle' });
+ }
+ // gather pieces and compute a random set of distinct destination squares across the whole board
+ const pieces = board.pieces;
+ const w = board.width || 8; const h = board.height || 8;
+ // build full list of all squares on the board
+ const allSquares = [];
+ for(let yy = 0; yy < h; yy++){
+ for(let xx = 0; xx < w; xx++){
+ allSquares.push(String.fromCharCode('a'.charCodeAt(0) + xx) + (yy + 1));
+ }
+ }
+ // if there are fewer available squares than pieces (shouldn't happen), abort gracefully
+ if(allSquares.length < pieces.length){
+ // can't place all pieces uniquely; leave board unchanged
+ played.payload = Object.assign({}, payload, { applied: 'melange_failed', reason: 'board_too_small' });
+ } else {
+ // Fisher-Yates shuffle the full board squares and take first N distinct
+ for(let i = allSquares.length - 1; i > 0; i--){ const j = Math.floor(Math.random() * (i + 1)); const tmp = allSquares[i]; allSquares[i] = allSquares[j]; allSquares[j] = tmp; }
+ const dests = allSquares.slice(0, pieces.length);
+ // assign destinations to pieces in random order
+ const newSquareByPieceId = {};
+ for(let i = 0; i < pieces.length; i++){ const p = pieces[i]; newSquareByPieceId[p.id] = dests[i]; }
+ // apply new squares
+ pieces.forEach(p => { try{ p.square = newSquareByPieceId[p.id] || p.square; }catch(_){ } });
+ // update any active effects that are bound to pieces (by pieceId) so their pieceSquare follows
+ try{
+ room.activeCardEffects = room.activeCardEffects || [];
+ room.activeCardEffects.forEach(e => {
+ if(!e) return;
+ try{ if(e.pieceId && newSquareByPieceId[e.pieceId]){ e.pieceSquare = newSquareByPieceId[e.pieceId]; } }catch(_){ }
+ });
+ }catch(_){ }
+ played.payload = Object.assign({}, payload, { applied: 'melange', count: pieces.length });
+ }
+ // update any active effects that are bound to pieces (by pieceId) so their pieceSquare follows
+ try{
+ room.activeCardEffects = room.activeCardEffects || [];
+ room.activeCardEffects.forEach(e => {
+ if(!e) return;
+ try{
+ if(e.pieceId && newSquareByPieceId[e.pieceId]){
+ e.pieceSquare = newSquareByPieceId[e.pieceId];
+ }
+ }catch(_){ }
+ });
+ }catch(_){ }
+ // bump board version
+ board.version = (board.version || 0) + 1;
+ // notify clients with an effect so they can show animation
+ const effect = { id: played.id, type: 'melange', playerId: senderId, ts: Date.now(), note: 'pieces shuffled' };
+ room.activeCardEffects = room.activeCardEffects || [];
+ room.activeCardEffects.push(effect);
+ try{ io.to(room.id).emit('card:effect:applied', { roomId: room.id, effect }); }catch(_){ }
+ played.payload = Object.assign({}, payload, { applied: 'melange', count: pieces.length });
+ }catch(e){ console.error('melange error', e); }
+ }
// invisible: make one of your pieces invisible to the opponent for a number of turns
else if(cardId === 'invisible' || (typeof cardId === 'string' && cardId.indexOf('invis') !== -1)){
try{
@@ -1663,6 +1736,7 @@ io.on('connection', (socket) => {
try{ const victim = (room.players || []).find(p => p.id === targetPlayerId); if(victim && victim.socketId) io.to(victim.socketId).emit('card:lost', { roomId: room.id, lostCount: 1 }); }catch(_){ }
}
}
+
}catch(e){ console.error('steal-card effect error', e); }
}
// carte sans effet: consumed but does nothing