From dd12b2c1a45028567d7f7f1b7399207471106d55 Mon Sep 17 00:00:00 2001 From: Didictateur Date: Sun, 23 Nov 2025 15:42:08 +0100 Subject: [PATCH] sniper --- public/game.html | 2 + server.js | 114 +++++++++++++++++++++++++++++++++++++---------- 2 files changed, 92 insertions(+), 24 deletions(-) diff --git a/public/game.html b/public/game.html index 3fab29d..9d2f2e8 100644 --- a/public/game.html +++ b/public/game.html @@ -96,6 +96,8 @@ if(id.indexOf('mine') !== -1) return 'empty'; // revolution is a global immediate effect: it should NOT require any piece selection 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'; // '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 f768a5a..cd90ceb 100644 --- a/server.js +++ b/server.js @@ -214,7 +214,7 @@ function buildDefaultDeck(){ // ['réinitialisation','Toutes les pièces reviennent à leur position initiale. S il y a des pièces supplémenaires, se rangent devant les pions'], ["toucher c'est jouer","Toucher une pièce adverse qu'il sera obligé de jouer"], // ['marécage','Pendant X tours, toutes les pièces ne peuvent se déplacer que comme un roi'], - // ['sniper','Capturer une pièce sans avoir à bouger la pièce capturante'], + ['sniper','Capturer une pièce sans avoir à bouger la pièce capturante'], ['inversion',"Échange la position d'une pièce avec une pièce adverse.\n\nCompte comme un mouvement"], // ['jeu des 7 différences','Déplace une pièce du plateau pendant que le joueur adverse à les yeux fermés. S il la retrouve, elle est capturée, laissée sinon'], // ['punching ball','Replace le roi dans sa position initiale, et place un nouveau pion à l ancienne position du roi'], @@ -827,34 +827,69 @@ 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; if(targetIndex >= 0){ - // remove captured piece and record it for potential resurrection - const capturedPiece = pieces.splice(targetIndex, 1)[0]; - 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() }); - // ensure the captured piece object no longer carries the 'invisible' flag so it won't - // incorrectly reappear invisible if resurrected or inspected elsewhere - try{ if(capturedPiece && capturedPiece.invisible) delete capturedPiece.invisible; }catch(_){ } - }catch(_){ /* ignore bookkeeping errors */ } - // If there were any 'invisible' effects targeting the captured piece or its square, - // remove them so the square does not remain hidden for non-owners when another piece arrives. + // check for sniper effect bound to the moving piece for this player try{ room.activeCardEffects = room.activeCardEffects || []; - for(let ei = room.activeCardEffects.length - 1; ei >= 0; ei--){ - const ev = room.activeCardEffects[ei]; - if(!ev) continue; - if(ev.type === 'invisible' && (ev.pieceId === capturedPiece.id || ev.pieceSquare === capturedPiece.square)){ - // remove effect and notify clients - try{ room.activeCardEffects.splice(ei,1); }catch(_){ } - try{ io.to(room.id).emit('card:effect:removed', { roomId: room.id, effectId: ev.id, type: ev.type, playerId: ev.playerId }); }catch(_){ } - } + const sniperIdx = room.activeCardEffects.findIndex(e => e && e.type === 'sniper' && e.pieceId === moving.id && e.playerId === senderId); + if(sniperIdx !== -1){ + // perform sniper capture: remove target piece and record it for potential resurrection + const capturedPiece = pieces.splice(targetIndex, 1)[0]; + 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() }); + try{ if(capturedPiece && capturedPiece.invisible) delete capturedPiece.invisible; }catch(_){ } + }catch(_){ /* ignore bookkeeping errors */ } + // remove any invisible effects targeting the captured piece/square + try{ + for(let ei = room.activeCardEffects.length - 1; ei >= 0; ei--){ + const ev = room.activeCardEffects[ei]; + if(!ev) continue; + if(ev.type === 'invisible' && (ev.pieceId === capturedPiece.id || ev.pieceSquare === capturedPiece.square)){ + try{ room.activeCardEffects.splice(ei,1); }catch(_){ } + try{ io.to(room.id).emit('card:effect:removed', { roomId: room.id, effectId: ev.id, type: ev.type, playerId: ev.playerId }); }catch(_){ } + } + } + }catch(_){ } + // consume the sniper effect (one-time) + try{ + const removedEffect = room.activeCardEffects.splice(sniperIdx, 1)[0]; + 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 { + // no sniper: normal capture + const capturedPiece = pieces.splice(targetIndex, 1)[0]; + 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() }); + try{ if(capturedPiece && capturedPiece.invisible) delete capturedPiece.invisible; }catch(_){ } + }catch(_){ /* ignore bookkeeping errors */ } + // If there were any 'invisible' effects targeting the captured piece or its square, + // remove them so the square does not remain hidden for non-owners when another piece arrives. + try{ + for(let ei = room.activeCardEffects.length - 1; ei >= 0; ei--){ + const ev = room.activeCardEffects[ei]; + if(!ev) continue; + if(ev.type === 'invisible' && (ev.pieceId === capturedPiece.id || ev.pieceSquare === capturedPiece.square)){ + try{ room.activeCardEffects.splice(ei,1); }catch(_){ } + try{ io.to(room.id).emit('card:effect:removed', { roomId: room.id, effectId: ev.id, type: ev.type, playerId: ev.playerId }); }catch(_){ } + } + } + }catch(_){ } } - }catch(_){ } + }catch(_){ + // fallback: perform normal capture + const capturedPiece = pieces.splice(targetIndex, 1)[0]; + 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(_){ } + } } - // move the piece - moving.square = to; + // 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 @@ -1488,6 +1523,37 @@ io.on('connection', (socket) => { }catch(e){ console.error('parrure apply error', e); } }catch(e){ console.error('parrure effect error', e); } } + // sniper: bind a one-time sniper effect to one of your pieces. + // After playing this card the owner selects one of their pieces; the effect is recorded and + // when that piece later makes a capturing move, the capture is performed without moving the capturer. + else if((typeof cardId === 'string' && cardId.indexOf('sniper') !== -1) || cardId === 'sniper'){ + try{ + const board = room.boardState; + // client may send selected piece in payload.targetSquare (legacy owned selection) or payload.sourceSquare + let source = (payload && payload.sourceSquare) || (payload && payload.targetSquare) || null; + 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 srcPiece = (board && board.pieces || []).find(p => p.square === source); + if(!board || !source || !srcPiece || srcPiece.color !== playerColorShort){ + // 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(_){ } + return cb && cb({ error: 'no valid source selected' }); + } + // bind sniper effect to the piece id (one-time use) + room.activeCardEffects = room.activeCardEffects || []; + const effect = { id: played.id, type: 'sniper', playerId: senderId, pieceId: srcPiece.id, pieceSquare: srcPiece.square, remainingUses: 1, imposedBy: senderId, ts: Date.now() }; + room.activeCardEffects.push(effect); + played.payload = Object.assign({}, payload, { applied: 'sniper_bound', appliedTo: source }); + try{ io.to(room.id).emit('card:effect:applied', { roomId: room.id, effect }); }catch(_){ } + }catch(e){ console.error('sniper binding error', e); } + } // tout ou rien: choose a piece; that piece may only move if it captures (one owner turn) else if((typeof cardId === 'string' && cardId.indexOf('tout') !== -1 && cardId.indexOf('rien') !== -1) || cardId === 'tout_ou_rien'){ try{