Skip to content

feat(ranked): choose a 2v2 teammate before queueing - #4788

Open
Zixer1 wants to merge 5 commits into
openfrontio:mainfrom
Zixer1:feat/2v2-party-queue
Open

feat(ranked): choose a 2v2 teammate before queueing#4788
Zixer1 wants to merge 5 commits into
openfrontio:mainfrom
Zixer1:feat/2v2-party-queue

Conversation

@Zixer1

@Zixer1 Zixer1 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

What

An optional Teammate field inside the 2v2 card on the ranked screen. Enter a friend's player id, and once they enter yours you queue as a duo on the same team. Left empty, 2v2 behaves exactly as it does today.

Why this shape

  • Chosen before the queue is joined. Picking the teammate on the ranked screen rather than mid-queue removes the race by construction: there is no window where you get matched with strangers while your friend is still typing your id. That also means no hold/cancel message, no debounce and no live socket update.
  • Stored in localStorage, not passed through the event. The post-game Requeue path navigates to /?requeue=2v2, which reloads the page, so no in-memory value or event payload can survive it. Reading storage at queue time makes the ranked screen, the requeue URL and an in-place requeue all agree, and it means a regular duo types the id once (public ids are permanent). Main.ts therefore needs no changes at all.
  • partyStatus is only ever set from the server's party-status message, never assumed locally. An optimistic "held" would leave a client deployed ahead of the server stuck on "waiting for teammate" forever while it was in fact queueing normally.
  • Own id is rejected on input: it would hold the player out of matching forever waiting on themselves.

Layout notes

  • The field is pinned to the bottom of the card and taken out of the normal flow, so the 2v2 title and ELO line stay vertically aligned with the 1v1 card's. In the flow it would push that block upward and the two cards could never match.
  • The 2v2 card becomes a div with role="button", tabindex and Enter/Space handling, because a <button> may not contain an <input> (invalid HTML, and every click in the field would fire the queue action). The input stops its own click/keydown from reaching the card.
  • Card classes are now shared in one constant with min-h-[9.5rem], so both live cards keep identical dimensions; the title/subtitle block and the ELO-subtitle expression are shared helpers rather than repeated per card.

Tests

RankedTeammate.test.ts covers the round trip, the empty-value normalisation (so callers only test for null) and survival across a reload. Client suites green at 746/746; prettier, eslint and tsc clean.

Server

Needs openfrontio/infra#484 for the pairing itself. That should merge first; against an older server this degrades to ordinary solo 2v2 queueing.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Ranked 2v2 now persists teammate IDs, validates self-reference, includes teammates in queue joins, and displays waiting or ready party states. Ranked card rendering is consolidated, with keyboard interaction and localized teammate guidance. The Las Vegas Strip map translation is also added.

Changes

Ranked 2v2 pairing

Layer / File(s) Summary
Teammate persistence and selection
src/client/RankedTeammate.ts, src/client/components/RankedModal.ts, tests/client/RankedTeammate.test.ts
Teammate IDs use local storage with an in-memory fallback, are restored in the ranked modal, rejected when matching the player’s own ID, and covered by tests.
Ranked modal card UI
src/client/components/RankedModal.ts
Shared title and subtitle rendering is used for 1v1, 2v2, and disabled cards; the 2v2 card supports keyboard activation and isolated teammate input events.
Matchmaking party flow and localization
src/client/Matchmaking.ts, resources/lang/en.json
2v2 joins include the stored teammate ID, server party-status messages update waiting or ready state, and ranked party guidance plus the Las Vegas Strip map label are added.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Player
  participant RankedModal
  participant RankedTeammate
  participant MatchmakingModal
  participant MatchmakingServer
  Player->>RankedModal: enter teammate public ID
  RankedModal->>RankedTeammate: persist teammate selection
  MatchmakingModal->>RankedTeammate: read teammate selection
  MatchmakingModal->>MatchmakingServer: join 2v2 with teammatePublicId
  MatchmakingServer-->>MatchmakingModal: send party-status
  MatchmakingModal-->>Player: show waiting or duo searching state
Loading

Possibly related PRs

Suggested labels: UI/UX, Translation

Poem

A teammate waits beside the queue,
Saved for ranked battles two by two.
When both are ready,
The search runs steady—
Las Vegas lights shine bright and new!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: adding teammate selection for ranked 2v2 before queueing.
Description check ✅ Passed The description matches the change set and explains teammate selection, storage, queueing, and fallback behavior.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Adds an optional Teammate field inside the 2v2 card on the ranked screen. Enter
a friend's player id, and when they enter yours you queue as a duo on the same
team. Left empty, 2v2 behaves exactly as it does today.

