dot-achievements states an achievement as data — rules over per-player
numbers — rather than as code inside whatever awards it. A condition that lives
in the awarding code is one nobody can list, show progress towards, or check a
save file against.
Requires dot-core. dot-stats is the natural
source of the numbers and dot-auth supplies the
backbone client; neither is named in the source.
Usage
var catalogue := DotAchievementCatalogue.of([
DotAchievement.counter(&"k10", "First Blood", &"kills", 10),
DotAchievement.counter(&"k100", "Centurion", &"kills", 100),
DotAchievement.make(&"flawless", "Flawless", [
DotAchievementRule.make(&"round_wins", 1),
DotAchievementRule.make(&"deaths", 0, DotAchievementRule.Op.AT_MOST),
]),
])
var tracker := DotAchievementTracker.new()
tracker.catalogue = catalogue
tracker.store = DotAchievementStoreFile.new("user://achievements")
tracker.unlocked.connect(func(player: String, a: DotAchievement) -> void:
hud.toast("%s unlocked" % a.display_name))
add_child(tracker)
await tracker.begin(player_key) # loads their lifetime numbers
tracker.record(player_key, &"kills", 1)
await tracker.end(player_key) # saves
record() merges by the kind the rule declared, so a counter adds and a best
keeps the better. One call serves every stat, and a game cannot get it the wrong
way round.
Validating the catalogue loads no mesh, icon or scene, so a dedicated server can decide what somebody has earned and report it — the same rule as dot-loadout.
With dot-stats
var link := DotAchievementStatsLink.new()
link.tracker = tracker
link.stats = stats_tracker
add_child(link)
Session totals are not lifetime deltas
dot-stats’ recorded signal carries a player’s session total, and an
achievement is about a lifetime. Wiring the signal straight into record() adds
the running session total on every kill: three after the second, six after the
third, ten after the fourth.
The link holds a baseline and files the difference, and treats a total that goes down as a new session rather than a negative delta.
Showing them
for row in tracker.listing(player_key):
print(row["name"], row.get("progress", row.get("unlocked")))
Hidden entries are absent until earned and secret ones arrive without their description. The filtering happens here rather than in a UI, because a description withheld by the interface that drew it was still sent to the client.
Reporting to the backbone
Set report_to_backbone and the tracker drives its own DotAchievementReporter: every report_interval seconds (ten by default) it declares the catalogue once — retried on a doubling delay while the backbone is unreachable, dropped after a refusal — and sends the queued unlocks in one batch, so a round’s end that awards a dozen is one request. report_app is the app id sent with the declaration, and await tracker.report() sends on demand, which is what a host does before shutdown. The tracker owns the timer because it is the one node a host already places; before it did, nothing in the family ever flushed the queue and every unlock sat there until the queue limit dropped it.
The site serves the three routes the reporter speaks (define, unlock and player) in the shape the reporter sends. When a 200 names refused unlocks, achievements the site has no definition for, the reporter logs them at ERROR: those unlocks are gone, and before this nothing said so.
