From 153acb7f644d755c45533b7c5311e3ffa47e8973 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Fri, 18 Sep 2026 05:31:09 -0400 Subject: fix: harden engine across casino, economy, concurrency, security, and content --- internal/game/hardening_craft_message_item_test.go | 85 ++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 internal/game/hardening_craft_message_item_test.go (limited to 'internal/game/hardening_craft_message_item_test.go') diff --git a/internal/game/hardening_craft_message_item_test.go b/internal/game/hardening_craft_message_item_test.go new file mode 100644 index 0000000..b516852 --- /dev/null +++ b/internal/game/hardening_craft_message_item_test.go @@ -0,0 +1,85 @@ +package game + +// Regression: a %i success message must name the ingredient alternative +// the player actually used. Production resolved the message AFTER +// consumeIngredients, so p.HasItem was false for everything consumed and +// resolveCraftVar fell back to the entry's first alternative — naming the +// item the player never touched when an entry lists several (bucket vs jug +// of water). + +import ( + "strings" + "testing" + + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" +) + +const craftMessageOutputYAML = `id: test_water_potion +name: test water potion +stackable: false +value: 3 +craft: + - type: crafting + ingredients: + - items: + - bucket_of_water + - jug_of_water + quantity: 1 + byproducts: + - empty_bucket + - empty_jug + success_message: "You mix the %i1 into a %n." + output_qty: 1 +` + +func TestCraftSuccessMessageNamesConsumedAlternative(t *testing.T) { + dir := t.TempDir() + seedWorld(t, dir) + writeDataFile(t, dir, "items", "bucket_of_water.yaml", "id: bucket_of_water\nname: bucket of water\nstackable: false\nvalue: 1\n") + writeDataFile(t, dir, "items", "jug_of_water.yaml", "id: jug_of_water\nname: jug of water\nstackable: false\nvalue: 1\n") + writeDataFile(t, dir, "items", "empty_bucket.yaml", "id: empty_bucket\nname: empty bucket\nstackable: false\nvalue: 1\n") + writeDataFile(t, dir, "items", "empty_jug.yaml", "id: empty_jug\nname: empty jug\nstackable: false\nvalue: 1\n") + writeDataFile(t, dir, "items", "test_water_potion.yaml", craftMessageOutputYAML) + + g := newHardeningGame(t, dir) + + g.RunOnOwner(func() { + p := player.New("alice") + p.RoomID = 1 + // Only the jug is present: the entered alternative is bucket_of_water, + // so a post-consumption p.HasItem fallback would name the bucket. + p.SetInvSlot(0, &player.InventorySlot{ItemID: "jug_of_water", Quantity: 1}) + + conn := newChanConn() + sess := &net.Session{Conn: conn, Player: p} + sess.SetState(net.StateGame) + + def, err := g.ItemStore.Load("test_water_potion") + if err != nil { + t.Fatalf("load recipe: %v", err) + } + + g.startProductionFromItem(sess, p, def, 1) + if p.Action == nil { + t.Fatalf("production did not start; output=%q", conn.Bytes()) + } + g.advanceProduction(sess, p) // phase 0 -> 1 + g.advanceProduction(sess, p) // phase 1: consume + produce + + if got := p.CountItem("test_water_potion"); got != 1 { + t.Fatalf("potion = %d, want 1; output=%q", got, conn.Bytes()) + } + if got := p.CountItem("empty_jug"); got != 1 { + t.Fatalf("empty_jug = %d, want 1 (byproduct for the consumed jug)", got) + } + + out := string(conn.Bytes()) + if !strings.Contains(out, "jug of water") { + t.Errorf("success message does not name the consumed jug of water: %q", out) + } + if strings.Contains(out, "bucket of water") { + t.Errorf("success message names the bucket the player never used: %q", out) + } + }) +} -- cgit v1.2.3