diff --git a/README.md b/README.md index edee5ea..3248531 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,10 @@ To Be Played is split into three repositories: Play Next "Start", Questions, add game) `usePointsCheck` asks Core `GET /api/rewards/games/{appid}`, reloads the wallet when the balance moved and shows a toast ("+N points", "Already paid on ...", "No points yet: reason"). The game page shows the paid rows and the reason per missing kind (`GamePointsLine`); `pointsNotice` in `utils/rewards.js` picks the - toast, the reason codes come from Core and `POINTS.REASONS` only maps them to texts. + toast, the reason codes come from Core and `POINTS.REASONS` only maps them to texts. A finish without proof pays 10 + early points (toast "+10 points; the rest comes with proof", game line "The rest of the finish points: play once on + Steam after finishing"), and the rest once Steam play proves it. A "Finished?" card on the Questions page shows + "Up to N points" from `GET /api/rewards/expected`; when that call fails the card shows no line. ```mermaid flowchart LR @@ -65,6 +68,7 @@ flowchart LR Check --> Toast["Toast"] Check -->|balance moved| Wallet["reloadWallet: header"] Core --> Line["GamePointsLine on the game page"] + Questions["Questions page"] -->|"GET /api/rewards/expected"| CoreExpected["Core /api/rewards/expected"] ``` - **Landing page**: next to the community numbers and the top games, three blocks: games per platform, top genres and finished games per year of all users. diff --git a/public/i18n/de.json b/public/i18n/de.json index b4f5ca2..2966b5c 100644 --- a/public/i18n/de.json +++ b/public/i18n/de.json @@ -1180,15 +1180,15 @@ "HOW_TO": "So bekommst du Punkte", "RULES": { "STARTED": "5 Punkte, wenn du ein Spiel von deinem Stapel startest und spielst (Steam-Spiele, einmal pro Spiel).", - "FINISHED": "30, 50 oder 80 Punkte, wenn du ein Spiel durchspielst (nach seiner Spielzeit), sofern du es wirklich gespielt hast. Einmal pro Spiel.", - "COMPLETED": "+40 Punkte, wenn du in einem abgeschlossenen Spiel 100 % der Erfolge hast. Es braucht ein belegtes Steam-Durchspielen; ein manuelles öffnet es nie.", - "FINISHED_MANUAL": "15 Punkte für ein manuelles Spiel, das du durchspielst, höchstens 3 pro Woche (Montag bis Sonntag). Ein 4. Spiel in dieser Woche zahlt nichts.", + "FINISHED": "30, 50 oder 80 Punkte, wenn du ein Spiel durchspielst (nach seiner Spielzeit) und es danach gespielt hast. Ohne Beweis für Spielen bekommst du jetzt 10 Punkte und den Rest, sobald du es nach dem Durchspielen auf Steam spielst. Bei 0 Spielzeit bekommst du 10, den Rest nur mit Steam-Spielzeit. Einmal pro Spiel.", + "COMPLETED": "+40 Punkte, wenn du in einem abgeschlossenen Spiel 100 % der Erfolge hast. Es braucht ein belegtes Steam-Durchspielen (die frühen 10 Punkte zählen erst mit Beweis); ein manuelles öffnet es nie.", + "FINISHED_MANUAL": "15 Punkte für ein manuelles Spiel, das du durchspielst. Höchstens 3 Spiele pro Woche (Montag bis Sonntag) zahlen Punkte; manuelle Spiele und Steam-Spiele ohne Beweis teilen sich dieses Limit. Ein 4. manuelles Durchspielen in dieser Woche zahlt nichts; ein 4. Steam-Durchspielen ohne Beweis zahlt mit Beweis weiter voll.", "MONTH": "40 Punkte, wenn dein Stapel am Monatsende kleiner ist als am Monatsanfang und du in dem Monat ein Spiel durchgespielt hast.", "STREAK": "20 Punkte für jede Woche mit einem beendeten Spiel nach einer Woche mit einem beendeten Spiel.", "UNLOCK": "Gegenstände mit Schloss kann man nicht kaufen. Sie werden einmal und für immer freigeschaltet, wenn du ihr Ziel erreichst.", "PROOF": "Ein Steam-Spiel braucht als Beweis frisches Spielen: den Abschluss-Erfolg (höchstens 30 Tage vor dem Durchspielen freigeschaltet), Spielzeit ab 30 Tage vor dem Durchspielen oder die halbe Normalzeit danach gespielt.", "ONCE": "Jede Art zahlt einmal pro Spiel. Editionen eines Spiels zählen als eines.", - "PLAYSTATION": "PlayStation-Spielzeit zahlt nichts und ist kein Beweis. Ein Spiel, das mit einem PlayStation-Titel verknüpft ist, zahlt vorerst nichts.", + "PLAYSTATION": "PlayStation-Spiele zahlen nichts und PlayStation-Spielzeit ist kein Beweis. Ein manuelles oder Steam-Spiel, das mit einem PlayStation-Titel verknüpft ist, zahlt nach seinen eigenen Regeln.", "HIDDEN": "Versteckte Spiele zahlen nichts.", "CHECKED": "Punkte werden geprüft, wenn du einen Status änderst, ein Spiel oder die App öffnest, und jede Nacht.", "STARTER": "Durchgespielte Spiele aus der Zeit vor dem Status-Verlauf bekamen einmal Startpunkte (20 pro Spiel, höchstens 250)." @@ -1198,23 +1198,27 @@ "OTHER_EDITION": "(andere Edition des Spiels)", "WAITING": "Durchgespielt: wartet auf Beweis. Spiele das Spiel nach dem Durchspielen einmal.", "WAITING_START": "Gestartet: spiele das Spiel einmal, um die 5 Punkte zu bekommen.", + "WAITING_TOPUP": "Der Rest der Durchspiel-Punkte: spiele nach dem Durchspielen einmal auf Steam.", "NONE": "{{kind}}: keine Punkte - {{reason}}", "TOAST_BOOKED": "+{{count}} Punkte", + "TOAST_BOOKED_REST": "+{{count}} Punkte; den Rest gibt es mit Beweis", "TOAST_PAID": "Schon bezahlt am {{date}}", + "TOAST_PAID_REST": "Schon bezahlt am {{date}}; den Rest gibt es mit Beweis", "TOAST_NONE": "Noch keine Punkte: {{reason}}", "TOAST_STATUS": "Dieser Status bringt keine Punkte" }, "REASONS": { "HIDDEN": "das Spiel ist ausgeblendet", "PLAYSTATION": "PlayStation-Spiele zahlen nichts", - "PLAYSTATION_LINKED": "das Spiel ist mit einem PlayStation-Titel verknüpft (zahlt vorerst nichts)", "BEFORE_HISTORY": "durchgespielt, bevor der Status-Verlauf begann (siehe Startpunkte)", "MANUAL_GAME": "ein manuelles Spiel hat keine Spielzeit als Beweis", "NO_PLAY_AFTER_START": "seit dem Start nicht gespielt", "NO_PLAYTIME": "keine Spielzeit erfasst", + "PLAY_NOT_COUNTED": "PlayStation-Minuten sind in dieses Spiel eingerechnet, darum ist Spielen kein Beweis", "NO_PROOF": "kein Beweis für frisches Spielen, spiele einmal nach dem Durchspielen", "WEEKLY_LIMIT": "Wochenlimit erreicht", - "NEEDS_PROVEN_FINISH": "braucht zuerst ein bewiesenes Durchspielen", + "WEEKLY_LIMIT_NO_PROOF": "Wochenlimit erreicht, also keine frühen Punkte; ein Beweis zahlt weiter das volle Durchspielen", + "NEEDS_PROVEN_FINISH": "braucht zuerst einen Beweis für Spielen auf Steam", "NOT_ALL_ACHIEVEMENTS": "nicht alle Erfolge freigeschaltet" }, "SLOTS": { @@ -1290,6 +1294,8 @@ "STARTED": "Gestartet", "FINISHED": "Durchgespielt", "FINISHED_MANUAL": "Durchgespielt (manuelles Spiel)", + "FINISHED_EARLY": "Durchgespielt (frühe Punkte)", + "FINISHED_TOPUP": "Durchspielen bewiesen, Rest", "COMPLETED": "100-%-Bonus", "MONTH": "Kleinerer Stapel im", "STREAK": "Serien-Woche", @@ -1350,7 +1356,8 @@ "FINISHED": { "ASK": "Durchgespielt?", "REASON": "Du hast {{playtime}} gespielt.", - "REASON_TTB": "Du hast {{playtime}} gespielt. Die meisten brauchen {{ttb}}." + "REASON_TTB": "Du hast {{playtime}} gespielt. Die meisten brauchen {{ttb}}.", + "POINTS": "Bis zu {{count}} Punkte" }, "DROPPED": { "ASK": "Abgebrochen?", diff --git a/public/i18n/en.json b/public/i18n/en.json index c822556..592b153 100644 --- a/public/i18n/en.json +++ b/public/i18n/en.json @@ -1180,15 +1180,15 @@ "HOW_TO": "How to earn points", "RULES": { "STARTED": "5 points when you start a game from your pile and play it (Steam games, once per game).", - "FINISHED": "30, 50 or 80 points when you finish a game (by its time to beat), if you really played it. Once per game.", - "COMPLETED": "+40 points when you reach 100 % achievements in a completed game. It needs a proven Steam finish; a manual finish never opens it.", - "FINISHED_MANUAL": "15 points for a manual game you finish, at most 3 per week (Monday to Sunday). A 4th finish in that week pays nothing.", + "FINISHED": "30, 50 or 80 points when you finish a game (by its time to beat) and played it since. Without proof of play you get 10 points now, and the rest once you play it on Steam after finishing. With 0 play time on record you get 10, and the rest only with Steam play. Once per game.", + "COMPLETED": "+40 points when you reach 100 % achievements in a completed game. It needs a proven Steam finish (the early 10 points do not count until proof arrives); a manual finish never opens it.", + "FINISHED_MANUAL": "15 points for a manual game you finish. At most 3 finishes per week (Monday to Sunday) pay points; manual finishes and Steam finishes without proof share this limit. A 4th manual finish in that week pays nothing; a 4th Steam finish without proof still pays in full once proven.", "MONTH": "40 points when your pile is smaller at the end of a month than at its start and you finished a game that month.", "STREAK": "20 points for every week with a finished game after a week with a finished game.", "UNLOCK": "Items with a lock cannot be bought. They unlock once, for good, when you reach their goal.", "PROOF": "A Steam finish needs proof of fresh play: the ending achievement (not older than 30 days before the finish), any play from 30 days before the finish on, or half of the normal time played after it.", "ONCE": "Each kind pays once per game. Editions of one game count as one.", - "PLAYSTATION": "PlayStation play pays nothing and is no proof. A game linked to a PlayStation title pays nothing for now.", + "PLAYSTATION": "PlayStation games pay nothing and PlayStation play is no proof. A manual or Steam game linked to a PlayStation title pays by its own rules.", "HIDDEN": "Hidden games pay nothing.", "CHECKED": "Points are checked when you change a status, open a game or the app, and every night.", "STARTER": "Finishes from before the status history got starter points once (20 per game, 250 at most)." @@ -1198,23 +1198,27 @@ "OTHER_EDITION": "(other edition of the game)", "WAITING": "Finished: waiting for proof. Play the game once after finishing.", "WAITING_START": "Started: play the game once to get the 5 points.", + "WAITING_TOPUP": "The rest of the finish points: play once on Steam after finishing.", "NONE": "{{kind}}: no points - {{reason}}", "TOAST_BOOKED": "+{{count}} points", + "TOAST_BOOKED_REST": "+{{count}} points; the rest comes with proof", "TOAST_PAID": "Already paid on {{date}}", + "TOAST_PAID_REST": "Already paid on {{date}}; the rest comes with proof", "TOAST_NONE": "No points yet: {{reason}}", "TOAST_STATUS": "This status earns no points" }, "REASONS": { "HIDDEN": "the game is hidden", "PLAYSTATION": "PlayStation games pay nothing", - "PLAYSTATION_LINKED": "the game is linked to a PlayStation title (pays nothing for now)", "BEFORE_HISTORY": "finished before the status history began (see starter points)", "MANUAL_GAME": "a manual game has no play time to prove it", "NO_PLAY_AFTER_START": "no play since the start", "NO_PLAYTIME": "no play time on record", + "PLAY_NOT_COUNTED": "PlayStation minutes are mixed into this game, so play on it is no proof", "NO_PROOF": "no proof of fresh play, play once after finishing", "WEEKLY_LIMIT": "weekly limit reached", - "NEEDS_PROVEN_FINISH": "needs a proven finish first", + "WEEKLY_LIMIT_NO_PROOF": "weekly limit reached, so no early points; proof still pays the full finish", + "NEEDS_PROVEN_FINISH": "needs proof of play on Steam first", "NOT_ALL_ACHIEVEMENTS": "not all achievements unlocked" }, "SLOTS": { @@ -1290,6 +1294,8 @@ "STARTED": "Started", "FINISHED": "Finished", "FINISHED_MANUAL": "Finished (manual game)", + "FINISHED_EARLY": "Finished (early points)", + "FINISHED_TOPUP": "Finish proven, rest", "COMPLETED": "100 % bonus", "MONTH": "Smaller pile in", "STREAK": "Streak week", @@ -1350,7 +1356,8 @@ "FINISHED": { "ASK": "Finished?", "REASON": "You played {{playtime}}.", - "REASON_TTB": "You played {{playtime}}. Most players need {{ttb}}." + "REASON_TTB": "You played {{playtime}}. Most players need {{ttb}}.", + "POINTS": "Up to {{count}} points" }, "DROPPED": { "ASK": "Dropped?", diff --git a/src/components/GamePointsLine.jsx b/src/components/GamePointsLine.jsx index de96881..6149774 100644 --- a/src/components/GamePointsLine.jsx +++ b/src/components/GamePointsLine.jsx @@ -5,6 +5,10 @@ import {formatDate} from '../utils/format.js'; import {STATUS_NOT_REACHED} from '../utils/rewards.js'; const WAITING = {NO_PROOF: 'POINTS.GAME.WAITING', NO_PLAY_AFTER_START: 'POINTS.GAME.WAITING_START'}; +// The rest of an early finish waits for Steam play, with or without playtime on record. +const WAITING_TOPUP = ['NO_PROOF', 'NO_PLAYTIME']; + +const waitingKey = (kind, reason) => (kind === 'FINISHED_TOPUP' ? WAITING_TOPUP.includes(reason) && 'POINTS.GAME.WAITING_TOPUP' : WAITING[reason]); /** * What the game earned: one line per paid row and one per kind that is missing, with the reason from Core. Loading it @@ -36,7 +40,7 @@ export default function GamePointsLine({game}) { date: formatDate(row.paidAt, i18n.language, {dateStyle: 'medium'}), }) + (row.appid === game.appid ? '' : ` ${t('POINTS.GAME.OTHER_EDITION')}`)), ...Object.entries(points.missing).filter(([, reason]) => !STATUS_NOT_REACHED.includes(reason)).map(([kind, reason]) => ( - WAITING[reason] ? t(WAITING[reason]) + waitingKey(kind, reason) ? t(waitingKey(kind, reason)) : t('POINTS.GAME.NONE', {kind: kindText(kind), reason: t(`POINTS.REASONS.${reason}`)}))), ]; return lines.map((line, i) =>
{line}
); diff --git a/src/components/GamePointsLine.test.jsx b/src/components/GamePointsLine.test.jsx index c2c3b39..c3e20cb 100644 --- a/src/components/GamePointsLine.test.jsx +++ b/src/components/GamePointsLine.test.jsx @@ -50,6 +50,32 @@ describe('GamePointsLine', () => { expect(screen.getAllByText(/^POINTS.GAME.NONE/)).toHaveLength(1); }); + it('shows the early row and the waiting line for the rest of the finish', async () => { + renderLine('FINISHED', {balance: 10, paid: [row('FINISHED_EARLY', {points: 10})], missing: {FINISHED_TOPUP: 'NO_PROOF'}}); + + expect(await screen.findByText('POINTS.GAME.WAITING_TOPUP')).toBeTruthy(); + expect(screen.getAllByText(/^POINTS.GAME.PAID/)).toHaveLength(1); + expect(screen.queryByText(/^POINTS.GAME.NONE/)).toBeNull(); + }); + + it('shows the waiting line for the rest when there is no play time on record', async () => { + renderLine('FINISHED', {balance: 10, paid: [row('FINISHED_EARLY', {points: 10})], missing: {FINISHED_TOPUP: 'NO_PLAYTIME'}}); + + expect(await screen.findByText('POINTS.GAME.WAITING_TOPUP')).toBeTruthy(); + }); + + it('shows a reason line for a hidden game that waits for the rest', async () => { + renderLine('FINISHED', {balance: 10, paid: [row('FINISHED_EARLY', {points: 10})], missing: {FINISHED_TOPUP: 'HIDDEN'}}); + + expect(await screen.findByText(/^POINTS.GAME.NONE/)).toBeTruthy(); + }); + + it('shows the top-up row with its date', async () => { + renderLine('FINISHED', {balance: 30, paid: [row('FINISHED_EARLY', {points: 10}), row('FINISHED_TOPUP', {points: 20})], missing: {}}); + + expect(await screen.findAllByText(/^POINTS.GAME.PAID/)).toHaveLength(2); + }); + it('shows no line for a status the game never reached', async () => { renderLine('BACKLOG', {balance: 30, paid: [], missing: {STARTED: 'NOT_STARTED', FINISHED: 'NOT_FINISHED', COMPLETED: 'NOT_COMPLETED'}}); diff --git a/src/components/QuestionCard.jsx b/src/components/QuestionCard.jsx index 82d5fdc..70622dd 100644 --- a/src/components/QuestionCard.jsx +++ b/src/components/QuestionCard.jsx @@ -14,9 +14,10 @@ import {formatPlaytime} from '../utils/games.js'; * * @param item a result of `GET /api/library/questions` * @param busy true while an answer is on its way + * @param expectedPoints the points a finish would pay (`GET /api/rewards/expected`), absent or 0 shows nothing * @param onAnswer `({yes, rating?})`; a yes to the rating question comes with the stars */ -export default function QuestionCard({item, busy, onAnswer}) { +export default function QuestionCard({item, busy, expectedPoints, onAnswer}) { const {t, i18n} = useTranslation(); const {appid, name, imageUrl, question, playtimeMinutes, normallyMinutes, lastActivity} = item; const reasonKey = question === 'FINISHED' && normallyMinutes > 0 ? 'FINISHED.REASON_TTB' : `${question}.REASON`; @@ -38,6 +39,9 @@ export default function QuestionCard({item, busy, onAnswer}) {{t(`QUESTIONS.${question}.ASK`)}
{reason}
+ {question === 'FINISHED' && expectedPoints > 0 && ( +{t('QUESTIONS.FINISHED.POINTS', {count: expectedPoints})}
+ )} {question === 'RATE' ? ({t('QUESTIONS.EMPTY')}
)} {status === 'ready' && items.map((item) => ( -