Files
gogobee/internal/plugin/dnd_expedition_supplies_test.go
prosolis 5b8ef740f8 Adv 2.0 D&D Phase 12 E2c: Siege Mode effects + threat-70 warning
§8.3 siege economics:
- applyDailyBurn now takes an explicit siege flag and enforces a 2× floor
  on supply burn even when HarshMod is below 2 (tier-1 zones still get
  starved out per spec).
- currentBurn mirrors the same precedence so status/briefing readouts
  stay consistent.
- Briefing rollover passes e.SiegeMode through, decoupling siege from
  the harsh-conditions composite.

§8.3 threat-70 warning: applyDailyThreatDrift emits a one-time
appendApproachingSiegeLog when the level crosses 70 (prevLevel<70 and
new level≥70). Pulls from a new flavor.ThreatClockApproachingSiege pool
seeded with the spec's verbatim warning beat plus two voice-matched
alternates.

Other siege effects (boss +20 HP / Legendary Resistance, cleared-room
respawn, no-short-rest enforcement) stay deferred to the combat-link
phase — ThreatBandInfo already exposes the flags that engine will read.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:22 -07:00

173 lines
4.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
wantErr bool
}{
{SupplyPurchase{StandardPacks: 1}, false},
{SupplyPurchase{StandardPacks: 3, DeluxePacks: 1}, false}, // max
{SupplyPurchase{StandardPacks: 4}, true}, // over standard cap
{SupplyPurchase{DeluxePacks: 2}, true}, // over deluxe cap
{SupplyPurchase{StandardPacks: -1}, true},
{SupplyPurchase{}, true}, // none purchased
}
for _, c := range cases {
err := c.p.Validate()
if (err != nil) != c.wantErr {
t.Errorf("%+v: err = %v, wantErr=%v", c.p, err, c.wantErr)
}
}
}
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) {
s := ExpeditionSupplies{Current: 5, Max: 10, DailyBurn: 2, HarshMod: 1.5, ForagedToday: true}
s, burn := applyDailyBurn(s, false, false)
if burn != 2 {
t.Errorf("burn = %v, want 2", burn)
}
if s.Current != 3 {
t.Errorf("current = %v, want 3", s.Current)
}
if s.ForagedToday {
t.Error("forage flag should reset on day rollover")
}
// Harsh active doubles via mult.
s, burn = applyDailyBurn(s, true, false)
if burn != 3 { // 2 × 1.5
t.Errorf("harsh burn = %v, want 3", burn)
}
// Siege forces a 2× floor even when HarshMod is below 2 (tier 1).
t1 := ExpeditionSupplies{Current: 10, Max: 10, DailyBurn: 1, HarshMod: 1}
_, sb := applyDailyBurn(t1, false, true)
if sb != 2 {
t.Errorf("siege tier1 burn = %v, want 2 (forced floor)", sb)
}
// Siege at higher tier still uses HarshMod when it exceeds 2.
t5 := ExpeditionSupplies{Current: 10, Max: 10, DailyBurn: 1, HarshMod: 3}
_, sb5 := applyDailyBurn(t5, false, true)
if sb5 != 3 {
t.Errorf("siege tier5 burn = %v, want 3 (HarshMod)", sb5)
}
// Drain to floor.
for i := 0; i < 5; i++ {
s, _ = applyDailyBurn(s, false, false)
}
if s.Current != 0 {
t.Errorf("expected current clamped to 0, got %v", s.Current)
}
}