diff --git a/public/game.html b/public/game.html index 755a40c..1c558a1 100644 --- a/public/game.html +++ b/public/game.html @@ -111,6 +111,8 @@ if(id.indexOf('revol') !== -1) return null; // sniper: select one of your pieces to bind the sniper effect (capture later without moving) if(id.indexOf('sniper') !== -1) return 'owned'; + // doppelganger: select one of your pieces after playing the card (minimal flow) + if(id.indexOf('doppel') !== -1) return 'owned'; // 'toucher c'est jouer' : require selecting an enemy piece as the card target if(id.indexOf('toucher') !== -1) return 'enemy'; // la parrure: select an enemy queen to downgrade to a pawn diff --git a/server.js b/server.js index ee5b17f..b669be9 100644 --- a/server.js +++ b/server.js @@ -266,7 +266,7 @@ function buildDefaultDeck(){ // ['tous les mêmes','Au yeux de l ennemie, toutes les pièces se ressemblent pendant 2 tours.'], // ['petit pion','Le joueur choisit un pion. À partir du prochain tour, il est promu en reine dès qu il capture un pièce non pion.'], ['révolution','Tous les pions sont aléatoirement changés en Cavalier, Fou ou Tour et les Cavaliers, Fous et Tours sont changés en pions.'], - // ['doppelganger','Choisis une pièce. À partir de maintenant, devient chacune des pièces qu elle capture.'], + ["doppelganger","Choisis une pièce. À partir de maintenant, devient chacune des pièces qu'elle capture."], // ['kurby','Choisis une pièce. À sa prochaine capture, récupère les mouvements de la pièce capturée.'] ]; function cap(s){ if(!s) return s; s = String(s).trim(); return s.charAt(0).toUpperCase() + s.slice(1); } @@ -905,8 +905,10 @@ io.on('connection', (socket) => { // apply move: remove any piece on target (capture) const targetIndex = pieces.findIndex(p => p.square === to); - // sniper: special-case capture without moving when an active sniper effect is bound to the moving piece - let sniperTriggered = false; + // sniper: special-case capture without moving when an active sniper effect is bound to the moving piece + let sniperTriggered = false; + // remember captured piece for post-capture reactions (like doppelganger) + let capturedPieceForReactions = null; if(targetIndex >= 0){ // check for sniper effect bound to the moving piece for this player try{ @@ -915,6 +917,7 @@ io.on('connection', (socket) => { if(sniperIdx !== -1){ // perform sniper capture: remove target piece and record it for potential resurrection const capturedPiece = pieces.splice(targetIndex, 1)[0]; + capturedPieceForReactions = capturedPiece; try{ room.captured = room.captured || []; const originalOwner = (room.players || []).find(p => (p.color && p.color[0]) === capturedPiece.color); @@ -938,9 +941,10 @@ io.on('connection', (socket) => { try{ io.to(room.id).emit('card:effect:removed', { roomId: room.id, effectId: removedEffect && removedEffect.id, type: 'sniper', playerId: removedEffect && removedEffect.playerId }); }catch(_){ } }catch(_){ } sniperTriggered = true; - } else { + } else { // no sniper: normal capture const capturedPiece = pieces.splice(targetIndex, 1)[0]; + capturedPieceForReactions = capturedPiece; try{ room.captured = room.captured || []; const originalOwner = (room.players || []).find(p => (p.color && p.color[0]) === capturedPiece.color); @@ -963,11 +967,32 @@ io.on('connection', (socket) => { }catch(_){ // fallback: perform normal capture const capturedPiece = pieces.splice(targetIndex, 1)[0]; + capturedPieceForReactions = capturedPiece; try{ room.captured = room.captured || []; const originalOwner = (room.players || []).find(p => (p.color && p.color[0]) === capturedPiece.color); room.captured.push({ id: uuidv4().slice(0,8), piece: capturedPiece, originalOwnerId: (originalOwner && originalOwner.id) || null, capturedBy: senderId, ts: Date.now() }); }catch(_){ } } + } + // If a capture happened, and the moving piece has a doppelganger effect bound, adopt captured type + if(capturedPieceForReactions){ + try{ + room.activeCardEffects = room.activeCardEffects || []; + const doppel = room.activeCardEffects.find(e => e && e.type === 'doppelganger' && e.pieceId === moving.id); + if(doppel && capturedPieceForReactions && typeof capturedPieceForReactions.type !== 'undefined'){ + // change the moving piece's type to the captured piece's type (preserve color) + moving.type = capturedPieceForReactions.type; + if(capturedPieceForReactions.promoted) { + moving.promoted = true; + } else if(moving.promoted){ + try{ delete moving.promoted; }catch(_){ } + } + // update recorded square for the effect so it follows the piece + doppel.pieceSquare = to; + try{ io.to(room.id).emit('card:effect:updated', { roomId: room.id, effect: doppel }); }catch(_){ } + try{ io.to(room.id).emit('card:effect:applied', { roomId: room.id, effect: Object.assign({}, doppel, { appliedType: capturedPieceForReactions.type }) }); }catch(_){ } } - // move the piece only if sniper didn't trigger - if(!sniperTriggered){ moving.square = to; } + }catch(_){ } + } + // move the piece only if sniper didn't trigger + if(!sniperTriggered){ moving.square = to; } // consume any active card effects bound to this piece (rebondir is one-time and should be removed) // also update any persistent effects (like adoubement) to track the piece's new square so they remain active @@ -1378,6 +1403,7 @@ io.on('connection', (socket) => { room._cardPlayedThisTurn = room._cardPlayedThisTurn || {}; if(room._cardPlayedThisTurn[senderId]) return cb && cb({ error: 'card_already_played_this_turn' }); } + }catch(e){ console.error('card play pre-check error', e); } // store played card and apply card effects when applicable const played = { id: uuidv4().slice(0,8), playerId: senderId, cardId, payload, ts: Date.now() }; @@ -1513,6 +1539,38 @@ io.on('connection', (socket) => { played.payload = Object.assign({}, payload, { applied: 'adoubement', appliedTo: target }); }catch(e){ console.error('adoubement effect error', e); } } + // doppelganger (minimal): require selecting one of your pieces when playing the card; record target but do not apply any effect + else if((typeof cardId === 'string' && cardId.indexOf('doppelganger') !== -1) || cardId === 'doppelganger'){ + try{ + const board = room.boardState; + let target = payload && payload.targetSquare; + if(!target){ try{ target = socket.data && socket.data.lastSelectedSquare; }catch(e){ target = null; } } + const roomPlayer = room.players.find(p => p.id === senderId); + const playerColorShort = (roomPlayer && roomPlayer.color && roomPlayer.color[0]) || null; + const targetPiece = (board && board.pieces || []).find(p => p.square === target); + // validate target exists and belongs to the player + if(!board || !target || !targetPiece || targetPiece.color !== playerColorShort){ + // restore removed 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 target' }); + } + // create a persistent doppelganger effect bound to the selected piece + try{ + room.activeCardEffects = room.activeCardEffects || []; + const effect = { id: played.id, type: 'doppelganger', pieceId: targetPiece.id, pieceSquare: target, playerId: senderId }; + room.activeCardEffects.push(effect); + // inform clients that the effect was applied so they can show badges/UI + try{ io.to(room.id).emit('card:effect:applied', { roomId: room.id, effect }); }catch(_){ } + played.payload = Object.assign({}, payload, { applied: 'doppelganger', appliedTo: target }); + }catch(e){ console.error('apply persistent doppelganger error', e); played.payload = Object.assign({}, payload, { applied: 'doppelganger', appliedTo: target }); } + }catch(e){ console.error('doppelganger minimal apply error', e); } + } // folie: grant permanent bishop-move ability to the targeted piece else if(cardId === 'folie' || (typeof cardId === 'string' && cardId.indexOf('folie') !== -1)){ try{