297 lines
19 KiB
Markdown
297 lines
19 KiB
Markdown
# 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 supported Survival
|
||
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, Off/On setting, 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.
|