diff --git a/public/i18n/de.json b/public/i18n/de.json index e042edd..54e1ee4 100644 --- a/public/i18n/de.json +++ b/public/i18n/de.json @@ -288,7 +288,7 @@ }, "PLAY_NEXT": { "TITLE": "Was spiele ich als Nächstes?", - "SUBTITLE": "Drei Spiele aus deinem Backlog, hohe Priorität zuerst.", + "SUBTITLE": "Drei Spiele aus deinem Backlog, hohe Priorität zuerst. Wähle eine Länge und eine Stimmung.", "BUDGET_LABEL": "Spieldauer", "BUDGET": { "SHORT": "Bis 5 h", @@ -304,7 +304,18 @@ "START_OVER": "Von vorn", "EMPTY": "Dein Backlog ist leer. Stark!", "EMPTY_BUDGET": "Kein Backlog-Spiel passt zu dieser Dauer. Versuch eine längere.", - "ERROR_LOAD": "Die Vorschläge konnten nicht geladen werden." + "ERROR_LOAD": "Die Vorschläge konnten nicht geladen werden.", + "MOOD_LABEL": "Stimmung", + "MOOD": { + "ANY": "Jede Stimmung", + "STORY": "Story", + "COOP": "Koop", + "CHILL": "Entspannt", + "ACTION": "Action", + "SCARY": "Gruselig" + }, + "MOOD_HINT": "Nur Spiele, deren Genre IGDB kennt.", + "EMPTY_MOOD": "Kein Backlog-Spiel passt. Probier eine andere Stimmung oder Dauer. Neue Spiele bekommen ihre Stimmung kurz nach der ersten Auswahl." }, "MANUAL_GAME": { "ADD_BUTTON": "Spiel hinzufügen", diff --git a/public/i18n/en.json b/public/i18n/en.json index 2403cc5..751212a 100644 --- a/public/i18n/en.json +++ b/public/i18n/en.json @@ -288,7 +288,7 @@ }, "PLAY_NEXT": { "TITLE": "What to play next", - "SUBTITLE": "Three games from your backlog, high priority first.", + "SUBTITLE": "Three games from your backlog, high priority first. Pick a length and a mood.", "BUDGET_LABEL": "Time to beat", "BUDGET": { "SHORT": "Up to 5 h", @@ -304,7 +304,18 @@ "START_OVER": "Start over", "EMPTY": "Your backlog is empty. Nice!", "EMPTY_BUDGET": "No backlog game fits this length. Try a longer one.", - "ERROR_LOAD": "Could not load the picks." + "ERROR_LOAD": "Could not load the picks.", + "MOOD_LABEL": "Mood", + "MOOD": { + "ANY": "Any mood", + "STORY": "Story", + "COOP": "Co-op", + "CHILL": "Chill", + "ACTION": "Action", + "SCARY": "Scary" + }, + "MOOD_HINT": "Only games whose genre IGDB knows.", + "EMPTY_MOOD": "No backlog game fits. Try another mood or length. New games get their mood a moment after the first pick." }, "MANUAL_GAME": { "ADD_BUTTON": "Add game", diff --git a/src/pages/PlayNextPage.jsx b/src/pages/PlayNextPage.jsx index 3ea3908..7445877 100644 --- a/src/pages/PlayNextPage.jsx +++ b/src/pages/PlayNextPage.jsx @@ -23,13 +23,16 @@ const BUDGETS = [ {key: 'LONG', maxMinutes: 2400}, {key: 'ANY', maxMinutes: null}, ]; +// The moods Core derives from IGDB's genres, themes and game modes; a mood leaves out games without IGDB data. +const MOODS = ['ANY', 'STORY', 'COOP', 'CHILL', 'ACTION', 'SCARY']; const PICK_COUNT = 3; // The backend reads only this many excludes. With a bigger backlog the oldest seen games can come back. const MAX_EXCLUDES = 100; -const pickerQuery = (maxMinutes, exclude) => { +const pickerQuery = (maxMinutes, mood, exclude) => { const params = new URLSearchParams({count: String(PICK_COUNT)}); if (maxMinutes) params.set('maxMinutes', String(maxMinutes)); + if (mood !== 'ANY') params.set('mood', mood); exclude.forEach((appid) => params.append('exclude', String(appid))); return params.toString(); }; @@ -39,14 +42,15 @@ export default function PlayNextPage() { const navigate = useNavigate(); const expireSession = useSessionExpired(); const [budget, setBudget] = useState(BUDGETS[3]); + const [mood, setMood] = useState('ANY'); const [games, setGames] = useState(null); const [seen, setSeen] = useState([]); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); - // A slow answer for an older budget must not replace the newer one. + // A slow answer for an older budget or mood must not replace the newer one. const latestRequest = useRef(0); - const load = useCallback(async (maxMinutes, exclude) => { + const load = useCallback(async (maxMinutes, moodKey, exclude) => { if (!hasSession()) { navigate('/login'); return; @@ -56,7 +60,7 @@ export default function PlayNextPage() { setError(null); try { const response = await apiFetch( - `${import.meta.env.VITE_CORE_API_URL}/api/library/picker?${pickerQuery(maxMinutes, exclude)}`); + `${import.meta.env.VITE_CORE_API_URL}/api/library/picker?${pickerQuery(maxMinutes, moodKey, exclude)}`); if (request !== latestRequest.current) return; if (response.status === 401) { expireSession(); @@ -80,8 +84,8 @@ export default function PlayNextPage() { }, [navigate, expireSession]); useEffect(() => { - load(budget.maxMinutes, []); - }, [budget, load]); + load(budget.maxMinutes, mood, []); + }, [budget, mood, load]); const chooseBudget = (next) => { if (next.key === budget.key) return; @@ -90,6 +94,13 @@ export default function PlayNextPage() { setBudget(next); }; + const chooseMood = (next) => { + if (next === mood) return; + setGames(null); + setSeen([]); + setMood(next); + }; + const allShown = games?.length === 0 && seen.length > 0; return ( @@ -118,6 +129,25 @@ export default function PlayNextPage() { )} +
{t('PLAY_NEXT.MOOD_HINT')}
+ )} +{t(error)}
} {games === null && !error && ( @@ -133,7 +163,7 @@ export default function PlayNextPage() { )} {(games?.length > 0 || error) && ( -