- Chosen BEFORE the queue is joined, so there is no window where you can be
  matched solo while your teammate is still getting ready.
- Stored in localStorage rather than passed through the open-matchmaking event,
  because the post-game Requeue path reloads the page: no in-memory value or
  event payload survives it. One source of truth means the ranked screen, the
  requeue URL and an in-place requeue all agree.
- The field is pinned to the bottom of the card and out of the normal flow, so
  the 2v2 title and ELO stay aligned with the 1v1 card's.
- The card becomes a div with button semantics because a <button> may not
  contain an <input>, and the input stops its own events from reaching the card.
- Own id is rejected: it would hold the player out of matching forever.
- partyStatus is only set from the server's party-status message, never assumed,
  so a client deployed ahead of the server queues normally instead of sitting on
  "waiting" forever.

Needs openfrontio/infra#484 for the pairing itself.
@Zixer1
Zixer1 force-pushed the feat/2v2-party-queue branch from 8edabce to a558482 Compare July 30, 2026 01:00

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/client/Matchmaking.ts`:
- Around line 175-178: Reject a restored teammate ID that matches the
authenticated player’s public ID. In src/client/Matchmaking.ts lines 175-178,
update resetPartyState to filter the stored value before constructing a 2v2
join; in src/client/components/RankedModal.ts lines 84-88, revalidate the
restored value after loading the profile and clear the displayed self ID.

In `@src/client/RankedTeammate.ts`:
- Around line 9-29: Update getRankedTeammate and setRankedTeammate to maintain a
module-level fallback teammate value when localStorage is unavailable. Set or
clear this fallback alongside storage updates, and return it from
getRankedTeammate’s catch path so selections persist for the current session
while preserving existing storage behavior.

In `@tests/client/RankedTeammate.test.ts`:
- Around line 21-24: Update the test case around setRankedTeammate and
getRankedTeammate to store an actual empty string with
localStorage.setItem("ranked-2v2-teammate", "") instead of calling
setRankedTeammate(""). Keep the existing null assertion so the test covers
getRankedTeammate() handling a stored empty value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d3a36900-ef8f-49d7-8c6e-5da0717e3ac7

📥 Commits

Reviewing files that changed from the base of the PR and between e0b9826 and 8edabce.

📒 Files selected for processing (5)
  • resources/lang/en.json
  • src/client/Matchmaking.ts
  • src/client/RankedTeammate.ts
  • src/client/components/RankedModal.ts
  • tests/client/RankedTeammate.test.ts

Comment thread src/client/Matchmaking.ts Outdated
Comment thread src/client/RankedTeammate.ts Outdated
Comment thread tests/client/RankedTeammate.test.ts Outdated
@github-project-automation github-project-automation Bot moved this from Triage to Development in OpenFront Release Management Jul 30, 2026
…rage

- getRankedTeammate() takes the signed-in player's own id and rejects (and
  clears) a stored value equal to it. Two accounts in one browser could leave
  the other's id behind, which is this player's own; the server already refused
  it, but the field still displayed it, so the player believed they were duo
  queueing while actually solo.
- Mirror the value in memory so the field still works for the page when
  localStorage is unavailable (private mode) rather than silently queueing solo.
- Test a persisted empty string directly, since setRankedTeammate("") removes
  the key and never exercised that branch.
- Trim overlong comments.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/client/Matchmaking.ts`:
- Around line 178-184: Update the automatic reconnect retry path to call
resetPartyState() immediately before invoking connect(), ensuring stale waiting
or ready state is cleared before each retry. Keep the existing resetPartyState
behavior and normal connection flow unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e3f7f75b-2d7b-430a-b04a-3e5f00ef9d8f

📥 Commits

Reviewing files that changed from the base of the PR and between a558482 and d971551.

📒 Files selected for processing (4)
  • src/client/Matchmaking.ts
  • src/client/RankedTeammate.ts
  • src/client/components/RankedModal.ts
  • tests/client/RankedTeammate.test.ts

Comment thread src/client/Matchmaking.ts Outdated
The watchdog and close-retry paths call connect() directly, so a stale
waiting/ready survived the socket that reported it: after a reconnect the modal
could claim you were searching as a duo for a few seconds when your teammate had
dropped. partyStatus is per-connection state like connected/queueSize, so
connect() now clears it and every entry path is covered.
@FrqnquiTqu

Copy link
Copy Markdown

Nice idea

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Development

Development

Successfully merging this pull request may close these issues.

3 participants