diff --git a/public/game.html b/public/game.html index 3c84a3e..373ae31 100644 --- a/public/game.html +++ b/public/game.html @@ -98,6 +98,8 @@ if(id.indexOf('toucher') !== -1) return 'enemy'; // la parrure: select an enemy queen to downgrade to a pawn if(id.indexOf('parrure') !== -1) return 'enemy_queen'; + // tout ou rien: select a piece (any, except kings) to force it to only move when capturing + if(id.indexOf('tout') !== -1 && id.indexOf('rien') !== -1) return 'piece'; // 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'; @@ -577,6 +579,16 @@ }); pendingCard = null; clearTargetHighlights(); return; } + else if(req === 'piece'){ + const occ = currentBoardState && Array.isArray(currentBoardState.pieces) && currentBoardState.pieces.find(p => p.square === square); + if(!occ){ log('select a piece'); return; } + if(occ.type && occ.type.toLowerCase() === 'k'){ log('cannot select king'); return; } + socket.emit('card:play', { roomId, playerId: myPlayerId, cardId: pendingCard.cardId, payload: { targetSquare: square } }, (resp)=>{ + if(resp && resp.error){ log('card:play error', resp); alert(resp.error); } + else { log('card played with target', resp); } + }); + pendingCard = null; clearTargetHighlights(); return; + } } // If this square contains a move-marker (either clicked marker or square area), attempt the move. @@ -763,6 +775,9 @@ // initial phase: highlight only own pieces (client will switch to enemy after first selection) const occ = (currentBoardState.pieces||[]).find(p=>p.square===sq); if(occ && occ.color === myShort && occ.type && occ.type.toLowerCase() !== 'k') sqEl.classList.add('targetable'); + } else if(type === 'piece'){ + const occ = (currentBoardState.pieces||[]).find(p=>p.square===sq); + if(occ && occ.type && occ.type.toLowerCase() !== 'k') sqEl.classList.add('targetable'); } else if(type === 'enemy_queen'){ const occ = (currentBoardState.pieces||[]).find(p=>p.square===sq); if(occ && occ.color && occ.color !== myShort && occ.type && occ.type.toLowerCase() === 'q') sqEl.classList.add('targetable'); diff --git a/public/styles/style.css b/public/styles/style.css index 6280620..58e2a37 100644 --- a/public/styles/style.css +++ b/public/styles/style.css @@ -218,7 +218,7 @@ button:hover{transform:translateY(-3px);transition:all 180ms ease} .pokemon-hand-counts{display:flex;gap:6px;align-items:center} .opponent-count{background:rgba(11,18,32,0.03);padding:4px 8px;border-radius:8px;font-size:12px} .cards-row{display:flex;gap:12px;overflow-x:auto;padding-bottom:6px} -.pokemon-card{flex:0 0 180px;height:300px;border-radius:12px;background:linear-gradient(180deg,#fff,#f6f8ff);box-shadow:0 10px 24px rgba(11,18,32,0.06);border:1px solid rgba(11,18,32,0.04);display:flex;flex-direction:column;cursor:pointer} +.pokemon-card{flex:0 0 180px;height:350px;border-radius:12px;background:linear-gradient(180deg,#fff,#f6f8ff);box-shadow:0 10px 24px rgba(11,18,32,0.06);border:1px solid rgba(11,18,32,0.04);display:flex;flex-direction:column;cursor:pointer} .pokemon-card-top{flex:0 0 120px;padding:8px} .pokemon-card-art{width:100%;height:100%;background:linear-gradient(135deg,#f0f9ff,#eef2ff);border-radius:8px;display:flex;align-items:center;justify-content:center;color:#475569;font-weight:700} .pokemon-card-mid{padding:8px;flex:1 1 auto;display:flex;flex-direction:column;gap:6px} diff --git a/server.js b/server.js index 181b9cd..56fdb52 100644 --- a/server.js +++ b/server.js @@ -225,7 +225,7 @@ function buildDefaultDeck(){ ['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.'], + ['tout ou rien','Une pièce choisie ne peut maintenant se déplacer que si elle capture.'], // ['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.'], @@ -799,6 +799,19 @@ io.on('connection', (socket) => { } }catch(_){ } + // Enforce 'tout ou rien' restriction: if the moving piece is under 'tout_ou_rien', it may only move if the move captures + try{ + const effects2 = room.activeCardEffects || []; + const tout = effects2.find(e => e && e.type === 'tout_ou_rien' && e.pieceId === moving.id); + if(tout){ + // only allow moves that capture an occupied square + const targetIndexCheck = pieces.findIndex(p => p.square === to); + if(targetIndexCheck === -1){ + return cb && cb({ error: 'must_capture_to_move' }); + } + } + }catch(_){ } + // validate that 'to' is among legal moves const legal = computeLegalMoves(room, from) || []; const ok = legal.some(m => m.to === to); @@ -896,10 +909,22 @@ io.on('connection', (socket) => { // advance board version board.version = (board.version || 0) + 1; + // If the player was granted a free move by playing a card just now (room._freeMoveFor), consume it + // and treat it like a consumed 'double_move' (i.e. do NOT flip the turn). This allows the player to + // perform a single free piece move after playing a card without consuming their main turn. + let consumedDoubleMove = false; + try{ + if(room && room._freeMoveFor && room._freeMoveFor === senderId){ + // consume the free-move token + consumedDoubleMove = true; + try{ delete room._freeMoveFor; }catch(_){ room._freeMoveFor = null; } + try{ io.to(room.id).emit('card:free_move_consumed', { roomId: room.id, playerId: senderId }); }catch(_){ } + } + }catch(e){ /* ignore */ } + // Attempt to consume a double-move effect for the moving player. If present, this allows the player // to make an additional move without flipping the turn. We represent that effect as: // { type: 'double_move', playerId, remainingMoves } - let consumedDoubleMove = false; try{ room.activeCardEffects = room.activeCardEffects || []; for(let i = room.activeCardEffects.length - 1; i >= 0; i--){ @@ -1454,6 +1479,32 @@ io.on('connection', (socket) => { }catch(e){ console.error('parrure apply error', e); } }catch(e){ console.error('parrure effect 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{ + const board = room.boardState; + let target = payload && payload.targetSquare; + if(!target){ try{ target = socket.data && socket.data.lastSelectedSquare; }catch(e){ target = null; } } + const targetPiece = (board && board.pieces || []).find(p => p.square === target); + if(!board || !target || !targetPiece){ + // invalid target: do not restore card (consume) and return error + return cb && cb({ error: 'no valid target' }); + } + // do not allow kings to be affected + if(targetPiece.type && targetPiece.type.toLowerCase() === 'k'){ + // consume card but report invalid + return cb && cb({ error: 'cannot_target_king' }); + } + // find owner of the targeted piece + const targetOwner = (room.players || []).find(p => (p.color && p.color[0]) === targetPiece.color) || null; + room.activeCardEffects = room.activeCardEffects || []; + // Make 'tout_ou_rien' permanent until explicitly removed by another effect + const effect = { id: played.id, type: 'tout_ou_rien', playerId: (targetOwner && targetOwner.id) || null, pieceId: targetPiece.id, pieceSquare: targetPiece.square, imposedBy: senderId, ts: Date.now() }; + room.activeCardEffects.push(effect); + played.payload = Object.assign({}, payload, { applied: 'tout_ou_rien', appliedTo: target }); + try{ io.to(room.id).emit('card:effect:applied', { roomId: room.id, effect }); }catch(_){ } + }catch(e){ console.error('tout_ou_rien effect error', e); } + } // inversion: pick one of your pieces, then pick an enemy piece — swap their squares else if((typeof cardId === 'string' && cardId.indexOf('inversion') !== -1) || cardId === 'inversion'){ try{ @@ -2114,8 +2165,13 @@ io.on('connection', (socket) => { }catch(e){ console.error('mark card played error', e); } // emit card played to entire room (informational) io.to(roomId).emit('card:played', played); - // After playing a card, the player's turn is consumed: perform end-of-turn bookkeeping then send updates - try{ endTurnAfterCard(room, played.playerId); }catch(_){ /* fallback to simple update */ sendRoomUpdate(room); } + // After playing a card, allow the player one free piece move that does NOT consume their turn. + try{ + room._freeMoveFor = played.playerId; // client may use this flag to enable a free move UI + // broadcast updated room state so clients can reflect the free-move opportunity + sendRoomUpdate(room); + try{ io.to(room.id).emit('card:free_move_allowed', { roomId: room.id, playerId: played.playerId }); }catch(_){ } + }catch(e){ /* fallback */ sendRoomUpdate(room); } cb && cb({ ok: true, played }); });