Challenge-Logik überarbeitet und fertig gestellt
This commit is contained in:
parent
909eb97bbb
commit
5199b32471
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
BIN
docs/Bewertungsraster_Frontend.xlsx
Normal file
BIN
docs/Bewertungsraster_Frontend.xlsx
Normal file
Binary file not shown.
BIN
image/sieg.png
Normal file
BIN
image/sieg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 111 KiB |
BIN
image/unentschieden.png
Normal file
BIN
image/unentschieden.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 144 KiB |
BIN
image/verloren.png
Normal file
BIN
image/verloren.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 118 KiB |
@ -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>
|
||||
|
||||
317
js/messages.js
317
js/messages.js
@ -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,19 +527,29 @@
|
||||
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
|
||||
const fromToText = messageGroup.kind === "challenge"
|
||||
? "Challenge"
|
||||
: isOutgoing
|
||||
? "An " + message.recipient
|
||||
: "Von " + message.sender;
|
||||
|
||||
@ -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);
|
||||
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));
|
||||
}
|
||||
|
||||
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);
|
||||
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();
|
||||
|
||||
@ -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");
|
||||
|
||||
168
js/play.js
168
js/play.js
@ -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();
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user