This commit is contained in:
cay 2026-04-10 08:18:01 +01:00
parent 7d5209a45d
commit e572f0d3a1
2 changed files with 212 additions and 9 deletions

View File

@ -590,6 +590,53 @@ body {
box-shadow: 0 0 calc(var(--s) * 16) rgba(255, 255, 255, 0.2);
}
/* ── Zug-Timer (Top Bar Mitte) ── */
#turn-timer-wrap {
position: absolute;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
justify-content: center;
}
#turn-timer-svg {
display: block;
}
.tt-track {
fill: none;
stroke: rgba(255,255,255,0.1);
stroke-width: 4;
}
.tt-fill {
fill: none;
stroke: #27ae60;
stroke-width: 4;
stroke-linecap: round;
transform: rotate(-90deg);
transform-origin: center;
stroke-dasharray: 113.1; /* 2π×18 */
stroke-dashoffset: 0;
transition: stroke-dashoffset 0.9s linear, stroke 0.3s ease;
}
#turn-timer-num {
position: absolute;
font-family: "Cinzel", serif;
font-size: calc(var(--s) * 13);
font-weight: 700;
color: #fff;
text-shadow: 0 1px 4px rgba(0,0,0,0.9);
line-height: 1;
}
@keyframes tt-pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.6; transform: scale(1.15); }
}
/* ── Karten-Stats Overlay exakt wie Slotmaschine ── */
.card-stat-overlay {
position: absolute;

View File

@ -100,6 +100,16 @@
<div class="board">
<div class="top-bar">
<div class="game-title"><%= title || "Spielfeld" %></div>
<!-- Zug-Timer mittig -->
<div id="turn-timer-wrap" style="display:none;">
<svg id="turn-timer-svg" viewBox="0 0 44 44" width="44" height="44">
<circle cx="22" cy="22" r="18" class="tt-track"/>
<circle cx="22" cy="22" r="18" class="tt-fill" id="tt-circle"/>
</svg>
<div id="turn-timer-num">20</div>
</div>
<div class="top-icons">
<div class="top-icon">⚙</div><div class="top-icon">🗺</div>
<div class="top-icon">📖</div><div class="top-icon">🏆</div>
@ -363,10 +373,139 @@
}
})();
/* ── Zug beenden: CD ticken + Karte ziehen ─────────── */
/* ══════════════════════════════════════════════════════
ZUG-TIMER (20 Sekunden)
══════════════════════════════════════════════════════ */
const TURN_SECONDS = 20;
const TT_CIRCUM = 2 * Math.PI * 18; // r=18
let turnTimerInt = null;
let turnSecsLeft = TURN_SECONDS;
function startTurnTimer() {
clearInterval(turnTimerInt);
turnSecsLeft = TURN_SECONDS;
updateTimerUI(turnSecsLeft);
const wrap = document.getElementById("turn-timer-wrap");
if (wrap) wrap.style.display = "flex";
turnTimerInt = setInterval(() => {
turnSecsLeft--;
updateTimerUI(turnSecsLeft);
if (turnSecsLeft <= 0) {
clearInterval(turnTimerInt);
// Zeit abgelaufen → Zug automatisch beenden
if (isMyTurn) {
tickHandCooldowns();
drawNextCard();
setTurnState(false);
socket.emit("end_turn", { matchId, slot: mySlot });
}
}
}, 1000);
}
function stopTurnTimer() {
clearInterval(turnTimerInt);
const wrap = document.getElementById("turn-timer-wrap");
if (wrap) wrap.style.display = "none";
}
function updateTimerUI(secs) {
const num = document.getElementById("turn-timer-num");
const circle = document.getElementById("tt-circle");
if (num) num.textContent = secs;
if (circle) {
circle.style.strokeDashoffset = TT_CIRCUM * (1 - secs / TURN_SECONDS);
circle.style.stroke = secs > 10 ? "#27ae60" : secs > 5 ? "#f39c12" : "#e74c3c";
if (secs <= 5) {
num.style.color = "#e74c3c";
num.style.animation = "tt-pulse 0.5s ease-in-out infinite";
} else {
num.style.color = "#fff";
num.style.animation = "none";
}
}
}
/* ══════════════════════════════════════════════════════
ZUG-SYSTEM
══════════════════════════════════════════════════════ */
// Bin ich der linke Spieler? Wird nach Seitenzuweisung gesetzt.
let amILeftPlayer = null;
let isMyTurn = false;
function setTurnState(myTurn) {
isMyTurn = myTurn;
const btn = document.getElementById("end-turn-btn");
if (!btn) return;
if (myTurn) {
btn.disabled = false;
btn.textContent = "Zug beenden";
btn.style.opacity = "1";
document.getElementById("turn-indicator")?.remove();
startTurnTimer();
} else {
btn.disabled = true;
btn.style.opacity = "0.4";
stopTurnTimer();
showTurnIndicator();
}
}
function showTurnIndicator() {
document.getElementById("turn-indicator")?.remove();
const ind = document.createElement("div");
ind.id = "turn-indicator";
ind.style.cssText = `
position:fixed; bottom:calc(var(--s)*200); left:50%;
transform:translateX(-50%);
background:linear-gradient(135deg,rgba(20,20,40,0.95),rgba(10,10,25,0.95));
border:1px solid rgba(200,160,60,0.5);
border-radius:calc(var(--s)*8);
color:rgba(255,215,80,0.75);
font-family:'Cinzel',serif;
font-size:calc(var(--s)*11);
letter-spacing:calc(var(--s)*3);
padding:calc(var(--s)*8) calc(var(--s)*20);
z-index:100;
pointer-events:none;
text-transform:uppercase;
box-shadow:0 4px 20px rgba(0,0,0,0.6);
`;
ind.textContent = "⏳ Gegner ist am Zug";
document.body.appendChild(ind);
}
/* ── Zug beenden: CD ticken + Karte ziehen + Server informieren ── */
document.getElementById("end-turn-btn")?.addEventListener("click", () => {
if (!isMyTurn) return;
clearInterval(turnTimerInt);
tickHandCooldowns();
drawNextCard();
setTurnState(false);
socket.emit("end_turn", { matchId, slot: mySlot });
});
/* ── Server: Zugwechsel ──────────────────────────────── */
socket.on("turn_change", data => {
// data.activeSlot = "player1" | "player2"
const myActualSlot = amILeftPlayer === null
? mySlot
: (amILeftPlayer ? "player1" : "player2");
const nowMyTurn = data.activeSlot === myActualSlot ||
data.activeSlot === mySlot;
setTurnState(nowMyTurn);
});
// Fallback falls Server kein turn_change sendet:
// eigenes end_turn bestätigen wir nach kurzer Verzögerung selbst
socket.on("turn_started", data => {
const nowMyTurn = data.slot === mySlot;
setTurnState(nowMyTurn);
});
/* ── Hilfsfunktion: Karte mit Stats in einen Slot rendern ── */
@ -411,9 +550,14 @@
const socket = io();
/* Account-ID laden dann arena_join senden */
let myIngameName = null;
fetch("/arena/me")
.then(r => r.json())
.then(me => {
// Eigenen Namen sofort im Avatar anzeigen
myIngameName = me.ingame_name || me.username || me.name || me.id || "Ich";
const myNameEl = document.getElementById(amIPlayer1 ? "nameLeft" : "nameRight");
if (myNameEl) myNameEl.textContent = myIngameName;
socket.emit("arena_join", { matchId, slot: mySlot, accountId: me.id });
})
.catch(() => {
@ -487,22 +631,34 @@
document.getElementById("nameLeft").textContent = leftName;
document.getElementById("nameRight").textContent = rightName;
// Platzhalter-Icon durch Namen ersetzen falls noch kein Bild
// Platzhalter: nur den Ingame-Namen anzeigen, kein Icon
["avLeft", "avRight"].forEach(avId => {
const av = document.getElementById(avId);
const ph = av?.querySelector(".av-placeholder");
const name = avId === "avLeft" ? leftName : rightName;
if (ph) ph.innerHTML = `
<div class="av-icon" style="font-size:calc(var(--s)*26);opacity:0.5;">
${avId === "avLeft" ? "⚔" : "🛡"}
</div>
<div style="font-family:'Cinzel',serif;font-size:calc(var(--s)*11);
color:rgba(255,215,80,0.9);text-align:center;padding:0 6px;
word-break:break-word;line-height:1.3;">${name}</div>`;
<div style="
font-family:'Cinzel',serif;
font-size:calc(var(--s)*13);
font-weight:700;
color:#ffd750;
text-align:center;
padding:0 8px;
word-break:break-word;
line-height:1.4;
text-shadow:0 2px 8px rgba(0,0,0,0.9),0 0 20px rgba(0,0,0,0.8);
">${name}</div>`;
});
document.getElementById("board-lock-overlay")?.remove();
document.getElementById("end-turn-btn").disabled = false;
// Festlegen ob ich der linke Spieler bin
// flip=false: player1=links, flip=true: player1=rechts
amILeftPlayer = flip ? (mySlot === "player2") : (mySlot === "player1");
// Linker Spieler beginnt
setTurnState(amILeftPlayer);
socket.emit("end_turn_init", { matchId, starterSlot: flip ? "player2" : "player1" });
}
});