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 {
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...</pre
if (id.indexOf("sniper") !== -1) return "owned";
if (id.indexOf("doppel") !== -1) return "owned";
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("tout") !== -1) return "piece";
if (id.indexOf("kamikaz") !== -1) return "owned";
@ -349,7 +362,8 @@ logs...</pre
// ensure board orientation follows the (possibly changed) player color
try {
if (boardEl) {
if (myColor === "black") boardEl.classList.add("flipped");
if (myColor === "black")
boardEl.classList.add("flipped");
else boardEl.classList.remove("flipped");
}
} catch (_) {}
@ -386,27 +400,41 @@ logs...</pre
updateTurnBanner(data.boardState);
// re-apply any active 'toucher' effects sent by the server (useful after refresh/reconnect)
try {
if (Array.isArray(data.activeCardEffects) && data.activeCardEffects.length) {
if (
Array.isArray(data.activeCardEffects) &&
data.activeCardEffects.length
) {
data.activeCardEffects.forEach((eff) => {
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...</pre
const lm = JSON.parse(raw);
if (lm && lm.to && lm.from && lm.to !== lm.from) {
// 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 {
let showDest = false;
if (currentBoardState && Array.isArray(currentBoardState.pieces)) {
showDest = currentBoardState.pieces.some((x) => 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...</pre
// public notable effects
showEffectNotification(effect);
}
// toucher
// toucher + médusa
try {
if (effect.type === "toucher") {
if (effect.type === "toucher" || effect.type === "medusa_target") {
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 (
!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...</pre
}
});
// toucher
// toucher / medusa removals
socket.on("card:effect:removed", (data) => {
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...</pre
let hasPieceToBefore = false;
try {
const fromBeforeEl =
boardEl && boardEl.querySelector &&
boardEl.querySelector(`.square[data-square="${data && data.from}"]`);
if (fromBeforeEl && fromBeforeEl.querySelector && fromBeforeEl.querySelector('img.piece'))
boardEl &&
boardEl.querySelector &&
boardEl.querySelector(
`.square[data-square="${data && data.from}"]`,
);
if (
fromBeforeEl &&
fromBeforeEl.querySelector &&
fromBeforeEl.querySelector("img.piece")
)
hasPieceFromBefore = true;
} catch (_) {}
try {
const toBeforeEl =
boardEl && boardEl.querySelector &&
boardEl.querySelector(`.square[data-square="${data && data.to}"]`);
if (toBeforeEl && toBeforeEl.querySelector && toBeforeEl.querySelector('img.piece'))
boardEl &&
boardEl.querySelector &&
boardEl.querySelector(
`.square[data-square="${data && data.to}"]`,
);
if (
toBeforeEl &&
toBeforeEl.querySelector &&
toBeforeEl.querySelector("img.piece")
)
hasPieceToBefore = true;
} catch (_) {}
clearSelection(false);
try {
const prevHighlights = boardEl.querySelectorAll(".last-move-dest, .last-move-source");
const prevHighlights = boardEl.querySelectorAll(
".last-move-dest, .last-move-source",
);
prevHighlights.forEach((el) => {
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...</pre
let showDest = false;
try {
if (currentBoardState && Array.isArray(currentBoardState.pieces)) {
showDest = currentBoardState.pieces.some((x) => 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...</pre
if (!showDest) showDest = !!hasPieceToBefore;
try {
if (!showDest && dest && dest.querySelector && dest.querySelector('img.piece')) showDest = true;
if (
!showDest &&
dest &&
dest.querySelector &&
dest.querySelector("img.piece")
)
showDest = true;
} catch (_) {}
try {
if (!showSource && dest && dest.querySelector && dest.querySelector('img.piece')) showSource = true;
if (
!showSource &&
dest &&
dest.querySelector &&
dest.querySelector("img.piece")
)
showSource = true;
} catch (_) {}
try {
@ -860,7 +1027,14 @@ logs...</pre
try {
if (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) {}
}
@ -1479,15 +1653,12 @@ logs...</pre
});
} else {
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 {
setSelection(square);
} catch (_) {}
}
},
);
// keep pendingCard until server response processed (client will call setSelection on success)
pendingCard = null;
clearTargetHighlights();
return;
@ -1641,9 +1812,15 @@ logs...</pre
s.classList.remove("last-move-dest");
s.classList.remove("last-move-source");
} catch (_) {}
try {
s.classList.remove("petrified");
try {
delete s.dataset.medusaEffectId;
} catch (_) {}
} catch (_) {}
// remove any lingering circle markers
try {
const prev = s.querySelectorAll('.last-move-circle');
const prev = s.querySelectorAll(".last-move-circle");
prev.forEach((c) => c.remove());
} catch (_) {}
// clear square contents (pieces/markers)
@ -2068,6 +2245,7 @@ logs...</pre
imgSrc = "/assets/img/cards/vole_piece.png";
if (cid === "double") imgSrc = "/assets/img/cards/double.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 === "revolution")
imgSrc = "/assets/img/cards/révolution.png";

View file

@ -204,7 +204,7 @@
if (cid === "invisible")
imgSrc = "/assets/img/cards/invisible.png";
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 === "mine") imgSrc = "/assets/img/cards/mine.png";
if (cid === "promotion")

View file

@ -1,272 +1,736 @@
/* Compiled CSS (from style.scss) - included directly so no build step is required */
*{box-sizing:border-box}
html,body{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;
* {
box-sizing: border-box;
}
html,
body {
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}
header h2{margin:0 0 12px 0;color:#0b1220}
.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)}
.page {
max-width: 1100px;
margin: 0 auto;
}
.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 */
#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-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.show{display:inline-block}
#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.show {
display: inline-block;
}
/* 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{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}
.square {
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 */
.square{position:relative}
.square .move-marker{
position:absolute;
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 {
position: relative;
}
.square .move-marker {
position: absolute;
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.targetable{ outline: none; box-shadow: none; }
.square.targetable::after{
content: '';
.square.targetable {
outline: none;
box-shadow: none;
}
.square.targetable::after {
content: "";
position: absolute;
left: 50%; top: 50%; transform: translate(-50%, -50%);
width: 36%; height: 36%; border-radius: 50%;
border: 2px solid rgba(255,121,63,0.98);
box-shadow: 0 6px 18px rgba(255,121,63,0.10) inset;
pointer-events: none; z-index: 3;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 36%;
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 */
.square .mine-dot{
position:absolute;
left:50%; top:50%; transform:translate(-50%,-50%);
width:36%; 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.20);
pointer-events:none; z-index:2;
.square .mine-dot {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 36%;
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 */
.square .mine-img{
position:absolute;
left:50%; top:50%; transform:translate(-50%,-50%);
width:40%; height:auto; max-height:42%;
pointer-events:none; z-index:2; image-rendering: -webkit-optimize-contrast;
.square .mine-img {
position: absolute;
left: 50%;
top: 50%;
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
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. */
.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 */
.square.selected{
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);
.square.selected {
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);
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 */
.square.hide-selection.selected,
.square.hide-selection.selected-remote{
.square.hide-selection.selected-remote {
outline: 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) */
.square.selected-remote{
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);
.square.selected-remote {
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);
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 */
.square.light{background: linear-gradient(180deg,#fff8f4,#fff1ec);}
.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)}
.square.light {
background: linear-gradient(180deg, #fff8f4, #fff1ec);
}
.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 */
#board.flipped{transform:rotate(180deg)}
#board.flipped .square img{transform:rotate(180deg)}
#board.flipped {
transform: rotate(180deg);
}
#board.flipped .square img {
transform: rotate(180deg);
}
/* Lobby / hero styles - decorative and fun */
.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);
background: linear-gradient(135deg, rgba(99,102,241,0.06), rgba(99,102,241,0.03));
.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);
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 */
.btn-ghost{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)}
.btn-ghost {
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 */
.btn-draw{
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;
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 {
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;
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 */
.btn-cancel{
.btn-cancel {
background: #ffffff;
color: #b91c1c; /* red */
border: 1px solid rgba(185,28,28,0.14);
border: 1px solid rgba(185, 28, 28, 0.14);
padding: 6px 8px;
border-radius: 6px;
cursor: pointer;
font-weight: 700;
}
.btn-cancel:hover{ background: #fff5f5; }
.btn-cancel:hover {
background: #fff5f5;
}
/* explosion animation used when a mine detonates */
.explosion{
position:absolute;
left:50%; top:50%; transform:translate(-50%,-50%) scale(0.6);
width:64%; height:64%; 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 {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%) scale(0.6);
width: 64%;
height: 64%;
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; }
@keyframes explode{
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); }
.explosion.play {
animation: explode 520ms ease-out forwards;
}
@keyframes explode {
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 */
.explosion::after{
content:''; position:absolute; 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.0) 40%);
opacity:0; animation: debris 520ms ease-out forwards;
.explosion::after {
content: "";
position: absolute;
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/ */
.explosion-video{
position:absolute; 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;
.explosion-video {
position: absolute;
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-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 */
button:hover{transform:translateY(-3px);transition:all 180ms ease}
button:hover {
transform: translateY(-3px);
transition: all 180ms ease;
}
/* players area */
.players-list{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}
.players-list {
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 */
.player-hand{border:1px solid rgba(11,18,32,0.06);padding:8px;border-radius:8px;background:#fff}
.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}
.player-hand {
border: 1px solid rgba(11, 18, 32, 0.06);
padding: 8px;
border-radius: 8px;
background: #fff;
}
.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-hand{padding:10px;border-radius:10px;background:linear-gradient(180deg,#ffffff,#fbfdff);border:1px solid rgba(11,18,32,0.04)}
.pokemon-hand-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
.pokemon-hand-title{font-weight:800}
.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)}
.pokemon-hand {
padding: 10px;
border-radius: 10px;
background: linear-gradient(180deg, #ffffff, #fbfdff);
border: 1px solid rgba(11, 18, 32, 0.04);
}
.pokemon-hand-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.pokemon-hand-title {
font-weight: 800;
}
.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 */
@media (max-width:700px){
.pokemon-card{flex:0 0 140px;height:220px}
@media (max-width: 700px) {
.pokemon-card {
flex: 0 0 140px;
height: 220px;
}
}
/* shuffle animation: quick jitter + opacity pulse to emphasize randomization */
#board.shuffle-play{
transition: transform 700ms cubic-bezier(.2,.9,.2,1), opacity 700ms ease;
#board.shuffle-play {
transition:
transform 700ms cubic-bezier(0.2, 0.9, 0.2, 1),
opacity 700ms ease;
transform: translateY(-6px) rotate(-1deg) scale(0.995);
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{
content: '';
.square.targeted::after {
content: "";
position: absolute;
left: 0; top: 0; right: 0; bottom: 0;
left: 0;
top: 0;
right: 0;
bottom: 0;
background: rgb(255, 201, 201);
pointer-events: none;
z-index: 2;
}
.square.last-move-dest::after{
content: '';
.square.last-move-dest::after {
content: "";
position: absolute;
left: 0; top: 0; right: 0; bottom: 0;
left: 0;
top: 0;
right: 0;
bottom: 0;
background: rgb(212, 255, 241);
pointer-events: none;
z-index: 2;
}
.square.last-move-source::after{
content: '';
.square.last-move-source::after {
content: "";
position: absolute;
left: 0; top: 0; right: 0; bottom: 0;
left: 0;
top: 0;
right: 0;
bottom: 0;
background: rgb(212, 255, 241);
pointer-events: none;
z-index: 2;

View file

@ -3,93 +3,285 @@ $primary: #2b6cb0;
$accent: #f6ad55;
$bg1: #0f172a;
$bg2: #111827;
$card: rgba(255,255,255,0.04);
$glass: rgba(255,255,255,0.03);
$card: rgba(255, 255, 255, 0.04);
$glass: rgba(255, 255, 255, 0.03);
*{box-sizing:border-box}
html,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;
* {
box-sizing: border-box;
}
html,
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[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}
button {
background: $primary;
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:hover{text-decoration:underline}
a {
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 */
@media (max-width:900px){
#board{width:min(92vw,680px);height:min(92vw,680px)}
@media (max-width: 900px) {
#board {
width: min(92vw, 680px);
height: min(92vw, 680px);
}
}
.muted{opacity:0.8;color:#b9c6d9}
.muted {
opacity: 0.8;
color: #b9c6d9;
}
/* small helpers */
.controls{display:flex;gap:8px;align-items:center}
.controls {
display: flex;
gap: 8px;
align-items: center;
}
/* Chat / players list */
#players{margin:6px 0;padding-left:18px}
#players {
margin: 6px 0;
padding-left: 18px;
}
/* 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 */
#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-row{display:flex;gap:20px;align-items:flex-start}
.layout-row {
display: flex;
gap: 20px;
align-items: flex-start;
}
/* 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{width:100%;height:100%;display:flex;align-items:center;justify-content:center}
.square img, .piece{max-width:78%;max-height:78%;user-select:none}
.square {
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 */
.square.light{background: linear-gradient(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)}
.square.light {
background: linear-gradient(
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 */
#board.flipped{transform:rotate(180deg)}
#board.flipped .square img{transform:rotate(180deg)}
#board.flipped {
transform: rotate(180deg);
}
#board.flipped .square img {
transform: rotate(180deg);
}
/* Lobby / hero styles - decorative and fun */
.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);
background: linear-gradient(135deg, rgba(43,108,176,0.14), rgba(246,173,85,0.06));
.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);
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 */
.btn-ghost{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)}
.btn-ghost {
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-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 */
button:hover{transform:translateY(-3px);transition:all 180ms ease}
button:hover {
transform: translateY(-3px);
transition: all 180ms ease;
}
/* players area */
.players-list{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}
.players-list {
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.
function buildDefaultDeck() {
const cards = [
[
'médusa',
'La pièce désignée ne peut plus bouger pendant 4 tours',
'medusa',
],
[
"Rebondir 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'],
// ['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']
// ['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'],
// moyen facile mais intéressant
@ -752,6 +757,12 @@ function computeLegalMoves(room, square) {
const piece = getPieceAt(square);
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 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 {