Files
gogobee/internal/plugin/adventure_consumables_test.go
prosolis 4e412219f3 WIP: in-flight combat/expedition/flavor changes
Bundle of uncommitted working-tree edits across combat engine, expedition
cycle, flavor pools, and TwinBee/zone narration. Includes new files:
combat_debug.go, dnd_boss_consumables.go, dnd_dex_floor.go, plus
CHANGES_24H.md and REBALANCE_NOTES.md scratch notes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:59:19 -07:00

280 lines
7.7 KiB
Go

package plugin
import "testing"
func makeInventory(names ...string) []ConsumableItem {
var items []ConsumableItem
for i, name := range names {
def := consumableDefByName(name)
if def == nil {
panic("unknown consumable: " + name)
}
items = append(items, ConsumableItem{InventoryID: int64(i + 1), Def: def})
}
return items
}
func TestSelectConsumables_TrivialFightSkips(t *testing.T) {
strong := CombatStats{MaxHP: 200, Attack: 60}
weak := CombatStats{MaxHP: 30, Attack: 5}
inv := makeInventory("Berry Poultice", "Coal Bomb")
selected := SelectConsumables(inv, strong, weak, 0, 0, true)
if len(selected) != 0 {
t.Errorf("should skip consumables for trivial fight, got %d", len(selected))
}
}
func TestSelectConsumables_DangerousUsesHighTier(t *testing.T) {
weak := CombatStats{MaxHP: 80, Attack: 15}
strong := CombatStats{MaxHP: 200, Attack: 50}
inv := makeInventory("Berry Poultice", "Spirit Tonic", "Coal Bomb", "Ancient Artifact Oil")
selected := SelectConsumables(inv, weak, strong, 0, 0, true)
if len(selected) != 2 {
t.Fatalf("expected 2 consumables, got %d", len(selected))
}
hasHighTierOff, hasHighTierDef := false, false
for _, s := range selected {
if s.Def.Tier >= 4 && s.Def.Slot == "offensive" {
hasHighTierOff = true
}
if s.Def.Tier >= 4 && s.Def.Slot == "defensive" {
hasHighTierDef = true
}
}
if !hasHighTierOff {
t.Error("should pick high-tier offensive for dangerous fight")
}
if !hasHighTierDef {
t.Error("should pick high-tier defensive for dangerous fight")
}
}
func TestSelectConsumables_MaxTwo(t *testing.T) {
player := CombatStats{MaxHP: 100, Attack: 30}
enemy := CombatStats{MaxHP: 120, Attack: 35}
inv := makeInventory(
"Berry Poultice", "Herb Salve", "Mushroom Brew",
"Coal Bomb", "Goblin Grease", "Blooper Ink Vial",
)
selected := SelectConsumables(inv, player, enemy, 0, 0, true)
if len(selected) > 2 {
t.Errorf("max 2 consumables, got %d", len(selected))
}
offCount, defCount := 0, 0
for _, s := range selected {
if s.Def.Slot == "offensive" {
offCount++
} else {
defCount++
}
}
if offCount > 1 {
t.Errorf("max 1 offensive, got %d", offCount)
}
if defCount > 1 {
t.Errorf("max 1 defensive, got %d", defCount)
}
}
func TestSelectConsumables_EasyUsesLowTier(t *testing.T) {
player := CombatStats{MaxHP: 100, Attack: 30}
enemy := CombatStats{MaxHP: 50, Attack: 15} // easy: ratio ~0.55
inv := makeInventory("Berry Poultice", "Spirit Tonic", "Coal Bomb", "Ancient Artifact Oil")
selected := SelectConsumables(inv, player, enemy, 0, 0, true)
for _, s := range selected {
if s.Def.Tier > 2 {
t.Errorf("easy fight should use T1-T2, got %s (T%d)", s.Def.Name, s.Def.Tier)
}
}
}
func TestSelectConsumables_ArenaRoundEscalation(t *testing.T) {
player := CombatStats{MaxHP: 100, Attack: 30}
enemy := CombatStats{MaxHP: 80, Attack: 25} // competitive
inv := makeInventory("Berry Poultice", "Spirit Tonic", "Coal Bomb", "Sapphire Elixir")
r1 := SelectConsumables(inv, player, enemy, 1, 0, true)
r4 := SelectConsumables(inv, player, enemy, 4, 0, true)
maxTierR1 := 0
for _, s := range r1 {
if s.Def.Tier > maxTierR1 {
maxTierR1 = s.Def.Tier
}
}
maxTierR4 := 0
for _, s := range r4 {
if s.Def.Tier > maxTierR4 {
maxTierR4 = s.Def.Tier
}
}
if maxTierR4 <= maxTierR1 && len(r4) > 0 && len(r1) > 0 {
t.Logf("arena round escalation: R1 maxTier=%d, R4 maxTier=%d", maxTierR1, maxTierR4)
}
}
func TestApplyConsumableMods_Heal(t *testing.T) {
stats := CombatStats{MaxHP: 100}
mods := CombatModifiers{DamageReduct: 1.0}
items := makeInventory("Herb Salve")
ApplyConsumableMods(&stats, &mods, items)
if mods.HealItem != 20 {
t.Errorf("heal item = %d, want 20", mods.HealItem)
}
}
func TestApplyConsumableMods_AtkBoost(t *testing.T) {
stats := CombatStats{}
mods := CombatModifiers{}
items := makeInventory("Goblin Grease")
ApplyConsumableMods(&stats, &mods, items)
if mods.DamageBonus < 0.24 || mods.DamageBonus > 0.26 {
t.Errorf("damage bonus = %f, want ~0.25", mods.DamageBonus)
}
}
func TestApplyConsumableMods_Ward(t *testing.T) {
stats := CombatStats{}
mods := CombatModifiers{}
items := makeInventory("Quartz Ward")
ApplyConsumableMods(&stats, &mods, items)
if mods.WardCharges != 1 {
t.Errorf("ward charges = %d, want 1", mods.WardCharges)
}
}
func TestApplyConsumableMods_AutoCrit(t *testing.T) {
stats := CombatStats{}
mods := CombatModifiers{}
items := makeInventory("Ancient Artifact Oil")
ApplyConsumableMods(&stats, &mods, items)
if !mods.AutoCritFirst {
t.Error("AutoCritFirst should be true")
}
if mods.DamageBonus < 0.34 {
t.Errorf("damage bonus = %f, want ~0.35", mods.DamageBonus)
}
}
func TestApplyConsumableMods_SporeCloud(t *testing.T) {
stats := CombatStats{}
mods := CombatModifiers{}
items := makeInventory("Spore Cloud")
ApplyConsumableMods(&stats, &mods, items)
if mods.SporeCloud != 2 {
t.Errorf("spore rounds = %d, want 2", mods.SporeCloud)
}
}
func TestApplyConsumableMods_Reflect(t *testing.T) {
stats := CombatStats{}
mods := CombatModifiers{}
items := makeInventory("Voidstone Shard")
ApplyConsumableMods(&stats, &mods, items)
if mods.ReflectNext < 0.49 {
t.Errorf("reflect = %f, want ~0.50", mods.ReflectNext)
}
}
func TestApplyConsumableMods_DefBoost(t *testing.T) {
stats := CombatStats{}
mods := CombatModifiers{DamageReduct: 1.0}
items := makeInventory("Mushroom Brew")
ApplyConsumableMods(&stats, &mods, items)
if mods.DamageReduct < 0.79 || mods.DamageReduct > 0.81 {
t.Errorf("damage reduct = %f, want ~0.80", mods.DamageReduct)
}
}
func TestApplyConsumableMods_SpeedBoost(t *testing.T) {
stats := CombatStats{Speed: 10}
mods := CombatModifiers{}
items := makeInventory("Blooper Ink Vial")
ApplyConsumableMods(&stats, &mods, items)
if stats.Speed != 13 { // 10 * 1.30 = 13
t.Errorf("speed = %d, want 13", stats.Speed)
}
}
func TestApplyConsumableMods_CritBoost(t *testing.T) {
stats := CombatStats{CritRate: 0.05}
mods := CombatModifiers{}
items := makeInventory("Sapphire Elixir")
ApplyConsumableMods(&stats, &mods, items)
if stats.CritRate < 0.19 || stats.CritRate > 0.21 {
t.Errorf("crit rate = %f, want ~0.20", stats.CritRate)
}
}
func TestApplyConsumableMods_FlatDmg(t *testing.T) {
stats := CombatStats{}
mods := CombatModifiers{}
items := makeInventory("Coal Bomb")
ApplyConsumableMods(&stats, &mods, items)
if mods.FlatDmgStart != 8 {
t.Errorf("flat damage = %d, want 8", mods.FlatDmgStart)
}
}
func TestRollConsumableDrop_ValidActivity(t *testing.T) {
drops := 0
for i := 0; i < 1000; i++ {
item := RollConsumableDrop(AdvActivityMining, 3)
if item != nil {
drops++
if item.Type != "consumable" {
t.Errorf("drop type = %q, want consumable", item.Type)
}
if item.Name != "Quartz Ward" {
t.Errorf("mining T3 drop = %q, want Quartz Ward", item.Name)
}
if item.Value <= 0 {
t.Errorf("drop sell value = %d, want > 0", item.Value)
}
}
}
// 15% chance → expect ~150 in 1000
if drops < 100 || drops > 220 {
t.Errorf("drop rate: %d/1000 (expect ~150 at 15%%)", drops)
}
}
func TestRollConsumableDrop_InvalidActivity(t *testing.T) {
item := RollConsumableDrop(AdvActivityRest, 3)
if item != nil {
t.Error("rest activity should not drop consumables")
}
}
func TestRollConsumableDrop_T1NoDrop(t *testing.T) {
for i := 0; i < 100; i++ {
item := RollConsumableDrop(AdvActivityForaging, 1)
if item != nil {
t.Fatal("T1 should not drop consumables")
}
}
}
func TestBuyableConsumables(t *testing.T) {
buyable := BuyableConsumables()
if len(buyable) == 0 {
t.Fatal("should have buyable consumables")
}
for _, def := range buyable {
if !def.Buyable {
t.Errorf("%s marked buyable=false but returned by BuyableConsumables", def.Name)
}
if def.Price <= 0 {
t.Errorf("%s has no price", def.Name)
}
}
}