# Leaderboards

Create competitive scoreboards, submit scores, and query player rankings

Source: https://docs.wavedash.com/sdk/leaderboards

Leaderboards let players compete by ranking scores on shared scoreboards. Each leaderboard has a **name** you pick (e.g. `"speedrun-times"`), a sort direction, and an update policy.

<Note>
**Name vs. ID.** Only `getLeaderboard(name)` and `getOrCreateLeaderboard(name, ...)` take the leaderboard's name. Every other call — `uploadLeaderboardScore`, `listLeaderboardEntries`, `getMyLeaderboardEntries`, etc. — takes the **ID** returned from those lookups (`response.data.id`)
</Note>

## Getting or creating a leaderboard

Call this once per leaderboard (for example during startup) and reuse the returned ID for every other leaderboard call.

<CodeGroup>
```gdscript Godot
func setup_leaderboard():
    var leaderboard = await WavedashSDK.get_or_create_leaderboard(
        "speedrun-times",
        WavedashConstants.LEADERBOARD_SORT_ASCENDING,
        WavedashConstants.LEADERBOARD_DISPLAY_TYPE_TIME_MILLISECONDS
    )
    var leaderboard_id = leaderboard.data.id if leaderboard.success else ""
    print("Leaderboard ID: ", leaderboard_id)
```

```csharp Unity
var leaderboard = await Wavedash.SDK.GetOrCreateLeaderboard(
    "speedrun-times",
    sortMethod: WavedashConstants.LeaderboardSortMethod.ASCENDING,
    displayType: WavedashConstants.LeaderboardDisplayType.TIME_MILLISECONDS
);
string leaderboardId = leaderboard != null ? (string)leaderboard["id"] : null;
Debug.Log($"Leaderboard ID: {leaderboardId}");
```

```lua Defold
local co = coroutine.create(function()
    local leaderboard = wavedash.get_or_create_leaderboard_async(
        "speedrun-times",
        wavedash.LEADERBOARD_SORT_ORDER_ASC,
        wavedash.LEADERBOARD_DISPLAY_TYPETIME_MILLISECONDS
    )
    local leaderboard_id = leaderboard.success and leaderboard.data.id or nil
    print("Leaderboard ID:", leaderboard_id)
end)
assert(coroutine.resume(co))
```

```javascript JavaScript
const leaderboard = await Wavedash.getOrCreateLeaderboard(
  "speedrun-times",
  Wavedash.LeaderboardSortOrder.ASC,
  Wavedash.LeaderboardDisplayType.TIME_MILLISECONDS
);
const leaderboardId = leaderboard.success ? leaderboard.data.id : null;
```
</CodeGroup>

If the leaderboard already exists and you just need its ID, use `getLeaderboard(name)` instead — it fails if the leaderboard doesn't exist yet.

**Godot signal alternative.** If you prefer signal-based flow over `await`, every leaderboard call also emits a signal when its response arrives:

```gdscript Godot
func _ready():
    WavedashSDK.got_leaderboard.connect(_on_got_leaderboard)
    WavedashSDK.got_leaderboard_entries.connect(_on_got_entries)
    WavedashSDK.posted_leaderboard_score.connect(_on_posted_score)

func _on_got_leaderboard(response):
    print("ID: ", response.data.id if response.success else "?")

func _on_got_entries(response):
    if response.success:
        for entry in response.data:
            print(entry.globalRank, ": ", entry.score)

func _on_posted_score(response):
    if response.success:
        print("Rank: ", response.data.globalRank)
```

### Sort order

| Value | Constant | Description | Best for |
|-------|----------|-------------|----------|
| `0` | `ASC` | Lower scores rank higher | Time trials, golf |
| `1` | `DESC` | Higher scores rank higher | Points, high scores |

In JavaScript use `Wavedash.LeaderboardSortOrder.ASC` / `DESC`. In Godot use `WavedashConstants.LEADERBOARD_SORT_ASCENDING` / `LEADERBOARD_SORT_DESCENDING`. In Unity use `WavedashConstants.LeaderboardSortMethod.ASCENDING` / `DESCENDING`. In Defold use `wavedash.LEADERBOARD_SORT_ORDER_ASC` / `LEADERBOARD_SORT_ORDER_DESC`.

