N7/B2: Renown — prestige past the L20 cap

Overflow XP that grantDnDXP used to drop at L20 now accumulates as Renown
on player_meta.renown_xp (cumulative, atomic +=; renown_level derived as
renown_xp/25000). The reward is prestige-only: a derived rank ladder
(Renowned→…→Eternal), a cosmetic ✦N marker on the sheet and leaderboard,
a games-room shout on rank promotion, and !level progress once capped.

Renown perks are the two combat-neutral economy levers only — +loot / +XP,
capped at a streak-30 grant's economic half (+15% / +20%). combat_stats.go
reads DeathModifier/SuccessBonus/ExceptionalBonus (which map to Defense/
Attack/CritRate) but never LootQuality/XPMultiplier, so renown pays out even
through loadCombatBonuses without moving the golden or the balance corpus.
The plan's "-death penalty" perk is deliberately dropped (it would inflate
Defense).

The overflow→renown conversion and the character save commit in one
transaction (saveDnDCharacterExec now takes an executor), so a crash can
neither drop the overflow nor double-credit it on the next grant.

Schema: renown_xp column, DEFAULT 0 correct for every existing row, no
bootstrap (journal_pages/epilogue_cleared pattern).

Claude-Session: https://claude.ai/code/session_017mEwUmmS7aQTP2NQXj6rUa
This commit is contained in:
prosolis
2026-07-10 19:48:49 -07:00
parent 401b17c3bb
commit 38a3693832
13 changed files with 559 additions and 9 deletions

View File

@@ -332,8 +332,22 @@ func LoadDnDCharacter(userID id.UserID) (*DnDCharacter, error) {
return c, nil
}
// sqlExecer is the subset of *sql.DB / *sql.Tx that saveDnDCharacterExec needs,
// so the upsert can run either standalone or inside a caller's transaction.
type sqlExecer interface {
Exec(query string, args ...any) (sql.Result, error)
}
// SaveDnDCharacter upserts the row.
func SaveDnDCharacter(c *DnDCharacter) error {
return saveDnDCharacterExec(db.Get(), c)
}
// saveDnDCharacterExec runs the dnd_character upsert against any executor. Used
// directly by SaveDnDCharacter and inside grantDnDXP's overflow→renown
// transaction (adventure_renown.go), so the character save and the renown
// credit commit atomically.
func saveDnDCharacterExec(ex sqlExecer, c *DnDCharacter) error {
pending := 0
if c.PendingSetup {
pending = 1
@@ -346,7 +360,7 @@ func SaveDnDCharacter(c *DnDCharacter) error {
if c.OnboardingSent {
onboard = 1
}
_, err := db.Get().Exec(`
_, err := ex.Exec(`
INSERT INTO dnd_character (user_id, race, class, dnd_level, dnd_xp,
str_score, dex_score, con_score, int_score, wis_score, cha_score,
hp_current, hp_max, temp_hp, armor_class,