pets: earn XP from combat wins again

petGrantXP has been dead code since R1 deleted the legacy daily activity
loop it used to ride. Nothing replaced the call, so for the whole life of
Adventure 2.0 the only pet XP in the game came from a paid babysitter.
Prod bears it out: the one player who never subscribed has a pet sitting
at level 1 with 0 XP after months of play.

That is not cosmetic. DerivePlayerStats scales PetAttackProc,
PetDeflectProc and PetAttackDmg off pet level, so a frozen pet is a
permanently dead combat slot that the player has no way to revive.

Wire it into postCombatBookkeeping — the one seam all four combat
close-outs already meet, so a pet cannot level differently depending on
whether the fight auto-resolved or was played a round at a time. Both
slots earn on the same win, matching the babysit trickle: combat only
reads the two pets' averaged procs, so leveling both is not a spike.

Writes go through the narrow per-slot pet upserts rather than
saveAdvCharacter, because this runs on a path that does not hold the
per-user lock and a full-row write could clobber a concurrent save.

Verified against the sim: a level-3 pet finishes one L10 expedition at
level 4 with carryover, where before it finished exactly where it started.
This commit is contained in:
prosolis
2026-07-20 23:51:49 -07:00
parent 5a8d21f780
commit ca2d1a8ea3
3 changed files with 177 additions and 0 deletions
+14
View File
@@ -32,6 +32,20 @@ func (p *AdventurePlugin) postCombatBookkeeping(
if err := persistDnDPostCombatSubclass(dndChar, raged, result, mods); err != nil {
slog.Error("dnd: post-combat subclass persist", "user", userID, "err", err)
}
// The pet fought too. A win is its only earned XP — see grantPetCombatXP
// for why this seam and not the room-clear one: it is the single place all
// four close-outs already meet, so a pet cannot level differently depending
// on whether the fight auto-resolved or was played a round at a time.
if result.PlayerWon {
if leveled := grantPetCombatXP(userID); len(leveled) > 0 {
for _, line := range leveled {
slog.Info("adventure: pet leveled", "user", userID, "pet", line)
}
if err := p.SendDM(userID, "🐾 "+strings.Join(leveled, "\n🐾 ")); err != nil {
slog.Warn("adventure: pet level-up DM", "user", userID, "err", err)
}
}
}
}
// grantCombatAchievements checks combat results for achievement-worthy moments.