N7/B4: Renown achievements wing

Three passive achievements (renown_1/5/10) gated on the derived Renown level
via renownAtLeast, reading player_meta.renown_xp through the achievements
plugin's existing Check(d, userID) seam. No event hook — the passive checker
grants them on the next message once the level is reached.

Claude-Session: https://claude.ai/code/session_017mEwUmmS7aQTP2NQXj6rUa
This commit is contained in:
prosolis
2026-07-10 20:09:30 -07:00
parent aaa45eab14
commit 1a47a2fdee
2 changed files with 56 additions and 0 deletions

View File

@@ -1206,9 +1206,39 @@ func (p *AchievementsPlugin) buildAchievements() []achievementDef {
return false
},
},
// N7/B4 — the Renown wing (gogobee_engagement_plan.md §B4). Passive checks
// against the derived Renown level (N7/B2); no event hook needed.
{
ID: "renown_1", Name: "Beyond the Cap", Description: "Earned your first level of Renown. The story was supposed to be over.",
Emoji: "✦",
Check: func(d *sql.DB, u id.UserID) bool { return renownAtLeast(d, u, 1) },
},
{
ID: "renown_5", Name: "Storied", Description: "Reached Renown 5. They tell versions of your runs that never happened.",
Emoji: "✦",
Check: func(d *sql.DB, u id.UserID) bool { return renownAtLeast(d, u, 5) },
},
{
ID: "renown_10", Name: "Fabled", Description: "Reached Renown 10. Somewhere a bard is getting rich off your name.",
Emoji: "✦",
Check: func(d *sql.DB, u id.UserID) bool { return renownAtLeast(d, u, 10) },
},
}
}
// renownAtLeast reports whether the player's derived Renown level (N7/B2) has
// reached level, reading player_meta.renown_xp through the passed handle.
func renownAtLeast(d *sql.DB, userID id.UserID, level int) bool {
var xp int
if err := d.QueryRow(
`SELECT renown_xp FROM player_meta WHERE user_id = ?`,
string(userID),
).Scan(&xp); err != nil {
return false
}
return renownLevelFor(xp) >= level
}
// ── Expedition achievement helpers ──────────────────────────────────────────
// clearedZoneIDs returns the zones this player has beaten outright. Boss-

View File

@@ -147,6 +147,32 @@ func TestAddRenownXP_Accumulates(t *testing.T) {
}
}
// TestRenownAtLeast — the B4 achievement gate reads the derived level correctly.
func TestRenownAtLeast(t *testing.T) {
newRenownTestDB(t)
uid := id.UserID("@renown_ach:example")
if err := createAdvCharacter(uid, "Achiever"); err != nil {
t.Fatal(err)
}
d := db.Get()
if renownAtLeast(d, uid, 1) {
t.Errorf("no renown yet, should not meet level 1")
}
if _, _, err := addRenownXP(uid, renownXPPerLevel*5); err != nil {
t.Fatal(err)
}
if !renownAtLeast(d, uid, 5) {
t.Errorf("renown 5 should meet level 5")
}
if renownAtLeast(d, uid, 6) {
t.Errorf("renown 5 should not meet level 6")
}
// Absent player → false, not a crash.
if renownAtLeast(d, id.UserID("@nobody:nowhere.invalid"), 1) {
t.Errorf("absent player should not meet any renown level")
}
}
// TestRenownOverlay — applyPlayerMetaOverlay populates RenownXP and RenownLevel.
func TestRenownOverlay(t *testing.T) {
newRenownTestDB(t)