Skip to content

Friends

A player adds a friend by the name the leaderboards show for them (their nickname) or by their friend code. The other player accepts or declines; either can remove the friendship later. Friends send each other tunes and test-drive each other’s cars. There is no chat, and with no free text there is nothing to moderate. Why friends and not open tune copying: docs/2026-11-09-drag-racing-creative-mobile-research.md, “The social side”.

Shared by the game and the race server:

  • friendList(links, userId) turns the stored links into the player’s list: friend, received (a request to answer) or sent. Two players who each asked the other are friends. Friends come first, then requests to answer, then requests sent, by name within each. A player without a nickname shows as defaultNickname, as on the boards.
  • addFriendOutcome decides what adding a player does: send a request, accept their waiting request instead, or refuse with unknown, self, already, sent, full (FRIEND_LIMIT, 50 friends) or too-many-sent (SENT_LIMIT, 20 open requests). FRIEND_REFUSALS has the line the game shows for each.
  • acceptOutcome accepts only a request waiting for the player, while their list has room.
  • addedLine is the line after adding: the racer whose row is new or changed.

Default names (“Racer 4F2A”) are derived from the user id, not stored and not unique, so a player who never picked a nickname could not be found. Every player gets an eight-character friend code (no 0, O, 1 or I), shown as K7QX-2MPD at the top of their Friends screen next to their name. readFriendCode accepts it in any case, with or without the dash, spaces or a leading #; what cannot be a code is looked up as a nickname. A code wins over a nickname spelled the same, so nobody can take over another player’s code as a name.

The code is made the first time the player opens Friends (ensureFriendCode, party/src/db/friendCode.ts) and stored in profile.friend_code (party/migrations/0011_friend_code.sql, unique). When a guest signs in to an account that has its own profile, the account keeps its code.

Friends only: there is no “copy this tune” on a stranger’s leaderboard entry, since working out a tune is the game. A tune fits one car model, so it can go only to a friend who owns that model (sendTuneOutcome; refusals in TUNE_REFUSALS). readSentTune takes only a full tune with every slider in range.

In Tuning, the Friends button (garage/view/friendTunes.ts) opens a list: Send this tune (the sliders as they are, to a friend with the same car) and one row per tune friends sent for this car. Opening one offers to load it onto the sliders of the tune slot on screen, which the player then saves like any change, or to delete it. A tune stays on the list until deleted; sending again to the same friend for the same car replaces it.

A friend row’s Drive button (friends/view/testDrive.ts) lists the friend’s cars and starts TestRun with the one picked as a borrowed car (race/testCar.ts): their parts, look and active tune, against the ghost of the player’s own last run. No prize, no board time, nothing saved. sharedCar leaves out the other tune slots and the owner’s test runs, and the game never shows the tune sliders: the timeslip is what the player learns from. The leaderboards test-drive the car behind any entry the same way (Leaderboards).

friend_link (party/migrations/0010_friend.sql) holds one row per request: from_id asked to_id, accepted_at set once accepted. Declining, cancelling and removing all delete the row, and the tunes the two sent each other. friend_tune (0012_friend_tune.sql) holds one waiting tune per sender, receiver and car model. Queries are in party/src/db/friends.ts, friendCode.ts and friendTunes.ts; the endpoints in party/src/api/friendApi.ts and friendTuneApi.ts.

The friends calls answer { friends, refused } with the list as it now is (409 when refused):

Endpoint Body Does
GET /api/friends the player’s list, and their own friend code
POST /api/friends { name } send a request, found by friend code, else by nickname ignoring case
POST /api/friends/accept { userId } accept a waiting request
POST /api/friends/remove { userId } decline, cancel or remove, whichever applies

Tunes and cars:

Endpoint Body Answers
GET /api/friend-tunes?model=<id> { received, owners }: tunes waiting for this car model, and the friends who own it
POST /api/friend-tunes/send { userId, modelId, tune } { refused } (409 when refused)
POST /api/friend-tunes/dismiss { fromId, modelId } { received } left for this car model
GET /api/friend-cars?user=<id> { cars }, a friend’s only (403 otherwise)

Both tables name two players, so USER_TABLES (party/src/db/userData.ts) gives each a second key column (also: 'to_id'): deleting an account deletes the rows at both ends, and the data export lists both. When a guest signs in with Apple or Google, moveFriendLinks moves its links and tunes to the account; a row the account already has, or one pointing the account at itself, is dropped (party/src/db/linkGuest.ts). party/scripts/friendCheck.ts (npm --prefix party run test:friends) checks all of this against a running wrangler dev.

The Friends screen (friends/scenes/Friends.ts) opens from the Friends button on the Leaderboards screen and goes back there. It shows the player’s own name and friend code at the top, seven rows per page (friends/view/friendRows.ts, pages by friends/view/pager.ts), and Add friend opens a text entry for a name or code. Removing a friend asks first; declining and cancelling do not. Calls go through player/account/friendRequests.ts and friendTuneRequests.ts. Pick lists longer than a choice dialog holds (friends to send to, a friend’s cars, waiting tunes) page through ui/pagedChoice.ts.

A red count shows how many requests wait for an answer (waitingRequests), on the main menu’s Leaderboards button and on the Friends button (ui/friendRequestBadge.ts). It is read once when the screen opens, like the inbox’s dot; offline the buttons stay plain.