carte médusa

This commit is contained in:
Didictateur 2025-12-08 12:34:20 +01:00
parent be03e157a5
commit 05c51584ec
6 changed files with 1223 additions and 263 deletions

View file

Before

Width:  |  Height:  |  Size: 227 KiB

After

Width:  |  Height:  |  Size: 227 KiB

View file

@ -9,6 +9,18 @@
.square { .square {
position: relative; 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 { .square .veil {
position: absolute; position: absolute;
left: 0; left: 0;
@ -305,6 +317,7 @@ logs...</pre
if (id.indexOf("sniper") !== -1) return "owned"; if (id.indexOf("sniper") !== -1) return "owned";
if (id.indexOf("doppel") !== -1) return "owned"; if (id.indexOf("doppel") !== -1) return "owned";
if (id.indexOf("toucher") !== -1) return "enemy"; if (id.indexOf("toucher") !== -1) return "enemy";
if (id.indexOf("medusa") !== -1) return "enemy";
if (id.indexOf("parrure") !== -1) return "enemy_queen"; if (id.indexOf("parrure") !== -1) return "enemy_queen";
if (id.indexOf("tout") !== -1) return "piece"; if (id.indexOf("tout") !== -1) return "piece";
if (id.indexOf("kamikaz") !== -1) return "owned"; if (id.indexOf("kamikaz") !== -1) return "owned";
@ -349,7 +362,8 @@ logs...</pre
// ensure board orientation follows the (possibly changed) player color // ensure board orientation follows the (possibly changed) player color
try { try {
if (boardEl) { if (boardEl) {
if (myColor === "black") boardEl.classList.add("flipped"); if (myColor === "black")
boardEl.classList.add("flipped");
else boardEl.classList.remove("flipped"); else boardEl.classList.remove("flipped");
} }
} catch (_) {} } catch (_) {}
@ -386,27 +400,41 @@ logs...</pre
updateTurnBanner(data.boardState); updateTurnBanner(data.boardState);
// re-apply any active 'toucher' effects sent by the server (useful after refresh/reconnect) // re-apply any active 'toucher' effects sent by the server (useful after refresh/reconnect)
try { try {
if (Array.isArray(data.activeCardEffects) && data.activeCardEffects.length) { if (
Array.isArray(data.activeCardEffects) &&
data.activeCardEffects.length
) {
data.activeCardEffects.forEach((eff) => { data.activeCardEffects.forEach((eff) => {
try { try {
if (eff && eff.type === 'toucher') { if (eff && eff.type === "toucher") {
let targetSquare = eff.pieceSquare || null; let targetSquare = eff.pieceSquare || null;
if (!targetSquare && eff.pieceId && data.boardState && Array.isArray(data.boardState.pieces)) { if (
const p = data.boardState.pieces.find((x) => x.id === eff.pieceId); !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 (p && p.square) targetSquare = p.square;
} }
if (targetSquare && boardEl) { if (targetSquare && boardEl) {
const el = boardEl.querySelector(`.square[data-square="${targetSquare}"]`); const el = boardEl.querySelector(
`.square[data-square="${targetSquare}"]`,
);
if (el) { if (el) {
el.classList.add('targeted'); el.classList.add("targeted");
try { el.dataset.toucherEffectId = eff.id || ''; } catch(_) {} try {
el.dataset.toucherEffectId = eff.id || "";
} catch (_) {}
} }
} }
} }
} catch(_) {} } catch (_) {}
}); });
} }
} catch(_) {} } catch (_) {}
// restore last-move highlight from localStorage (persist across refresh) // restore last-move highlight from localStorage (persist across refresh)
try { try {
if (roomId) { if (roomId) {
@ -416,21 +444,35 @@ logs...</pre
const lm = JSON.parse(raw); const lm = JSON.parse(raw);
if (lm && lm.to && lm.from && lm.to !== lm.from) { if (lm && lm.to && lm.from && lm.to !== lm.from) {
// apply visuals (renderBoardFromState cleared old highlights) // apply visuals (renderBoardFromState cleared old highlights)
const dest = boardEl.querySelector(`.square[data-square="${lm.to}"]`); const dest = boardEl.querySelector(
`.square[data-square="${lm.to}"]`,
);
try { try {
let showDest = false; let showDest = false;
if (currentBoardState && Array.isArray(currentBoardState.pieces)) { if (
showDest = currentBoardState.pieces.some((x) => x && x.square === lm.to); currentBoardState &&
Array.isArray(currentBoardState.pieces)
) {
showDest = currentBoardState.pieces.some(
(x) => x && x.square === lm.to,
);
} }
if (showDest && dest) { if (showDest && dest) {
dest.classList.add("last-move-dest"); dest.classList.add("last-move-dest");
} }
} catch (_) {} } catch (_) {}
const fromEl = boardEl.querySelector(`.square[data-square="${lm.from}"]`); const fromEl = boardEl.querySelector(
`.square[data-square="${lm.from}"]`,
);
try { try {
let showSource = false; let showSource = false;
if (currentBoardState && Array.isArray(currentBoardState.pieces)) { if (
showSource = currentBoardState.pieces.some((x) => x && x.square === lm.from); currentBoardState &&
Array.isArray(currentBoardState.pieces)
) {
showSource = currentBoardState.pieces.some(
(x) => x && x.square === lm.from,
);
} }
if (showSource && fromEl) { if (showSource && fromEl) {
fromEl.classList.add("last-move-source"); fromEl.classList.add("last-move-source");
@ -601,20 +643,60 @@ logs...</pre
// public notable effects // public notable effects
showEffectNotification(effect); showEffectNotification(effect);
} }
// toucher
// toucher + médusa
try { try {
if (effect.type === "toucher") { if (effect.type === "toucher" || effect.type === "medusa_target") {
const sq = effect.pieceSquare || null; const sq = effect.pieceSquare || null;
let targetSquare = sq; let targetSquare = sq;
if (!targetSquare && effect.pieceId && currentBoardState && Array.isArray(currentBoardState.pieces)) { if (
const p = currentBoardState.pieces.find((x) => x.id === effect.pieceId); !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 (p && p.square) targetSquare = p.square;
} }
if (targetSquare && boardEl) { if (targetSquare && boardEl) {
const el = boardEl.querySelector(`.square[data-square="${targetSquare}"]`); const el = boardEl.querySelector(
`.square[data-square="${targetSquare}"]`,
);
if (el) { if (el) {
el.classList.add("targeted"); 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...</pre
} }
}); });
// toucher // toucher / medusa removals
socket.on("card:effect:removed", (data) => { socket.on("card:effect:removed", (data) => {
try { try {
const effId = data && (data.effectId || data.id); const effId = data && (data.effectId || data.id);
const type = data && data.type; const type = data && data.type;
if (!effId) return; if (!effId) return;
if (type === "toucher") { if (type === "toucher" || type === "medusa_target") {
// find any square with matching dataset.toucherEffectId and remove the visual // find any square with matching dataset.toucherEffectId and remove the visual
try { 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) { if (sq) {
sq.classList.remove("last-move-source"); sq.classList.remove("last-move-source");
sq.classList.remove("targeted"); sq.classList.remove("targeted");
try { delete sq.dataset.toucherEffectId; } catch(_) {} try {
delete sq.dataset.toucherEffectId;
} catch (_) {}
} else { } else {
// fallback: remove any .last-move-source or .targeted if its dataset matches // 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) => { all.forEach((s) => {
try { try {
if (s.dataset && s.dataset.toucherEffectId === String(effId)) { if (
s.classList.remove('last-move-source'); s.dataset &&
s.classList.remove('targeted'); s.dataset.toucherEffectId === String(effId)
) {
s.classList.remove("last-move-source");
s.classList.remove("targeted");
delete s.dataset.toucherEffectId; 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 (_) {}
} }
} catch (e) { } 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) // when the server sends the stolen card to the stealer (private)
@ -781,42 +904,69 @@ logs...</pre
let hasPieceToBefore = false; let hasPieceToBefore = false;
try { try {
const fromBeforeEl = const fromBeforeEl =
boardEl && boardEl.querySelector && boardEl &&
boardEl.querySelector(`.square[data-square="${data && data.from}"]`); boardEl.querySelector &&
if (fromBeforeEl && fromBeforeEl.querySelector && fromBeforeEl.querySelector('img.piece')) boardEl.querySelector(
`.square[data-square="${data && data.from}"]`,
);
if (
fromBeforeEl &&
fromBeforeEl.querySelector &&
fromBeforeEl.querySelector("img.piece")
)
hasPieceFromBefore = true; hasPieceFromBefore = true;
} catch (_) {} } catch (_) {}
try { try {
const toBeforeEl = const toBeforeEl =
boardEl && boardEl.querySelector && boardEl &&
boardEl.querySelector(`.square[data-square="${data && data.to}"]`); boardEl.querySelector &&
if (toBeforeEl && toBeforeEl.querySelector && toBeforeEl.querySelector('img.piece')) boardEl.querySelector(
`.square[data-square="${data && data.to}"]`,
);
if (
toBeforeEl &&
toBeforeEl.querySelector &&
toBeforeEl.querySelector("img.piece")
)
hasPieceToBefore = true; hasPieceToBefore = true;
} catch (_) {} } catch (_) {}
clearSelection(false); clearSelection(false);
try { try {
const prevHighlights = boardEl.querySelectorAll(".last-move-dest, .last-move-source"); const prevHighlights = boardEl.querySelectorAll(
".last-move-dest, .last-move-source",
);
prevHighlights.forEach((el) => { prevHighlights.forEach((el) => {
el.classList.remove("last-move-dest"); el.classList.remove("last-move-dest");
el.classList.remove("last-move-source"); el.classList.remove("last-move-source");
}); });
} catch (_) {} } catch (_) {}
try { try {
const prevCircles = boardEl.querySelectorAll(".last-move-circle"); const prevCircles =
boardEl.querySelectorAll(".last-move-circle");
prevCircles.forEach((c) => c.remove()); prevCircles.forEach((c) => c.remove());
} catch (_) {} } catch (_) {}
if (data && data.boardState) renderBoardFromState(data.boardState); if (data && data.boardState)
renderBoardFromState(data.boardState);
if (data && data.boardState) updateTurnBanner(data.boardState); if (data && data.boardState) updateTurnBanner(data.boardState);
try { try {
if (data && data.from && data.to && data.from !== data.to) { if (data && data.from && data.to && data.from !== data.to) {
const dest = boardEl.querySelector(`.square[data-square="${data.to}"]`); const dest = boardEl.querySelector(
const fromEl = boardEl.querySelector(`.square[data-square="${data.from}"]`); `.square[data-square="${data.to}"]`,
);
const fromEl = boardEl.querySelector(
`.square[data-square="${data.from}"]`,
);
let showSource = false; let showSource = false;
try { try {
if (currentBoardState && Array.isArray(currentBoardState.pieces)) { if (
showSource = currentBoardState.pieces.some((x) => x && x.square === data.from); currentBoardState &&
Array.isArray(currentBoardState.pieces)
) {
showSource = currentBoardState.pieces.some(
(x) => x && x.square === data.from,
);
} }
} catch (_) { } catch (_) {
showSource = false; showSource = false;
@ -825,8 +975,13 @@ logs...</pre
let showDest = false; let showDest = false;
try { try {
if (currentBoardState && Array.isArray(currentBoardState.pieces)) { if (
showDest = currentBoardState.pieces.some((x) => x && x.square === data.to); currentBoardState &&
Array.isArray(currentBoardState.pieces)
) {
showDest = currentBoardState.pieces.some(
(x) => x && x.square === data.to,
);
} }
} catch (_) { } catch (_) {
showDest = false; showDest = false;
@ -834,10 +989,22 @@ logs...</pre
if (!showDest) showDest = !!hasPieceToBefore; if (!showDest) showDest = !!hasPieceToBefore;
try { try {
if (!showDest && dest && dest.querySelector && dest.querySelector('img.piece')) showDest = true; if (
!showDest &&
dest &&
dest.querySelector &&
dest.querySelector("img.piece")
)
showDest = true;
} catch (_) {} } catch (_) {}
try { try {
if (!showSource && dest && dest.querySelector && dest.querySelector('img.piece')) showSource = true; if (
!showSource &&
dest &&
dest.querySelector &&
dest.querySelector("img.piece")
)
showSource = true;
} catch (_) {} } catch (_) {}
try { try {
@ -860,7 +1027,14 @@ logs...</pre
try { try {
if (roomId) { if (roomId) {
const key = `cn:lastMove:${roomId}`; const key = `cn:lastMove:${roomId}`;
localStorage.setItem(key, JSON.stringify({ from: data.from, to: data.to, ts: Date.now() })); localStorage.setItem(
key,
JSON.stringify({
from: data.from,
to: data.to,
ts: Date.now(),
}),
);
} }
} catch (e) {} } catch (e) {}
} }
@ -1479,15 +1653,12 @@ logs...</pre
}); });
} else { } else {
log("card played with target", resp); log("card played with target", resp);
// after server applied the card effect, request legal moves for the selected piece so the client
// will receive the updated legal moves (including teleport to empty squares)
try { try {
setSelection(square); setSelection(square);
} catch (_) {} } catch (_) {}
} }
}, },
); );
// keep pendingCard until server response processed (client will call setSelection on success)
pendingCard = null; pendingCard = null;
clearTargetHighlights(); clearTargetHighlights();
return; return;
@ -1641,9 +1812,15 @@ logs...</pre
s.classList.remove("last-move-dest"); s.classList.remove("last-move-dest");
s.classList.remove("last-move-source"); s.classList.remove("last-move-source");
} catch (_) {} } catch (_) {}
try {
s.classList.remove("petrified");
try {
delete s.dataset.medusaEffectId;
} catch (_) {}
} catch (_) {}
// remove any lingering circle markers // remove any lingering circle markers
try { try {
const prev = s.querySelectorAll('.last-move-circle'); const prev = s.querySelectorAll(".last-move-circle");
prev.forEach((c) => c.remove()); prev.forEach((c) => c.remove());
} catch (_) {} } catch (_) {}
// clear square contents (pieces/markers) // clear square contents (pieces/markers)
@ -2068,6 +2245,7 @@ logs...</pre
imgSrc = "/assets/img/cards/vole_piece.png"; imgSrc = "/assets/img/cards/vole_piece.png";
if (cid === "double") imgSrc = "/assets/img/cards/double.png"; if (cid === "double") imgSrc = "/assets/img/cards/double.png";
if (cid === "empathie") imgSrc = "/assets/img/cards/empathie.png"; if (cid === "empathie") imgSrc = "/assets/img/cards/empathie.png";
if (cid === "medusa") imgSrc = "/assets/img/cards/medusa.png";
if (cid === "parrure") imgSrc = "/assets/img/cards/parrure.png"; if (cid === "parrure") imgSrc = "/assets/img/cards/parrure.png";
if (cid === "revolution") if (cid === "revolution")
imgSrc = "/assets/img/cards/révolution.png"; imgSrc = "/assets/img/cards/révolution.png";

View file

@ -204,7 +204,7 @@
if (cid === "invisible") if (cid === "invisible")
imgSrc = "/assets/img/cards/invisible.png"; imgSrc = "/assets/img/cards/invisible.png";
if (cid === "kamikaze") if (cid === "kamikaze")
imgSrc = "/assets/img/cards/kamikaz.png"; imgSrc = "/assets/img/cards/kamikaze.png";
if (cid === "melange") imgSrc = "/assets/img/cards/mélange.png"; if (cid === "melange") imgSrc = "/assets/img/cards/mélange.png";
if (cid === "mine") imgSrc = "/assets/img/cards/mine.png"; if (cid === "mine") imgSrc = "/assets/img/cards/mine.png";
if (cid === "promotion") if (cid === "promotion")

View file

@ -1,272 +1,736 @@
/* Compiled CSS (from style.scss) - included directly so no build step is required */ /* Compiled CSS (from style.scss) - included directly so no build step is required */
*{box-sizing:border-box} * {
html,body{height:100%} box-sizing: border-box;
body{ }
font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; html,
margin:0;padding:28px;background: linear-gradient(180deg,#fff7f0 0%, #f3f6f9 60%);color:#0b1220; body {
-webkit-font-smoothing:antialiased; -moz-osx-font-smoothing:grayscale; height: 100%;
}
body {
font-family:
Inter,
system-ui,
-apple-system,
"Segoe UI",
Roboto,
"Helvetica Neue",
Arial;
margin: 0;
padding: 28px;
background: linear-gradient(180deg, #fff7f0 0%, #f3f6f9 60%);
color: #0b1220;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
} }
.page{max-width:1100px;margin:0 auto} .page {
max-width: 1100px;
header h2{margin:0 0 12px 0;color:#0b1220} margin: 0 auto;
.card{background: linear-gradient(180deg,#fffaf6,#ffffff);border:1px solid rgba(11,18,32,0.06);padding:18px;border-radius:14px;box-shadow:0 10px 30px rgba(11,18,32,0.06);}
button{background:linear-gradient(90deg,#ff7a59,#ffb199);color:#1b1b1f;border:none;padding:10px 14px;border-radius:12px;cursor:pointer;font-weight:700;box-shadow:0 8px 20px rgba(255,122,89,0.12);transition:transform 160ms ease, box-shadow 160ms ease}
button[disabled]{opacity:0.6;cursor:not-allowed}
input[type="text"], input[type="search"]{padding:8px;border-radius:8px;border:1px solid rgba(11,18,32,0.06);background:transparent;color:inherit}
a{color:#2563eb;text-decoration:none}
a:hover{text-decoration:underline}
#log{white-space:pre-wrap;border-radius:8px;background:#f3f6f9;padding:8px;height:280px;overflow:auto;color:#0b1220}
#board{width:800px;height:800px;display:grid;grid-template-columns:repeat(8,1fr);border-radius:6px;overflow:hidden;background-size:cover}
@media (max-width:900px){
#board{width:min(92vw,680px);height:min(92vw,680px)}
} }
.muted{opacity:0.9;color:#475569} header h2 {
margin: 0 0 12px 0;
color: #0b1220;
}
.controls{display:flex;gap:8px;align-items:center} .card {
background: linear-gradient(180deg, #fffaf6, #ffffff);
border: 1px solid rgba(11, 18, 32, 0.06);
padding: 18px;
border-radius: 14px;
box-shadow: 0 10px 30px rgba(11, 18, 32, 0.06);
}
#players{margin:6px 0;padding-left:18px} button {
background: linear-gradient(90deg, #ff7a59, #ffb199);
color: #1b1b1f;
border: none;
padding: 10px 14px;
border-radius: 12px;
cursor: pointer;
font-weight: 700;
box-shadow: 0 8px 20px rgba(255, 122, 89, 0.12);
transition:
transform 160ms ease,
box-shadow 160ms ease;
}
button[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
input[type="text"],
input[type="search"] {
padding: 8px;
border-radius: 8px;
border: 1px solid rgba(11, 18, 32, 0.06);
background: transparent;
color: inherit;
}
a {
color: #2563eb;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
#log {
white-space: pre-wrap;
border-radius: 8px;
background: #f3f6f9;
padding: 8px;
height: 280px;
overflow: auto;
color: #0b1220;
}
#board {
width: 800px;
height: 800px;
display: grid;
grid-template-columns: repeat(8, 1fr);
border-radius: 6px;
overflow: hidden;
background-size: cover;
}
@media (max-width: 900px) {
#board {
width: min(92vw, 680px);
height: min(92vw, 680px);
}
}
.muted {
opacity: 0.9;
color: #475569;
}
.controls {
display: flex;
gap: 8px;
align-items: center;
}
#players {
margin: 6px 0;
padding-left: 18px;
}
/* invite button - dark text for light theme */ /* invite button - dark text for light theme */
#copyInviteBtn{background:transparent;border:1px solid rgba(11,18,32,0.06);color:#0b1220;padding:6px 10px;border-radius:8px} #copyInviteBtn {
background: transparent;
border: 1px solid rgba(11, 18, 32, 0.06);
color: #0b1220;
padding: 6px 10px;
border-radius: 8px;
}
#youAreHost{background:rgba(255,255,255,0.04);padding:6px 8px;border-radius:6px;margin-left:6px} #youAreHost {
background: rgba(255, 255, 255, 0.04);
padding: 6px 8px;
border-radius: 6px;
margin-left: 6px;
}
/* layout helpers used from game.html */ /* layout helpers used from game.html */
.layout-row{display:flex;gap:20px;align-items:flex-start} .layout-row {
display: flex;
gap: 20px;
align-items: flex-start;
}
#turnBanner{color:#5a2b12;background:linear-gradient(90deg,#fff1e6,#ffe6da);padding:10px 14px;border-radius:12px;font-weight:800;display:none;border:1px solid rgba(90,43,18,0.06);box-shadow:0 6px 18px rgba(90,43,18,0.04)} #turnBanner {
#turnBanner.show{display:inline-block} color: #5a2b12;
background: linear-gradient(90deg, #fff1e6, #ffe6da);
padding: 10px 14px;
border-radius: 12px;
font-weight: 800;
display: none;
border: 1px solid rgba(90, 43, 18, 0.06);
box-shadow: 0 6px 18px rgba(90, 43, 18, 0.04);
}
#turnBanner.show {
display: inline-block;
}
/* board tweaks */ /* board tweaks */
#board{border:1px solid rgba(11,18,32,0.06);position:relative;background-image: url('/assets/chess-pieces/boards/8x8_wood.svg');background-size:cover} #board {
border: 1px solid rgba(11, 18, 32, 0.06);
position: relative;
background-image: url("/assets/chess-pieces/boards/8x8_wood.svg");
background-size: cover;
}
/* square cells and piece images (used by renderFen) */ /* square cells and piece images (used by renderFen) */
.square{width:100%;height:100%;display:flex;align-items:center;justify-content:center;overflow:hidden} .square {
.square img, .piece{max-width:78%;max-height:78%;user-select:none;position:relative;z-index:3} width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.square img,
.piece {
max-width: 78%;
max-height: 78%;
user-select: none;
position: relative;
z-index: 3;
}
/* marker shown on target squares for legal moves */ /* marker shown on target squares for legal moves */
.square{position:relative} .square {
.square .move-marker{ position: relative;
position:absolute; }
left:50%;top:50%;transform:translate(-50%,-50%); .square .move-marker {
width:18%;height:18%;border-radius:50%;pointer-events:auto;cursor:pointer;background:rgba(37,99,235,0.95); position: absolute;
box-shadow:0 2px 6px rgba(11,18,32,0.06);z-index:3;transition:transform 120ms ease, opacity 120ms ease; left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 18%;
height: 18%;
border-radius: 50%;
pointer-events: auto;
cursor: pointer;
background: rgba(37, 99, 235, 0.95);
box-shadow: 0 2px 6px rgba(11, 18, 32, 0.06);
z-index: 3;
transition:
transform 120ms ease,
opacity 120ms ease;
}
.square .move-marker:hover {
transform: translate(-50%, -50%) scale(1.12);
opacity: 0.95;
} }
.square .move-marker:hover{ transform:translate(-50%,-50%) scale(1.12); opacity:0.95 }
/* square highlight when selecting a target for a card: a small hollow ring strictly inside the square */ /* square highlight when selecting a target for a card: a small hollow ring strictly inside the square */
.square.targetable{ outline: none; box-shadow: none; } .square.targetable {
.square.targetable::after{ outline: none;
content: ''; box-shadow: none;
}
.square.targetable::after {
content: "";
position: absolute; position: absolute;
left: 50%; top: 50%; transform: translate(-50%, -50%); left: 50%;
width: 36%; height: 36%; border-radius: 50%; top: 50%;
border: 2px solid rgba(255,121,63,0.98); transform: translate(-50%, -50%);
box-shadow: 0 6px 18px rgba(255,121,63,0.10) inset; width: 36%;
pointer-events: none; z-index: 3; height: 36%;
border-radius: 50%;
border: 2px solid rgba(255, 121, 63, 0.98);
box-shadow: 0 6px 18px rgba(255, 121, 63, 0.1) inset;
pointer-events: none;
z-index: 3;
} }
/* small circular mine marker strictly inside the square */ /* small circular mine marker strictly inside the square */
.square .mine-dot{ .square .mine-dot {
position:absolute; position: absolute;
left:50%; top:50%; transform:translate(-50%,-50%); left: 50%;
width:36%; height:36%; border-radius:50%; top: 50%;
background: radial-gradient(circle at 30% 30%, #fff2ea 0%, #ff9a76 30%, #c94b2b 100%); transform: translate(-50%, -50%);
box-shadow: inset 0 2px 8px rgba(255,255,255,0.28), 0 4px 12px rgba(0,0,0,0.20); width: 36%;
pointer-events:none; z-index:2; height: 36%;
border-radius: 50%;
background: radial-gradient(
circle at 30% 30%,
#fff2ea 0%,
#ff9a76 30%,
#c94b2b 100%
);
box-shadow:
inset 0 2px 8px rgba(255, 255, 255, 0.28),
0 4px 12px rgba(0, 0, 0, 0.2);
pointer-events: none;
z-index: 2;
} }
/* image-based mine: prefer SVG with transparent background for crisp edges */ /* image-based mine: prefer SVG with transparent background for crisp edges */
.square .mine-img{ .square .mine-img {
position:absolute; position: absolute;
left:50%; top:50%; transform:translate(-50%,-50%); left: 50%;
width:40%; height:auto; max-height:42%; top: 50%;
pointer-events:none; z-index:2; image-rendering: -webkit-optimize-contrast; transform: translate(-50%, -50%);
width: 40%;
height: auto;
max-height: 42%;
pointer-events: none;
z-index: 2;
image-rendering: -webkit-optimize-contrast;
} }
/* When a square is veiled by brouillard, hide its piece images to ensure full concealment /* When a square is veiled by brouillard, hide its piece images to ensure full concealment
This is a robust fallback in case stacking context / transform interactions make an overlay This is a robust fallback in case stacking context / transform interactions make an overlay
not fully cover the image. Hiding the image guarantees the piece is not visible. */ not fully cover the image. Hiding the image guarantees the piece is not visible. */
.square.veiled img, .square.veiled .piece{ visibility: hidden !important; opacity: 0 !important; } .square.veiled img,
.square.veiled .piece {
visibility: hidden !important;
opacity: 0 !important;
}
/* highlighted/selected square when a player selects a piece */ /* highlighted/selected square when a player selects a piece */
.square.selected{ .square.selected {
outline: 3px solid rgba(245,158,11,0.95); /* amber */ outline: 3px solid rgba(245, 158, 11, 0.95); /* amber */
box-shadow: inset 0 0 0 3px rgba(245,158,11,0.06), 0 8px 20px rgba(11,18,32,0.04); box-shadow:
inset 0 0 0 3px rgba(245, 158, 11, 0.06),
0 8px 20px rgba(11, 18, 32, 0.04);
z-index: 4; z-index: 4;
} }
.square.selected .piece{transform: scale(1.04); transition: transform 140ms ease} .square.selected .piece {
transform: scale(1.04);
transition: transform 140ms ease;
}
/* hide the visual selection outline for a square while keeping the piece visible */ /* hide the visual selection outline for a square while keeping the piece visible */
.square.hide-selection.selected, .square.hide-selection.selected,
.square.hide-selection.selected-remote{ .square.hide-selection.selected-remote {
outline: none !important; outline: none !important;
box-shadow: none !important; box-shadow: none !important;
} }
.square.hide-selection{transition:background 120ms ease} .square.hide-selection {
transition: background 120ms ease;
}
/* remote selection (other player's selection) */ /* remote selection (other player's selection) */
.square.selected-remote{ .square.selected-remote {
outline: 3px solid rgba(37,99,235,0.95); /* blue */ outline: 3px solid rgba(37, 99, 235, 0.95); /* blue */
box-shadow: inset 0 0 0 3px rgba(37,99,235,0.06), 0 6px 16px rgba(11,18,32,0.04); box-shadow:
inset 0 0 0 3px rgba(37, 99, 235, 0.06),
0 6px 16px rgba(11, 18, 32, 0.04);
z-index: 3; z-index: 3;
} }
.square.selected-remote .piece{transform: scale(1.02); transition: transform 140ms ease} .square.selected-remote .piece {
transform: scale(1.02);
transition: transform 140ms ease;
}
/* basic checkerboard coloring */ /* basic checkerboard coloring */
.square.light{background: linear-gradient(180deg,#fff8f4,#fff1ec);} .square.light {
.quare.dark{background: linear-gradient(180deg,#fdeee2,#f6d9c5);} background: linear-gradient(180deg, #fff8f4, #fff1ec);
.square.dark{background: linear-gradient(180deg,#fdeee2,#f6d9c5);} }
.square{border:1px solid rgba(0,0,0,0.06)} .quare.dark {
background: linear-gradient(180deg, #fdeee2, #f6d9c5);
}
.square.dark {
background: linear-gradient(180deg, #fdeee2, #f6d9c5);
}
.square {
border: 1px solid rgba(0, 0, 0, 0.06);
}
/* flipped board for black perspective */ /* flipped board for black perspective */
#board.flipped{transform:rotate(180deg)} #board.flipped {
#board.flipped .square img{transform:rotate(180deg)} transform: rotate(180deg);
}
#board.flipped .square img {
transform: rotate(180deg);
}
/* Lobby / hero styles - decorative and fun */ /* Lobby / hero styles - decorative and fun */
.hero{ .hero {
display:flex;align-items:center;justify-content:space-between;padding:28px;border-radius:12px;margin-bottom:18px;overflow:hidden;position:relative;border:1px solid rgba(11,18,32,0.04); display: flex;
background: linear-gradient(135deg, rgba(99,102,241,0.06), rgba(99,102,241,0.03)); align-items: center;
justify-content: space-between;
padding: 28px;
border-radius: 12px;
margin-bottom: 18px;
overflow: hidden;
position: relative;
border: 1px solid rgba(11, 18, 32, 0.04);
background: linear-gradient(
135deg,
rgba(99, 102, 241, 0.06),
rgba(99, 102, 241, 0.03)
);
}
.hero .title {
font-size: 28px;
font-weight: 800;
letter-spacing: -0.5px;
}
.hero .subtitle {
color: rgba(11, 18, 32, 0.65);
margin-top: 6px;
font-weight: 500;
}
.hero .decor {
position: absolute;
right: -80px;
top: -60px;
opacity: 0.06;
width: 420px;
height: 420px;
filter: blur(6px);
transform: rotate(20deg);
} }
.hero .title{font-size:28px;font-weight:800;letter-spacing:-0.5px}
.hero .subtitle{color:rgba(11,18,32,0.65);margin-top:6px;font-weight:500}
.hero .decor{position:absolute;right:-80px;top:-60px;opacity:0.06;width:420px;height:420px;filter:blur(6px);transform:rotate(20deg)}
.card.lobby{display:flex;gap:12px;align-items:center;justify-content:space-between;padding:14px} .card.lobby {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
padding: 14px;
}
/* buttons variants */ /* buttons variants */
.btn-ghost{background:transparent;border:1px solid rgba(11,18,32,0.06);padding:8px 10px;border-radius:8px;color:inherit} .btn-ghost {
.btn-cta{background:linear-gradient(90deg,#3b82f6,#60a5fa);box-shadow:0 8px 20px rgba(59,130,246,0.18)} background: transparent;
border: 1px solid rgba(11, 18, 32, 0.06);
padding: 8px 10px;
border-radius: 8px;
color: inherit;
}
.btn-cta {
background: linear-gradient(90deg, #3b82f6, #60a5fa);
box-shadow: 0 8px 20px rgba(59, 130, 246, 0.18);
}
/* draw button: compact rectangular CTA used in the cards area */ /* draw button: compact rectangular CTA used in the cards area */
.btn-draw{ .btn-draw {
background:linear-gradient(90deg,#ff7a59,#ffb199); /* warm gradient */ background: linear-gradient(90deg, #ff7a59, #ffb199); /* warm gradient */
color:#1b1b1f;padding:10px 14px;border-radius:12px;border:none;cursor:pointer;font-weight:900;font-size:14px; color: #1b1b1f;
min-width:140px;height:46px;display:inline-flex;align-items:center;justify-content:center;box-shadow:0 10px 22px rgba(255,122,89,0.12); padding: 10px 14px;
border-radius: 12px;
border: none;
cursor: pointer;
font-weight: 900;
font-size: 14px;
min-width: 140px;
height: 46px;
display: inline-flex;
align-items: center;
justify-content: center;
box-shadow: 0 10px 22px rgba(255, 122, 89, 0.12);
}
.btn-draw[disabled] {
opacity: 0.6;
cursor: not-allowed;
} }
.btn-draw[disabled]{opacity:0.6;cursor:not-allowed}
/* cancel button shown during pending card selection: white background, red text for clear contrast */ /* cancel button shown during pending card selection: white background, red text for clear contrast */
.btn-cancel{ .btn-cancel {
background: #ffffff; background: #ffffff;
color: #b91c1c; /* red */ color: #b91c1c; /* red */
border: 1px solid rgba(185,28,28,0.14); border: 1px solid rgba(185, 28, 28, 0.14);
padding: 6px 8px; padding: 6px 8px;
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
font-weight: 700; font-weight: 700;
} }
.btn-cancel:hover{ background: #fff5f5; } .btn-cancel:hover {
background: #fff5f5;
}
/* explosion animation used when a mine detonates */ /* explosion animation used when a mine detonates */
.explosion{ .explosion {
position:absolute; position: absolute;
left:50%; top:50%; transform:translate(-50%,-50%) scale(0.6); left: 50%;
width:64%; height:64%; border-radius:50%; pointer-events:none; z-index:6; top: 50%;
background: radial-gradient(circle at 30% 30%, rgba(255,255,200,0.98) 0%, rgba(255,160,60,0.95) 40%, rgba(220,40,40,0.95) 60%, rgba(120,10,10,0.85) 100%); transform: translate(-50%, -50%) scale(0.6);
box-shadow: 0 6px 18px rgba(0,0,0,0.35), inset 0 6px 18px rgba(255,255,255,0.08); width: 64%;
opacity: 1; transform-origin:50% 50%; height: 64%;
transform: translate(-50%,-50%) scale(0.2); border-radius: 50%;
pointer-events: none;
z-index: 6;
background: radial-gradient(
circle at 30% 30%,
rgba(255, 255, 200, 0.98) 0%,
rgba(255, 160, 60, 0.95) 40%,
rgba(220, 40, 40, 0.95) 60%,
rgba(120, 10, 10, 0.85) 100%
);
box-shadow:
0 6px 18px rgba(0, 0, 0, 0.35),
inset 0 6px 18px rgba(255, 255, 255, 0.08);
opacity: 1;
transform-origin: 50% 50%;
transform: translate(-50%, -50%) scale(0.2);
} }
.explosion.play{ animation: explode 520ms ease-out forwards; } .explosion.play {
@keyframes explode{ animation: explode 520ms ease-out forwards;
0%{ transform: translate(-50%,-50%) scale(0.2); opacity:1; filter: blur(0px); } }
60%{ transform: translate(-50%,-50%) scale(1.15); opacity:0.95; filter: blur(1px); } @keyframes explode {
100%{ transform: translate(-50%,-50%) scale(1.6); opacity:0; filter: blur(6px); } 0% {
transform: translate(-50%, -50%) scale(0.2);
opacity: 1;
filter: blur(0px);
}
60% {
transform: translate(-50%, -50%) scale(1.15);
opacity: 0.95;
filter: blur(1px);
}
100% {
transform: translate(-50%, -50%) scale(1.6);
opacity: 0;
filter: blur(6px);
}
} }
/* small debris puffs for a subtle effect */ /* small debris puffs for a subtle effect */
.explosion::after{ .explosion::after {
content:''; position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); content: "";
width:120%; height:120%; border-radius:50%; background: radial-gradient(circle, rgba(255,200,100,0.6) 0%, rgba(255,200,100,0.0) 40%); position: absolute;
opacity:0; animation: debris 520ms ease-out forwards; left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 120%;
height: 120%;
border-radius: 50%;
background: radial-gradient(
circle,
rgba(255, 200, 100, 0.6) 0%,
rgba(255, 200, 100, 0) 40%
);
opacity: 0;
animation: debris 520ms ease-out forwards;
}
@keyframes debris {
0% {
opacity: 0.9;
transform: translate(-50%, -50%) scale(0.8);
}
100% {
opacity: 0;
transform: translate(-50%, -50%) scale(1.8);
}
} }
@keyframes debris{ 0%{ opacity:0.9; transform:translate(-50%,-50%) scale(0.8); } 100%{ opacity:0; transform:translate(-50%,-50%) scale(1.8); } }
/* video-based explosion: used when a .mov/mp4 file is provided in /assets/media/ */ /* video-based explosion: used when a .mov/mp4 file is provided in /assets/media/ */
.explosion-video{ .explosion-video {
position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); z-index:7; pointer-events:none; position: absolute;
width:160%; height:auto; max-width:220px; max-height:220px; display:block; object-fit:contain; left: 50%;
top: 50%;
transform: translate(-50%, -50%);
z-index: 7;
pointer-events: none;
width: 160%;
height: auto;
max-width: 220px;
max-height: 220px;
display: block;
object-fit: contain;
} }
/* floating small piece icons */ /* floating small piece icons */
.floating-piece{position:absolute;opacity:0.06;width:160px;height:160px;pointer-events:none;transform:translate(-10%,-10%);} .floating-piece {
position: absolute;
opacity: 0.06;
width: 160px;
height: 160px;
pointer-events: none;
transform: translate(-10%, -10%);
}
/* subtle button hover */ /* subtle button hover */
button:hover{transform:translateY(-3px);transition:all 180ms ease} button:hover {
transform: translateY(-3px);
transition: all 180ms ease;
}
/* players area */ /* players area */
.players-list{list-style:none;padding-left:0;margin:0;display:flex;gap:10px} .players-list {
.players-list li{background:rgba(11,18,32,0.03);padding:6px 10px;border-radius:8px} list-style: none;
padding-left: 0;
margin: 0;
display: flex;
gap: 10px;
}
.players-list li {
background: rgba(11, 18, 32, 0.03);
padding: 6px 10px;
border-radius: 8px;
}
/* card hand UI */ /* card hand UI */
.player-hand{border:1px solid rgba(11,18,32,0.06);padding:8px;border-radius:8px;background:#fff} .player-hand {
.card-item{display:flex;gap:8px;align-items:flex-start;background:#f8fafc;padding:8px;border-radius:8px;border:1px solid rgba(11,18,32,0.04)} border: 1px solid rgba(11, 18, 32, 0.06);
.card-art{width:64px;height:60px;background:linear-gradient(180deg,#eef2ff,#ffffff);display:flex;align-items:center;justify-content:center;color:#475569;border-radius:6px;font-size:12px} padding: 8px;
.card-meta{display:flex;flex-direction:column} border-radius: 8px;
.card-title{font-weight:700;color:#0b1220} background: #fff;
.card-desc{color:#475569;font-size:13px} }
.card-item {
display: flex;
gap: 8px;
align-items: flex-start;
background: #f8fafc;
padding: 8px;
border-radius: 8px;
border: 1px solid rgba(11, 18, 32, 0.04);
}
.card-art {
width: 64px;
height: 60px;
background: linear-gradient(180deg, #eef2ff, #ffffff);
display: flex;
align-items: center;
justify-content: center;
color: #475569;
border-radius: 6px;
font-size: 12px;
}
.card-meta {
display: flex;
flex-direction: column;
}
.card-title {
font-weight: 700;
color: #0b1220;
}
.card-desc {
color: #475569;
font-size: 13px;
}
/* Pokemon-style horizontal card hand */ /* Pokemon-style horizontal card hand */
.pokemon-hand{padding:10px;border-radius:10px;background:linear-gradient(180deg,#ffffff,#fbfdff);border:1px solid rgba(11,18,32,0.04)} .pokemon-hand {
.pokemon-hand-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px} padding: 10px;
.pokemon-hand-title{font-weight:800} border-radius: 10px;
.pokemon-hand-counts{display:flex;gap:6px;align-items:center} background: linear-gradient(180deg, #ffffff, #fbfdff);
.opponent-count{background:rgba(11,18,32,0.03);padding:4px 8px;border-radius:8px;font-size:12px} border: 1px solid rgba(11, 18, 32, 0.04);
.cards-row{display:flex;gap:12px;overflow-x:auto;padding-bottom:6px} }
.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-hand-header {
.pokemon-card-top{flex:0 0 120px;padding:8px} display: flex;
.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} align-items: center;
.pokemon-card-mid{padding:8px;flex:1 1 auto;display:flex;flex-direction:column;gap:6px} justify-content: space-between;
.pokemon-card-title{font-weight:800;color:#0b1220} margin-bottom: 8px;
.pokemon-card-desc{color:#475569;font-size:13px} }
.pokemon-card-footer{padding:8px;border-top:1px solid rgba(11,18,32,0.02);display:flex;justify-content:flex-end} .pokemon-hand-title {
.card-play-btn{background:linear-gradient(90deg,#3b82f6,#60a5fa);color:white;border:none;padding:6px 10px;border-radius:8px;cursor:pointer} font-weight: 800;
.card-play-btn:hover{transform:translateY(-2px)} }
.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: 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;
}
.pokemon-card-title {
font-weight: 800;
color: #0b1220;
}
.pokemon-card-desc {
color: #475569;
font-size: 13px;
}
.pokemon-card-footer {
padding: 8px;
border-top: 1px solid rgba(11, 18, 32, 0.02);
display: flex;
justify-content: flex-end;
}
.card-play-btn {
background: linear-gradient(90deg, #3b82f6, #60a5fa);
color: white;
border: none;
padding: 6px 10px;
border-radius: 8px;
cursor: pointer;
}
.card-play-btn:hover {
transform: translateY(-2px);
}
/* small responsive tweak */ /* small responsive tweak */
@media (max-width:700px){ @media (max-width: 700px) {
.pokemon-card{flex:0 0 140px;height:220px} .pokemon-card {
flex: 0 0 140px;
height: 220px;
}
} }
/* shuffle animation: quick jitter + opacity pulse to emphasize randomization */ /* shuffle animation: quick jitter + opacity pulse to emphasize randomization */
#board.shuffle-play{ #board.shuffle-play {
transition: transform 700ms cubic-bezier(.2,.9,.2,1), opacity 700ms ease; transition:
transform 700ms cubic-bezier(0.2, 0.9, 0.2, 1),
opacity 700ms ease;
transform: translateY(-6px) rotate(-1deg) scale(0.995); transform: translateY(-6px) rotate(-1deg) scale(0.995);
opacity: 0.9; opacity: 0.9;
} }
#board.shuffle-play .square img{ transition: transform 700ms cubic-bezier(.2,.9,.2,1); transform: rotate(2deg) scale(0.995); filter: drop-shadow(0 6px 18px rgba(0,0,0,0.06)); } #board.shuffle-play .square img {
transition: transform 700ms cubic-bezier(0.2, 0.9, 0.2, 1);
transform: rotate(2deg) scale(0.995);
filter: drop-shadow(0 6px 18px rgba(0, 0, 0, 0.06));
}
.square.targeted::after{ .square.targeted::after {
content: ''; content: "";
position: absolute; position: absolute;
left: 0; top: 0; right: 0; bottom: 0; left: 0;
top: 0;
right: 0;
bottom: 0;
background: rgb(255, 201, 201); background: rgb(255, 201, 201);
pointer-events: none; pointer-events: none;
z-index: 2; z-index: 2;
} }
.square.last-move-dest::after{ .square.last-move-dest::after {
content: ''; content: "";
position: absolute; position: absolute;
left: 0; top: 0; right: 0; bottom: 0; left: 0;
top: 0;
right: 0;
bottom: 0;
background: rgb(212, 255, 241); background: rgb(212, 255, 241);
pointer-events: none; pointer-events: none;
z-index: 2; z-index: 2;
} }
.square.last-move-source::after{ .square.last-move-source::after {
content: ''; content: "";
position: absolute; position: absolute;
left: 0; top: 0; right: 0; bottom: 0; left: 0;
top: 0;
right: 0;
bottom: 0;
background: rgb(212, 255, 241); background: rgb(212, 255, 241);
pointer-events: none; pointer-events: none;
z-index: 2; z-index: 2;

View file

@ -3,93 +3,285 @@ $primary: #2b6cb0;
$accent: #f6ad55; $accent: #f6ad55;
$bg1: #0f172a; $bg1: #0f172a;
$bg2: #111827; $bg2: #111827;
$card: rgba(255,255,255,0.04); $card: rgba(255, 255, 255, 0.04);
$glass: rgba(255,255,255,0.03); $glass: rgba(255, 255, 255, 0.03);
*{box-sizing:border-box} * {
html,body{height:100%} box-sizing: border-box;
body{ }
font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; html,
margin:0;padding:24px;background: linear-gradient(180deg, #0b1220 0%, #0f172a 60%);color:#e6eef8; body {
height: 100%;
}
body {
font-family:
Inter,
system-ui,
-apple-system,
"Segoe UI",
Roboto,
"Helvetica Neue",
Arial;
margin: 0;
padding: 24px;
background: linear-gradient(180deg, #0b1220 0%, #0f172a 60%);
color: #e6eef8;
} }
.page{max-width:1100px;margin:0 auto} .page {
max-width: 1100px;
margin: 0 auto;
}
header h2{margin:0 0 12px 0;color:#fff;text-shadow:0 1px 0 rgba(0,0,0,0.4)} header h2 {
margin: 0 0 12px 0;
color: #fff;
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.4);
}
.card{background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));border:1px solid rgba(255,255,255,0.04);padding:16px;border-radius:10px;box-shadow:0 6px 20px rgba(2,6,23,0.6);} .card {
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.02),
rgba(255, 255, 255, 0.01)
);
border: 1px solid rgba(255, 255, 255, 0.04);
padding: 16px;
border-radius: 10px;
box-shadow: 0 6px 20px rgba(2, 6, 23, 0.6);
}
button{background:$primary;color:white;border:none;padding:8px 12px;border-radius:8px;cursor:pointer;font-weight:600} button {
button[disabled]{opacity:0.5;cursor:not-allowed} background: $primary;
input[type="text"], input[type="search"]{padding:8px;border-radius:8px;border:1px solid rgba(255,255,255,0.08);background:transparent;color:inherit} color: white;
border: none;
padding: 8px 12px;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
}
button[disabled] {
opacity: 0.5;
cursor: not-allowed;
}
input[type="text"],
input[type="search"] {
padding: 8px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: transparent;
color: inherit;
}
a{color:$accent;text-decoration:none} a {
a:hover{text-decoration:underline} color: $accent;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
#log{white-space:pre-wrap;border-radius:8px;background:$glass;padding:8px;height:280px;overflow:auto;color:#dfeeff} #log {
white-space: pre-wrap;
border-radius: 8px;
background: $glass;
padding: 8px;
height: 280px;
overflow: auto;
color: #dfeeff;
}
#board{width:800px;height:800px;display:grid;grid-template-columns:repeat(8,1fr);border-radius:6px;overflow:hidden;background-size:cover} #board {
width: 800px;
height: 800px;
display: grid;
grid-template-columns: repeat(8, 1fr);
border-radius: 6px;
overflow: hidden;
background-size: cover;
}
/* make board responsive on smaller screens */ /* make board responsive on smaller screens */
@media (max-width:900px){ @media (max-width: 900px) {
#board{width:min(92vw,680px);height:min(92vw,680px)} #board {
width: min(92vw, 680px);
height: min(92vw, 680px);
}
} }
.muted{opacity:0.8;color:#b9c6d9} .muted {
opacity: 0.8;
color: #b9c6d9;
}
/* small helpers */ /* small helpers */
.controls{display:flex;gap:8px;align-items:center} .controls {
display: flex;
gap: 8px;
align-items: center;
}
/* Chat / players list */ /* Chat / players list */
#players{margin:6px 0;padding-left:18px} #players {
margin: 6px 0;
padding-left: 18px;
}
/* visually prominent copy button */ /* visually prominent copy button */
#copyInviteBtn{background:transparent;border:1px solid rgba(255,255,255,0.06);color:#fff;padding:6px 10px;border-radius:8px} #copyInviteBtn {
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.06);
color: #fff;
padding: 6px 10px;
border-radius: 8px;
}
/* host badge */ /* host badge */
#youAreHost{background:rgba(255,255,255,0.04);padding:6px 8px;border-radius:6px;margin-left:6px} #youAreHost {
background: rgba(255, 255, 255, 0.04);
padding: 6px 8px;
border-radius: 6px;
margin-left: 6px;
}
/* layout helpers used from game.html */ /* layout helpers used from game.html */
.layout-row{display:flex;gap:20px;align-items:flex-start} .layout-row {
display: flex;
gap: 20px;
align-items: flex-start;
}
/* board tweaks */ /* board tweaks */
#board{border:1px solid #333;position:relative;background-image: url('/assets/chess-pieces/boards/8x8_wood.svg');background-size:cover} #board {
border: 1px solid #333;
position: relative;
background-image: url("/assets/chess-pieces/boards/8x8_wood.svg");
background-size: cover;
}
/* square cells and piece images (used by renderFen) */ /* square cells and piece images (used by renderFen) */
.square{width:100%;height:100%;display:flex;align-items:center;justify-content:center} .square {
.square img, .piece{max-width:78%;max-height:78%;user-select:none} width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.square img,
.piece {
max-width: 78%;
max-height: 78%;
user-select: none;
}
/* basic checkerboard coloring */ /* basic checkerboard coloring */
.square.light{background: linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));} .square.light {
.square.dark{background: linear-gradient(180deg, rgba(0,0,0,0.18), rgba(0,0,0,0.12));} background: linear-gradient(
.square{border:1px solid rgba(0,0,0,0.06)} 180deg,
rgba(255, 255, 255, 0.02),
rgba(255, 255, 255, 0.01)
);
}
.square.dark {
background: linear-gradient(180deg, rgba(0, 0, 0, 0.18), rgba(0, 0, 0, 0.12));
}
.square {
border: 1px solid rgba(0, 0, 0, 0.06);
}
/* flipped board for black perspective */ /* flipped board for black perspective */
#board.flipped{transform:rotate(180deg)} #board.flipped {
#board.flipped .square img{transform:rotate(180deg)} transform: rotate(180deg);
}
#board.flipped .square img {
transform: rotate(180deg);
}
/* Lobby / hero styles - decorative and fun */ /* Lobby / hero styles - decorative and fun */
.hero{ .hero {
display:flex;align-items:center;justify-content:space-between;padding:28px;border-radius:12px;margin-bottom:18px;overflow:hidden;position:relative;border:1px solid rgba(255,255,255,0.04); display: flex;
background: linear-gradient(135deg, rgba(43,108,176,0.14), rgba(246,173,85,0.06)); align-items: center;
justify-content: space-between;
padding: 28px;
border-radius: 12px;
margin-bottom: 18px;
overflow: hidden;
position: relative;
border: 1px solid rgba(255, 255, 255, 0.04);
background: linear-gradient(
135deg,
rgba(43, 108, 176, 0.14),
rgba(246, 173, 85, 0.06)
);
}
.hero .title {
font-size: 28px;
font-weight: 800;
letter-spacing: -0.5px;
}
.hero .subtitle {
color: rgba(255, 255, 255, 0.85);
margin-top: 6px;
font-weight: 500;
}
.hero .decor {
position: absolute;
right: -80px;
top: -60px;
opacity: 0.06;
width: 420px;
height: 420px;
filter: blur(6px);
transform: rotate(20deg);
} }
.hero .title{font-size:28px;font-weight:800;letter-spacing:-0.5px}
.hero .subtitle{color:rgba(255,255,255,0.85);margin-top:6px;font-weight:500}
.hero .decor{position:absolute;right:-80px;top:-60px;opacity:0.06;width:420px;height:420px;filter:blur(6px);transform:rotate(20deg)}
.card.lobby{display:flex;gap:12px;align-items:center;justify-content:space-between;padding:14px} .card.lobby {
display: flex;
gap: 12px;
align-items: center;
justify-content: space-between;
padding: 14px;
}
/* buttons variants */ /* buttons variants */
.btn-ghost{background:transparent;border:1px solid rgba(255,255,255,0.06);padding:8px 10px;border-radius:8px;color:inherit} .btn-ghost {
.btn-cta{background:linear-gradient(90deg,#3b82f6,#60a5fa);box-shadow:0 8px 20px rgba(59,130,246,0.18)} background: transparent;
border: 1px solid rgba(255, 255, 255, 0.06);
padding: 8px 10px;
border-radius: 8px;
color: inherit;
}
.btn-cta {
background: linear-gradient(90deg, #3b82f6, #60a5fa);
box-shadow: 0 8px 20px rgba(59, 130, 246, 0.18);
}
/* floating small piece icons */ /* floating small piece icons */
.floating-piece{position:absolute;opacity:0.06;width:160px;height:160px;pointer-events:none;transform:translate(-10%,-10%);} .floating-piece {
position: absolute;
opacity: 0.06;
width: 160px;
height: 160px;
pointer-events: none;
transform: translate(-10%, -10%);
}
/* subtle button hover */ /* subtle button hover */
button:hover{transform:translateY(-3px);transition:all 180ms ease} button:hover {
transform: translateY(-3px);
transition: all 180ms ease;
}
/* players area */ /* players area */
.players-list{list-style:none;padding-left:0;margin:0;display:flex;gap:10px} .players-list {
.players-list li{background:rgba(255,255,255,0.02);padding:6px 10px;border-radius:8px} list-style: none;
padding-left: 0;
margin: 0;
display: flex;
gap: 10px;
}
.players-list li {
background: rgba(255, 255, 255, 0.02);
padding: 6px 10px;
border-radius: 8px;
}

132
server.js
View file

@ -439,6 +439,12 @@ function checkAndHandleVictory(room) {
// Each card has a unique id, cardId (slug), title and description. // Each card has a unique id, cardId (slug), title and description.
function buildDefaultDeck() { function buildDefaultDeck() {
const cards = [ const cards = [
[
'médusa',
'La pièce désignée ne peut plus bouger pendant 4 tours',
'medusa',
],
[ [
"Rebondir sur les bords", "Rebondir sur les bords",
"Les déplacements en diagonales de la pièce sélectionnée peuvent rebondir une fois sur les bords", "Les déplacements en diagonales de la pièce sélectionnée peuvent rebondir une fois sur les bords",
@ -613,7 +619,6 @@ function buildDefaultDeck() {
// ['tricherie','Choisis une carte de la pioche parmis trois'], // ['tricherie','Choisis une carte de la pioche parmis trois'],
// ['trêve','Aucun capture ne peut avoir lieu pendant 4 tours'], // ['trêve','Aucun capture ne peut avoir lieu pendant 4 tours'],
// ['traversti','La pièce désignée change aléatoirement de type à chaque tour'] // ['traversti','La pièce désignée change aléatoirement de type à chaque tour']
// ['médusa','La pièce désignée ne peut plus bouger pendant 4 tours'],
// ['pièce berserk','Une pièce choisie doit capturer dans les trois prochains tours, sinon elle est capturée'], // ['pièce berserk','Une pièce choisie doit capturer dans les trois prochains tours, sinon elle est capturée'],
// moyen facile mais intéressant // moyen facile mais intéressant
@ -752,6 +757,12 @@ function computeLegalMoves(room, square) {
const piece = getPieceAt(square); const piece = getPieceAt(square);
if (!piece) return []; if (!piece) return [];
try {
const medusa = (room.activeCardEffects || []).find(
(e) => e && e.type === "medusa" && (e.pieceId === piece.id || e.pieceSquare === square),
);
if (medusa) return [];
} catch (_) {}
const color = piece.color; const color = piece.color;
const fromCoord = squareToCoord(square); const fromCoord = squareToCoord(square);
if (!fromCoord) return []; if (!fromCoord) return [];
@ -1483,6 +1494,24 @@ io.on("connection", (socket) => {
} }
} catch (_) {} } 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 // tout ou rien
try { try {
const effects2 = room.activeCardEffects || []; const effects2 = room.activeCardEffects || [];
@ -1594,7 +1623,7 @@ io.on("connection", (socket) => {
const ev = room.activeCardEffects[ei]; const ev = room.activeCardEffects[ei];
if (!ev) continue; if (!ev) continue;
if ( if (
ev.type === "invisible" && (ev.type === "invisible" || ev.type === "medusa") &&
(ev.pieceId === capturedPiece.id || (ev.pieceId === capturedPiece.id ||
ev.pieceSquare === capturedPiece.square) ev.pieceSquare === capturedPiece.square)
) { ) {
@ -1654,7 +1683,7 @@ io.on("connection", (socket) => {
const ev = room.activeCardEffects[ei]; const ev = room.activeCardEffects[ei];
if (!ev) continue; if (!ev) continue;
if ( if (
ev.type === "invisible" && (ev.type === "invisible" || ev.type === "medusa") &&
(ev.pieceId === capturedPiece.id || (ev.pieceId === capturedPiece.id ||
ev.pieceSquare === capturedPiece.square) 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 // parrure
else if (cardId === "parrure") { else if (cardId === "parrure") {
try { try {