adventure: tell Pete who else is on the expedition

Party seats ride the roster detail, beside the supply and threat numbers the
same expedition already publishes. Leader first, the hireling named without a
token he has no board row for, and an opted-out player's seat kept and
anonymised — dropping it would make a party of three read as a pair while the
burn rate and enemy scaling beside it still felt three bodies.

The board itself was wrong about this: it resolved an expedition with a lookup
keyed on dnd_expedition.user_id, which is blind to members, so for the whole
life of N3 parties a seated player has shown as idle in town while standing in
a dungeon.

Pets also send the engine's XP threshold for their current level band, so the
web can draw progress without keeping a copy of a curve that would drift.
This commit is contained in:
prosolis
2026-07-24 20:26:28 -07:00
parent f73ab56ac8
commit e5d4bd6dc5
5 changed files with 374 additions and 7 deletions
+22 -4
View File
@@ -15,6 +15,11 @@ import (
const petXPPerAction = 1.5
// petMaxLevel is where the curve stops. Named because two things read it for
// different reasons: the level-up loop stops here, and the web push reports "no
// more to earn" from it.
const petMaxLevel = 10
var petNameValid = regexp.MustCompile(`^[a-zA-Z0-9 '\-]+$`)
// petXPToNextLevel returns XP needed for a given pet level.
@@ -31,6 +36,19 @@ func petXPToNextLevel(level int) int {
}
}
// petXPNeededCenti is petXPToNextLevel in the unit the stored ledger actually
// uses — centi-XP — and 0 once the pet is capped, so a caller can tell "nothing
// left to earn" from "needs another 10 points". Every comparison against a
// stored PetXP multiplies by 100 (see advancePetLevelsFromXP); anything reading
// the curve for display has to do the same, and doing it in one place is how the
// web push avoids getting it wrong.
func petXPNeededCenti(level int) int {
if level >= petMaxLevel {
return 0
}
return petXPToNextLevel(level) * 100
}
// petGrantXP adds a per-action XP grant to the pet and handles level-ups.
// Returns true if leveled up. Shares the level-up loop with the babysit trickle
// via advancePetLevelsFromXP.
@@ -90,13 +108,13 @@ func grantPetCombatXP(userID id.UserID) []string {
// to the level-10 cap, stamping the level-10 date on first reaching it. Shared
// by both pet slots (the babysit trickle). Returns true if the pet leveled.
func advancePetLevelsFromXP(xp, level *int, level10Date *string, addCentiXP int) bool {
if *level >= 10 {
if *level >= petMaxLevel {
return false
}
*xp += addCentiXP
leveled := false
for *level < 10 {
needed := petXPToNextLevel(*level) * 100
for *level < petMaxLevel {
needed := petXPNeededCenti(*level)
if *xp < needed {
break
}
@@ -104,7 +122,7 @@ func advancePetLevelsFromXP(xp, level *int, level10Date *string, addCentiXP int)
*level++
leveled = true
}
if *level >= 10 && *level10Date == "" {
if *level >= petMaxLevel && *level10Date == "" {
*level10Date = time.Now().UTC().Format("2006-01-02")
}
return leveled