package game // Regression: a harvest that cannot be stored must not consume the crop or // report a yield that was never delivered. Stackable products merge into an // existing stack (so they fit a full inventory) but need their own slot // otherwise; non-stackable products need one slot per unit, and the reported // yield must match what was actually placed. import ( "path/filepath" "strings" "testing" "thehouseoficarus/internal/behavior" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" ) const ( hardeningCropPrefix = "farm_allot_1" hardeningCropSeed = "test_seed" hardeningCropID = "test_potato" ) // readyHarvest installs the patch flags a ready crop has and an in-flight // harvest action for hardeningCropPrefix, matching what cmd_farm.go queues. func readyHarvest(p *player.Player, minYield, maxYield int) { p.EnsureFlags() p.Flags[hardeningCropPrefix+"_weeds"] = false p.Flags[hardeningCropPrefix+"_seed"] = hardeningCropSeed p.Flags[hardeningCropPrefix+"_stage"] = 4 p.Flags[hardeningCropPrefix+"_watered"] = false p.Flags[hardeningCropPrefix+"_diseased"] = false p.Flags[hardeningCropPrefix+"_dead"] = false p.Flags[hardeningCropPrefix+"_ready"] = true p.Action = &behavior.Action{ Type: "harvest", Data: &behavior.FarmData{ Prefix: hardeningCropPrefix, SeedID: hardeningCropSeed, Product: hardeningCropID, MinYield: float64(minYield), MaxYield: float64(maxYield), }, } } func TestHarvestStackableFullInventoryKeepsCrop(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\n")) sess := &net.Session{Conn: newChanConn(), Player: p} sess.SetState(net.StateGame) h.g.RunOnOwner(func() { h.g.ItemStore.Reload(h.dir) for i := 0; i < 28; i++ { p.SetInvSlot(i, &player.InventorySlot{ItemID: "filler", Quantity: 1}) } readyHarvest(p, 1, 1) h.g.advanceHarvest(sess, p) if got := p.CountItem(hardeningCropID); got != 0 { t.Errorf("harvest stored %d potatoes with no room", got) } if ready, _ := p.Flags[hardeningCropPrefix+"_ready"].(bool); !ready { t.Error("crop was consumed even though nothing could be stored") } if seed, _ := p.Flags[hardeningCropPrefix+"_seed"].(string); seed != hardeningCropSeed { t.Errorf("patch seed = %q, want it left planted", seed) } out := string(sess.Conn.(*chanConn).Bytes()) if strings.Contains(out, "You harvest") { t.Errorf("reported a yield that was not delivered: %q", out) } if !strings.Contains(out, "too full") { t.Errorf("expected an inventory-full message, got %q", out) } }) } func TestHarvestStackableMergesIntoExistingStackWhenFull(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\n")) sess := &net.Session{Conn: newChanConn(), Player: p} sess.SetState(net.StateGame) h.g.RunOnOwner(func() { h.g.ItemStore.Reload(h.dir) for i := 0; i < 28; i++ { p.SetInvSlot(i, &player.InventorySlot{ItemID: "filler", Quantity: 1}) } p.SetInvSlot(5, &player.InventorySlot{ItemID: hardeningCropID, Quantity: 4}) readyHarvest(p, 1, 1) h.g.advanceHarvest(sess, p) if got := p.CountItem(hardeningCropID); got != 5 { t.Errorf("stackable harvest did not merge into the existing stack: have %d, want 5", got) } if ready, _ := p.Flags[hardeningCropPrefix+"_ready"].(bool); ready { t.Error("patch still ready after a successful harvest") } if out := string(sess.Conn.(*chanConn).Bytes()); !strings.Contains(out, "You harvest 1") { t.Errorf("expected the delivered yield in the message, got %q", out) } }) } func TestHarvestNonStackableReportsPlacedYield(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\n")) sess := &net.Session{Conn: newChanConn(), Player: p} sess.SetState(net.StateGame) h.g.RunOnOwner(func() { h.g.ItemStore.Reload(h.dir) for i := 0; i < 26; i++ { p.SetInvSlot(i, &player.InventorySlot{ItemID: "filler", Quantity: 1}) } readyHarvest(p, 5, 5) h.g.advanceHarvest(sess, p) if got := p.CountItem(hardeningCropID); got != 2 { t.Fatalf("placed %d potatoes, want 2 (two free slots)", got) } out := string(sess.Conn.(*chanConn).Bytes()) if !strings.Contains(out, "You harvest 2 ") { t.Errorf("reported yield must match what was placed: %q", out) } }) } func TestHarvestNonStackableFullInventoryKeepsCrop(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\n")) sess := &net.Session{Conn: newChanConn(), Player: p} sess.SetState(net.StateGame) h.g.RunOnOwner(func() { h.g.ItemStore.Reload(h.dir) for i := 0; i < 28; i++ { p.SetInvSlot(i, &player.InventorySlot{ItemID: "filler", Quantity: 1}) } readyHarvest(p, 5, 5) h.g.advanceHarvest(sess, p) if got := p.CountItem(hardeningCropID); got != 0 { t.Errorf("harvest stored %d potatoes with no room", got) } if ready, _ := p.Flags[hardeningCropPrefix+"_ready"].(bool); !ready { t.Error("crop was consumed even though nothing could be stored") } if out := string(sess.Conn.(*chanConn).Bytes()); strings.Contains(out, "You harvest") { t.Errorf("reported a yield that was not delivered: %q", out) } }) }