mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 16:42:41 +00:00
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
92 lines
2.9 KiB
Go
92 lines
2.9 KiB
Go
package plugin
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"gogobee/internal/db"
|
|
)
|
|
|
|
// N1/A1 — treasure drops were orphaned by the Phase R transition and now
|
|
// hang off zone combat. The weight is what makes that safe: expedition
|
|
// combat rolls far more often than the one-a-day legacy activity the base
|
|
// rates were tuned for, so only boss/elite moments get a multiplier.
|
|
|
|
func TestAdvTreasureDropRate_ScalesWithWeight(t *testing.T) {
|
|
// A roll under the (weighted) rate reaches the duplicate check, which
|
|
// reads the treasure table.
|
|
if err := db.Init(t.TempDir()); err != nil {
|
|
t.Fatalf("db.Init: %v", err)
|
|
}
|
|
const user = "@weight:example.org"
|
|
base := advTreasureDropRates[1]
|
|
|
|
cases := []struct {
|
|
name string
|
|
weight float64
|
|
want float64
|
|
}{
|
|
{"standard kill", advTreasureWeightStandard, base},
|
|
{"elite doubles", advTreasureWeightElite, base * 2},
|
|
{"boss quadruples", advTreasureWeightBoss, base * 4},
|
|
{"zone clear is one bonus roll", advTreasureWeightZoneClear, base},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
_, _, rate := rollAdvTreasureDropDetailed(1, user, 0, c.weight)
|
|
if rate != c.want {
|
|
t.Fatalf("rate for weight %v = %v, want %v", c.weight, rate, c.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAdvTreasureDropRate_ZeroWeightNeverDrops(t *testing.T) {
|
|
for i := 0; i < 200; i++ {
|
|
drop, _, rate := rollAdvTreasureDropDetailed(1, "@zero:example.org", 0, 0)
|
|
if drop != nil || rate != 0 {
|
|
t.Fatalf("weight 0 produced drop=%v rate=%v", drop, rate)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A forced roll must actually yield a treasure — this is the "the seam is
|
|
// live again" assertion. A weight above 1/rate drives the effective rate
|
|
// past 1.0, so every roll lands on the drop path.
|
|
func TestAdvTreasureDropDetailed_ForcedRollGrantsTreasure(t *testing.T) {
|
|
if err := db.Init(t.TempDir()); err != nil {
|
|
t.Fatalf("db.Init: %v", err)
|
|
}
|
|
|
|
drop, _, _ := rollAdvTreasureDropDetailed(1, "@forced:example.org", 0, 1000)
|
|
if drop == nil || drop.Def == nil {
|
|
t.Fatal("forced roll produced no treasure")
|
|
}
|
|
if drop.Def.Tier != 1 {
|
|
t.Fatalf("tier-1 roll produced a tier-%d treasure", drop.Def.Tier)
|
|
}
|
|
}
|
|
|
|
// The treasure and masterwork systems predate zones and speak AdvLocation.
|
|
func TestAdvLocForZone(t *testing.T) {
|
|
loc := advLocForZone(ZoneGoblinWarrens)
|
|
if loc.Activity != AdvActivityDungeon {
|
|
t.Errorf("activity = %q, want dungeon", loc.Activity)
|
|
}
|
|
if want := zoneTierFromID(ZoneGoblinWarrens); loc.Tier != want {
|
|
t.Errorf("tier = %d, want %d", loc.Tier, want)
|
|
}
|
|
if loc.Name == "" || loc.Name == string(ZoneGoblinWarrens) {
|
|
t.Errorf("name = %q, want the zone's display name", loc.Name)
|
|
}
|
|
}
|
|
|
|
// TestMaxTreasuresForTier pins the T3 "trophy room" 4th slot: the base cap is
|
|
// 3, and only a tier-3+ home lifts it to 4.
|
|
func TestMaxTreasuresForTier(t *testing.T) {
|
|
for tier, want := range map[int]int{0: 3, 1: 3, 2: 3, 3: 4, 4: 4} {
|
|
if got := maxTreasuresForTier(tier); got != want {
|
|
t.Errorf("maxTreasuresForTier(%d) = %d, want %d", tier, got, want)
|
|
}
|
|
}
|
|
}
|