diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b30f42..91a5bdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- „Von Level 1 neu starten" setzt den Galerie-Speicherstatus zurück (vorher blieb „In Galerie speichern" ggf. gesperrt) +- Ergebnis-Feedback wechselte bei jedem Re-Render, weil die rotierende Nachricht im Render gewählt wurde — jetzt einmal pro Bewertung; DE/EN-Texte für 1–2 Sterne laufen nicht mehr auseinander + ### Changed - Startseite entschlackt: „Spiel starten" ist die einzige auffällige Aktion (ohne Dauer-Puls); Tägliche Challenge als dezenter Textlink darunter (ohne ⚡, Farbrahmen, Countdown); Level/Galerie/Freies Malen als einfarbige Textlinks im Footer; Statistik als eine Textzeile statt drei Emoji-Karten; Streak-Badge und schwebende Sterne entfernt — die Serie steht nur noch in Malis Begrüßung diff --git a/components/__tests__/GamePhaseComponents.test.tsx b/components/__tests__/GamePhaseComponents.test.tsx index fa508ac..ccf8c3b 100644 --- a/components/__tests__/GamePhaseComponents.test.tsx +++ b/components/__tests__/GamePhaseComponents.test.tsx @@ -316,6 +316,16 @@ describe('ResultPhase', () => { expect(texts).toContain('feedback-en-5'); }); + it('selects feedback once per rating, not on every re-render', () => { + const { getRatingFeedback } = require('../../services/RatingManager'); + getRatingFeedback.mockClear(); + const { rerender } = render(); + rerender( + , + ); + expect(getRatingFeedback).toHaveBeenCalledTimes(1); + }); + it('shows saved label when savedToGallery is true', () => { const { UNSAFE_getAllByType } = render(); const texts = getAllTexts(UNSAFE_getAllByType); diff --git a/components/game/ResultPhase.tsx b/components/game/ResultPhase.tsx index 89b87fe..35e37ea 100644 --- a/components/game/ResultPhase.tsx +++ b/components/game/ResultPhase.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Modal } from 'react-native'; import DrawingCanvas from '@components/DrawingCanvas'; import LevelImageDisplay from '@components/LevelImageDisplay'; @@ -50,13 +50,17 @@ export default function ResultPhase({ } }, [isLastLevel, userRating]); - const getFeedbackText = (rating: number) => { - if (rating >= 1 && rating <= 5) { - const feedback = getRatingFeedback(rating as StarRating); - return currentLang === 'en' ? feedback.messageEn : feedback.message; - } - return t('game.result.tapStars'); - }; + // Feedback einmal pro Bewertung auswählen: getRatingFeedback() rotiert durch + // Nachrichten, ein Aufruf im Render ließ den Text bei jedem Re-Render wechseln. + const feedback = useMemo( + () => (userRating >= 1 && userRating <= 5 ? getRatingFeedback(userRating as StarRating) : null), + [userRating], + ); + const feedbackText = feedback + ? currentLang === 'en' + ? feedback.messageEn + : feedback.message + : t('game.result.tapStars'); const imageSize = Math.min(Math.max(screenWidth * 0.44, 140), 220); @@ -230,7 +234,7 @@ export default function ResultPhase({ )} - {getFeedbackText(userRating)} + {feedbackText} diff --git a/services/RatingManager.ts b/services/RatingManager.ts index 798b145..3037be7 100644 --- a/services/RatingManager.ts +++ b/services/RatingManager.ts @@ -25,11 +25,12 @@ const lowRatingMessages = { let lowRatingMessageIndex = 0; -function getRotatingLowRatingMessage(language: 'de' | 'en' = 'de'): string { - const messages = lowRatingMessages[language]; - const message = messages[lowRatingMessageIndex % messages.length]; +// DE und EN als Paar mit demselben Index (vorher zwei getrennte Aufrufe, +// die den Zähler doppelt weiterschoben und die Sprachen auseinanderlaufen ließen). +function getRotatingLowRatingMessages(): { de: string; en: string } { + const idx = lowRatingMessageIndex % lowRatingMessages.de.length; lowRatingMessageIndex++; - return message; + return { de: lowRatingMessages.de[idx], en: lowRatingMessages.en[idx] }; } /** @@ -91,14 +92,16 @@ function getRotatingHighRatingMessages(stars: 4 | 5): { de: string; en: string } export function getRatingFeedback(stars: StarRating, language: 'de' | 'en' = 'de'): RatingFeedback { switch (stars) { case 1: - case 2: + case 2: { + const low = getRotatingLowRatingMessages(); return { stars, - message: getRotatingLowRatingMessage('de'), - messageEn: getRotatingLowRatingMessage('en'), + message: low.de, + messageEn: low.en, animation: 'none', sound: 'huchhu.mp3', }; + } case 3: return { diff --git a/services/__tests__/useGamePhase.test.ts b/services/__tests__/useGamePhase.test.ts index 771959b..39e189b 100644 --- a/services/__tests__/useGamePhase.test.ts +++ b/services/__tests__/useGamePhase.test.ts @@ -290,6 +290,24 @@ describe('useGamePhase', () => { expect(clearCanvas).toHaveBeenCalled(); }); + it('resets savedToGallery when restarting from level 1', async () => { + const { result } = renderGamePhase({ + initialLevel: 5, + drawingPaths: [{ points: [{ x: 0, y: 0 }], color: '#000', strokeWidth: 3 }], + }); + + await act(async () => { + await result.current.saveToGallery(); + }); + expect(result.current.savedToGallery).toBe(true); + + act(() => { + result.current.restartFromLevel1(); + }); + + expect(result.current.savedToGallery).toBe(false); + }); + // --- Replay --- it('starts replay', () => { diff --git a/services/useGamePhase.ts b/services/useGamePhase.ts index 86bd102..132912f 100644 --- a/services/useGamePhase.ts +++ b/services/useGamePhase.ts @@ -291,6 +291,7 @@ export function useGamePhase({ const restartFromLevel1 = () => { setPhase('memorize'); setUserRating(0); + setSavedToGallery(false); clearCanvas(); setLevelNumber(1); // triggers level-init useEffect };