From c9b8588c4fb4c241dbb0663f4f07362279a49d53 Mon Sep 17 00:00:00 2001 From: Didictateur Date: Sat, 22 Nov 2025 22:07:55 +0100 Subject: [PATCH] vole d'une carte --- public/game.html | 72 ++++++++++++++++++++++++++++++++++++++++++++++-- server.js | 42 ++++++++++++++++++++++++++-- 2 files changed, 108 insertions(+), 6 deletions(-) diff --git a/public/game.html b/public/game.html index 81bd40a..a71df20 100644 --- a/public/game.html +++ b/public/game.html @@ -68,7 +68,7 @@ } } - // helper: decide if a card requires a target selected after clicking 'Jouer' + // helper: decide if a card requires a target selected after clicking 'Jouer' function cardRequiresTarget(cardId){ if(!cardId) return null; const id = String(cardId).toLowerCase(); @@ -79,8 +79,11 @@ 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/vol card targets an enemy piece - if(id.indexOf('vol') !== -1 || id.indexOf('vole') !== -1 || id.indexOf('steal') !== -1) return 'enemy'; + // 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'; + } return null; } @@ -178,6 +181,22 @@ } }catch(e){ console.warn('card:effect:applied handler error', e); } }); + // when the server sends the stolen card to the stealer (private) + socket.on('card:stolen', (data) => { + try{ + log('recv card:stolen', data); + // show notification and a small highlight in hands + const handsEl = document.getElementById('hands'); + if(handsEl){ + const note = document.createElement('div'); note.className = 'card-draw-note'; + note.textContent = data && data.card ? `Vous avez volé : ${data.card.title || data.card.cardId || 'une carte'}` : 'Vous avez volé une carte'; + handsEl.prepend(note); setTimeout(()=> note.remove(), 6000); + } + }catch(e){ console.warn('card:stolen handler error', e); } + }); + socket.on('card:lost', (data) => { + try{ log('recv card:lost', data); showEffectNotification({ type: 'card_lost' }); }catch(e){ } + }); // selection events (include moves) broadcasted by server socket.on('game:select', (data) => { try{ @@ -756,6 +775,27 @@ const cid = cardEl.dataset.cardId; const requireType = cardRequiresTarget(cid); if(requireType){ + if(requireType === 'player'){ + // If there's only one opponent, auto-select them (no modal needed). Otherwise show modal. + const opponents = Object.keys(handCounts || {}).filter(pid => pid !== myPlayerId); + if(opponents.length === 1){ + const targetPlayerId = opponents[0]; + socket.emit('card:play', { roomId, playerId: myPlayerId, cardId: cid, payload: { targetPlayerId } }, (resp)=>{ + if(resp && resp.error){ log('card:play error', resp); alert(resp.error); } + else { log('card played (steal card)', resp); } + }); + return; + } + // show modal to pick which player to steal from + showStealModal(handCounts, (targetPlayerId)=>{ + if(!targetPlayerId) return; // canceled + socket.emit('card:play', { roomId, playerId: myPlayerId, cardId: cid, payload: { targetPlayerId } }, (resp)=>{ + if(resp && resp.error){ log('card:play error', resp); alert(resp.error); } + else { log('card played (steal card)', resp); } + }); + }); + return; + } // enter pending target mode: next square click will send the card with payload.targetSquare pendingCard = { cardId: cid, cardUuid: cardEl.dataset.cardUuid || '', requireType }; highlightTargets(requireType); @@ -805,6 +845,7 @@ else if(effect.type === 'promotion') note.textContent = 'Promotion réussie !'; else if(effect.type === 'mine_failed') note.textContent = 'Placement de mine échoué — carte consommée'; else if(effect.type === 'steal_failed') note.textContent = 'Vol échoué — carte consommée'; + else if(effect.type === 'card_lost') note.textContent = 'Une de vos cartes a été volée'; else note.textContent = `Effet: ${effect.type}`; note.style.padding = '6px 8px'; note.style.border = '1px solid #ccc'; note.style.background = '#fff8e1'; note.style.marginTop = '8px'; handsEl.prepend(note); @@ -861,6 +902,31 @@ document.body.appendChild(modal); }catch(e){ console.warn('showPromotionModal error', e); callback(null); } } + + // show a modal to pick a player to steal from. handCounts is an object mapping playerId -> count + function showStealModal(handCounts, callback){ + try{ + if(document.getElementById('stealModal')) return; + const modal = document.createElement('div'); modal.id = 'stealModal'; + modal.style.position = 'fixed'; modal.style.left='0'; modal.style.top='0'; modal.style.right='0'; modal.style.bottom='0'; + modal.style.display='flex'; modal.style.alignItems='center'; modal.style.justifyContent='center'; modal.style.background='rgba(0,0,0,0.45)'; modal.style.zIndex='9999'; + const box = document.createElement('div'); box.style.background='#fff'; box.style.padding='12px'; box.style.borderRadius='8px'; box.style.minWidth='320px'; + const title = document.createElement('div'); title.textContent = 'Choisir le joueur à voler'; title.style.fontWeight='700'; title.style.marginBottom='8px'; box.appendChild(title); + const list = document.createElement('div'); list.style.display='flex'; list.style.flexDirection='column'; list.style.gap='8px'; list.style.marginBottom='8px'; + Object.entries(handCounts || {}).forEach(([pid, count])=>{ + if(pid === myPlayerId) return; // skip self + const row = document.createElement('div'); row.style.display='flex'; row.style.justifyContent='space-between'; row.style.alignItems='center'; + const label = document.createElement('div'); label.textContent = `${pid} — ${count} carte(s)`; label.style.fontWeight='600'; + const btn = document.createElement('button'); btn.type='button'; btn.textContent = 'Voler'; btn.style.padding='6px 10px'; + btn.addEventListener('click', (ev)=>{ ev.stopPropagation(); try{ modal.remove(); }catch(_){ } callback(pid); }); + row.appendChild(label); row.appendChild(btn); list.appendChild(row); + }); + box.appendChild(list); + const cancel = document.createElement('button'); cancel.type='button'; cancel.textContent='Annuler'; cancel.style.padding='8px 10px'; cancel.addEventListener('click',(ev)=>{ ev.stopPropagation(); try{ modal.remove(); }catch(_){ } callback(null); }); + box.appendChild(cancel); + modal.appendChild(box); document.body.appendChild(modal); + }catch(e){ console.warn('showStealModal error', e); callback(null); } + } diff --git a/server.js b/server.js index 66bcc5a..a6e347a 100644 --- a/server.js +++ b/server.js @@ -108,7 +108,8 @@ function buildDefaultDeck(){ // ['annulation d une carte','Annule l effet d une carte qui est jouée par l adversaire'], ['placement de mines','Le joueur place une mine sur une case vide sans la révéler au joueur adverse. Une pièce qui se pose dessus explose et est capturée par le joueur ayant placé la mine'], ['vole d une pièce','Désigne une pièce non roi qui change de camp'], - ['promotion','Un pion au choix est promu reine'], + ['promotion','Un pion au choix est promu'], + ['vole d une carte','Vole une carte aléatoirement au joueur adverse'], // ['vole d une carte','Vole une carte aléatoirement au joueur adverse'], // ['resurection','Choisie une pièce capturée pour la ressuciter dans son camp'], // ['carte sans effet','N a aucun effet'], @@ -1234,8 +1235,9 @@ io.on('connection', (socket) => { played.payload = Object.assign({}, payload, { applied: 'double_move', moves: effect.remainingMoves }); }catch(e){ console.error('double_move effect error', e); } } - // vol de pièce: transfer ownership of a targeted enemy piece to the playing player - else if((typeof cardId === 'string' && (cardId.indexOf('vol') !== -1 || cardId.indexOf('vole') !== -1 || cardId.indexOf('steal') !== -1))){ + // vol de pièce: transfer ownership of a targeted enemy piece to the playing player + // Note: do NOT handle "vole ... carte" here (steal-a-card) — that is handled by a separate branch below. + else if((typeof cardId === 'string' && (cardId.indexOf('vol') !== -1 || cardId.indexOf('vole') !== -1 || cardId.indexOf('steal') !== -1) && !(cardId.indexOf('carte') !== -1 || cardId.indexOf('card') !== -1))){ try{ const board = room.boardState; let target = payload && payload.targetSquare; @@ -1266,6 +1268,40 @@ io.on('connection', (socket) => { } }catch(e){ console.error('steal effect error', e); } } + // vole d'une carte: steal one random card from the target player's hand + else if((typeof cardId === 'string' && cardId.indexOf('vole') !== -1 && (cardId.indexOf('carte') !== -1 || cardId.indexOf('card') !== -1))){ + try{ + // determine target player id: payload.targetPlayerId or pick the opponent + let targetPlayerId = payload && payload.targetPlayerId; + if(!targetPlayerId){ const opp = (room.players || []).find(p => p.id !== senderId); targetPlayerId = opp && opp.id; } + const targetPlayer = (room.players || []).find(p => p.id === targetPlayerId); + if(!targetPlayer || targetPlayer.id === senderId){ + // invalid target: consume card and notify owner + played.payload = Object.assign({}, payload, { applied: 'steal_card_failed', attemptedTo: targetPlayerId }); + try{ const owner = (room.players || []).find(p => p.id === senderId); if(owner && owner.socketId) io.to(owner.socketId).emit('card:effect:applied', { roomId: room.id, effect: { id: played.id, type: 'steal_card_failed', playerId: senderId, targetPlayerId, ts: Date.now() } }); }catch(_){ } + } else { + room.hands = room.hands || {}; + const victimHand = room.hands[targetPlayerId] || []; + if(!victimHand || victimHand.length === 0){ + // nothing to steal + played.payload = Object.assign({}, payload, { applied: 'steal_card_failed_empty', attemptedTo: targetPlayerId }); + try{ const owner = (room.players || []).find(p => p.id === senderId); if(owner && owner.socketId) io.to(owner.socketId).emit('card:effect:applied', { roomId: room.id, effect: { id: played.id, type: 'steal_card_failed_empty', playerId: senderId, targetPlayerId, ts: Date.now() } }); }catch(_){ } + } else { + // pick random card from victim + const idx = Math.floor(Math.random() * victimHand.length); + const stolen = victimHand.splice(idx,1)[0]; + // give to stealer + room.hands[senderId] = room.hands[senderId] || []; + room.hands[senderId].push(stolen); + played.payload = Object.assign({}, payload, { applied: 'steal_card', stolenFrom: targetPlayerId, stolenCardId: stolen.cardId || stolen.id }); + // inform the stealer privately about the stolen card details + try{ const stealer = (room.players || []).find(p => p.id === senderId); if(stealer && stealer.socketId) io.to(stealer.socketId).emit('card:stolen', { roomId: room.id, from: targetPlayerId, card: stolen }); }catch(_){ } + // inform the victim privately that they lost a card (do not reveal which) + 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); } + } // rebondir: grant a one-time bounce ability to a specific piece (targetSquare required in payload) else if(cardId === 'rebondir_sur_les_bords' || cardId === 'rebondir' || (typeof cardId === 'string' && cardId.indexOf('rebondir') !== -1)){ try{