aboutsummaryrefslogtreecommitdiff
path: root/internal/game/correct_quality_merge_test.go
blob: d54c8c9ec122a2d7d2d01597090639f68ba30c0a (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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
package game

// Regression: every stack-creation and stack-merge site must agree on the
// Quality/MaxQuality a fresh stack gets (newInventorySlot / stackProps).
// Pickups merge only into a stack with those exact properties, so a stack
// produced by crafting, harvesting, or ammo removal with Quality 0 could no
// longer be merged into - a full inventory then refused a pickup of an item
// the player already carried.

import (
	"path/filepath"
	"testing"

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

const correctQualityItemYAML = `id: correct_quality_gem
name: correct quality gem
stackable: true
max_quality: 5
craft:
  - type: crafting
    ingredients:
      - items:
          - correct_quality_shard
        quantity: 1
    xp: 1
    ticks_per_cycle: 1
`

func seedCorrectQualityItems(t *testing.T, h *harness) {
	t.Helper()
	writeSeedFile(t, filepath.Join(h.dir, "items", "correct_quality_gem.yaml"), []byte(correctQualityItemYAML))
	writeSeedFile(t, filepath.Join(h.dir, "items", "correct_quality_shard.yaml"),
		[]byte("id: correct_quality_shard\nname: correct quality shard\nstackable: true\nmax_quality: 5\nvalue: 1\n"))
}

// fillInventoryExcept occupies every slot but slot 0 with filler.
func fillInventoryExcept(p *player.Player) {
	for i := 0; i < 28; i++ {
		if i != 0 {
			p.SetInvSlot(i, &player.InventorySlot{ItemID: "filler", Quantity: 1})
		}
	}
}

// TestStackPropsMatchNewInventorySlot guards the one rule that stack creation
// and stack merging both depend on: the helper the craft/harvest/ammo sites
// use must stamp what newInventorySlot stamps.
func TestStackPropsMatchNewInventorySlot(t *testing.T) {
	h := newHarness(t)
	seedCorrectQualityItems(t, h)
	h.g.RunOnOwner(func() {
		h.g.ItemStore.Reload(h.dir)
		gem := h.g.newInventorySlot("correct_quality_gem", 1)
		if q, mq := stackProps(mustLoadItem(t, h.g, "correct_quality_gem")); q != gem.Quality || mq != gem.MaxQuality {
			t.Errorf("stackProps = (%d,%d), newInventorySlot = (%d,%d)", q, mq, gem.Quality, gem.MaxQuality)
		}
		shard := h.g.newInventorySlot("correct_quality_shard", 1)
		if q, mq := stackProps(mustLoadItem(t, h.g, "correct_quality_shard")); q != shard.Quality || mq != shard.MaxQuality {
			t.Errorf("quality-free stackProps = (%d,%d), newInventorySlot = (%d,%d)", q, mq, shard.Quality, shard.MaxQuality)
		}
	})
}

func TestCraftOutputMergesIntoQualityStack(t *testing.T) {
	h := newHarness(t)
	h.seedAccount("alice")
	p := h.seedCharacter("alice", "alice")
	seedCorrectQualityItems(t, h)

	h.g.RunOnOwner(func() {
		h.g.ItemStore.Reload(h.dir)
		def := mustLoadItem(t, h.g, "correct_quality_gem")
		p.SetInvSlot(0, &player.InventorySlot{ItemID: def.ID, Quantity: 10, Quality: 5, MaxQuality: 5})
		fillInventoryExcept(p)

		if !craftOutputFits(p, def.ID, def) {
			t.Fatal("a full inventory must accept a craft output that merges into an identical stack")
		}
		if !placeCraftOutput(p, def.ID, def, 2) {
			t.Fatal("craft output was not placed")
		}
		if slot := p.InvSlot(0); slot.Quantity != 12 || slot.Quality != 5 || slot.MaxQuality != 5 {
			t.Errorf("merged stack = %+v, want quantity 12 at quality 5", slot)
		}

		// A differently qualified stack is not a merge target: the same
		// full inventory must report that nothing fits.
		p.SetInvSlot(0, &player.InventorySlot{ItemID: def.ID, Quantity: 10, Quality: 1, MaxQuality: 5})
		if craftOutputFits(p, def.ID, def) {
			t.Error("craft output would merge into a stack with a different quality")
		}
		if placeCraftOutput(p, def.ID, def, 2) {
			t.Error("craft output was placed with no free slot and no matching stack")
		}
	})
}

func TestCraftOutputCreatesStackWithFreshProps(t *testing.T) {
	h := newHarness(t)
	h.seedAccount("alice")
	p := h.seedCharacter("alice", "alice")
	seedCorrectQualityItems(t, h)

	h.g.RunOnOwner(func() {
		h.g.ItemStore.Reload(h.dir)
		def := mustLoadItem(t, h.g, "correct_quality_gem")
		if !placeCraftOutput(p, def.ID, def, 3) {
			t.Fatal("craft output was not placed in an empty inventory")
		}
		slot := p.InvSlot(0)
		if slot == nil || slot.ItemID != def.ID {
			t.Fatalf("slot 0 = %+v", slot)
		}
		if q, mq := h.g.pickedStackProps(def.ID); slot.Quality != q || slot.MaxQuality != mq {
			t.Fatalf("crafted stack %+v does not match what a pickup receives (%d,%d)", slot, q, mq)
		}
	})
}

func TestPickupMergesIntoHarvestedStackWhenFull(t *testing.T) {
	h := newHarness(t)
	h.seedAccount("alice")
	p := h.seedCharacter("alice", "alice")
	writeSeedFile(t, filepath.Join(h.dir, "items", hardeningCropID+".yaml"),
		[]byte("id: test_potato\nname: test potato\nstackable: true\nmax_quality: 5\n"))

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

	h.g.RunOnOwner(func() {
		h.g.ItemStore.Reload(h.dir)
		readyHarvest(p, 3, 3)
		h.g.advanceHarvest(sess, p)

		stack := p.InvSlot(0)
		if stack == nil || stack.ItemID != hardeningCropID {
			t.Fatalf("harvested stack = %+v", stack)
		}
		quality, maxQuality := h.g.pickedStackProps(hardeningCropID)
		if stack.Quality != quality || stack.MaxQuality != maxQuality {
			t.Fatalf("harvested stack %+v does not match what a pickup receives (%d,%d)", stack, quality, maxQuality)
		}

		// A full inventory must still merge a ground pickup into the stack
		// the harvest just created.
		fillInventoryExcept(p)
		h.g.World.AddGroundItem(p.RoomID, hardeningCropID, 2)
		h.g.doGet(sess, "test potato")

		if got := p.CountItem(hardeningCropID); got != 5 {
			t.Errorf("potatoes carried = %d, want 5", got)
		}
		if got := h.g.World.GroundItems(p.RoomID)[hardeningCropID]; got != 0 {
			t.Errorf("ground potatoes = %d, want 0 (they fit the existing stack)", got)
		}
	})
}

func TestPickupMergesIntoAmmoReturnedStack(t *testing.T) {
	h := newHarness(t)
	h.seedAccount("alice")
	p := h.seedCharacter("alice", "alice")
	seedCorrectQualityItems(t, h)

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

	h.g.RunOnOwner(func() {
		h.g.ItemStore.Reload(h.dir)
		p.Equipment[item.SlotAmmo] = "correct_quality_shard"
		p.AmmoQty = 4
		if !h.g.unequipAmmo(p) {
			t.Fatal("unequipAmmo refused an empty inventory")
		}
		stack := p.InvSlot(0)
		if stack == nil || stack.ItemID != "correct_quality_shard" {
			t.Fatalf("returned ammo stack = %+v", stack)
		}
		quality, maxQuality := h.g.pickedStackProps("correct_quality_shard")
		if stack.Quality != quality || stack.MaxQuality != maxQuality {
			t.Fatalf("returned ammo stack %+v does not match what a pickup receives (%d,%d)", stack, quality, maxQuality)
		}

		fillInventoryExcept(p)
		h.g.World.AddGroundItem(p.RoomID, "correct_quality_shard", 3)
		h.g.doGet(sess, "correct quality shard")

		if got := p.CountItem("correct_quality_shard"); got != 7 {
			t.Errorf("shards carried = %d, want 7", got)
		}
		if got := h.g.World.GroundItems(p.RoomID)["correct_quality_shard"]; got != 0 {
			t.Errorf("ground shards = %d, want 0 (they fit the returned stack)", got)
		}
	})
}