diff --git a/public/game.html b/public/game.html
index ea2d68a..56c242b 100644
--- a/public/game.html
+++ b/public/game.html
@@ -72,21 +72,22 @@
function cardRequiresTarget(cardId){
if(!cardId) return null;
const id = String(cardId).toLowerCase();
- if(id.indexOf('mine') !== -1) return 'empty';
- // kamikaz targets one of your pieces (play card, then choose a piece)
- if(id.indexOf('kamikaz') !== -1) return 'owned';
- if(id.indexOf('invis') !== -1 || id.indexOf('invisible') !== -1) return 'owned';
- if(id.indexOf('rebond') !== -1) return 'owned';
- if(id.indexOf('adoub') !== -1) return 'owned';
- if(id.indexOf('folie') !== -1 || id.indexOf('fou') !== -1) return 'owned';
- if(id.indexOf('fortif') !== -1 || id.indexOf('fortification') !== -1) return 'owned';
- // promotion targets one of your pawns (special handling client-side to show promotion choice)
- if(id.indexOf('promotion') !== -1 || id.indexOf('promot') !== -1 || id.indexOf('promouvoir') !== -1) return 'owned_pawn';
- // steal a card (target a player) if the card mentions 'carte'/'card' — otherwise a 'vol' often targets a piece
- if(id.indexOf('vol') !== -1 || id.indexOf('vole') !== -1 || id.indexOf('steal') !== -1){
- if(id.indexOf('carte') !== -1 || id.indexOf('card') !== -1) return 'player';
- return 'enemy';
- }
+ if(id.indexOf('mine') !== -1) return 'empty';
+ // kamikaz targets one of your pieces (play card, then choose a piece)
+ if(id.indexOf('kamikaz') !== -1) return 'owned';
+ if(id.indexOf('invis') !== -1 || id.indexOf('invisible') !== -1) return 'owned';
+ if(id.indexOf('rebond') !== -1) return 'owned';
+ if(id.indexOf('adoub') !== -1) return 'owned';
+ if(id.indexOf('folie') !== -1 || id.indexOf('fou') !== -1) return 'owned';
+ if(id.indexOf('fortif') !== -1 || id.indexOf('fortification') !== -1) return 'owned';
+ // promotion targets one of your pawns (special handling client-side to show promotion choice)
+ if(id.indexOf('promotion') !== -1 || id.indexOf('promot') !== -1 || id.indexOf('promouvoir') !== -1) return 'owned_pawn';
+ // steal a card (target a player) if the card mentions 'carte'/'card' — otherwise a 'vol' often targets a piece
+ if(id.indexOf('vol') !== -1 || id.indexOf('vole') !== -1 || id.indexOf('steal') !== -1){
+ if(id.indexOf('carte') !== -1 || id.indexOf('card') !== -1) return 'player';
+ return 'enemy';
+ }
+ if(id.indexOf('coin') !== -1 || id === 'coin_coin') return 'owned';
return null;
}
diff --git a/server.js b/server.js
index 6d62707..649b907 100644
--- a/server.js
+++ b/server.js
@@ -151,7 +151,7 @@ function buildDefaultDeck(){
['invisible','Une des pièces devient invisible pour l adversaire'],
// ['épidémie','Toutes les pièces sur le territoire enemie est est capturée'],
// ['glue','Toutes les pièces autour de la pièce désignée ne peuvent pas bouger tant que cette dernière ne bouge pas'],
- // ['coin coin','Possibilité de se téléporter depuis un coin vers n importe quel autre coin'],
+ ["coin coin","Possibilité de se téléporter depuis un coin vers n'importe quel autre coin"],
// ['téléportation','Téléporte n importe quelle pièce de son camp sur une case vide'],
// ['changement de camp','On retourne le plateau'],
// ['ça tangue','Toutes les pièces se décale du même côté'],
@@ -298,6 +298,21 @@ function computeLegalMoves(room, square){
const x = fromCoord.x, y = fromCoord.y;
const type = (piece.type || '').toUpperCase();
+ // If this piece has been granted corner-teleport moves by 'coincoin', include those allowed squares
+ try{
+ const coin = (room.activeCardEffects || []).find(e => e && e.type === 'coincoin' && ((e.pieceId && e.pieceId === piece.id) || e.pieceSquare === square));
+ if(coin && Array.isArray(coin.allowedSquares) && coin.allowedSquares.length > 0){
+ coin.allowedSquares.forEach(sq => {
+ try{
+ // only add empty corner squares (do not allow capture via coincoin)
+ if(!moves.some(m => m.to === sq)){
+ const occ = getPieceAt(sq);
+ if(!occ) moves.push({ from: square, to: sq });
+ }
+ }catch(_){ }
+ });
+ }
+ }catch(_){ }
// detect if this specific piece has a permanent adoubement effect (grants knight moves)
// effects may be bound either to the piece's current square or to the piece id (preferred)
const hasAdoubement = (room.activeCardEffects || []).some(e => e.type === 'adoubement' && ((e.pieceId && e.pieceId === piece.id) || e.pieceSquare === square));
@@ -1307,6 +1322,59 @@ io.on('connection', (socket) => {
}catch(_){ }
}catch(e){ console.error('kamikaz effect error', e); }
}
+ // coin coin: teleport one of your pieces from a corner to another empty corner
+ else if((typeof cardId === 'string' && (cardId.indexOf('coin') !== -1 || cardId.indexOf('coincoin') !== -1)) || cardId === 'coin_coin'){
+ try{
+ const board = room.boardState;
+ let source = payload && payload.targetSquare;
+ if(!source){ try{ source = socket.data && socket.data.lastSelectedSquare; }catch(e){ source = null; } }
+ const roomPlayer = room.players.find(p => p.id === senderId);
+ const playerColorShort = (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null;
+ const piece = (board && board.pieces || []).find(p => p.square === source);
+ // determine corner squares for the current board
+ const w = (board && board.width) || 8;
+ const h = (board && board.height) || 8;
+ const left = 'a';
+ const right = String.fromCharCode('a'.charCodeAt(0) + (w - 1));
+ const corners = [ left + '1', left + String(h), right + '1', right + String(h) ];
+ // validate source exists, belongs to the player and is on a corner
+ if(!board || !source || !piece || piece.color !== playerColorShort || corners.indexOf(source) === -1){
+ // invalid target: restore card to hand and abort
+ 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){ console.error('restore removed card error', e); }
+ return cb && cb({ error: 'no valid corner piece selected' });
+ }
+ // find available destination corners (empty)
+ const emptyCorners = corners.filter(c => { return !(board.pieces || []).some(p => p.square === c); });
+ // remove the source corner from choices
+ const destChoices = emptyCorners.filter(c => c !== source);
+ if(!destChoices || destChoices.length === 0){
+ // nothing to teleport to: 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){ console.error('restore removed card error', e); }
+ return cb && cb({ error: 'no empty destination corner available' });
+ }
+ // instead of teleporting immediately, record an effect that grants this piece the ability
+ // to move to any of the available corner squares for one turn
+ const effect = { id: played.id, type: 'coincoin', playerId: senderId, pieceId: piece.id, pieceSquare: source, allowedSquares: destChoices.slice(0), remainingTurns: 1, ts: Date.now() };
+ room.activeCardEffects = room.activeCardEffects || [];
+ room.activeCardEffects.push(effect);
+ // bump version so clients refresh legal moves when they request them
+ board.version = (board.version || 0) + 1;
+ try{ io.to(room.id).emit('card:effect:applied', { roomId: room.id, effect }); }catch(_){ }
+ played.payload = Object.assign({}, payload, { applied: 'coincoin', from: source, allowed: destChoices.slice(0) });
+ }catch(e){ console.error('coincoin effect 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{