### Display type

| Value | Constant | Description |
|-------|----------|-------------|
| `0` | `NUMERIC` | Display as a number |
| `1` | `TIME_SECONDS` | Display as time in seconds |
| `2` | `TIME_MILLISECONDS` | Display as time with milliseconds |
| `3` | `TIME_GAME_TICKS` | Display as game ticks (assumes 60fps) |

In JavaScript use `Wavedash.LeaderboardDisplayType.NUMERIC` / `TIME_SECONDS` / `TIME_MILLISECONDS` / `TIME_GAME_TICKS`. In Defold use `wavedash.LEADERBOARD_DISPLAY_TYPENUMERIC` / `LEADERBOARD_DISPLAY_TYPETIME_SECONDS` / `wavedash.LEADERBOARD_DISPLAY_TYPETIME_MILLISECONDS` / `wavedash.LEADERBOARD_DISPLAY_TYPETIME_GAME_TICKS`.

## Submitting a score

Resolve the name to an ID first, then pass that ID to `uploadLeaderboardScore`.

<CodeGroup>
```gdscript Godot
func submit_score(score: int):
    var leaderboard = await WavedashSDK.get_leaderboard("speedrun-times")
    if not leaderboard.success:
        return
    var leaderboard_id = leaderboard.data.id

    var result = await WavedashSDK.post_leaderboard_score(leaderboard_id, score, true)
    if result.success:
        print("Your leaderboard rank: ", result.data.globalRank)
        # Where this run placed, even if your leaderboard standing didn't change
        print("This run ranked: ", result.data.submittedRank)
```

```csharp Unity
var leaderboard = await Wavedash.SDK.GetLeaderboard("speedrun-times");
string leaderboardId = leaderboard != null ? (string)leaderboard["id"] : null;

var result = await Wavedash.SDK.UploadLeaderboardScore(
    leaderboardId, score, keepBest: true
);
if (result != null)
{
    Debug.Log($"Your leaderboard rank: {result["globalRank"]}");
    // Where this run placed, even if your leaderboard standing didn't change
    Debug.Log($"This run ranked: {result["submittedRank"]}");
}
```

```lua Defold
local co = coroutine.create(function()
    local leaderboard = wavedash.get_leaderboard_async("speedrun-times")
    if not leaderboard.success then
        return
    end

    local leaderboard_id = leaderboard.data.id
    local result = wavedash.upload_leaderboard_score_async(leaderboard_id, score, true)
    if result.success then
        print("Your leaderboard rank:", result.data.globalRank)
        -- Where this run placed, even if your leaderboard standing didn't change
        print("This run ranked:", result.data.submittedRank)
    end
end)
assert(coroutine.resume(co))
```

```javascript JavaScript
const leaderboard = await Wavedash.getLeaderboard("speedrun-times");
const leaderboardId = leaderboard.success ? leaderboard.data.id : null;

const response = await Wavedash.uploadLeaderboardScore(
  leaderboardId, 1500, true
);
if (response.success) {
  console.log(`Your rank: ${response.data.globalRank}`);
  // Where this run placed, even if your saved standing didn't change
  console.log(`This run ranked: ${response.data.submittedRank}`);
}
```
</CodeGroup>

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `leaderboardId` | `Id<"leaderboards">` | Yes | The leaderboard's ID (from `getLeaderboard` / `getOrCreateLeaderboard`) |
| `score` | number | Yes | The score to submit |
| `keepBest` | boolean | Yes | If true, only updates if score is better |
| `ugcId` | `Id<"userGeneratedContent">` | No | Optional UGC attachment (replay, screenshot) |

<Tip>
Use `keepBest: true` for competitive leaderboards. The SDK handles personal best tracking automatically.
</Tip>

## Fetching scores

### Top scores

<CodeGroup>
```gdscript Godot
func get_top_scores():
    var leaderboard = await WavedashSDK.get_leaderboard("speedrun-times")
    if not leaderboard.success:
        return
    var leaderboard_id = leaderboard.data.id

    var response = await WavedashSDK.get_leaderboard_entries(leaderboard_id, 0, 10, false)
    if response.success:
        for entry in response.data:
            print("#", entry.globalRank, " ", entry.username, ": ", entry.score)
```

