diff --git a/assets/img/cards/kamikaz.png b/assets/img/cards/kamikaze.png similarity index 100% rename from assets/img/cards/kamikaz.png rename to assets/img/cards/kamikaze.png diff --git a/public/game.html b/public/game.html index 46dbe6e..49ce036 100644 --- a/public/game.html +++ b/public/game.html @@ -9,6 +9,18 @@ .square { position: relative; } + /* médusa (petrified) visual */ + .square.petrified::after { + content: ""; + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + background: rgba(120, 120, 160, 0.35); + pointer-events: none; + z-index: 60; + } .square .veil { position: absolute; left: 0; @@ -305,6 +317,7 @@ logs... { try { - if (eff && eff.type === 'toucher') { + if (eff && eff.type === "toucher") { let targetSquare = eff.pieceSquare || null; - if (!targetSquare && eff.pieceId && data.boardState && Array.isArray(data.boardState.pieces)) { - const p = data.boardState.pieces.find((x) => x.id === eff.pieceId); + if ( + !targetSquare && + eff.pieceId && + data.boardState && + Array.isArray(data.boardState.pieces) + ) { + const p = data.boardState.pieces.find( + (x) => x.id === eff.pieceId, + ); if (p && p.square) targetSquare = p.square; } if (targetSquare && boardEl) { - const el = boardEl.querySelector(`.square[data-square="${targetSquare}"]`); + const el = boardEl.querySelector( + `.square[data-square="${targetSquare}"]`, + ); if (el) { - el.classList.add('targeted'); - try { el.dataset.toucherEffectId = eff.id || ''; } catch(_) {} + el.classList.add("targeted"); + try { + el.dataset.toucherEffectId = eff.id || ""; + } catch (_) {} } } } - } catch(_) {} + } catch (_) {} }); } - } catch(_) {} + } catch (_) {} // restore last-move highlight from localStorage (persist across refresh) try { if (roomId) { @@ -416,21 +444,35 @@ logs... x && x.square === lm.to); + if ( + currentBoardState && + Array.isArray(currentBoardState.pieces) + ) { + showDest = currentBoardState.pieces.some( + (x) => x && x.square === lm.to, + ); } if (showDest && dest) { dest.classList.add("last-move-dest"); } } catch (_) {} - const fromEl = boardEl.querySelector(`.square[data-square="${lm.from}"]`); + const fromEl = boardEl.querySelector( + `.square[data-square="${lm.from}"]`, + ); try { let showSource = false; - if (currentBoardState && Array.isArray(currentBoardState.pieces)) { - showSource = currentBoardState.pieces.some((x) => x && x.square === lm.from); + if ( + currentBoardState && + Array.isArray(currentBoardState.pieces) + ) { + showSource = currentBoardState.pieces.some( + (x) => x && x.square === lm.from, + ); } if (showSource && fromEl) { fromEl.classList.add("last-move-source"); @@ -601,20 +643,60 @@ logs... x.id === effect.pieceId); + if ( + !targetSquare && + effect.pieceId && + currentBoardState && + Array.isArray(currentBoardState.pieces) + ) { + const p = currentBoardState.pieces.find( + (x) => x.id === effect.pieceId, + ); if (p && p.square) targetSquare = p.square; } if (targetSquare && boardEl) { - const el = boardEl.querySelector(`.square[data-square="${targetSquare}"]`); + const el = boardEl.querySelector( + `.square[data-square="${targetSquare}"]`, + ); if (el) { el.classList.add("targeted"); - try { el.dataset.toucherEffectId = effect.id || ""; } catch(_) {} + try { + el.dataset.toucherEffectId = effect.id || ""; + } catch (_) {} + } + } + } + } catch (e) {} + try { + if (effect.type === "medusa") { + const sq = effect.pieceSquare || null; + let targetSquare = sq; + if ( + !targetSquare && + effect.pieceId && + currentBoardState && + Array.isArray(currentBoardState.pieces) + ) { + const p = currentBoardState.pieces.find( + (x) => x.id === effect.pieceId, + ); + if (p && p.square) targetSquare = p.square; + } + if (targetSquare && boardEl) { + const el = boardEl.querySelector( + `.square[data-square="${targetSquare}"]`, + ); + if (el) { + el.classList.add("petrified"); + try { + el.dataset.medusaEffectId = effect.id || ""; + } catch (_) {} } } } @@ -640,37 +722,78 @@ logs... { try { const effId = data && (data.effectId || data.id); const type = data && data.type; if (!effId) return; - if (type === "toucher") { + if (type === "toucher" || type === "medusa_target") { // find any square with matching dataset.toucherEffectId and remove the visual try { - const sq = boardEl && boardEl.querySelector && boardEl.querySelector(`.square[data-toucher-effect-id][data-toucher-effect-id="${effId}"]`); + const sq = + boardEl && + boardEl.querySelector && + boardEl.querySelector( + `.square[data-toucher-effect-id][data-toucher-effect-id="${effId}"]`, + ); if (sq) { sq.classList.remove("last-move-source"); sq.classList.remove("targeted"); - try { delete sq.dataset.toucherEffectId; } catch(_) {} + try { + delete sq.dataset.toucherEffectId; + } catch (_) {} } else { // fallback: remove any .last-move-source or .targeted if its dataset matches - const all = boardEl.querySelectorAll('.square.last-move-source, .square.targeted'); + const all = boardEl.querySelectorAll( + ".square.last-move-source, .square.targeted", + ); all.forEach((s) => { try { - if (s.dataset && s.dataset.toucherEffectId === String(effId)) { - s.classList.remove('last-move-source'); - s.classList.remove('targeted'); + if ( + s.dataset && + s.dataset.toucherEffectId === String(effId) + ) { + s.classList.remove("last-move-source"); + s.classList.remove("targeted"); delete s.dataset.toucherEffectId; } - } catch(_) {} + } catch (_) {} + }); + } + } catch (_) {} + } + if (type === "medusa") { + try { + const sq = + boardEl && + boardEl.querySelector && + boardEl.querySelector( + `.square[data-medusa-effect-id][data-medusa-effect-id="${effId}"]`, + ); + if (sq) { + sq.classList.remove("petrified"); + try { + delete sq.dataset.medusaEffectId; + } catch (_) {} + } else { + const all = boardEl.querySelectorAll(".square.petrified"); + all.forEach((s) => { + try { + if ( + s.dataset && + s.dataset.medusaEffectId === String(effId) + ) { + s.classList.remove("petrified"); + delete s.dataset.medusaEffectId; + } + } catch (_) {} }); } } catch (_) {} } } catch (e) { - console.warn('card:effect:removed handler error', e); + console.warn("card:effect:removed handler error", e); } }); // when the server sends the stolen card to the stealer (private) @@ -781,42 +904,69 @@ logs... { el.classList.remove("last-move-dest"); el.classList.remove("last-move-source"); }); } catch (_) {} try { - const prevCircles = boardEl.querySelectorAll(".last-move-circle"); + const prevCircles = + boardEl.querySelectorAll(".last-move-circle"); prevCircles.forEach((c) => c.remove()); } catch (_) {} - if (data && data.boardState) renderBoardFromState(data.boardState); + if (data && data.boardState) + renderBoardFromState(data.boardState); if (data && data.boardState) updateTurnBanner(data.boardState); try { if (data && data.from && data.to && data.from !== data.to) { - const dest = boardEl.querySelector(`.square[data-square="${data.to}"]`); - const fromEl = boardEl.querySelector(`.square[data-square="${data.from}"]`); + const dest = boardEl.querySelector( + `.square[data-square="${data.to}"]`, + ); + const fromEl = boardEl.querySelector( + `.square[data-square="${data.from}"]`, + ); let showSource = false; try { - if (currentBoardState && Array.isArray(currentBoardState.pieces)) { - showSource = currentBoardState.pieces.some((x) => x && x.square === data.from); + if ( + currentBoardState && + Array.isArray(currentBoardState.pieces) + ) { + showSource = currentBoardState.pieces.some( + (x) => x && x.square === data.from, + ); } } catch (_) { showSource = false; @@ -825,8 +975,13 @@ logs... x && x.square === data.to); + if ( + currentBoardState && + Array.isArray(currentBoardState.pieces) + ) { + showDest = currentBoardState.pieces.some( + (x) => x && x.square === data.to, + ); } } catch (_) { showDest = false; @@ -834,10 +989,22 @@ logs... c.remove()); } catch (_) {} // clear square contents (pieces/markers) @@ -2068,6 +2245,7 @@ logs... e && e.type === "medusa" && (e.pieceId === piece.id || e.pieceSquare === square), + ); + if (medusa) return []; + } catch (_) {} const color = piece.color; const fromCoord = squareToCoord(square); if (!fromCoord) return []; @@ -1483,6 +1494,24 @@ io.on("connection", (socket) => { } } catch (_) {} + // médusa + try { + const effects = room.activeCardEffects || []; + const medusa = effects.find( + (e) => e && e.type === "medusa" && e.pieceId === moving.id, + ); + if (medusa) { + return ( + cb && + cb({ + error: "medusa_immobilization", + message: + "Mouvement impossible : cette pièce est pétrifiée.", + }) + ); + } + } catch (_) {} + // tout ou rien try { const effects2 = room.activeCardEffects || []; @@ -1594,7 +1623,7 @@ io.on("connection", (socket) => { const ev = room.activeCardEffects[ei]; if (!ev) continue; if ( - ev.type === "invisible" && + (ev.type === "invisible" || ev.type === "medusa") && (ev.pieceId === capturedPiece.id || ev.pieceSquare === capturedPiece.square) ) { @@ -1654,7 +1683,7 @@ io.on("connection", (socket) => { const ev = room.activeCardEffects[ei]; if (!ev) continue; if ( - ev.type === "invisible" && + (ev.type === "invisible" || ev.type === "medusa") && (ev.pieceId === capturedPiece.id || ev.pieceSquare === capturedPiece.square) ) { @@ -3116,6 +3145,103 @@ io.on("connection", (socket) => { } } + // médusa + else if (cardId === "medusa") { + 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, + ); + if ( + !board || + !target || + !targetPiece || + targetPiece.color === playerColorShort || + (targetPiece.type && String(targetPiece.type).toLowerCase() === "k") + ) { + 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) {} + return ( + cb && + cb({ error: "no valid target", message: "Aucune cible valide n'a été sélectionnée." }) + ); + } + room.activeCardEffects = room.activeCardEffects || []; + const effect = { + id: played.id, + type: "medusa", + playerId: senderId, + pieceId: targetPiece.id, + pieceSquare: targetPiece.square, + remainingTurns: 4, + decrementOn: "owner", + imposedBy: senderId, + ts: Date.now(), + }; + room.activeCardEffects.push(effect); + // Also add a one-turn visual marker effect so clients show .targeted like 'toucher' + try { + const marker = { + id: uuidv4().slice(0, 8), + type: "medusa_target", + playerId: (room.players || []).find((p) => (p.color && p.color[0]) === targetPiece.color)?.id || null, + pieceId: targetPiece.id, + pieceSquare: targetPiece.square, + remainingTurns: 1, + decrementOn: "owner", + imposedBy: senderId, + ts: Date.now(), + }; + room.activeCardEffects.push(marker); + try { + io.to(room.id).emit("card:effect:applied", { + roomId: room.id, + effect: marker, + }); + } catch (_) {} + } catch (_) {} + played.payload = Object.assign({}, payload, { + applied: "medusa", + appliedTo: target, + }); + try { + io.to(room.id).emit("card:effect:applied", { + roomId: room.id, + effect, + }); + } catch (_) {} + try { + sendRoomUpdate(room); + } catch (_) {} + } catch (e) { + console.error("medusa effect error", e); + } + } + // parrure else if (cardId === "parrure") { try {