Files
gogobee/internal/plugin/adventure_well_rested.go
prosolis d01d3e277a N4/E1: T3 housing payoffs + a home-rest "well-rested" buff
Turn the dead top housing tiers into something worth buying. All three
land without a schema bump, and TestCombatCharacterization stays
byte-identical (the balance corpus never sets the new fields).

T3 trophy room: treasure cap 3->4 at HouseTier>=3 (maxTreasuresForTier).
Enforced at the save gate only -- HouseTier is never written downward,
so a held 4th treasure below tier 3 is unreachable and a load-time cap in
computeAdvBonuses would just add a query for an impossible state. The
all-irreplaceable manual discard prompt now lists the 4th slot too.

T3 workshop: +5% craft success at HouseTier>=3, lifting the cap 0.95->0.98
so a maxed forager still gains. craftingSuccessRate takes a workshopBonus,
threaded through autoCraftConsumables and renderRecipesKnown.

Well-rested buff (replaces the plan's T4 "inn-quality rest", a verified
no-op -- home rest already equals inn rest). Home-only (the inn and a
tier-1 shack grant nothing), starts at T2 and grows through T4, expires at
the next long rest:
  - Temp HP cushion (8/12/16% of MaxHP). TempHP was a dormant field; wired
    into applyDnDHPScaling as MaxHP headroom -- additive and golden-safe.
  - Bonus spell slots (+1/+2/+3 at the caster's highest slot level), the
    real reward, lifting casters who trail on spell-pool richness.
    applyLongRestSpellSlots resets the pool to base then folds in the
    bonus; expiry is stateless (next rest's reset drops it).

Magnitudes are tunable defaults; revisit against the post-parties
re-baseline.

Claude-Session: https://claude.ai/code/session_017mEwUmmS7aQTP2NQXj6rUa
2026-07-10 13:36:32 -07:00

111 lines
3.1 KiB
Go

package plugin
import (
"fmt"
"strings"
)
// Well-rested — a long rest taken at a home of tier 2+ grants a temporary buff
// that lasts until the next long rest. Renting a room at Thom Krooke's inn does
// not: the perk exists to give owning (and upgrading) a house a payoff the inn
// can't match, and it turns the dead T3/T4 prestige tiers into something worth
// buying.
//
// Two halves, both scaling with house tier (2 < 3 < 4):
// - wellRestedTempHP: a temporary hit-point cushion, layered above MaxHP for
// the day's fights via applyDnDHPScaling; and
// - wellRestedSlotBonus: bonus spell slots folded into the caster's pool at
// their highest available slot level.
//
// The tuning below is deliberately generous toward casters, who trail on
// spell-pool richness — the bonus slots are the substantive reward and the
// temp HP is a lighter feel-good cushion. All values are tunable.
//
// Nothing here is a secret discovery mechanic (cf. Misty/Arina), so the rest
// message surfaces exactly what was granted.
// homeRestTempHPPct maps house tier → temp-HP fraction of MaxHP.
func homeRestTempHPPct(houseTier int) float64 {
switch {
case houseTier >= 4:
return 0.16
case houseTier >= 3:
return 0.12
case houseTier >= 2:
return 0.08
default:
return 0
}
}
// homeRestBonusSlots maps house tier → number of bonus spell slots.
func homeRestBonusSlots(houseTier int) int {
switch {
case houseTier >= 4:
return 3
case houseTier >= 3:
return 2
case houseTier >= 2:
return 1
default:
return 0
}
}
// wellRestedTempHP returns the temporary HP granted by a home long rest at the
// given house tier. Tier 1 and the inn grant none. Always at least 1 when the
// tier qualifies, so a low-HP character still sees the buff.
func wellRestedTempHP(hpMax, houseTier int) int {
pct := homeRestTempHPPct(houseTier)
if pct <= 0 {
return 0
}
hp := int(float64(hpMax) * pct)
if hp < 1 {
hp = 1
}
return hp
}
// wellRestedSlotBonus returns the temporary bonus spell slots granted by a home
// long rest, placed entirely at the caster's highest available slot level (the
// most valuable, and one a caster is guaranteed to have). Tier 1, the inn, and
// non-casters (empty pool) grant none.
func wellRestedSlotBonus(houseTier int, pool map[int]int) map[int]int {
n := homeRestBonusSlots(houseTier)
if n == 0 || len(pool) == 0 {
return nil
}
highest := 0
for lvl := range pool {
if lvl > highest {
highest = lvl
}
}
if highest == 0 {
return nil
}
return map[int]int{highest: n}
}
// wellRestedSummary renders the player-facing description of a granted buff,
// e.g. "+18 temp HP, +2 level-5 spell slots". Returns "" when nothing was
// granted (tier 1, the inn, or a non-caster with no temp HP).
func wellRestedSummary(tempHP int, slotBonus map[int]int) string {
var bits []string
if tempHP > 0 {
bits = append(bits, fmt.Sprintf("+%d temp HP", tempHP))
}
for lvl, n := range slotBonus {
if n <= 0 {
continue
}
plural := ""
if n != 1 {
plural = "s"
}
bits = append(bits, fmt.Sprintf("+%d level-%d spell slot%s", n, lvl, plural))
}
return strings.Join(bits, ", ")
}