```csharp Unity
var leaderboard = await Wavedash.SDK.GetLeaderboard("speedrun-times");
string leaderboardId = leaderboard != null ? (string)leaderboard["id"] : null;

var entries = await Wavedash.SDK.ListLeaderboardEntries(leaderboardId, 0, 10);
foreach (var entry in entries)
    Debug.Log($"#{entry["globalRank"]} {entry["username"]}: {entry["score"]}");
```

```lua Defold
local co = coroutine.create(function()
    local leaderboard = wavedash.get_leaderboard_async("speedrun-times")
    if not leaderboard.success then
        return
    end

    local leaderboard_id = leaderboard.data.id
    local response = wavedash.list_leaderboard_entries_async(leaderboard_id, 0, 10, false)
    if response.success then
        for _, entry in ipairs(response.data) do
            print("#" .. entry.globalRank, entry.username .. ":", entry.score)
        end
    end
end)
assert(coroutine.resume(co))
```

```javascript JavaScript
const leaderboard = await Wavedash.getLeaderboard("speedrun-times");
const leaderboardId = leaderboard.success ? leaderboard.data.id : null;

const response = await Wavedash.listLeaderboardEntries(
  leaderboardId, 0, 10, false
);
if (response.success) {
  response.data.forEach(entry => {
    console.log(`#${entry.globalRank} ${entry.username}: ${entry.score}`);
  });
}
```
</CodeGroup>

### Nearby scores

<CodeGroup>
```gdscript Godot
func get_scores_around_me():
    var leaderboard = await WavedashSDK.get_leaderboard("speedrun-times")
    if not leaderboard.success:
        return
    var leaderboard_id = leaderboard.data.id

    var nearby = await WavedashSDK.get_leaderboard_entries_around_player(leaderboard_id, 5, 5, false)
```

```csharp Unity
var leaderboard = await Wavedash.SDK.GetLeaderboard("speedrun-times");
string leaderboardId = leaderboard != null ? (string)leaderboard["id"] : null;

var nearby = await Wavedash.SDK.ListLeaderboardEntriesAroundUser(
    leaderboardId, countAhead: 5, countBehind: 5
);
```

```lua Defold
local co = coroutine.create(function()
    local leaderboard = wavedash.get_leaderboard_async("speedrun-times")
    if not leaderboard.success then
        return
    end

    local leaderboard_id = leaderboard.data.id
    local nearby = wavedash.list_leaderboard_entries_around_user_async(leaderboard_id, 5, 5, false)
end)
assert(coroutine.resume(co))
```

```javascript JavaScript
const leaderboard = await Wavedash.getLeaderboard("speedrun-times");
const leaderboardId = leaderboard.success ? leaderboard.data.id : null;

const nearby = await Wavedash.listLeaderboardEntriesAroundUser(
  leaderboardId, 5, 5, false
);
```
</CodeGroup>

### Player's own entry

<CodeGroup>
```gdscript Godot
func get_my_entry():
    var leaderboard = await WavedashSDK.get_leaderboard("speedrun-times")
    if not leaderboard.success:
        return
    var leaderboard_id = leaderboard.data.id

    var entries = await WavedashSDK.get_my_leaderboard_entries(leaderboard_id)
```

```csharp Unity
var leaderboard = await Wavedash.SDK.GetLeaderboard("speedrun-times");
string leaderboardId = leaderboard != null ? (string)leaderboard["id"] : null;

var entries = await Wavedash.SDK.GetMyLeaderboardEntries(leaderboardId);
```

```lua Defold
local co = coroutine.create(function()
    local leaderboard = wavedash.get_leaderboard_async("speedrun-times")
    if not leaderboard.success then
        return
    end

    local leaderboard_id = leaderboard.data.id
    local entries = wavedash.get_my_leaderboard_entries_async(leaderboard_id)
end)
assert(coroutine.resume(co))
```

```javascript JavaScript
const leaderboard = await Wavedash.getLeaderboard("speedrun-times");
const leaderboardId = leaderboard.success ? leaderboard.data.id : null;

