fix: T2A menus only offer what the chosen resource can make (#2695)

## Summary
The T2A crafting menus offered items the chosen resource couldn't make:

- **Tailoring honours the targeted cloth hue.** The menu was built from all cloth in the pack, but the craft consumes only the targeted hue, so it offered items that then failed on resources. `CanCraftItem` / `FilterEntries` / `AnyCraftableInCategory` now take the hue and count with the same helper the craft uses.
- **Uncut cloth counts in the hued path.** The hue-aware count and consume matched only `Cloth`, so targeting uncut cloth gave an empty menu. The hued path now uses `CraftItem`'s equivalence table like the unhued path. `T2ACraftSystem` reuses that table instead of a narrower copy, which also covers the coloured woods and leathers on later-era shards.
- **T2A tinkering only uses iron ingots.** Coloured ingots are refused with a re-prompt. The menu filter now uses the selected ingot, and Make Last for tinkering always resolves to iron, so a coloured choice left over from the gump flow can't be used.

## Testing
- `dotnet build`: 0 warnings, 0 errors
- New `T2AHuedFilterTests`: only cloth of the targeted hue counts, and uncut cloth of the same hue counts toward the recipe
- No test for the tinkering target and message flow; not yet exercised in game
This commit is contained in:
Kamron Batman 2026-10-20 23:12:10 -07:00 • committed by GitHub
parent 54c0040ac8
commit ef8057d38e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 206 additions and 56 deletions

View file

@ -0,0 +1,92 @@
using Server;
using Server.Engines.Craft;
using Server.Engines.Craft.T2A;
using Server.Items;
using Server.Mobiles;
using Xunit;
namespace UOContent.Tests;
[Collection("Sequential UOContent Tests")]
public class T2AHuedFilterTests
{
private static PlayerMobile CreatePlayer(Point3D location)
{
var m = new PlayerMobile(World.NewMobile);
m.DefaultMobileInit();
m.MoveToWorld(location, Map.Felucca);
m.AddItem(new Backpack());
return m;
}
private static CraftItem MakeRecipe()
{
var item = new CraftItem(typeof(Shirt), "shirt", "shirt");
item.AddRes(typeof(Cloth), "cloth", 8, "You do not have enough cloth.");
item.AddSkill(SkillName.Tailoring, 0.0, 100.0);
return item;
}
private static CraftSystem GetTailoringSystem()
{
if (DefTailoring.CraftSystem == null)
{
DefTailoring.Initialize();
}
return DefTailoring.CraftSystem;
}
[Fact]
public void CanCraftItem_WithHue_CountsOnlyMatchingHueCloth()
{
var player = CreatePlayer(new Point3D(4140, 500, 0));
try
{
var pack = player.Backpack;
pack.AddItem(new Cloth(5) { Hue = 0x21 });
pack.AddItem(new Cloth(5) { Hue = 0x30 });
var system = GetTailoringSystem();
var recipe = MakeRecipe();
// 10 cloth total, but no single hue reaches the 8 the recipe needs
Assert.True(T2ACraftSystem.CanCraftItem(player, recipe, system));
Assert.False(T2ACraftSystem.CanCraftItem(player, recipe, system, null, 0x21));
Assert.False(T2ACraftSystem.CanCraftItem(player, recipe, system, null, 0x30));
pack.AddItem(new Cloth(3) { Hue = 0x21 });
Assert.True(T2ACraftSystem.CanCraftItem(player, recipe, system, null, 0x21));
Assert.False(T2ACraftSystem.CanCraftItem(player, recipe, system, null, 0x30));
}
finally
{
player.Delete();
}
}
[Fact]
public void CanCraftItem_WithHue_CountsSameHueUncutCloth()
{
var player = CreatePlayer(new Point3D(4140, 500, 0));
try
{
var pack = player.Backpack;
pack.AddItem(new Cloth(5) { Hue = 0x21 });
pack.AddItem(new UncutCloth(3) { Hue = 0x21 });
pack.AddItem(new UncutCloth(5) { Hue = 0x30 });
var system = GetTailoringSystem();
var recipe = MakeRecipe();
Assert.True(T2ACraftSystem.CanCraftItem(player, recipe, system, null, 0x21));
Assert.False(T2ACraftSystem.CanCraftItem(player, recipe, system, null, 0x30));
}
finally
{
player.Delete();
}
}
}