diff --git a/docs/README.md b/docs/README.md index ff3ded0..f7f567e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -33,6 +33,10 @@ and launch your first server. ## Plans +[Player achievements and visual refresh](plans/player-achievements-and-visual-refresh-plan.md) +proposes calendar leaderboards, personal-best highlights, shareable result cards, +and consistent map and status presentation. + [Lobby autoscaling](plans/lobby-autoscaling-plan.md) records the implemented design and the production dry run, pilot measurements, and rollout still to complete. Current operating instructions live in the guides above. diff --git a/docs/plans/player-achievements-and-visual-refresh-plan.md b/docs/plans/player-achievements-and-visual-refresh-plan.md new file mode 100644 index 0000000..44e39ca --- /dev/null +++ b/docs/plans/player-achievements-and-visual-refresh-plan.md @@ -0,0 +1,296 @@ +# Player achievements and visual refresh implementation plan + +Status: Proposed October 7, 2026. Implementation has not started. + +Make Electric Cherry more rewarding to revisit by showing recent player +achievements, making match results easy to share, and giving maps a recognizable +visual identity. Deliver player achievements first, then refresh the presentation +across Fleet, Autoscaling, and operations. Each workstream should be useful on +its own. + +## Intended experience + +- A player can compare this week's or this month's results, open the match that + earned a record, and see how it improved on their previous best. +- An operator can export a readable match or player result card without manually + assembling a screenshot. +- Maps and games are recognizable at a glance. Player counts, rounds, health, + and required actions have a clear visual order. +- Routine states look calm. Failures, stale observations, and safety blockers + remain visible without opening technical details. + +## Scope and defaults + +| Area | First release | +| --- | --- | +| Leaderboard periods | All time, This week, and This month, with previous/next period navigation. Keep All time as the default. | +| Calendar | Weeks start Monday at 00:00 UTC; months start on the first at 00:00 UTC. Show the boundaries explicitly. Personal timezone preferences format timestamps without changing competition membership. | +| Personal bests | Highest round, best game kills, and highest reported score, compared within the same game and map variant. | +| Sharing | A preview and PNG download for a match summary or one player's result, plus an authenticated match link. Native device sharing is an optional enhancement with download/copy fallbacks. | +| Visual direction | Existing dark surfaces, electric-blue accent, typography, and brand mark, extended with original SVG map emblems and consistent game/status treatments. | +| Initial visual coverage | Fleet, server detail, Autoscaling, provisioning and host operation history, player highlights, and match/share cards. | + +Season rewards, player-account linking, public profile/match pages, notification +subscriptions, rank-change tracking, custom themes, and photographic map artwork +are outside this release. These features need separate product decisions. + +## Existing foundations + +The [leaderboard guide](../guides/leaderboard.md) defines the current qualification +rules, player identity, record evidence, and access model. Completed matches and +player participation persist independently of live servers and operational +history retention. Reuse these records rather than requiring reporter changes. + +The leaderboard already has game/map/metric filters, shareable filter URLs, +player profiles, source-match links, and refresh behavior that protects someone +inspecting a row. Fleet already has search, pins, grouping, cards/table views, +and a live match spotlight. Preserve those behaviors. + +Extend the [shared UI patterns](../development/ui-patterns.md), semantic tokens, +`ServerIdentity`, `StatusBadge`, and existing disclosure controls. The visual +work should refine their use across pages. + +## Player achievements + +### Calendar rankings + +Add validated `period=all|week|month` and optional `start=YYYY-MM-DD` query +parameters. An omitted start selects the current period; an explicit start must +be a Monday for a week or the first day for a month. Reject malformed dates, +misaligned boundaries, and a start supplied with All time. Disable navigation +into future periods. Existing URLs without these parameters retain their behavior. + +Return resolved period metadata, including inclusive start, exclusive end, and +the fixed UTC timezone. Use the same resolved boundaries for rankings, summary +totals, eligibility thresholds, exact-player rank, selected profile history, +and record evidence. Apply a period to a match's completion time, so a match +crossing midnight belongs entirely to the period in which it completed. + +Retain the existing rules: only completed games reaching round 2 with qualifying +player participation contribute; ties share rank; search preserves actual rank; +GUIDs combine across T5/T6; map variants remain distinct. Efficiency thresholds +apply to matches inside the selected period. Keep known map options available +when a selected period has no results. + +On the profile, label lifetime totals separately from the selected competition +and filtered match history. Carry period/start through profile links, record +links, pagination, and return navigation. Reset pagination when the period +changes. A copy-link action should resolve the current period to an explicit +start so the shared link still identifies the same competition next week. + +Add an additive nullable `first_completed_at` field to historical matches. Set it +once when a match first becomes completed. The current ingestion path can update +`ended_at` when a changed final report arrives; a correction must not move a +match into another competition. Preserve the existing `ended_at` behavior. +Backfill completed rows from valid `ended_at` values as the best available +historical estimate. Unknown completion times remain eligible for All time but +are excluded from dated competitions, with that limitation explained in report +help. + +Create any index on the new field after the additive column migration. Startup +currently executes the base schema before adding columns to existing databases. +Use a dedicated, idempotent migration that normalizes valid values to ISO UTC; +existing timestamp-normalization markers must not cause upgrades to skip it. +Evaluate a partial completion-time index for completed matches using query plans +and the existing synthetic benchmark before selecting the final index. Period +membership stays stable after correction, while corrected counters can update +the rankings for a past period. + +Resolve the period before report submission, and include its bounds in all +in-flight, worker-response, and ranked-scope cache keys. A week or month must +roll over without requiring a database write. Extend evidence requests with the +same bounds so a period's record never links to an equal result from another +period. Preserve the current all-time evidence tie-breaking behavior. + +Primary changes: + +- [Leaderboard types](../../src/lib/leaderboard.ts), API validation, page and + profile loaders, and [navigation context](../../src/lib/leaderboard-navigation.ts). +- [Ranking queries](../../src/lib/server/fleet/leaderboard.ts), + [record evidence](../../src/lib/server/fleet/leaderboard-records.ts), player + profile queries, and report worker/cache inputs. +- Database schema/migration and + [completion ingestion](../../src/lib/server/fleet/leaderboard-ingest.ts). + +### Personal best highlights + +Show a compact Recent personal bests section on player profiles and annotate +the relevant results on completed match pages. Use copy such as “Highest round +on Town Survival: 31, up from 27.” Identify whose record it is; dashboard accounts +are not currently linked to player GUIDs. + +A milestone is a strict improvement over all earlier qualifying results for the +same GUID, game, map key, and metric. Order by `first_completed_at`, then match ID +for deterministic simultaneous completions. An equal result is not a new record. +A first qualifying result is labeled First recorded result rather than an +improvement. A later record does not remove the earlier match's “Personal best +at the time” annotation. Current record evidence remains a separate concept. + +Derive milestones in the report worker from retained history for the requested +player or bounded match roster. Compare against the complete earlier history +before applying the displayed date range or result limit. Do not make the first +match of each week a false personal best by limiting the comparison to that week. +Batch the requested player/map scopes to avoid one query per metric or row. + +Use undated historical values as a conservative comparison floor and do not +emit dated milestones for them. Explain in methodology that incomplete older +timestamps can suppress a highlight. Recompute derived milestones after counter +corrections using the existing report revision invalidation. Duplicate reports +must not create duplicate highlights. No new event journal, notification history, +or full-history startup backfill is needed. + +Initially show at most five grouped match highlights on a profile, with a link +to its existing match history. Follow the selected game/map and competition +period for displayed highlights while comparing against all earlier history. +Group multiple improved metrics from one match. +Keep the existing qualification caveats near the evidence: highest round can +include late joins, score is the highest observed balance, and first observation +does not prove continuous participation. Add no confetti or repeating animation. + +Add a dedicated milestone query/presentation module alongside the existing +leaderboard records and pass only the needed results to profile and match pages. +Use explicit report-worker job handling for these reads rather than expanding +the worker's current player-report fallback implicitly. + +### Shareable match results + +Add Share result to completed match pages, and a player-specific action to each +eligible result. Open an accessible dialog with two layouts: Match summary and +Player result. The preview and exported image use the same bounded presentation +model. + +The summary includes map, game, final round, completion date, and known duration. +The player layout adds display name, recorded player statistics, and applicable +personal-best improvements. Use the player's observed highest round for their +result, which may differ from the match's final round. Include relevant late-join +or missing-start context. Omit unknown values rather than rendering them as zero. + +Render a 1200 × 630 PNG locally using bundled fonts and assets. Share-card layout +code must wrap or deliberately truncate long names without dropping essential +qualifications. Provide semantic HTML for the preview; the canvas is an export +implementation detail. Wait for fonts/assets before export and show a retryable +error if generation fails. Native sharing, when supported, must run only from an +explicit user action. + +Construct an explicit export model. Exclude chat, GUIDs, internal instance IDs, +addresses, keys, and raw diagnostics; the current match detail object includes +chat and must not be exported wholesale. Offer a plain match link labeled as +requiring Electric Cherry access. Do not change route permissions or introduce +an unauthenticated image endpoint. Downloads remain useful to recipients without +a dashboard account. + +Implement the first version with the current brand styles. Apply the new map +emblems during the visual workstream without changing result semantics. + +## Visual refresh + +### Map and game identity + +Add a presentation resolver keyed by game and selected map slug, backed by the +existing [map catalog](../../src/lib/map-aliases.ts). Use original small SVG +emblems: a theater marquee for Kino, a rocket for Ascension, a moon for Moon, +prison bars for Mob, and excavation/gears for Origins. Complete the catalog with +the same simple line or silhouette style and a neutral custom-map fallback. + +Maps sharing an environment may share artwork, while Survival, Grief, Turned, +and other variants retain explicit names. Historical views resolve identity from +their stored game/map key and variant, never from a replacement live server. +An ambiguous engine map must use a fallback rather than inventing a variant. +Extend `MatchDetail`, `PlayerMatch`, and their queries with the stored map key; +they currently expose friendly map names without that identity. Do not infer +historical variants from display names. + +Introduce `MapEmblem` and a shared game label/badge. Keep Black Ops I/II readable +without color, using distinct I/II marks where space is limited. Put larger +emblems on map headings and result cards, smaller ones on lobby cards, and keep +table mode compact. Artwork is decorative when adjacent text names the map. + +Use locally shipped, dimensioned SVG assets or components. Keep the current +fonts and dark/electric-blue palette. Game decoration must not reuse danger or +warning tones. Add only the semantic tokens needed for identity and achievement +styling, and document their use. + +### Information order and status consistency + +| Surface | Presentation changes | +| --- | --- | +| Fleet cards | Map/lobby/game first; health, player count, and round next; roster and match duration below; lower-priority timing and technical identity in supporting details. Preserve compact empty cards, search, pins, table mode, and the match spotlight. | +| Server detail | Add map/game identity to the shared header with health. Keep player counts, round, and timing in the Live content so Configuration and Diagnostics remain focused on their task. | +| Autoscaling | Use shared identity and mode badges, a compact count strip, and a prominent current decision/blocker. Keep per-map targets, Dry run/live mode, ownership, global pause, and unsettled operations visible. Reuse existing evidence/history disclosures. | +| Provisioning and host operations | Use consistent labels and status components while preserving action, target, result, elapsed time, and specific wait reasons. Put long IDs, source paths, and low-level metadata behind explicit details/copy controls. | +| Player and match results | Reuse map identity and achievement treatment across profile highlights, match results, and downloaded cards. Distinguish a current record from a record earned at that time. | + +Align status presentation by meaning, while preserving domain-specific labels. +For example, `StatusPip` currently colors starting/provisioning amber while shared +badges use an informational treatment. Normal queued work and waiting for players +should not look like failures. Keep the specialized provisioning status helpers +that explain why an operation is waiting. + +Do not collapse freshness warnings, unknown player counts, failed operations, +safety blockers, pending destructive work, or unsettled commands. Raw identities +must remain reachable through keyboard-accessible details and copy controls. +Preserve navigation context, permission checks, confirmations, and existing +polling/store behavior. + +## Delivery sequence + +Complete, validate, and commit each phase before starting the next. Finish the +player-achievement workstream before starting the visual refresh. Commit any +unrelated fix separately. + +| Phase | Reviewable result | Acceptance gate | +| --- | --- | --- | +| A1 Completion time and period queries | Stable completion field/migration, period parsing, scoped ranking/evidence/profile queries, and cache keys. | Boundary, correction, qualification, and all-time compatibility tests pass; query plans and representative timing are recorded. | +| A2 Period navigation | Week/month controls, history navigation, period metadata, profile context, and stable share URLs. | Direct links, back/forward, rollover, empty periods, and inspection-preserving refresh work on desktop and mobile. | +| A3 Personal best highlights | Derived milestones, profile section, and match annotations. | Strict improvements, ties, first results, variants, corrections, and undated history behave as specified; long player histories remain practical. | +| A4 Result sharing | Accessible preview, PNG download, authenticated copy-link action, and supported native sharing. | Export matches the preview, contains only selected result data, handles long/incomplete data, and works after live-server deletion. Update leaderboard documentation and commit the completed workstream. | +| B1 Shared visual identity | Original emblems, resolver, game labels, achievement tokens, and usage guidance. | Every map selection resolves correctly or falls back; no misleading variant, color-only meaning, or missing-asset layout shift. | +| B2 Fleet and operational presentation | Apply identity/hierarchy/status rules to Fleet, server detail, Autoscaling, provisioning, and host operation history. | Existing actions and critical states remain visible and usable; no new polling or behavior changes. | +| B3 Player presentation and review | Apply the shared visual identity to player highlights, match pages, and export layouts; refresh documentation images. | Complete the visual/accessibility review and update UI guidance, then commit the completed workstream. | + +## Validation + +Use an isolated synthetic database. Extend existing fixture helpers; the current +`seed:fixture` package script points to a missing script and must not be assumed +to provide a working preview dataset. + +For achievements, cover Monday/month/year boundaries, leap years, viewer timezone +differences, cross-boundary matches, unknown completion timestamps, and corrections +after completion. Test upgrade from an existing database, timestamp normalization, +and restart after the completion migration. Verify that an idle database still +changes the current period at rollover. Test period-scoped record sources, ties, efficiency eligibility, +GUID identity across games, T6 variants, deleted servers, profile pagination, +and all-time response compatibility. Match/player qualification must be shared +with existing leaderboard logic. + +Test personal-best comparison against full prior history, simultaneous completion +ordering, duplicate final reports, first results, later-surpassed records, and +grouping several improvements from one match. Benchmark a player with a long +history and a full roster. Bound request scopes and response sizes; do not hide +query costs by silently ignoring older matches. Keep report work off the main +request thread. Extend the existing leaderboard benchmark for period queries +and record before/after cold and warm timings under the same fixture. + +For sharing, verify the explicit export model, missing values, long/Unicode +names, font/asset readiness, downloaded-image readability, and browser fallbacks. +Confirm that anonymous access to match/profile pages remains unchanged and that +sharing requires an explicit action. + +Review representative screens at 1440, 390, and 320 pixels, with 200% zoom, +keyboard navigation, reduced motion, and long custom names. Include empty, +occupied, stale, offline, starting, failed, waiting, and paused states. Check +contrast, focus order, non-color status labels, readable export text, and absence +of nested interactive targets. Capture before/after screenshots using identical +fixtures; verify that map, players/round, health, and required action are easy to +identify without opening details. + +During implementation, run `npm run check`, focused Vitest tests for changed +behavior, and `npm run build` for each relevant phase. Before completing each +workstream, run the full controller test suite and browser review. Agent tests +are needed only if agent code or its protocol changes; neither is planned here. +Use meaningful behavior/query tests for new rules and manual visual checks for +decorative changes. + +After delivery, fold lasting behavior into the leaderboard guide and UI patterns, +refresh the Fleet documentation image, and remove this completed plan according +to the repository documentation convention.