# Achievements & stats

Track player statistics and unlock achievements

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

Stats are numbers that track progress per player — total kills, distance run, games played. Achievements are milestones players unlock, like "First Blood" or "Centurion." You define both in the Developer Portal and interact with them from your game through the SDK.

## Setting up achievements

Go to **Developer Portal → your game → Achievements** to create and manage achievements. You can add them one at a time or bulk import via JSON.

Each achievement has:

| Field | Required | Description |
|-------|----------|-------------|
| **ID** | Yes | A unique identifier like `FIRST_BLOOD` |
| **Title** | Yes | The display name players see, like "First Blood" |
| **Description** | Yes | What the player did to earn it |
| **Image** | No | A custom icon shown in the achievement list |
| **Trigger rule** | No | Automatically unlock when a stat reaches a threshold |

Achievements can be unlocked in two ways:
1. **Manually** — your game calls the SDK to unlock it
2. **Automatically** — you set a trigger rule (e.g. "Total Kills is at least 100") and Wavedash unlocks it when the stat threshold is reached

### Bulk import

Click **Add achievement → Import JSON** to import multiple achievements and stats at once. The expected format:

```json
{
  "achievements": [
    {
      "identifier": "FIRST_KILL",
      "display_name": "First Blood",
      "description": "Get your first kill",
      "stat_requirement": null
    },
    {
      "identifier": "GOLD_100K",
      "display_name": "Rich!",
      "description": "Collect 100,000 gold",
      "stat_requirement": {
        "stat": "GOLD_COLLECTED",
        "threshold": 100000
      }
    }
  ],
  "stats": [
    {
      "identifier": "GOLD_COLLECTED",
      "display_name": "Gold Collected"
    }
  ]
}
```

Existing items (matched by identifier) are skipped.

## Setting up stats

Go to **Developer Portal → your game → In-Game Stats** to define stats. Each stat has an ID and a display name. Stats persist across sessions as numeric values (Godot and Unity expose both int and float variants; JavaScript and Defold store them through a single numeric API).

## Working with stats

<CodeGroup>
```gdscript Godot
func _ready():
    WavedashSDK.current_stats_received.connect(_on_got_stats)

func load_stats():
    WavedashSDK.request_stats()

func _on_got_stats(response):
    if response.get("success", false):
        var kills = WavedashSDK.get_stat_int("total_kills")
        print("total_kills: ", kills)

func add_kill():
    var kills = WavedashSDK.get_stat_int("total_kills") + 1
    WavedashSDK.set_stat_int("total_kills", kills, true)
```

```csharp Unity
async void Start()
{
    bool ok = await Wavedash.SDK.RequestStats();
    if (ok)
    {
        int kills = Wavedash.SDK.GetStatInt("total_kills");
        Debug.Log($"total_kills: {kills}");
    }
}

public void AddKill()
{
    int kills = Wavedash.SDK.GetStatInt("total_kills") + 1;
    Wavedash.SDK.SetStatInt("total_kills", kills, storeNow: true);
}
```

```lua Defold
function init(self)
    local co = coroutine.create(function()
        local response = wavedash.request_stats_async()
        if response.success then
            local kills = wavedash.get_stat("total_kills")
            print("total_kills:", kills)
        end
    end)
    assert(coroutine.resume(co))
end

local function add_kill()
    local kills = (wavedash.get_stat("total_kills") or 0) + 1
    wavedash.set_stat("total_kills", kills, true)
end
```

```javascript JavaScript
const response = await Wavedash.requestStats();
if (response.success) {
  const kills = Wavedash.getStat("total_kills");
  console.log(`total_kills: ${kills}`);
}

function addKill() {
  const kills = Wavedash.getStat("total_kills") + 1;
  Wavedash.setStat("total_kills", kills, true);
}
```
</CodeGroup>

<Tip>
All platforms support a `storeNow` parameter: `setStat("id", value, true)` and `setAchievement("id", true)`. When `storeNow` is true, the SDK schedules a persist immediately (debounced over 1 second). For an immediate flush, call `storeStats()` explicitly. The platform also flushes pending stats when the session ends.
</Tip>

### Float stats

Use the float variants for stats that aren't whole numbers — distance travelled, completion percentages, best times.

<CodeGroup>
```gdscript Godot
WavedashSDK.set_stat_float("distance_run_km", 42.195, true)
var distance = WavedashSDK.get_stat_float("distance_run_km")
```

```csharp Unity
Wavedash.SDK.SetStatFloat("distance_run_km", 42.195f, storeNow: true);
float distance = Wavedash.SDK.GetStatFloat("distance_run_km");
```