const response = await Wavedash.getMyLeaderboardEntries(leaderboardId);
```
</CodeGroup>

### Entry count

`getLeaderboardEntryCount` is a synchronous accessor that returns the cached total from the last fetch — but it still takes the leaderboard **ID**, not the name, so you must resolve the name first.

<CodeGroup>
```gdscript Godot
func get_count():
    var leaderboard = await WavedashSDK.get_leaderboard("speedrun-times")
    if not leaderboard.success:
        return
    var leaderboard_id = leaderboard.data.id

    var count = WavedashSDK.get_leaderboard_entry_count(leaderboard_id)
```

```csharp Unity
var leaderboard = await Wavedash.SDK.GetLeaderboard("speedrun-times");
string leaderboardId = leaderboard != null ? (string)leaderboard["id"] : null;

var count = Wavedash.SDK.GetLeaderboardEntryCount(leaderboardId);
```

```lua Defold
local co = coroutine.create(function()
    local leaderboard = wavedash.get_leaderboard_async("speedrun-times")
    if not leaderboard.success then
        return
    end

    local leaderboard_id = leaderboard.data.id
    local count = wavedash.get_leaderboard_entry_count(leaderboard_id)
end)
assert(coroutine.resume(co))
```

```javascript JavaScript
const leaderboard = await Wavedash.getLeaderboard("speedrun-times");
const leaderboardId = leaderboard.success ? leaderboard.data.id : null;

const count = Wavedash.getLeaderboardEntryCount(leaderboardId);
```
</CodeGroup>

<Note>
Returns a cached value from the last query. Returns -1 if the leaderboard has not been queried yet.
</Note>

## Attaching replays or screenshots

Pass a `ugcId` from [UGC](/sdk/ugc) into the score upload call. Note that `uploadLeaderboardScore` takes `lb.data.id` (the ID), not the name:

```javascript
const lb = await Wavedash.getOrCreateLeaderboard(
  "speedrun-times",
  Wavedash.LeaderboardSortOrder.ASC,
  Wavedash.LeaderboardDisplayType.TIME_MILLISECONDS
);
const ugc = await Wavedash.createUGCItem(
  Wavedash.UGCType.GAME_MANAGED,
  "Replay",
  "replay of level 1",
  Wavedash.UGCVisibility.PUBLIC,
  "replays/run.dat"
);
if (lb.success && ugc.success) {
  await Wavedash.uploadLeaderboardScore(lb.data.id, 12345, true, ugc.data);
}
```

## Return types

### `Leaderboard`

Returned from `getLeaderboard` and `getOrCreateLeaderboard`. Use `id` for every subsequent leaderboard call.

```typescript
interface Leaderboard {
  id: Id<"leaderboards">;   // pass this to uploadLeaderboardScore, listLeaderboardEntries, etc.
  name: string;
  totalEntries: number;
  created?: boolean;        // only set by getOrCreateLeaderboard — true if newly created
}
```

### `LeaderboardEntry`

An entry in the list returned by `listLeaderboardEntries`, `listLeaderboardEntriesAroundUser`, and `getMyLeaderboardEntries`.

```typescript
interface LeaderboardEntry {
  userId: Id<"users">;
  username: string;
  userAvatarUrl?: string;
  score: number;
  globalRank: number;
  timestamp: number;
  metadata?: ArrayBuffer;
  ugcId?: Id<"userGeneratedContent">;
}
```

### `UpsertedLeaderboardEntry`

Returned from `uploadLeaderboardScore`. Note that this is a **different** shape from `LeaderboardEntry` — it reports what changed on upload rather than the full entry.

It carries two pairs of fields: your **saved standing** (`score` / `globalRank`) and the **submitted attempt** (`submittedScore` / `submittedRank`). These differ when `keepBest` is `true` and the run you just submitted was worse than your saved best — `score`/`globalRank` stay at your best, while `submittedScore`/`submittedRank` reflect where this particular run would have placed.

```typescript
interface UpsertedLeaderboardEntry {
  entryId: Id<"leaderboardEntries">;
  score: number; // your saved standing's score
  scoreChanged: boolean; // false if keepBest was true and the pre-existing score was better
  globalRank: number; // your saved standing's rank
  submittedScore: number; // the score of the run you just submitted
  submittedRank: number; // where that submitted run would rank
  userId: Id<"users">;
  username: string;
  userAvatarUrl?: string;
}
```
