aboutsummaryrefslogtreecommitdiff
path: root/internal/game/polish_craft_fail_delivery_test.go
blob: f16e2a9e9011c9a48a2a61fe1782f284351837f5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
package game

// Regression: failed-craft delivery used FirstFreeSlot only, while the
// pre-roll fit gate (craftFailFits -> craftOutputFits) counts a matching
// stack. A stackable fail replacement could therefore be destroyed on a full
// inventory even though the gate promised it would fit. Delivery must merge
// like craft/harvest/pickup does.

import (
	"path/filepath"
	"testing"

	"thehouseoficarus/internal/behavior"
	"thehouseoficarus/internal/item"
	"thehouseoficarus/internal/net"
	"thehouseoficarus/internal/player"
)

const polishFailRecipeYAML = `id: polish_out
name: polish out
stackable: true
craft:
  - type: smithing
    subtype: smelting
    fail: polish_ash
    ingredients:
      - items:
          - polish_ore
        quantity: 1
    success:
      base: 0
      cap: 0
      per_level: 0
    output_qty: 1
    ticks_per_cycle: 1
`

func seedPolishFailItems(t *testing.T, dir string) {
	t.Helper()
	writeSeedFile(t, filepath.Join(dir, "items", "polish_out.yaml"), []byte(polishFailRecipeYAML))
	writeSeedFile(t, filepath.Join(dir, "items", "polish_ash.yaml"),
		[]byte("id: polish_ash\nname: polish ash\nstackable: true\nvalue: 1\n"))
	writeSeedFile(t, filepath.Join(dir, "items", "polish_ore.yaml"),
		[]byte("id: polish_ore\nname: polish ore\nstackable: true\nvalue: 1\n"))
}

func TestPlaceCraftFailMergesIntoMatchingStack(t *testing.T) {
	h := newHarness(t)
	seedPolishFailItems(t, h.dir)
	p := player.New("alice")

	h.g.RunOnOwner(func() {
		h.g.ItemStore.Reload(h.dir)
		p.SetInvSlot(0, &player.InventorySlot{ItemID: "polish_ash", Quantity: 3})
		fillInventoryExcept(p)

		h.g.placeCraftFail(p, &item.CraftDef{Fail: "polish_ash"})
		if slot := p.InvSlot(0); slot == nil || slot.Quantity != 4 {
			t.Fatalf("fail replacement was not merged into the matching stack: %+v", p.InvSlot(0))
		}
	})
}

func TestCraftFailureMergesFailStackWhenFull(t *testing.T) {
	h := newHarness(t)
	h.seedAccount("alice")
	p := h.seedCharacter("alice", "alice")
	seedPolishFailItems(t, h.dir)

	sess := &net.Session{Conn: newChanConn(), Player: p}
	sess.SetState(net.StateGame)

	h.g.RunOnOwner(func() {
		h.g.ItemStore.Reload(h.dir)
		// Full inventory: a matching output stack, a matching fail stack and
		// the ingredient. Consuming the ingredient frees its slot, so the old
		// FirstFreeSlot delivery landed the fail item there as a second stack.
		p.SetInvSlot(0, &player.InventorySlot{ItemID: "polish_ash", Quantity: 1})
		p.SetInvSlot(1, &player.InventorySlot{ItemID: "polish_ore", Quantity: 1})
		p.SetInvSlot(2, &player.InventorySlot{ItemID: "polish_out", Quantity: 1})
		for i := 3; i < 28; i++ {
			p.SetInvSlot(i, &player.InventorySlot{ItemID: "filler", Quantity: 1})
		}

		p.Action = &behavior.Action{
			Type:     behavior.TypeSmelt,
			TargetID: "polish_out",
			Data:     &behavior.ProductionData{ItemID: "polish_out", Phase: 1, Remaining: 1},
		}
		h.g.advanceProduction(sess, p)

		if got := p.CountItem("polish_ore"); got != 0 {
			t.Errorf("ingredient = %d, want 0 (consumed)", got)
		}
		if got := p.CountItem("polish_ash"); got != 2 {
			t.Errorf("fail product = %d, want 2", got)
		}
		stacks := 0
		for i := 0; i < 28; i++ {
			if s := p.InvSlot(i); s != nil && s.ItemID == "polish_ash" {
				stacks++
			}
		}
		if stacks != 1 {
			t.Errorf("fail product was delivered as %d stacks, want 1 merged stack", stacks)
		}
		if slot := p.InvSlot(0); slot == nil || slot.Quantity != 2 {
			t.Errorf("matching fail stack = %+v, want quantity 2", p.InvSlot(0))
		}
	})
}