```lua Defold
wavedash.set_stat("distance_run_km", 42.195, true)
local distance = wavedash.get_stat("distance_run_km")
```

```javascript JavaScript
Wavedash.setStat("distance_run_km", 42.195, true);
const distance = Wavedash.getStat("distance_run_km");
```
</CodeGroup>

## Unlocking achievements

<CodeGroup>
```gdscript Godot
WavedashSDK.set_achievement("first_win", true)

func has_first_win() -> bool:
    return WavedashSDK.get_achievement("first_win")
```

```csharp Unity
Wavedash.SDK.SetAchievement("first_win", storeNow: true);

bool unlocked = Wavedash.SDK.GetAchievement("first_win");
```

```lua Defold
wavedash.set_achievement("first_win", true)

local unlocked = wavedash.get_achievement("first_win")
```

```javascript JavaScript
Wavedash.setAchievement("first_win", true);

const unlocked = Wavedash.getAchievement("first_win");
```
</CodeGroup>

## Storing stats and achievements together

Batch stat updates and achievement unlocks, then send them in one call:

<CodeGroup>
```gdscript Godot
WavedashSDK.set_stat_int("total_kills", 200, false)
WavedashSDK.set_achievement("first_blood")
WavedashSDK.store_stats()
```

```csharp Unity
Wavedash.SDK.SetStatInt("total_kills", 200, storeNow: false);
Wavedash.SDK.SetAchievement("first_blood");
await Wavedash.SDK.StoreStats();
```

```lua Defold
wavedash.set_stat("total_kills", 200, false)
wavedash.set_achievement("first_blood")
wavedash.store_stats()
```

```javascript JavaScript
Wavedash.setStat("total_kills", 200);
Wavedash.setAchievement("first_blood");
Wavedash.storeStats();
```
</CodeGroup>

### Persistence events

The SDK raises an event after each `storeStats()` flush so you can show a confirmation, retry on failure, or wait to quit until stats are safely persisted. Unity separately raises `OnCurrentStatsReceived` after `RequestStats()` completes.

<CodeGroup>
```gdscript Godot
func _ready():
    WavedashSDK.stats_stored.connect(_on_stats_stored)

func _on_stats_stored(payload):
    if payload.get("success", false):
        print("Stats saved")
    else:
        push_warning("Stats failed to save: ", payload.get("message", ""))
```

```csharp Unity
void Awake()
{
    Wavedash.SDK.OnCurrentStatsReceived += data => Debug.Log("Stats loaded");
    Wavedash.SDK.OnStatsStored += data =>
    {
        bool ok = (bool)data["success"];
        Debug.Log(ok ? "Stats saved" : $"Stats failed: {data["message"]}");
    };
}
```

```lua Defold
function init(self)
    wavedash.init({}, function(_, event, payload)
        if event == wavedash.EVENT_STATS_STORED then
            if payload.success then
                print("Stats saved")
            else
                print("Stats failed:", payload.message or "")
            end
        end
    end)
end
```

```javascript JavaScript
Wavedash.on(Wavedash.Events.STATS_STORED, (payload) => {
  if (payload.success) console.log("Stats saved");
  else console.warn("Stats failed:", payload.message);
});
```
</CodeGroup>

## Example: kill tracking

<CodeGroup>
```gdscript Godot
var session_kills: int = 0

func on_enemy_killed() -> void:
    var kills = WavedashSDK.get_stat_int("total_kills") + 1
    WavedashSDK.set_stat_int("total_kills", kills, true)
    session_kills += 1
    if kills == 1:
        WavedashSDK.set_achievement("first_blood", true)
```

```csharp Unity
private int sessionKills;

public void OnEnemyKilled()
{
    int kills = Wavedash.SDK.GetStatInt("total_kills") + 1;
    Wavedash.SDK.SetStatInt("total_kills", kills, storeNow: true);
    sessionKills++;
    if (kills == 1)
        Wavedash.SDK.SetAchievement("first_blood", storeNow: true);
}
```

```lua Defold
local session_kills = 0

local function on_enemy_killed()
    local kills = (wavedash.get_stat("total_kills") or 0) + 1
    wavedash.set_stat("total_kills", kills, true)
    session_kills = session_kills + 1
    if kills == 1 then
        wavedash.set_achievement("first_blood", true)
    end
end
```

```javascript JavaScript
let sessionKills = 0;

function onEnemyKilled() {
  const kills = Wavedash.getStat("total_kills") + 1;
  Wavedash.setStat("total_kills", kills, true);
  sessionKills++;
  if (kills === 1) Wavedash.setAchievement("first_blood", true);
}
```
</CodeGroup>
