mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
Today's global SupplyPackStandardMax=3 / SupplyPackDeluxeMax=1 were a 2-day shape; with D1's longer room budgets and D2-b's event-anchored night burns, a T4/T5 player can't legally buy enough supplies for the intended duration. supplyPackCaps(tier) now returns (std,dlx) per tier — T1/T2: (2,1); T3: (3,1) unchanged; T4: (5,1); T5: (7,2) — sized to clear DailyBurn(raw) × intendedDays × 1.3 even with the harsh×3 multiplier layered on. Validate takes a tier; both call sites (!expedition start, !resume) pass the resolved zone's tier. Holiday +1 standard pack still bypasses the cap on purpose. DailyBurn / phase5BDailyBurnRatePct unchanged; that's a D7 lever once the sim can measure event-anchored rollovers.
214 lines
6.4 KiB
Go
214 lines
6.4 KiB
Go
package plugin
|
||
|
||
import "testing"
|
||
|
||
func TestSupplyDailyBurn_AllTiers(t *testing.T) {
|
||
cases := []struct {
|
||
tier ZoneTier
|
||
want float32
|
||
}{
|
||
{ZoneTierBeginner, 1.0},
|
||
{ZoneTierApprentice, 1.5},
|
||
{ZoneTierJourneyman, 2.0},
|
||
{ZoneTierVeteran, 3.0},
|
||
{ZoneTierLegendary, 4.0},
|
||
}
|
||
for _, c := range cases {
|
||
if got := supplyDailyBurn(c.tier); got != c.want {
|
||
t.Errorf("tier %d: got %v, want %v", c.tier, got, c.want)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestSupplyHarshMultiplier_AllTiers(t *testing.T) {
|
||
cases := []struct {
|
||
tier ZoneTier
|
||
want float32
|
||
}{
|
||
{ZoneTierBeginner, 1.0},
|
||
{ZoneTierApprentice, 1.5},
|
||
{ZoneTierJourneyman, 2.0},
|
||
{ZoneTierVeteran, 2.5},
|
||
{ZoneTierLegendary, 3.0},
|
||
}
|
||
for _, c := range cases {
|
||
if got := supplyHarshMultiplier(c.tier); got != c.want {
|
||
t.Errorf("tier %d: got %v, want %v", c.tier, got, c.want)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestSupplyDepletion_Brackets(t *testing.T) {
|
||
cases := []struct {
|
||
current float32
|
||
max float32
|
||
want SupplyDepletionState
|
||
}{
|
||
{10, 10, SupplyNormal}, // 100%
|
||
{3, 10, SupplyNormal}, // 30%
|
||
{2.5, 10, SupplyNormal}, // 25% — boundary stays normal
|
||
{2, 10, SupplyRationing}, // 20%
|
||
{1, 10, SupplyRationing}, // 10% — boundary stays rationing
|
||
{0.9, 10, SupplySevereRationing},
|
||
{0.1, 10, SupplySevereRationing},
|
||
{0, 10, SupplyStarvation},
|
||
}
|
||
for _, c := range cases {
|
||
s := ExpeditionSupplies{Current: c.current, Max: c.max}
|
||
if got := supplyDepletion(s); got != c.want {
|
||
t.Errorf("%v/%v: got %d, want %d", c.current, c.max, got, c.want)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestSupplyRollModifier(t *testing.T) {
|
||
cases := map[SupplyDepletionState]int{
|
||
SupplyNormal: 0,
|
||
SupplyRationing: -1,
|
||
SupplySevereRationing: -2,
|
||
SupplyStarvation: -2,
|
||
}
|
||
for state, want := range cases {
|
||
if got := supplyRollModifier(state); got != want {
|
||
t.Errorf("state %d: got %d, want %d", state, got, want)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestSupplyAllowsLongRest(t *testing.T) {
|
||
cases := map[SupplyDepletionState]bool{
|
||
SupplyNormal: true,
|
||
SupplyRationing: true,
|
||
SupplySevereRationing: false,
|
||
SupplyStarvation: false,
|
||
}
|
||
for state, want := range cases {
|
||
if got := supplyAllowsLongRest(state); got != want {
|
||
t.Errorf("state %d: got %v, want %v", state, got, want)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestSupplyPurchase_Validate(t *testing.T) {
|
||
cases := []struct {
|
||
p SupplyPurchase
|
||
tier ZoneTier
|
||
wantErr bool
|
||
}{
|
||
// T3 is the only tier whose caps are unchanged from the pre-D5
|
||
// shape (3 standard / 1 deluxe); use it to anchor the
|
||
// happy-path / over-cap parity assertions.
|
||
{SupplyPurchase{StandardPacks: 1}, ZoneTierJourneyman, false},
|
||
{SupplyPurchase{StandardPacks: 3, DeluxePacks: 1}, ZoneTierJourneyman, false},
|
||
{SupplyPurchase{StandardPacks: 4}, ZoneTierJourneyman, true},
|
||
{SupplyPurchase{DeluxePacks: 2}, ZoneTierJourneyman, true},
|
||
{SupplyPurchase{StandardPacks: -1}, ZoneTierJourneyman, true},
|
||
{SupplyPurchase{}, ZoneTierJourneyman, true}, // none purchased
|
||
|
||
// D5-a per-tier caps. T1/T2 tighten to (2,1); T4 widens to (5,1);
|
||
// T5 widens to (7,2). A T3-legal 3-standard loadout fails on T1.
|
||
{SupplyPurchase{StandardPacks: 2, DeluxePacks: 1}, ZoneTierBeginner, false},
|
||
{SupplyPurchase{StandardPacks: 3}, ZoneTierBeginner, true},
|
||
{SupplyPurchase{StandardPacks: 5, DeluxePacks: 1}, ZoneTierVeteran, false},
|
||
{SupplyPurchase{StandardPacks: 6}, ZoneTierVeteran, true},
|
||
{SupplyPurchase{StandardPacks: 7, DeluxePacks: 2}, ZoneTierLegendary, false},
|
||
{SupplyPurchase{StandardPacks: 7, DeluxePacks: 3}, ZoneTierLegendary, true},
|
||
}
|
||
for _, c := range cases {
|
||
err := c.p.Validate(c.tier)
|
||
if (err != nil) != c.wantErr {
|
||
t.Errorf("%+v T%d: err = %v, wantErr=%v", c.p, int(c.tier), err, c.wantErr)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestSupplyPackCaps_PerTier(t *testing.T) {
|
||
cases := []struct {
|
||
tier ZoneTier
|
||
wantStd int
|
||
wantDlx int
|
||
}{
|
||
{ZoneTierBeginner, 2, 1},
|
||
{ZoneTierApprentice, 2, 1},
|
||
{ZoneTierJourneyman, 3, 1},
|
||
{ZoneTierVeteran, 5, 1},
|
||
{ZoneTierLegendary, 7, 2},
|
||
}
|
||
for _, c := range cases {
|
||
s, d := supplyPackCaps(c.tier)
|
||
if s != c.wantStd || d != c.wantDlx {
|
||
t.Errorf("T%d caps: got (%d,%d), want (%d,%d)", int(c.tier), s, d, c.wantStd, c.wantDlx)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestSupplyPurchase_TotalAndCost(t *testing.T) {
|
||
p := SupplyPurchase{StandardPacks: 3, DeluxePacks: 1}
|
||
if p.Total() != 50 {
|
||
t.Errorf("total = %v, want 50", p.Total())
|
||
}
|
||
if p.Cost() != 240 {
|
||
t.Errorf("cost = %d, want 240", p.Cost())
|
||
}
|
||
}
|
||
|
||
func TestMakeSupplies_FillsFromTier(t *testing.T) {
|
||
p := SupplyPurchase{StandardPacks: 2}
|
||
s := makeSupplies(ZoneTierJourneyman, p)
|
||
if s.Max != 20 || s.Current != 20 {
|
||
t.Errorf("supplies: %+v", s)
|
||
}
|
||
if s.DailyBurn != 2.0 {
|
||
t.Errorf("daily burn = %v, want 2", s.DailyBurn)
|
||
}
|
||
if s.PacksStandard != 2 {
|
||
t.Errorf("packs std = %d", s.PacksStandard)
|
||
}
|
||
}
|
||
|
||
func TestApplyDailyBurn_DrainsAndClamps(t *testing.T) {
|
||
// Phase 5-B (shipped): applyDailyBurn now scales by
|
||
// phase5BDailyBurnRatePct = 50 by default — every expected value
|
||
// here is halved relative to the pre-Phase-5-B baseline. The
|
||
// math-pure shape (harsh-mult, siege-floor) is unchanged; only
|
||
// the final multiplier is new. See applyDailyBurn doc for the
|
||
// difficulty-pass motivation.
|
||
s := ExpeditionSupplies{Current: 5, Max: 10, DailyBurn: 2, HarshMod: 1.5, ForagedToday: true}
|
||
s, burn := applyDailyBurn(s, false, false)
|
||
if burn != 1 { // 2 × 50%
|
||
t.Errorf("burn = %v, want 1", burn)
|
||
}
|
||
if s.Current != 4 { // 5 - 1
|
||
t.Errorf("current = %v, want 4", s.Current)
|
||
}
|
||
if s.ForagedToday {
|
||
t.Error("forage flag should reset on day rollover")
|
||
}
|
||
// Harsh active applies HarshMod (1.5), then phase5B halves: 2*1.5*0.5 = 1.5.
|
||
s, burn = applyDailyBurn(s, true, false)
|
||
if burn != 1.5 {
|
||
t.Errorf("harsh burn = %v, want 1.5", burn)
|
||
}
|
||
// Siege forces a 2× floor even when HarshMod is below 2 (tier 1);
|
||
// phase5B halves: 1 × 2 × 0.5 = 1.
|
||
t1 := ExpeditionSupplies{Current: 10, Max: 10, DailyBurn: 1, HarshMod: 1}
|
||
_, sb := applyDailyBurn(t1, false, true)
|
||
if sb != 1 {
|
||
t.Errorf("siege tier1 burn = %v, want 1 (forced 2× floor × phase5B 50%%)", sb)
|
||
}
|
||
// Siege at higher tier still uses HarshMod when it exceeds 2:
|
||
// 1 × 3 × 0.5 = 1.5.
|
||
t5 := ExpeditionSupplies{Current: 10, Max: 10, DailyBurn: 1, HarshMod: 3}
|
||
_, sb5 := applyDailyBurn(t5, false, true)
|
||
if sb5 != 1.5 {
|
||
t.Errorf("siege tier5 burn = %v, want 1.5 (HarshMod × phase5B)", sb5)
|
||
}
|
||
// Drain to floor.
|
||
for i := 0; i < 10; i++ {
|
||
s, _ = applyDailyBurn(s, false, false)
|
||
}
|
||
if s.Current != 0 {
|
||
t.Errorf("expected current clamped to 0, got %v", s.Current)
|
||
}
|
||
}
|