You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
FlexCoins currently runs as a fully client-side game with no user identity and no persistence — every session starts fresh. To drive engagement and replayability, we want to add Google Sign-In so players have a persistent identity, and a global leaderboard so they can compete on high scores.
This feature introduces the first backend infrastructure to the project: an API layer (Lambda + API Gateway via SST), DynamoDB tables for scores and leaderboard data, and a Google OAuth integration. The Godot client will authenticate on the start screen, track session performance, submit scores at session end, and display a leaderboard UI.
Prerequisite decision: Before implementation begins, we must resolve Open Question #3 — what metric defines "score." The data model, submission payload, and leaderboard display all depend on this choice. The current proposal uses total currency earned (cumulative across all add_currency calls, independent of spending). See Open Questions for alternatives. Assign a decision owner and resolve before this issue is picked up for implementation.
Scope
In scope
Google OAuth 2.0 sign-in flow (web export is primary; desktop exports as stretch)
Backend API for score submission and leaderboard retrieval (SST Lambda + API Gateway + DynamoDB)
Session score tracking in GameManager (total currency earned in a session)
Score submission at end-of-session (explicit "End Game" button in HUD)
Leaderboard UI: global top scores and current user's rank/personal best
Anonymous play remains possible (leaderboard features are gated behind sign-in, not gameplay)
Out of scope
Save/load of game state or upgrade progress across sessions
Social features (friends, sharing, chat)
Anti-cheat beyond basic server-side validation
Auth providers other than Google (Apple, GitHub, email/password)
Admin dashboard or moderation tools
Pagination beyond top-N scores
User Stories
As a player, I want to sign in with my Google account on the start screen so that my scores are attributed to me.
As a player, I want to skip sign-in and play anonymously, with the option to sign in later.
As a signed-in player, I want my session score automatically submitted when I end a game so I don't have to do anything extra.
As a player, I want to view a global leaderboard showing the top scores and each player's display name and avatar.
As a signed-in player, I want to see my personal best score and current rank on the leaderboard, even if I'm not in the top N.
As a signed-in player, I want to see a "New personal best!" notification when I beat my previous high score.
As a returning player, I want to remain signed in across browser sessions so I don't have to re-authenticate every visit.
Technical Details
Authentication Flow
Web export (primary):
Start screen displays a "Sign in with Google" button alongside the existing "Play" button.
Clicking "Sign in with Google" initiates Google OAuth 2.0 Authorization Code flow via a same-window redirect. The client navigates to Google's authorization URL (constructed client-side or returned by an optional /auth/google endpoint) with a state parameter for CSRF protection. The state value must always be validated server-side in the /auth/callback handler.
The OAuth redirect URI points to a /auth/callback Lambda that exchanges the authorization code for tokens, extracts the user profile (sub, name, picture), creates or updates the user record, mints a short-lived session JWT, and redirects back to the game URL with the JWT as a URL fragment (e.g., https://game.example.com/#token=<jwt>). Using a fragment rather than a query parameter ensures the token is not sent to the server on subsequent page loads. Google OAuth client ID and client secret are stored as SST Secrets and injected as environment variables into the /auth/callback Lambda.
On page load, the Godot client (via JavaScriptBridge.eval()) checks for a JWT in the URL fragment. If present, it strips the fragment from the URL (using history.replaceState) and stores the JWT in-memory. This completes the sign-in flow.
JWTs are signed with an HMAC-SHA256 secret stored as an SST Secret, verified by middleware on all authenticated endpoints. JWTs expire after 1 hour. The refresh token cookie has a 30-day expiry.
The JWT is stored in the Godot client (in-memory) and passed as a Bearer token on all API requests.
A refresh token is stored in an HttpOnly cookie (set by the /auth/callback response, before the redirect) to keep the user signed in across page reloads. This requires CORS with credentials: include on the client and Access-Control-Allow-Credentials: true on the server. Tradeoff: HttpOnly cookies are not accessible to JavaScript (preventing XSS theft) but require explicit CORS credential support on every API call.
If the JWT expires mid-session, the next API call transparently refreshes the token using the HttpOnly cookie before retrying the original request (silent refresh — no user-facing interruption).
COOP/COEP compatibility note: The existing SST deployment sets Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp on CloudFront, which are required for Godot's SharedArrayBuffer threading. These headers sever the window.opener relationship between windows, making popup-based OAuth flows unworkable (postMessage back to the game window will fail). The redirect flow described above avoids this conflict entirely by keeping everything in a single window.
Desktop exports (stretch):
Use a localhost redirect URI (http://localhost:<port>/callback) with a temporary HTTP listener, or embed a minimal webview. This can be deferred to a follow-up issue.
Backend API (SST)
Extend the existing SST deployment with new constructs. All endpoints sit behind API Gateway with CORS configured as follows:
Allowed origin: The CloudFront distribution domain (e.g., https://d1234abcdef.cloudfront.net)
Allowed headers: Authorization, Content-Type
Allowed methods: GET, POST, OPTIONS
Credentials: Access-Control-Allow-Credentials: true (required for HttpOnly refresh token cookies)
Endpoints:
Method
Path
Auth
Description
GET
/auth/google
None
(Optional) Returns the Google OAuth authorization URL with a server-generated state parameter for CSRF protection. If the authorization URL is constructed client-side instead, the state parameter must still be validated server-side in the callback.
GET
/auth/callback
None
Handles OAuth callback, validates state, exchanges code, sets HttpOnly refresh token cookie, redirects to game URL with JWT in URL fragment
GET
/auth/me
JWT
Returns current user profile
POST
/scores
JWT
Submit a session score
GET
/leaderboard
None
Returns top N scores (default 50)
GET
/leaderboard/me
JWT
Returns the authenticated user's rank and personal best
The score field in the API payload corresponds to session_score on the Godot side (the GameManager variable). This value represents the cumulative total of all currency earned during the session (every add_currency call is summed). It is not the player's current spendable balance — spending on upgrades does not reduce this value.
Rank is calculated by counting Leaderboard table items with score greater than the user's score, plus one. This is an O(n) scan against the GSI, which is acceptable at the target scale (up to ~10,000 players). If the player base grows significantly, this can be replaced with a materialized rank or a purpose-built leaderboard service.
Data Model (DynamoDB)
Users table:
Attribute
Type
Description
user_id (PK)
String
Google sub claim
display_name
String
Google display name
avatar_url
String
Google profile picture URL
created_at
String (ISO 8601)
First sign-in timestamp
last_seen_at
String (ISO 8601)
Most recent sign-in
Scores table (historical session data — used for future analytics, not for leaderboard ranking):
Attribute
Type
Description
user_id (PK)
String
References Users table
session_id (SK)
String
UUID v4, ensures uniqueness per session
score
Number
Total currency earned in the session (cumulative add_currency, not current balance)
session_duration_sec
Number
How long the session lasted
submitted_at
String (ISO 8601)
Server-side timestamp
The Scores table retains all session history. It is optional for the initial release and primarily supports future analytics (session trends, play duration analysis). It has no GSI — leaderboard queries do not read from this table.
Leaderboard table (one row per user, personal best only):
Attribute
Type
Description
user_id (PK)
String
References Users table
score
Number
User's all-time best session score
display_name
String
Denormalized from Users table for fast reads
avatar_url
String
Denormalized from Users table for fast reads
submitted_at
String (ISO 8601)
When the personal best was achieved
Denormalized display_name and avatar_url fields are updated on each sign-in. Between sign-ins, leaderboard entries may show stale profile data. This is acceptable.
Query with ScanIndexForward=false to retrieve scores in descending order
Upsert pattern for score submission:
Write session to Scores table (append-only historical record).
Read the user's current row in the Leaderboard table.
If no row exists or new_score > existing_score, put/overwrite the Leaderboard row with the new score.
Query the GSI to determine the user's updated rank (count of items with score greater than the user's, plus one).
Scaling note: The single-partition "GLOBAL" GSI works well up to approximately 10,000 players. Beyond that, write throttling may occur due to DynamoDB's hot partition limits — it is write throughput to the single partition, not player count per se, that determines when this becomes a bottleneck. At that scale, introduce write sharding (e.g., GLOBAL#0 through GLOBAL#N partition keys with scatter-gather queries) or migrate to a purpose-built leaderboard service.
Godot Integration Points
Autoload registration order in project.godot: GameManager → AuthManager → DevTools. AuthManager must load after GameManager since it may read game state on initialization.
New scripts:
scripts/auth_manager.gd — Autoload. Manages auth state (signed_out, signing_in, signed_in), stores JWT in-memory, exposes sign_in(), sign_out(), get_user(). Emits signals: auth_state_changed, sign_in_succeeded, sign_in_failed. Handles silent token refresh when JWT expires. On load (web export), checks the URL fragment for a JWT returned by the OAuth redirect flow.
scripts/leaderboard_api.gd — Autoload or service node. Wraps HTTPRequest calls to /scores and /leaderboard endpoints. Handles JWT injection via Authorization: Bearer header, automatic retry (up to 3 attempts with exponential backoff), and error states. Listens to GameManager's session_ended signal to trigger score submission for signed-in users.
Modified scripts:
scripts/game_manager.gd — Add session_score: int (cumulative total of all currency earned via add_currency() — never decremented by spending on upgrades), session_id: String (UUID generated at session start), and a session_ended signal. Emit session_ended when the player explicitly ends the session. Note: session_score is the Godot-side variable that maps to the score field in the API payload. Session tracking (score accumulation, session ID generation) lives directly in GameManager — no separate session tracker script is needed.
scripts/start_screen.gd — Add sign-in button, display user avatar/name when authenticated, add "Leaderboard" button.
scripts/hud.gd — Add "End Game" button to the bottom bar (see End Game Button section under UI/UX).
New scenes:
scenes/leaderboard_panel.tscn — Modal panel (similar style to the existing UpgradePanel but wider at 700x600 to accommodate avatar thumbnails and rank numbers). Navy background, Yellow title ("Leaderboard"), close button in the header. Centered modal over a semi-transparent backdrop.
Web-specific considerations:
Use JavaScriptBridge.eval() to read the URL fragment on page load and extract the JWT returned by the OAuth redirect. After extracting the token, strip the fragment from the URL using history.replaceState to avoid leaking the token in browser history or referrer headers.
Store no sensitive tokens in JavaScript-accessible storage — the refresh token lives in an HttpOnly cookie managed by the server.
Server-Side Validation
The /scores endpoint performs basic sanity checks before accepting a submission:
score must be a non-negative integer, capped at a reasonable maximum (e.g., 1,000,000 per session)
session_duration_sec must be positive and less than 24 hours
session_id must not already exist for this user (no resubmission)
Rate limit: max 1 submission per user per 10 seconds
UI/UX
Start Screen (modified)
The existing start screen gains two new elements:
"Sign in with Google" button — Positioned below or alongside the Play button. Uses the standard Google branding guidelines (white background, Google "G" logo, "Sign in with Google" text). On success, replaced by a small user card showing avatar thumbnail and display name, plus a "Sign out" link.
"Leaderboard" button — Positioned near the Play button. Opens the leaderboard panel. Visible regardless of auth state (anonymous users can view but won't see their own rank).
All new buttons follow the existing Flexion brand color palette: Blue (#155FC8) for primary actions, Navy (#1F2937) for panel backgrounds, Yellow (#FAAE3B) for titles. The Google Sign-In button is an exception — it uses Google's required branding.
End Game Button
Position: In the HUD bottom bar (BottomBar HBoxContainer), alongside the existing Shop and Settings buttons.
Visibility: Only visible during active gameplay. Hidden on the start screen.
Style: Uses Flexion Blue (#155FC8) for normal state, matching the Shop and Settings button style defined in assets/ui_theme.tres.
Text: "End Game"
Behavior: On press, emits GameManager.session_ended, transitions back to the start screen, and triggers score submission via leaderboard_api.gd if the player is signed in.
Leaderboard Panel
Style: Matches the existing UpgradePanel — centered modal over a semi-transparent backdrop, Navy background, Yellow title ("Leaderboard"), close button in the header. Dimensions: 700x600 (wider than the 500x600 UpgradePanel to fit avatar thumbnails and rank numbers).
Content: A scrollable list of entries, each row showing: rank number, avatar (32x32 circle), display name, and score. Top 3 ranks are visually distinguished (Gold/Silver/Bronze accent on the rank number using Yellow #FAAE3B, Concrete #DFE3E6, and Orange #E05F1A respectively).
User's rank: If signed in and not in the visible top N, a sticky row at the bottom of the list shows "Your rank: #X — Score: Y" separated by a Concrete (#DFE3E6) divider.
Empty state: "No scores yet. Play a game and submit your score!" centered text.
Loading state: A simple "Loading..." label.
Error state: "Could not load leaderboard. Try again later." with a retry button.
Score Submission Flow
Player ends a session via the "End Game" button in the HUD (primary trigger).
If signed in, the score is submitted automatically in the background.
New personal best: "New personal best! Rank: #X" (Green #3BB273).
Failure (after 3 retry attempts): "Score submission failed. Your score was not saved." (Tango #DE4829).
If anonymous, no submission occurs — a subtle prompt appears: "Sign in to save your score to the leaderboard."
Acceptance Criteria
A "Sign in with Google" button is visible on the start screen and initiates the OAuth redirect flow.
The OAuth redirect flow works with the existing COOP/COEP headers (same-origin / require-corp) required for Godot's SharedArrayBuffer threading.
After the OAuth redirect returns to the game, the JWT is extracted from the URL fragment and the fragment is stripped from the browser URL.
After successful sign-in, the user's display name and avatar are shown on the start screen.
Auth state persists across page reloads (web export) via HttpOnly refresh token cookie without requiring re-authentication.
JWTs expire after 1 hour and are signed with HMAC-SHA256; the refresh token cookie has a 30-day expiry.
If the JWT expires mid-session, the token is silently refreshed on the next API call with no user-facing interruption.
A "Sign out" action is available and clears the session.
Anonymous play works identically to the current game — sign-in is not required to play.
session_score in GameManager accumulates all currency earned (every add_currency call) and is not reduced by upgrade purchases.
An "End Game" button is visible in the HUD bottom bar during active gameplay, styled with Flexion Blue (#155FC8), and hidden on the start screen.
Pressing "End Game" emits GameManager.session_ended, transitions to the start screen, and triggers score submission if signed in.
POST /scores accepts a valid score submission (with score in the payload, mapped from GameManager's session_score), upserts the Leaderboard table if the score is a new personal best, and returns the user's rank and personal best.
POST /scores rejects submissions that fail validation (negative score, duplicate session_id, rate limit exceeded).
GET /leaderboard returns the top 50 scores with display names, avatars, and timestamps, sorted descending by score.
GET /leaderboard/me returns the authenticated user's rank (calculated as count of higher scores plus one) and personal best.
The leaderboard panel (700x600) opens from the start screen and displays entries matching the API response.
The user's own rank appears at the bottom of the leaderboard panel if they are signed in and not in the top 50.
Score is submitted automatically when a signed-in player clicks "End Game."
A toast notification confirms successful submission or reports failure (after up to 3 retries).
A "New personal best!" toast is shown when the submitted score exceeds the user's previous best.
Score submission failure after 3 retries shows an error toast.
All new UI elements use the Flexion brand color palette.
All new API endpoints are deployed via SST alongside the existing static web deployment.
CORS is configured with the CloudFront domain as allowed origin, credentials support enabled, and Authorization/Content-Type as allowed headers.
Google OAuth client ID and client secret are stored as SST Secrets.
Existing unit tests continue to pass (godot --headless --script res://tools/run_tests.gd).
Heartbeat/auto-save safety net: The "End Game" button is the primary session-end trigger. Should we also implement a periodic heartbeat or auto-save mechanism as a safety net for unexpected disconnects (e.g., browser tab closed, network loss)? Submitting scores on beforeunload is unreliable and not recommended.
Leaderboard time scoping: Should the leaderboard be all-time only, or should we add daily/weekly views? (Suggest starting with all-time and adding time filters in a follow-up.)
Score metric: Is total currency earned (cumulative add_currency, independent of spending) the right score metric? Alternatives: peak currency (before spending on upgrades), total coins caught, or a composite score. This must be decided before implementation begins — the data model, submission payload, and leaderboard display all depend on this choice. Assign a decision owner and resolve before this issue is picked up for implementation.
Display name editing: Should users be able to set a custom display name, or always use their Google profile name?
Desktop OAuth: What is the preferred approach for desktop exports — localhost redirect, OS browser with deep link, or defer to a future issue?
Rate limiting strategy: API Gateway throttling per API key, or per-user rate limiting in the Lambda? Need to decide on limits.
GDPR/privacy: Do we need a privacy policy page and consent prompt before collecting Google profile data?
Summary
FlexCoins currently runs as a fully client-side game with no user identity and no persistence — every session starts fresh. To drive engagement and replayability, we want to add Google Sign-In so players have a persistent identity, and a global leaderboard so they can compete on high scores.
This feature introduces the first backend infrastructure to the project: an API layer (Lambda + API Gateway via SST), DynamoDB tables for scores and leaderboard data, and a Google OAuth integration. The Godot client will authenticate on the start screen, track session performance, submit scores at session end, and display a leaderboard UI.
Prerequisite decision: Before implementation begins, we must resolve Open Question #3 — what metric defines "score." The data model, submission payload, and leaderboard display all depend on this choice. The current proposal uses total currency earned (cumulative across all
add_currencycalls, independent of spending). See Open Questions for alternatives. Assign a decision owner and resolve before this issue is picked up for implementation.Scope
In scope
Out of scope
User Stories
Technical Details
Authentication Flow
Web export (primary):
/auth/googleendpoint) with astateparameter for CSRF protection. Thestatevalue must always be validated server-side in the/auth/callbackhandler./auth/callbackLambda that exchanges the authorization code for tokens, extracts the user profile (sub, name, picture), creates or updates the user record, mints a short-lived session JWT, and redirects back to the game URL with the JWT as a URL fragment (e.g.,https://game.example.com/#token=<jwt>). Using a fragment rather than a query parameter ensures the token is not sent to the server on subsequent page loads. Google OAuth client ID and client secret are stored as SST Secrets and injected as environment variables into the/auth/callbackLambda.JavaScriptBridge.eval()) checks for a JWT in the URL fragment. If present, it strips the fragment from the URL (usinghistory.replaceState) and stores the JWT in-memory. This completes the sign-in flow.Bearertoken on all API requests./auth/callbackresponse, before the redirect) to keep the user signed in across page reloads. This requires CORS withcredentials: includeon the client andAccess-Control-Allow-Credentials: trueon the server. Tradeoff: HttpOnly cookies are not accessible to JavaScript (preventing XSS theft) but require explicit CORS credential support on every API call.COOP/COEP compatibility note: The existing SST deployment sets
Cross-Origin-Opener-Policy: same-originandCross-Origin-Embedder-Policy: require-corpon CloudFront, which are required for Godot's SharedArrayBuffer threading. These headers sever thewindow.openerrelationship between windows, making popup-based OAuth flows unworkable (postMessageback to the game window will fail). The redirect flow described above avoids this conflict entirely by keeping everything in a single window.Desktop exports (stretch):
http://localhost:<port>/callback) with a temporary HTTP listener, or embed a minimal webview. This can be deferred to a follow-up issue.Backend API (SST)
Extend the existing SST deployment with new constructs. All endpoints sit behind API Gateway with CORS configured as follows:
https://d1234abcdef.cloudfront.net)Authorization,Content-TypeGET,POST,OPTIONSAccess-Control-Allow-Credentials: true(required for HttpOnly refresh token cookies)Endpoints:
GET/auth/googlestateparameter for CSRF protection. If the authorization URL is constructed client-side instead, thestateparameter must still be validated server-side in the callback.GET/auth/callbackstate, exchanges code, sets HttpOnly refresh token cookie, redirects to game URL with JWT in URL fragmentGET/auth/mePOST/scoresGET/leaderboardGET/leaderboard/mePOST /scores request body:
{ "session_id": "uuid-v4", "score": 12450, "session_duration_sec": 342 }The
scorefield in the API payload corresponds tosession_scoreon the Godot side (theGameManagervariable). This value represents the cumulative total of all currency earned during the session (everyadd_currencycall is summed). It is not the player's current spendable balance — spending on upgrades does not reduce this value.POST /scores response:
{ "rank": 23, "personal_best": 18200, "is_new_personal_best": false }When
is_new_personal_bestistrue, the client displays a "New personal best!" toast.GET /leaderboard response:
{ "entries": [ { "rank": 1, "display_name": "Alice", "avatar_url": "https://lh3.googleusercontent.com/...", "score": 98500, "submitted_at": "2026-04-15T10:32:00Z" } ], "total_players": 412 }GET /leaderboard/me response:
{ "rank": 23, "display_name": "Bob", "score": 18200, "submitted_at": "2026-04-14T08:15:00Z" }Rank is calculated by counting Leaderboard table items with
scoregreater than the user's score, plus one. This is an O(n) scan against the GSI, which is acceptable at the target scale (up to ~10,000 players). If the player base grows significantly, this can be replaced with a materialized rank or a purpose-built leaderboard service.Data Model (DynamoDB)
Users table:
user_id(PK)subclaimdisplay_nameavatar_urlcreated_atlast_seen_atScores table (historical session data — used for future analytics, not for leaderboard ranking):
user_id(PK)session_id(SK)scoreadd_currency, not current balance)session_duration_secsubmitted_atThe Scores table retains all session history. It is optional for the initial release and primarily supports future analytics (session trends, play duration analysis). It has no GSI — leaderboard queries do not read from this table.
Leaderboard table (one row per user, personal best only):
user_id(PK)scoredisplay_nameavatar_urlsubmitted_atDenormalized
display_nameandavatar_urlfields are updated on each sign-in. Between sign-ins, leaderboard entries may show stale profile data. This is acceptable.Leaderboard GSI (on the Leaderboard table):
"GLOBAL"(fixed string)scoreuser_id,score,display_name,avatar_url,submitted_atScanIndexForward=falseto retrieve scores in descending orderUpsert pattern for score submission:
new_score > existing_score, put/overwrite the Leaderboard row with the new score.scoregreater than the user's, plus one).Scaling note: The single-partition
"GLOBAL"GSI works well up to approximately 10,000 players. Beyond that, write throttling may occur due to DynamoDB's hot partition limits — it is write throughput to the single partition, not player count per se, that determines when this becomes a bottleneck. At that scale, introduce write sharding (e.g.,GLOBAL#0throughGLOBAL#Npartition keys with scatter-gather queries) or migrate to a purpose-built leaderboard service.Godot Integration Points
Autoload registration order in
project.godot: GameManager → AuthManager → DevTools. AuthManager must load after GameManager since it may read game state on initialization.New scripts:
scripts/auth_manager.gd— Autoload. Manages auth state (signed_out,signing_in,signed_in), stores JWT in-memory, exposessign_in(),sign_out(),get_user(). Emits signals:auth_state_changed,sign_in_succeeded,sign_in_failed. Handles silent token refresh when JWT expires. On load (web export), checks the URL fragment for a JWT returned by the OAuth redirect flow.scripts/leaderboard_api.gd— Autoload or service node. Wraps HTTPRequest calls to/scoresand/leaderboardendpoints. Handles JWT injection viaAuthorization: Bearerheader, automatic retry (up to 3 attempts with exponential backoff), and error states. Listens to GameManager'ssession_endedsignal to trigger score submission for signed-in users.Modified scripts:
scripts/game_manager.gd— Addsession_score: int(cumulative total of all currency earned viaadd_currency()— never decremented by spending on upgrades),session_id: String(UUID generated at session start), and asession_endedsignal. Emitsession_endedwhen the player explicitly ends the session. Note:session_scoreis the Godot-side variable that maps to thescorefield in the API payload. Session tracking (score accumulation, session ID generation) lives directly in GameManager — no separate session tracker script is needed.scripts/start_screen.gd— Add sign-in button, display user avatar/name when authenticated, add "Leaderboard" button.scripts/hud.gd— Add "End Game" button to the bottom bar (see End Game Button section under UI/UX).New scenes:
scenes/leaderboard_panel.tscn— Modal panel (similar style to the existing UpgradePanel but wider at 700x600 to accommodate avatar thumbnails and rank numbers). Navy background, Yellow title ("Leaderboard"), close button in the header. Centered modal over a semi-transparent backdrop.Web-specific considerations:
JavaScriptBridge.eval()to read the URL fragment on page load and extract the JWT returned by the OAuth redirect. After extracting the token, strip the fragment from the URL usinghistory.replaceStateto avoid leaking the token in browser history or referrer headers.Server-Side Validation
The
/scoresendpoint performs basic sanity checks before accepting a submission:scoremust be a non-negative integer, capped at a reasonable maximum (e.g., 1,000,000 per session)session_duration_secmust be positive and less than 24 hourssession_idmust not already exist for this user (no resubmission)UI/UX
Start Screen (modified)
The existing start screen gains two new elements:
All new buttons follow the existing Flexion brand color palette: Blue (
#155FC8) for primary actions, Navy (#1F2937) for panel backgrounds, Yellow (#FAAE3B) for titles. The Google Sign-In button is an exception — it uses Google's required branding.End Game Button
BottomBarHBoxContainer), alongside the existing Shop and Settings buttons.#155FC8) for normal state, matching the Shop and Settings button style defined inassets/ui_theme.tres.GameManager.session_ended, transitions back to the start screen, and triggers score submission vialeaderboard_api.gdif the player is signed in.Leaderboard Panel
#FAAE3B, Concrete#DFE3E6, and Orange#E05F1Arespectively).#DFE3E6) divider.Score Submission Flow
#3BB273).#3BB273).#DE4829).Acceptance Criteria
same-origin/require-corp) required for Godot's SharedArrayBuffer threading.session_scorein GameManager accumulates all currency earned (everyadd_currencycall) and is not reduced by upgrade purchases.#155FC8), and hidden on the start screen.GameManager.session_ended, transitions to the start screen, and triggers score submission if signed in.POST /scoresaccepts a valid score submission (withscorein the payload, mapped from GameManager'ssession_score), upserts the Leaderboard table if the score is a new personal best, and returns the user's rank and personal best.POST /scoresrejects submissions that fail validation (negative score, duplicate session_id, rate limit exceeded).GET /leaderboardreturns the top 50 scores with display names, avatars, and timestamps, sorted descending by score.GET /leaderboard/mereturns the authenticated user's rank (calculated as count of higher scores plus one) and personal best.Authorization/Content-Typeas allowed headers.godot --headless --script res://tools/run_tests.gd).godot --headless --script res://tools/lint_project.gd).Open Questions
beforeunloadis unreliable and not recommended.add_currency, independent of spending) the right score metric? Alternatives: peak currency (before spending on upgrades), total coins caught, or a composite score. This must be decided before implementation begins — the data model, submission payload, and leaderboard display all depend on this choice. Assign a decision owner and resolve before this issue is picked up for implementation.