Challenge-Logik überarbeitet und fertig gestellt

This commit is contained in:
DST81 2026-05-28 15:22:35 +02:00
parent 909eb97bbb
commit 5199b32471
10 changed files with 556 additions and 38 deletions

View File

@ -296,6 +296,7 @@ localStorage.setItem('auth', JSON.stringify({
- Fehlersuche in asynchronen Promise-Ketten
- Code-Style-Verbesserungen (ES6+ Best Practices)
- Regex-Optimierungen für Text-Vergleich
- Generierung visueller Assets: Logo, Icon sowie Challenge-Grafiken für Gewonnen/Verloren/Unentschieden wurden mit ChatGPT erstellt und anschließend im Projekt eingebunden
**Kritische Bewertung:**
- ✅ **Effektiv für**: Schnelle Fehlerbehebung, Best-Practice-Vorschläge

View File

@ -109,6 +109,13 @@ p {
.btn:hover {
background-color: #2b4a7c;
}
.btn:disabled,
.btn.disabled {
background-color: #adb5bd;
color: #495057;
cursor: not-allowed;
opacity: 1;
}
.btn-danger {
background-color: #a84848;
color: #fff;
@ -370,6 +377,51 @@ p {
background: #fff4f4;
}
.challenge-message-group {
border-left-color: #6f7fb7;
}
.challenge-thread {
display: grid;
gap: 8px;
margin-top: 12px;
border-top: 1px solid #e5e7eb;
padding-top: 10px;
}
.challenge-thread.d-none {
display: none;
}
.challenge-thread-toggle {
border: none;
background: transparent;
color: #4a6fa5;
display: inline-flex;
font-size: 0.9rem;
font-weight: 600;
margin-top: 10px;
padding: 0;
}
.challenge-thread-toggle:hover {
color: #2b4a7c;
text-decoration: underline;
}
.challenge-thread-item {
border-radius: 6px;
background: #f8fafc;
padding: 8px 10px;
}
.challenge-thread-item span {
display: block;
color: #64748b;
font-size: 0.82rem;
margin-bottom: 4px;
}
.message-meta {
display: flex;
justify-content: space-between;
@ -389,6 +441,12 @@ p {
margin: 0;
}
.challenge-status-button:disabled {
background-color: #e5e7eb;
border: 1px solid #cbd5e1;
color: #64748b;
}
.challenge-result-graphic,
.play-challenge-result {
margin-top: 12px;
@ -405,6 +463,38 @@ p {
color: #ffffff;
}
.challenge-outcome {
align-items: center;
background: #f8fafc;
display: flex;
gap: 12px;
padding: 14px;
}
.challenge-outcome img {
border-radius: 8px;
height: 72px;
object-fit: cover;
width: 72px;
}
.challenge-outcome strong {
color: #1b1b2f;
font-size: 1.5rem;
}
.challenge-outcome-win {
background: #e8f7ee;
}
.challenge-outcome-loss {
background: #fdecec;
}
.challenge-outcome-draw {
background: #eef3fb;
}
.challenge-result-win,
.play-challenge-result-winner .play-challenge-result-title {
background: #198754;

Binary file not shown.

BIN
image/sieg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

BIN
image/unentschieden.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

BIN
image/verloren.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

View File

@ -7,7 +7,7 @@
<link rel="icon" type="image/png" href="image/icon_l.png">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="assets/bootstrap-5.3.8-dist/css/bootstrap.min.css">
<link rel="stylesheet" href="assets/css/custom.css">
<link rel="stylesheet" href="assets/css/custom.css?v=challenge-flow-20260528e">
<!-- Kleine Korrektur direkt hier, falls Sie custom.css nicht sofort ändern wollen -->
<style>
@ -136,9 +136,9 @@
<script src="js/login.js"></script>
<script src="js/leaderboard.js"></script>
<script src="js/scores.js"></script>
<script src="js/messages.js?v=challenge-flow-20260526"></script>
<script src="js/messages.js?v=challenge-flow-20260528f"></script>
<!--Navigation Script -->
<script src="js/play.js?v=challenge-flow-20260525b"></script>
<script src="js/navigation.js?v=challenge-flow-20260526"></script>
<script src="js/play.js?v=challenge-flow-20260528d"></script>
<script src="js/navigation.js?v=challenge-flow-20260528"></script>
</body>
</html>

View File

@ -51,6 +51,8 @@
}
function normalizeMessage(message) {
// Das Backend und unser Nachrichten-Fallback liefern Challenge-Daten leicht unterschiedlich.
// Hier werden beide Formen in ein einheitliches Message-Objekt gebracht.
const embeddedChallenge = extractEmbeddedChallenge(message.text ?? "");
const type = message.type ?? MESSAGE_TYPE_CHALLENGE;
const challenge = message.challenge
@ -71,6 +73,8 @@
}
function extractEmbeddedChallenge(text) {
// Fallback fuer Challenge-Daten, die im Nachrichtentext mitgesendet werden.
// So koennen wir bestehende Message-Endpunkte nutzen, ohne neue Backend-Felder zu verlangen.
const rawText = String(text ?? "");
if (!rawText.startsWith(CHALLENGE_DATA_PREFIX)) {
return {
@ -180,7 +184,62 @@
&& !hasScore(challenge.challengerScore);
}
function getChallengeButtonState(message) {
// Bestimmt den sichtbaren Status einer Challenge aus Sicht des eingeloggten Users.
// Dadurch gibt es pro Challenge genau einen aktiven oder deaktivierten Button.
const challenge = message.challenge;
const role = getChallengeRole(message);
if (!challenge || !role) {
return null;
}
const opponentScore = getOpponentScore(challenge);
const challengerHasScore = hasScore(challenge.challengerScore);
const opponentHasScore = hasScore(opponentScore);
if (challengerHasScore && opponentHasScore) {
return {
disabled: true,
label: "Challenge erledigt",
};
}
if (role === "opponent") {
if (!opponentHasScore && !challengerHasScore) {
return {
disabled: false,
label: "Challenge annehmen",
role: "opponent",
};
}
return {
disabled: true,
label: "Schon gespielt",
};
}
if (role === "challenger") {
if (opponentHasScore && !challengerHasScore) {
return {
disabled: false,
label: "Challenge spielen",
role: "challenger",
};
}
return {
disabled: true,
label: "Warte auf Gegner",
};
}
return null;
}
function startChallenge(message, role) {
// Der aktive Challenge-Kontext wird nur fuer die naechste Spielrunde im Session Storage abgelegt.
// play.js liest diesen Zustand aus und weiss dadurch, ob es die erste oder finale Runde ist.
const challenge = message.challenge;
const challenger = getChallengeChallenger(challenge, message.sender);
const opponent = getChallengeOpponent(challenge, message.recipient);
@ -263,6 +322,148 @@
navLink.classList.toggle("has-unread-messages", hasUnreadMessages);
}
async function markMessageReadOnClick(message, item) {
const auth = getAuth();
const messageService = getMessageService();
if (!auth || !messageService || !message.id || message.read) {
return;
}
const isIncoming = normalizeUsername(message.recipient) === normalizeUsername(auth.username)
|| normalizeUsername(message.sender) !== normalizeUsername(auth.username);
if (!isIncoming) {
return;
}
message.read = true;
item.classList.remove("message-item-unread");
updateMessagesNavState(currentMessages);
const result = await messageService.markMessageAsRead(
auth.username,
auth.password,
message.id,
);
if (!result.ok) {
message.read = false;
item.classList.add("message-item-unread");
updateMessagesNavState(currentMessages);
setFeedback("Nachricht konnte nicht als gelesen markiert werden.", "warning");
}
}
async function markMessageGroupReadOnClick(messages, item) {
// Eine gruppierte Challenge-Karte kann mehrere einzelne Nachrichten enthalten.
// Beim Anklicken werden deshalb alle ungelesenen eingehenden Nachrichten der Gruppe markiert.
const auth = getAuth();
const messageService = getMessageService();
if (!auth || !messageService) {
return;
}
const unreadIncomingMessages = messages.filter((message) => {
const isIncoming = normalizeUsername(message.recipient) === normalizeUsername(auth.username)
|| normalizeUsername(message.sender) !== normalizeUsername(auth.username);
return isIncoming && !message.read && message.id;
});
if (unreadIncomingMessages.length === 0) {
return;
}
unreadIncomingMessages.forEach((message) => {
message.read = true;
});
item.classList.remove("message-item-unread");
updateMessagesNavState(currentMessages);
const results = await Promise.all(
unreadIncomingMessages.map((message) =>
messageService.markMessageAsRead(auth.username, auth.password, message.id),
),
);
if (results.some((result) => !result.ok)) {
unreadIncomingMessages.forEach((message) => {
message.read = false;
});
item.classList.add("message-item-unread");
updateMessagesNavState(currentMessages);
setFeedback("Challenge-Nachrichten konnten nicht als gelesen markiert werden.", "warning");
}
}
function getMessageTimeValue(message) {
const time = new Date(message.createdAt).getTime();
return Number.isNaN(time) ? 0 : time;
}
function mergeChallengeData(messages) {
// Fuer die Challenge-Karte werden Teildaten aus mehreren Nachrichten zusammengefuehrt
// (z. B. zuerst Challenge-ID, spaeter Gegner-Score, am Schluss Gewinner).
return messages
.slice()
.sort((a, b) => getMessageTimeValue(a) - getMessageTimeValue(b))
.reduce((merged, message) => {
if (!message.challenge) {
return merged;
}
Object.keys(message.challenge).forEach((key) => {
const value = message.challenge[key];
if (value !== null && value !== undefined && value !== "") {
merged[key] = value;
}
});
return merged;
}, {});
}
function groupMessagesByChallenge(messages) {
// Nachrichten mit gleicher Challenge-ID werden zu einer Karte gruppiert,
// damit alte Zwischen-Nachrichten nicht einzeln die Inbox ueberladen.
const groupsByChallenge = new Map();
const standaloneGroups = [];
messages.forEach((message) => {
const challengeId = getChallengeId(message.challenge);
if (challengeId === null || challengeId === undefined) {
standaloneGroups.push({
kind: "message",
latest: message,
messages: [message],
});
return;
}
const key = String(challengeId);
if (!groupsByChallenge.has(key)) {
groupsByChallenge.set(key, []);
}
groupsByChallenge.get(key).push(message);
});
const challengeGroups = Array.from(groupsByChallenge.values()).map((groupMessages) => {
const sortedMessages = groupMessages
.slice()
.sort((a, b) => getMessageTimeValue(b) - getMessageTimeValue(a));
const latest = { ...sortedMessages[0] };
latest.challenge = mergeChallengeData(sortedMessages);
return {
kind: "challenge",
latest: latest,
messages: sortedMessages,
};
});
return standaloneGroups
.concat(challengeGroups)
.sort((a, b) => getMessageTimeValue(b.latest) - getMessageTimeValue(a.latest));
}
function renderUserList(users) {
const userList = document.getElementById("messages-user-list");
const recipientSelect = document.getElementById("challenge-recipient");
@ -308,6 +509,7 @@
}
function renderMessages(messages = currentMessages) {
// Rendert normale Nachrichten einzeln und Challenge-Nachrichten als gruppierte Karten.
const messageList = document.getElementById("message-list");
if (!messageList) {
return;
@ -325,21 +527,31 @@
return;
}
messages
.slice()
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
.forEach((message) => {
groupMessagesByChallenge(messages)
.forEach((messageGroup) => {
const message = messageGroup.latest;
const item = document.createElement("article");
const isOutgoing = normalizeUsername(message.sender) === ownName;
const hasUnreadIncoming = messageGroup.messages.some((groupMessage) => {
const isIncoming = normalizeUsername(groupMessage.recipient) === ownName
|| normalizeUsername(groupMessage.sender) !== ownName;
return isIncoming && !groupMessage.read;
});
item.className = "message-item";
if (!message.read && !isOutgoing) {
if (messageGroup.kind === "challenge") {
item.classList.add("challenge-message-group");
}
if (hasUnreadIncoming) {
item.classList.add("message-item-unread");
item.addEventListener("click", () => markMessageGroupReadOnClick(messageGroup.messages, item));
}
const fromToText = isOutgoing
? "An " + message.recipient
: "Von " + message.sender;
const fromToText = messageGroup.kind === "challenge"
? "Challenge"
: isOutgoing
? "An " + message.recipient
: "Von " + message.sender;
const meta = document.createElement("div");
meta.className = "message-meta";
@ -356,26 +568,56 @@
meta.append(sender, time);
item.append(meta, text);
if (message.type === MESSAGE_TYPE_CHALLENGE_RESULT && message.challenge) {
if (messageGroup.kind === "challenge" && messageGroup.messages.length > 1) {
const toggleButton = document.createElement("button");
toggleButton.type = "button";
toggleButton.className = "challenge-thread-toggle";
toggleButton.textContent = "Verlauf anzeigen";
const thread = document.createElement("div");
thread.className = "challenge-thread d-none";
messageGroup.messages.forEach((threadMessage) => {
const threadItem = document.createElement("div");
threadItem.className = "challenge-thread-item";
const threadMeta = document.createElement("span");
threadMeta.textContent = (normalizeUsername(threadMessage.sender) === ownName
? "Du"
: threadMessage.sender || "System") + " · " + formatMessageTime(threadMessage.createdAt);
const threadText = document.createElement("p");
threadText.textContent = threadMessage.text;
threadItem.append(threadMeta, threadText);
thread.appendChild(threadItem);
});
toggleButton.addEventListener("click", (event) => {
event.stopPropagation();
const isCollapsed = thread.classList.toggle("d-none");
toggleButton.textContent = isCollapsed ? "Verlauf anzeigen" : "Verlauf ausblenden";
});
item.appendChild(toggleButton);
item.appendChild(thread);
}
if (message.challenge && hasScore(message.challenge.challengerScore) && hasScore(message.challenge.opponentScore)) {
item.appendChild(createChallengeResultGraphic(message.challenge));
}
if (canOpponentAcceptChallenge(message)) {
const acceptButton = document.createElement("button");
acceptButton.type = "button";
acceptButton.className = "btn btn-sm mt-3";
acceptButton.textContent = "Challenge annehmen";
acceptButton.addEventListener("click", () => startChallenge(message, "opponent"));
item.appendChild(acceptButton);
}
if (canChallengerPlayChallenge(message)) {
const playButton = document.createElement("button");
playButton.type = "button";
playButton.className = "btn btn-sm mt-3";
playButton.textContent = "Challenge spielen";
playButton.addEventListener("click", () => startChallenge(message, "challenger"));
item.appendChild(playButton);
const challengeButtonState = getChallengeButtonState(message);
if (challengeButtonState) {
const challengeButton = document.createElement("button");
challengeButton.type = "button";
challengeButton.className = "btn btn-sm mt-3 challenge-status-button";
challengeButton.textContent = challengeButtonState.label;
challengeButton.disabled = challengeButtonState.disabled;
if (!challengeButtonState.disabled) {
challengeButton.addEventListener("click", () => startChallenge(message, challengeButtonState.role));
}
item.appendChild(challengeButton);
}
messageList.appendChild(item);
@ -383,11 +625,38 @@
}
function createChallengeResultGraphic(result) {
// Baut die kompakte Ergebnisgrafik mit passendem Bild aus Sicht des aktuellen Users.
const graphic = document.createElement("div");
graphic.className = "challenge-result-graphic";
const winnerName = result.winner ?? "Unentschieden";
const isDraw = result.winner === null || result.winner === "draw";
const auth = getAuth();
const isOwnWin = !isDraw && normalizeUsername(winnerName) === normalizeUsername(auth?.username);
const outcome = isDraw ? "draw" : isOwnWin ? "win" : "loss";
const outcomeText = outcome === "draw"
? "Unentschieden"
: outcome === "win"
? "Gewonnen"
: "Verloren";
const outcomeImage = outcome === "draw"
? "image/unentschieden.png"
: outcome === "win"
? "image/sieg.png"
: "image/verloren.png";
const outcomeHeader = document.createElement("div");
outcomeHeader.className = "challenge-outcome challenge-outcome-" + outcome;
const outcomeImg = document.createElement("img");
outcomeImg.src = outcomeImage;
outcomeImg.alt = outcomeText;
const outcomeLabel = document.createElement("strong");
outcomeLabel.textContent = outcomeText;
outcomeHeader.append(outcomeImg, outcomeLabel);
const headline = document.createElement("div");
headline.className = "challenge-result-headline " + (isDraw ? "challenge-result-draw" : "challenge-result-win");
headline.textContent = isDraw ? "Unentschieden" : "Sieger: " + winnerName;
@ -408,7 +677,7 @@
opponent.querySelector("span").textContent = String(result.opponentScore ?? "-") + " Punkte";
scores.append(challenger, opponent);
graphic.append(headline, scores);
graphic.append(outcomeHeader, headline, scores);
return graphic;
}
@ -432,6 +701,7 @@
}
async function loadMessages(options = {}) {
// Laedt Nachrichten vom Backend und blendet die Seite bei fehlendem Login in einen Hinweiszustand.
const auth = getAuth();
const messageService = getMessageService();
const loggedInDiv = document.getElementById("messages-content");
@ -478,6 +748,7 @@
}
async function handleChallengeSubmit(event) {
// Erstellt eine Challenge. Der Gegner spielt zuerst; der weitere Ablauf wird ueber Nachrichten gesteuert.
event.preventDefault();
const auth = getAuth();

View File

@ -9,6 +9,9 @@ document.addEventListener("DOMContentLoaded", () => {
const activeLink = document.getElementById(id);
if (activeLink) activeLink.classList.add("active");
}
// Laedt Teilseiten dynamisch in den Hauptbereich.
// Danach wird die passende Init-Funktion aufgerufen, weil die Elemente erst nach dem Laden existieren.
window.loadPage = function loadPage(page, menuId) {
fetch("pages/" + page + ".html", { cache: "no-store" })
.then(response => {
@ -42,6 +45,7 @@ document.addEventListener("DOMContentLoaded", () => {
});
};
// Navigation wird zentral verdrahtet, damit alle Seiten ueber denselben Lade-Mechanismus laufen.
const navHome = document.getElementById("nav-home");
const navPlay = document.getElementById("nav-play");
const navMyScores = document.getElementById("nav-my-scores");

View File

@ -178,7 +178,13 @@
// Entfernt alles, was beim Vergleichen nicht zaehlen soll.
function normalizeWord(word) {
return word.toLowerCase().replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, "");
return word
.toLowerCase()
.replace(/ä/g, "ae")
.replace(/ö/g, "oe")
.replace(/ü/g, "ue")
.replace(/ß/g, "ss")
.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, "");
}
// Behält die sichtbaren Woerter separat, damit Satzzeichen in der Ergebnisanzeige erhalten bleiben.
@ -276,6 +282,8 @@
}
function readActiveChallenge() {
// Challenge-Runden werden aus der Nachrichten-Seite gestartet.
// Der dafuer gespeicherte Kontext entscheidet, welcher API-/Nachrichten-Flow nach dem Spiel laeuft.
const raw = sessionStorage.getItem(ACTIVE_CHALLENGE_STORAGE_KEY);
if (!raw) {
return null;
@ -298,6 +306,7 @@
}
function isChallengeSecondRound() {
// In der zweiten Runde spielt der Herausforderer gegen den bereits bekannten Gegner-Score.
return hasActiveChallenge()
&& activeChallenge.role === "challenger"
&& activeChallenge.opponentScore !== null
@ -305,6 +314,7 @@
}
function isChallengeFirstRound() {
// In der ersten Runde spielt der herausgeforderte User und sendet sein Resultat an den Herausforderer.
return hasActiveChallenge() && activeChallenge.role === "opponent";
}
@ -317,6 +327,8 @@
}
async function saveScore(scoreData) {
// Normale Spielrunden werden direkt im Score-/Leaderboard-Backend gespeichert.
// Challenge-Runden verwenden je nach Rolle einen separaten Ablauf weiter unten.
const auth = getAuth();
if (!auth || !auth.username || !auth.password) {
showScoreSaveFeedback(
@ -374,6 +386,8 @@
}
function renderChallengeResult(score) {
// Zeigt das lokale Ergebnis der finalen Challenge-Runde sofort an,
// unabhaengig davon, ob der Backend-Abschluss erfolgreich ist.
if (!scoreSaveFeedback || !isChallengeSecondRound()) {
return;
}
@ -397,10 +411,32 @@
: result === "loser"
? opponentName + " gewinnt die Challenge"
: "Unentschieden";
const outcomeText = result === "winner"
? "Gewonnen"
: result === "loser"
? "Verloren"
: "Unentschieden";
const outcomeImage = result === "winner"
? "image/sieg.png"
: result === "loser"
? "image/verloren.png"
: "image/unentschieden.png";
const graphic = document.createElement("div");
graphic.className = "play-challenge-result play-challenge-result-" + result;
const outcomeHeader = document.createElement("div");
outcomeHeader.className = "challenge-outcome challenge-outcome-" + (result === "winner" ? "win" : result === "loser" ? "loss" : "draw");
const outcomeImg = document.createElement("img");
outcomeImg.src = outcomeImage;
outcomeImg.alt = outcomeText;
const outcomeLabel = document.createElement("strong");
outcomeLabel.textContent = outcomeText;
outcomeHeader.append(outcomeImg, outcomeLabel);
const title = document.createElement("div");
title.className = "play-challenge-result-title";
title.textContent = headline;
@ -421,11 +457,13 @@
otherScore.querySelector("span").textContent = opponentScore + " Punkte";
scores.append(ownScore, otherScore);
graphic.append(title, scores);
graphic.append(outcomeHeader, title, scores);
scoreSaveFeedback.insertAdjacentElement("afterend", graphic);
}
async function completeChallenge(scoreData) {
// Primaerer Abschluss ueber das Challenge-Backend.
// Wenn dieser Endpoint ablehnt, faellt submitScore auf den Nachrichten-Fallback zurueck.
const auth = getAuth();
if (!auth || !hasActiveChallenge()) {
return;
@ -451,11 +489,8 @@
);
if (!result.ok) {
showScoreSaveFeedback(
"Challenge konnte nicht abgeschlossen werden (Status " + result.status + ").",
"danger",
);
return;
console.warn("Challenge-Abschluss nicht moeglich, nutze Nachrichten-Fallback. Status:", result.status);
return false;
}
sessionStorage.removeItem(ACTIVE_CHALLENGE_STORAGE_KEY);
@ -468,9 +503,12 @@
if (typeof window.updateMessagesNavState === "function") {
window.updateMessagesNavState();
}
return true;
}
async function finishFirstChallengeRound(scoreData) {
// Der Gegner speichert zuerst seinen Score und informiert danach den Herausforderer.
await saveScore(scoreData);
await notifyChallenger(scoreData);
@ -484,6 +522,7 @@
}
async function notifyChallenger(scoreData) {
// Nutzt den bestehenden Nachrichten-Endpunkt, um dem Herausforderer den Gegner-Score zu senden.
const auth = getAuth();
const messageService = getMessageService();
if (!auth || !messageService || !activeChallenge.challenger) {
@ -523,7 +562,110 @@
}
}
function getChallengeWinner(challengerScore, opponentScore) {
if (challengerScore > opponentScore) {
return activeChallenge.challenger || getAuth()?.username || "Herausforderer";
}
if (challengerScore < opponentScore) {
return activeChallenge.opponent || "Gegner";
}
return "draw";
}
async function sendChallengeResultMessage(recipient, resultData, text) {
const auth = getAuth();
const messageService = getMessageService();
if (!auth || !messageService || !recipient) {
return false;
}
const messageText =
CHALLENGE_DATA_PREFIX +
JSON.stringify(resultData) +
"]]" +
"\n" +
text;
const result = await messageService.postMessage(
auth.username,
auth.password,
recipient,
"challenge",
messageText,
);
return result.ok;
}
async function finishFinalChallengeWithMessages(scoreData) {
// Fallback fuer den finalen Challenge-Abschluss:
// Score speichern, Gewinner lokal berechnen und Ergebnis per Nachricht an den Gegner senden.
const auth = getAuth();
if (!auth || !activeChallenge) {
return;
}
await saveScore(scoreData);
const opponentScore = Number(activeChallenge.opponentScore);
const winner = getChallengeWinner(scoreData.score, opponentScore);
const resultData = {
id: activeChallenge.id,
challenger: activeChallenge.challenger || auth.username,
opponent: activeChallenge.opponent,
challengerScore: scoreData.score,
opponentScore: opponentScore,
winner: winner,
};
const winnerText = winner === "draw"
? "Unentschieden"
: "Gewinner: " + winner;
const resultText =
"Challenge abgeschlossen. " +
resultData.challenger +
": " +
resultData.challengerScore +
" Punkte, " +
resultData.opponent +
": " +
resultData.opponentScore +
" Punkte. " +
winnerText +
".";
const opponentMessageSent = await sendChallengeResultMessage(
activeChallenge.opponent,
resultData,
resultText,
);
sessionStorage.removeItem(ACTIVE_CHALLENGE_STORAGE_KEY);
activeChallenge = null;
if (!opponentMessageSent) {
showScoreSaveFeedback(
"Score gespeichert, aber die Ergebnisnachricht an den Gegner konnte nicht gesendet werden.",
"warning",
);
return;
}
showScoreSaveFeedback(
"Challenge abgeschlossen. Ergebnisnachricht wurde an den Gegner gesendet.",
"success",
);
if (typeof window.updateMessagesNavState === "function") {
window.updateMessagesNavState();
}
}
async function submitScore() {
// Gemeinsamer Abschluss fuer normale Spiele und Challenge-Runden.
// Der aktive Challenge-Kontext entscheidet, welcher Speicher-/Nachrichtenfluss verwendet wird.
if (!userTextInput) return;
const userInput = userTextInput.value.trim();
@ -572,7 +714,16 @@
await finishFirstChallengeRound(scoreData);
} else if (hasActiveChallenge()) {
showScoreSaveFeedback("Challenge-Resultat wird gesendet...", "info");
await completeChallenge(scoreData);
let completed = false;
try {
completed = await completeChallenge(scoreData);
} catch (error) {
console.warn("Challenge-Abschluss fehlgeschlagen, nutze Nachrichten-Fallback.", error);
}
if (!completed) {
showScoreSaveFeedback("Backend-Abschluss nicht moeglich. Ergebnis wird ueber Nachrichten gesendet...", "info");
await finishFinalChallengeWithMessages(scoreData);
}
} else {
await saveScore(scoreData);
}
@ -591,6 +742,7 @@
}
window.initPlayPage = function initPlayPage() {
// Die Spielseite wird dynamisch geladen; daher werden DOM-Elemente und Events hier initialisiert.
clearInterval(timerInterval);
activeChallenge = readActiveChallenge();