Skip to content

[DX-1425] guide(xp): add project demonstrating player xp and levels - #39

Merged
trandromeda merged 3 commits into
mainfrom
fs/xp_guide
Jul 16, 2026
Merged

[DX-1425] guide(xp): add project demonstrating player xp and levels#39
trandromeda merged 3 commits into
mainfrom
fs/xp_guide

Conversation

@DannyIsYog

@DannyIsYog DannyIsYog commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Add XP progression guide (Nakama + Hiro + Unity)

Summary

Adds the XP Guide companion project under Guides/: an end-to-end XP progression system built on Hiro's Achievements and Economy systems, with a Nakama server plugin and a Unity client.

The guide demonstrates how to model player levels as sub-achievements driven by an XP currency, so that granting XP anywhere in the game automatically advances the player's level without callers needing to know levelling exists.

How it works

XP is a currency, and levels are sub-achievements of a player_levels group, each with a max_count equal to the XP required to complete it (100, 200, ... 1000 for levels 1–10).

The flow:

  1. The client calls rpc_grant_xp with a base amount.
  2. The RPC rolls and grants a reward containing the xp currency via the Economy reward APIs, so active modifiers (e.g. a double-XP booster) apply.
  3. XPLevelPublisher listens for the currencyGranted event on xp and advances the level sub-achievements by the actual granted amount.
  4. XP overflows across levels in a single batched UpdateAchievements call, so one large grant can complete several levels and partially fill the next.
  5. Levels are auto_claim, so completing one grants its gem reward immediately.

Routing progression through the publisher rather than the RPC means any future XP source gets levelling for free.

The quest group (defeat_thunder_world) exists to exercise the loop from a realistic source: its sub-quests grant XP, gems, and coins, and one of them grants a 2× XP reward modifier for 120 seconds, which the client surfaces as an active-booster badge.

Testing

  • XPControllerTests pass against a local Nakama instance.
  • Manually verified the full loop: grant XP → level up → auto-claimed gem reward, and completing defend_the_village activates the 2× booster badge and doubles subsequent XP grants.

interval: 3s
timeout: 3s
retries: 5
image: postgres:12.2-alpine

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I make it a habit of using latest postgres, no real difference for development but since this is for the public I think it's best to show latest, latest is 18

@@ -0,0 +1,18 @@
ARG NAKAMA_VERSION=3.38.0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

latest nakama is 3.40

Comment thread Guides/XP_Guide/Nakama+Hiro/main.go Outdated
// Sub-achievements are stored in a map, and Go map iteration order is random.
// Sort level IDs numerically so level_9 always comes before level_10.
// (A plain alphabetical sort would incorrectly place level_10 before level_2.)
levelIDs := make([]string, 0, len(playerLevels.SubAchievements))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I don't think there is a need to sort the keys, if you know the format do a for loop and build the key during iteration key = "level_" + i, also works as a check that if the key is not found in the dict there is a level missing or a malformed key

Comment thread Guides/XP_Guide/Nakama+Hiro/main.go Outdated
}

// isLevelID reports whether id has the expected "level_<N>" format (e.g. "level_1").
func isLevelID(id string) bool {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I'm not fond of little helper functions like this, id rather inline it

Comment thread Guides/XP_Guide/Nakama+Hiro/main.go Outdated
}

// grantXPRequest is the JSON payload the client sends when calling rpc_grant_xp.
type grantXPRequest struct {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I like to group struct definitions at the top of the file

},
}

reward, err := econ.RewardRoll(ctx, logger, nk, userID, rewardConfig)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

since it's not a weighted reward I think it's simpler to build a hiro.reward object directly and call grant rather than calling roll

@trandromeda trandromeda changed the title XP Guide [DX-1425] Add Player XP how-to project Jul 16, 2026
@linear-code

linear-code Bot commented Jul 16, 2026

Copy link
Copy Markdown

DX-1425

@trandromeda trandromeda changed the title [DX-1425] Add Player XP how-to project [DX-1425] guide(xp): add new how-to project Jul 16, 2026
@trandromeda trandromeda changed the title [DX-1425] guide(xp): add new how-to project [DX-1425] guide(xp): add project demonstrating player xp and levels Jul 16, 2026
@trandromeda
trandromeda merged commit 8c82523 into main Jul 16, 2026
1 check passed
@trandromeda
trandromeda deleted the fs/xp_guide branch July 16, 2026 15:52
trandromeda pushed a commit that referenced this pull request Jul 20, 2026
)

* XP Guide

* Code improvements

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants