## Summary
Adds authentic **T2A-era (pre-UO:Third-Dawn) packet-based crafting menus**, enabled via the **`t2aCraftMenus` server setting** (read once at startup; default **`!Core.UOTD`**, so a pre-UO:TD shard gets them automatically). When enabled, double-clicking a crafting tool opens the classic `0x7C`/`0x7D` item-list menu — skill- and material-filtered — instead of the modern gump, covering all 8 tool/skill crafts (blacksmithy, tailoring, tinkering, carpentry, alchemy, bowcraft/fletching, inscription, cartography). It is **not** a runtime/admin-flippable feature flag.
This is the **definitive, reconciled** branch and **supersedes**:
- **#2181** (Delphi — `T2A_CraftingMenus`): the original effort.
- **#2381** (Jack/UOLL — `t2a_crafting_menus`): the research-grounded superset (Delphi's base + 12 corrections), rebased onto current `main`.
Original authorship is preserved across the cherry-picked history: foundation commit **@Delphi79**, mechanic fixes **@jackuoll (Jack Ward)**, reconciliation/fixes/docs mine.
## How it was built
1. Cherry-picked Jack's 13 commits onto current `main` (superset of Delphi's; only 2 trivial FeatureFlags conflicts).
2. Applied targeted fixes (below) with tests.
3. Full convention audit, build, and test pass.
Grounded in independent historical research plus Jack's deep dive. Maintainer reference: `dev-docs/t2a-crafting.md`.
## Mechanics (highlights)
- Double-click tool → target resource → skill/material-filtered menu → craft. Resource pre-selection per skill; make-last by targeting the tool.
- **Stacked-gem jewelry:** target a gem stack → the **full stack** is consumed and the piece is named by count ("a 1000 diamond ring"); count persists (`BaseJewel` serialization **v4 → v5**, new `_gemCount`).
- **Tool-less inscription & cartography** (skill-list invoked; no pen/sextant); inscription consumes reagents+scroll on success and failure, mana only on success.
- **Tailoring matching-hue consumption:** targeting hued cloth/leather consumes only that hue. Crafted items take color from their **`CraftResource`** (not the dyed hue), so dyed leather/cloth don't tint the product; in T2A only colored ingots/ore color items (metal armor/shields).
- **Half-resources on failed non-scroll crafts** (pre-UO:TD).
- **Maker's mark** always prompted for exceptional items, via the shared `QueryMakersMarkGump`.
- Server-side menu infra changes are additive (`ItemListEntry.CraftIndex`, `Entries` setter, `HasSent`).
## Notable changes on top of the cherry-pick
- **Toggle is a startup server setting, not a feature flag.** Removed `ContentFeatureFlags.T2ACraftMenus` (and its admin-flippable plumbing); the value is read once via `ServerConfiguration.GetSetting("t2aCraftMenus", !Core.UOTD)` into `T2ACraftSystem.Enabled`. Since the default tracks the era and it can't be flipped at runtime, there's no incoherent "menus-on / UO:TD-era" state.
- **Stacked-gem consumption (B3a/B3):** consume the full `PendingGemCount` (was deliberately consuming 1 while naming by the stack), null-safe gem type, plain-piece fallback + message. New `T2AJewelGemCraftTests`.
- **Convention audit:** `new List<Item>()` → `PooledRefList<Item>` on the hue-aware consume path; removed dead code.
## Decisions & deviations
- `make-last` kept as **QoL** (post-T2A gump-era feature).
- `half-on-failure` (non-scroll) kept as a **reconstruction** (not OSI-confirmed).
- **Stacked-gem** behavior set per shard authority (overrides the "single gem" reconstruction).
- **Cooking** out of scope (no T2A crafting menu existed for it).
- **No colored items from dyed materials:** crafted color comes from the `CraftResource` type. Pre-AOS leather has no colored variant, so leather is always uncolored; weapons retain resource color only in AOS+ (unchanged, intended).
## Test plan
- Automated: `dotnet build ModernUO.slnx -c Debug` clean; `dotnet test Projects/UOContent.Tests` → **421 passed** (incl. 3 new jewelry tests).
- Manual (needs a running T2A shard + client):
- [ ] Each of the 8 skills opens the correct menu; empty-menu guard fires.
- [ ] Make-last repeats the last craft (jewelry re-prompts gem).
- [ ] Jewelry consumes the full targeted gem stack and names by count.
- [ ] Cartography consumes blank maps only with T2A enabled / maps+scrolls when disabled.
- [ ] Tailoring consumes only the targeted-hue material; crafted items are not tinted by dyed cloth/leather.
- [ ] Maker's-mark prompt on exceptional.
- [ ] Failed non-scroll craft consumes half resources.
- [ ] Inscription: reagents+scroll on success/failure, mana only on success.
- [ ] T2A disabled: gump crafting unchanged.
## Credits
Co-authored-by: @Delphi79
Co-authored-by: @jackuoll
This commit is contained in:
parent
2e93201e51
commit
16bf3016fb
50 changed files with 4483 additions and 219 deletions
34
Projects/Server/Menus/BaseMenu.cs
Normal file
34
Projects/Server/Menus/BaseMenu.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
using Server.Network;
|
||||
|
||||
namespace Server.Menus;
|
||||
|
||||
public abstract class BaseMenu : IMenu
|
||||
{
|
||||
private static int _nextSerial;
|
||||
|
||||
public int Serial { get; }
|
||||
|
||||
public abstract int EntryLength { get; }
|
||||
|
||||
public BaseMenu()
|
||||
{
|
||||
var serial = ++_nextSerial;
|
||||
if (serial <= 0)
|
||||
{
|
||||
serial = 1;
|
||||
_nextSerial = 1;
|
||||
}
|
||||
|
||||
Serial = serial;
|
||||
}
|
||||
|
||||
public abstract void SendTo(NetState state);
|
||||
|
||||
public virtual void OnCancel(NetState state)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void OnResponse(NetState state, int index)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -4,11 +4,12 @@ namespace Server.Menus.ItemLists;
|
|||
|
||||
public class ItemListEntry
|
||||
{
|
||||
public ItemListEntry(string name, int itemID, int hue = 0)
|
||||
public ItemListEntry(string name, int itemID, int hue = 0, int craftIndex = 0)
|
||||
{
|
||||
Name = name?.Trim() ?? "";
|
||||
ItemID = itemID;
|
||||
Hue = hue;
|
||||
CraftIndex = craftIndex;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
|
|
@ -16,43 +17,25 @@ public class ItemListEntry
|
|||
public int ItemID { get; }
|
||||
|
||||
public int Hue { get; }
|
||||
|
||||
public int CraftIndex { get; }
|
||||
}
|
||||
|
||||
public class ItemListMenu : IMenu
|
||||
public class ItemListMenu : BaseMenu
|
||||
{
|
||||
private static int m_NextSerial;
|
||||
|
||||
public ItemListMenu(string question, ItemListEntry[] entries)
|
||||
{
|
||||
Question = question.Trim();
|
||||
Entries = entries;
|
||||
|
||||
do
|
||||
{
|
||||
Serial = m_NextSerial++;
|
||||
Serial &= 0x7FFFFFFF;
|
||||
} while (Serial == 0);
|
||||
|
||||
Serial = (int)((uint)Serial | 0x80000000);
|
||||
}
|
||||
|
||||
public string Question { get; }
|
||||
|
||||
public ItemListEntry[] Entries { get; set; }
|
||||
|
||||
public int Serial { get; }
|
||||
public override int EntryLength => Entries.Length;
|
||||
|
||||
public int EntryLength => Entries.Length;
|
||||
|
||||
public virtual void OnCancel(NetState state)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void OnResponse(NetState state, int index)
|
||||
{
|
||||
}
|
||||
|
||||
public void SendTo(NetState state)
|
||||
public override void SendTo(NetState state)
|
||||
{
|
||||
state.AddMenu(this);
|
||||
state.SendDisplayItemListMenu(this);
|
||||
|
|
|
|||
|
|
@ -2,39 +2,21 @@ using Server.Network;
|
|||
|
||||
namespace Server.Menus.Questions;
|
||||
|
||||
public class QuestionMenu : IMenu
|
||||
public class QuestionMenu : BaseMenu
|
||||
{
|
||||
private static int m_NextSerial;
|
||||
|
||||
public QuestionMenu(string question, string[] answers)
|
||||
{
|
||||
Question = question?.Trim() ?? "";
|
||||
Answers = answers;
|
||||
|
||||
do
|
||||
{
|
||||
Serial = ++m_NextSerial;
|
||||
Serial &= 0x7FFFFFFF;
|
||||
} while (Serial == 0);
|
||||
}
|
||||
|
||||
public string Question { get; }
|
||||
|
||||
public string[] Answers { get; }
|
||||
|
||||
public int Serial { get; }
|
||||
public override int EntryLength => Answers.Length;
|
||||
|
||||
public int EntryLength => Answers.Length;
|
||||
|
||||
public virtual void OnCancel(NetState state)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void OnResponse(NetState state, int index)
|
||||
{
|
||||
}
|
||||
|
||||
public void SendTo(NetState state)
|
||||
public override void SendTo(NetState state)
|
||||
{
|
||||
state.AddMenu(this);
|
||||
state.SendDisplayQuestionMenu(this);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,134 @@
|
|||
using Server;
|
||||
using Server.Engines.Craft;
|
||||
using Server.Items;
|
||||
using Server.Mobiles;
|
||||
using Xunit;
|
||||
|
||||
namespace UOContent.Tests;
|
||||
|
||||
[Collection("Sequential UOContent Tests")]
|
||||
public class T2AJewelGemCraftTests
|
||||
{
|
||||
// Y=500 keeps us inside Felucca bounds; X offset avoids other sequential tests.
|
||||
private static PlayerMobile CreatePlayerMobile(Map map, Point3D location)
|
||||
{
|
||||
var m = new PlayerMobile(World.NewMobile);
|
||||
m.DefaultMobileInit();
|
||||
m.MoveToWorld(location, map);
|
||||
m.AddItem(new Backpack());
|
||||
return m;
|
||||
}
|
||||
|
||||
// Builds a minimal tinkering ring recipe (2 iron ingots) without relying on
|
||||
// DefTinkering.InitCraftList, which is gated on the feature flag at startup.
|
||||
private static CraftItem MakeRingRecipe()
|
||||
{
|
||||
var item = new CraftItem(typeof(GoldRing), "ring", "gold ring");
|
||||
item.AddRes(typeof(IronIngot), "iron ingot", 2, "You do not have enough ingots.");
|
||||
return item;
|
||||
}
|
||||
|
||||
// Ensures DefTinkering.CraftSystem is available (not called by the test fixture).
|
||||
private static CraftSystem GetOrInitTinkeringSystem()
|
||||
{
|
||||
if (DefTinkering.CraftSystem == null)
|
||||
{
|
||||
DefTinkering.Initialize();
|
||||
}
|
||||
|
||||
return DefTinkering.CraftSystem;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnCraft_ConsumesEntireTargetedGemStack_AndNamesByCount()
|
||||
{
|
||||
var map = Map.Felucca;
|
||||
var player = CreatePlayerMobile(map, new Point3D(4100, 500, 0));
|
||||
var ring = new GoldRing();
|
||||
|
||||
try
|
||||
{
|
||||
var pack = player.Backpack;
|
||||
pack.AddItem(new IronIngot(10));
|
||||
pack.AddItem(new Diamond(50)); // a stack of 50 diamonds
|
||||
|
||||
var system = GetOrInitTinkeringSystem();
|
||||
var context = system.GetContext(player);
|
||||
context.PendingGemType = GemType.Diamond;
|
||||
context.PendingGemCount = 50;
|
||||
|
||||
ring.OnCraft(1, false, player, system, typeof(IronIngot), null, MakeRingRecipe(), 0);
|
||||
|
||||
Assert.Equal(0, pack.GetAmount(typeof(Diamond))); // all 50 consumed
|
||||
Assert.Equal(GemType.Diamond, ring.GemType);
|
||||
Assert.Equal(50, ring.GemCount);
|
||||
// pending state cleared so the next craft starts fresh
|
||||
Assert.Equal(GemType.None, context.PendingGemType);
|
||||
Assert.Equal(0, context.PendingGemCount);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ring.Delete();
|
||||
player.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnCraft_WithUnsetGemContext_LeavesPlainPiece()
|
||||
{
|
||||
var map = Map.Felucca;
|
||||
var player = CreatePlayerMobile(map, new Point3D(4120, 500, 0));
|
||||
var ring = new GoldRing();
|
||||
|
||||
try
|
||||
{
|
||||
player.Backpack.AddItem(new IronIngot(10));
|
||||
|
||||
var system = GetOrInitTinkeringSystem();
|
||||
var context = system.GetContext(player);
|
||||
context.PendingGemType = GemType.None; // no gem targeted
|
||||
context.PendingGemCount = 0;
|
||||
|
||||
ring.OnCraft(1, false, player, system, typeof(IronIngot), null, MakeRingRecipe(), 0);
|
||||
|
||||
Assert.Equal(GemType.None, ring.GemType);
|
||||
Assert.Equal(0, ring.GemCount);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ring.Delete();
|
||||
player.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnCraft_WhenGemsUnavailableAtCraftTime_CraftsPlainPiece()
|
||||
{
|
||||
var map = Map.Felucca;
|
||||
var player = CreatePlayerMobile(map, new Point3D(4140, 500, 0));
|
||||
var ring = new GoldRing();
|
||||
|
||||
try
|
||||
{
|
||||
player.Backpack.AddItem(new IronIngot(10));
|
||||
// deliberately do NOT add any diamonds
|
||||
|
||||
var system = GetOrInitTinkeringSystem();
|
||||
var context = system.GetContext(player);
|
||||
context.PendingGemType = GemType.Diamond;
|
||||
context.PendingGemCount = 5;
|
||||
|
||||
ring.OnCraft(1, false, player, system, typeof(IronIngot), null, MakeRingRecipe(), 0);
|
||||
|
||||
Assert.Equal(GemType.None, ring.GemType);
|
||||
Assert.Equal(0, ring.GemCount);
|
||||
Assert.Equal(GemType.None, context.PendingGemType);
|
||||
Assert.Equal(0, context.PendingGemCount);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ring.Delete();
|
||||
player.Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
using Server.Commands;
|
||||
using Server.Network;
|
||||
using Server.Targeting;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using Server.Engines.Craft.T2A;
|
||||
using Server.Network;
|
||||
|
||||
namespace Server
|
||||
|
|
@ -12,6 +13,7 @@ namespace Server
|
|||
Mobile.VisibleDamageType = visibleDamage ? VisibleDamageType.Related : VisibleDamageType.None;
|
||||
Mobile.GuildClickMessage = ServerConfiguration.GetSetting("guildClickMessage", !Core.AOS);
|
||||
Mobile.AsciiClickMessage = ServerConfiguration.GetSetting("asciiClickMessage", !Core.AOS);
|
||||
T2ACraftSystem.Enabled = ServerConfiguration.GetSetting("t2aCraftMenus", !Core.UOTD);
|
||||
|
||||
Mobile.ActionDelay = ServerConfiguration.GetSetting("actionDelay", Core.AOS ? 1000 : 500);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ModernUO.Serialization;
|
||||
using Server.Collections;
|
||||
using Server.Gumps;
|
||||
using Server.Network;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using ModernUO.Serialization;
|
||||
using Server.Gumps;
|
||||
|
||||
namespace Server.Engines.ConPVP;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using Server.Items;
|
||||
|
||||
namespace Server.Engines.Craft
|
||||
{
|
||||
|
|
@ -31,6 +32,13 @@ namespace Server.Engines.Craft
|
|||
|
||||
public CraftMarkOption MarkOption { get; set; }
|
||||
|
||||
// T2A: last hue used for hue-aware crafting (tailoring cloth)
|
||||
public int LastHue { get; set; } = -1;
|
||||
|
||||
// T2A jewelry: transient gem info set by GemSelectTarget, consumed by BaseJewel.OnCraft
|
||||
public GemType PendingGemType { get; set; }
|
||||
public int PendingGemCount { get; set; }
|
||||
|
||||
public CraftItem LastMade
|
||||
{
|
||||
get
|
||||
|
|
|
|||
|
|
@ -673,7 +673,6 @@ public class CraftGump : DynamicGump
|
|||
}
|
||||
|
||||
context.DoNotColor = !context.DoNotColor;
|
||||
|
||||
_from.SendGump(new CraftGump(_from, _craftSystem, _tool, null, _page));
|
||||
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -310,8 +310,7 @@ public class CraftGumpItem : DynamicGump
|
|||
// Back Button
|
||||
if (info.ButtonID == 0)
|
||||
{
|
||||
var craftGump = new CraftGump(from, _craftSystem, _tool, null);
|
||||
from.SendGump(craftGump);
|
||||
CraftItem.ShowCraftMenu(from, _craftSystem, _tool, null);
|
||||
}
|
||||
else // Make Button
|
||||
{
|
||||
|
|
@ -319,7 +318,7 @@ public class CraftGumpItem : DynamicGump
|
|||
|
||||
if (num > 0)
|
||||
{
|
||||
from.SendGump(new CraftGump(from, _craftSystem, _tool, num));
|
||||
CraftItem.ShowCraftMenu(from, _craftSystem, _tool, num);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Server.Collections;
|
||||
using Server.Commands;
|
||||
using Server.Factions;
|
||||
using Server.Gumps;
|
||||
using Server.Items;
|
||||
using Server.Logging;
|
||||
using Server.Mobiles;
|
||||
using Server.Engines.Craft.T2A;
|
||||
|
||||
namespace Server.Engines.Craft
|
||||
{
|
||||
|
|
@ -59,25 +61,37 @@ namespace Server.Engines.Craft
|
|||
0x192C, 0x192D, 0x192E, 0x129F, 0x1930, 0x1931, 0x1932, 0x1934
|
||||
};
|
||||
|
||||
private static readonly Type[][] m_TypesTable =
|
||||
private static readonly Type[][] m_TypesTable = InitTypesTable();
|
||||
|
||||
private static Type[][] InitTypesTable()
|
||||
{
|
||||
new[] { typeof(Log), typeof(Board) },
|
||||
new[] { typeof(HeartwoodLog), typeof(HeartwoodBoard) },
|
||||
new[] { typeof(BloodwoodLog), typeof(BloodwoodBoard) },
|
||||
new[] { typeof(FrostwoodLog), typeof(FrostwoodBoard) },
|
||||
new[] { typeof(OakLog), typeof(OakBoard) },
|
||||
new[] { typeof(AshLog), typeof(AshBoard) },
|
||||
new[] { typeof(YewLog), typeof(YewBoard) },
|
||||
new[] { typeof(Leather), typeof(Hides) },
|
||||
new[] { typeof(SpinedLeather), typeof(SpinedHides) },
|
||||
new[] { typeof(HornedLeather), typeof(HornedHides) },
|
||||
new[] { typeof(BarbedLeather), typeof(BarbedHides) },
|
||||
new[] { typeof(BlankMap), typeof(BlankScroll) },
|
||||
new[] { typeof(Cloth), typeof(UncutCloth) },
|
||||
new[] { typeof(CheeseWheel), typeof(CheeseWedge) },
|
||||
new[] { typeof(Pumpkin), typeof(SmallPumpkin) },
|
||||
new[] { typeof(WoodenBowlOfPeas), typeof(PewterBowlOfPeas) }
|
||||
};
|
||||
List<Type[]> types =
|
||||
[
|
||||
[typeof(Log), typeof(Board)],
|
||||
[typeof(HeartwoodLog), typeof(HeartwoodBoard)],
|
||||
[typeof(BloodwoodLog), typeof(BloodwoodBoard)],
|
||||
[typeof(FrostwoodLog), typeof(FrostwoodBoard)],
|
||||
[typeof(OakLog), typeof(OakBoard)],
|
||||
[typeof(AshLog), typeof(AshBoard)],
|
||||
[typeof(YewLog), typeof(YewBoard)],
|
||||
[typeof(Leather), typeof(Hides)],
|
||||
[typeof(SpinedLeather), typeof(SpinedHides)],
|
||||
[typeof(HornedLeather), typeof(HornedHides)],
|
||||
[typeof(BarbedLeather), typeof(BarbedHides)],
|
||||
[typeof(Cloth), typeof(UncutCloth)],
|
||||
[typeof(CheeseWheel), typeof(CheeseWedge)],
|
||||
[typeof(Pumpkin), typeof(SmallPumpkin)],
|
||||
[typeof(WoodenBowlOfPeas), typeof(PewterBowlOfPeas)]
|
||||
];
|
||||
|
||||
// Gump-based crafting allows blank scrolls as a substitute for blank maps in cartography
|
||||
if (!T2ACraftSystem.Enabled)
|
||||
{
|
||||
types.Add([typeof(BlankMap), typeof(BlankScroll)]);
|
||||
}
|
||||
|
||||
return types.ToArray();
|
||||
}
|
||||
|
||||
private static readonly Type[] m_ColoredItemTable =
|
||||
{
|
||||
|
|
@ -668,6 +682,10 @@ namespace Server.Engines.Craft
|
|||
{
|
||||
amounts[i] = 0;
|
||||
}
|
||||
else if (isFailure && !Core.UOTD)
|
||||
{
|
||||
amounts[i] -= amounts[i] / 2;
|
||||
}
|
||||
}
|
||||
|
||||
// We adjust the amount of each resource to consume the max possible
|
||||
|
|
@ -921,14 +939,22 @@ namespace Server.Engines.Craft
|
|||
if (!allRequiredSkills || chance <= 0.0)
|
||||
{
|
||||
from.EndAction<CraftSystem>();
|
||||
from.SendGump(
|
||||
new CraftGump(
|
||||
from,
|
||||
craftSystem,
|
||||
tool,
|
||||
1044153 // You don't have the required skills to attempt this item.
|
||||
)
|
||||
);
|
||||
if (T2ACraftSystem.Enabled)
|
||||
{
|
||||
from.SendAsciiMessage("You lack the required skill to craft this item.");
|
||||
}
|
||||
else
|
||||
{
|
||||
from.SendGump(
|
||||
new CraftGump(
|
||||
from,
|
||||
craftSystem,
|
||||
tool,
|
||||
1044153 // You don't have the required skills to attempt this item.
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -951,7 +977,7 @@ namespace Server.Engines.Craft
|
|||
if (badCraft > 0)
|
||||
{
|
||||
from.EndAction<CraftSystem>();
|
||||
from.SendGump(new CraftGump(from, craftSystem, tool, badCraft));
|
||||
ShowCraftMenu(from, craftSystem, tool, badCraft);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -962,7 +988,7 @@ namespace Server.Engines.Craft
|
|||
if (!ConsumeRes(from, typeRes, craftSystem, ref resHue, ref maxAmount, ConsumeType.None, ref message))
|
||||
{
|
||||
from.EndAction<CraftSystem>();
|
||||
from.SendGump(new CraftGump(from, craftSystem, tool, message));
|
||||
ShowCraftMenu(from, craftSystem, tool, message);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -971,7 +997,7 @@ namespace Server.Engines.Craft
|
|||
if (!ConsumeAttributes(from, ref message, false))
|
||||
{
|
||||
from.EndAction<CraftSystem>();
|
||||
from.SendGump(new CraftGump(from, craftSystem, tool, message));
|
||||
ShowCraftMenu(from, craftSystem, tool, message);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -986,6 +1012,293 @@ namespace Server.Engines.Craft
|
|||
new InternalTimer(from, craftSystem, this, typeRes, tool, iRandom).Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hue-aware craft entry point. Used by tailoring to carry the targeted resource hue
|
||||
/// through the craft timer to CompleteCraft, ensuring only matching-hue resources are consumed.
|
||||
/// </summary>
|
||||
public void Craft(Mobile from, CraftSystem craftSystem, Type typeRes, BaseTool tool, int resHue)
|
||||
{
|
||||
if (!from.BeginAction<CraftSystem>())
|
||||
{
|
||||
from.SendLocalizedMessage(500119); // You must wait to perform another action
|
||||
return;
|
||||
}
|
||||
|
||||
if (RequiredExpansion != Expansion.None && from.NetState?.SupportsExpansion(RequiredExpansion) != true)
|
||||
{
|
||||
from.EndAction<CraftSystem>();
|
||||
from.SendGump(
|
||||
new CraftGump(
|
||||
from,
|
||||
craftSystem,
|
||||
tool,
|
||||
RequiredExpansionMessage(RequiredExpansion)
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
var chance = GetSuccessChance(from, typeRes, craftSystem, false, out var allRequiredSkills);
|
||||
|
||||
if (!allRequiredSkills || chance <= 0.0)
|
||||
{
|
||||
from.EndAction<CraftSystem>();
|
||||
from.SendAsciiMessage("You lack the required skill to craft this item.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Recipe != null && (from as PlayerMobile)?.HasRecipe(Recipe) == false)
|
||||
{
|
||||
from.EndAction<CraftSystem>();
|
||||
from.SendGump(
|
||||
new CraftGump(
|
||||
from,
|
||||
craftSystem,
|
||||
tool,
|
||||
1072847 // You must learn that recipe from a scroll.
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
var badCraft = craftSystem.CanCraft(from, tool, ItemType);
|
||||
|
||||
if (badCraft > 0)
|
||||
{
|
||||
from.EndAction<CraftSystem>();
|
||||
ShowCraftMenu(from, craftSystem, tool, badCraft);
|
||||
return;
|
||||
}
|
||||
|
||||
// Dry run: check hued resources are available
|
||||
if (!CheckHuedRes(from, typeRes, craftSystem, resHue))
|
||||
{
|
||||
from.EndAction<CraftSystem>();
|
||||
// You don't have the resources required to make that item.
|
||||
from.SendLocalizedMessage(502925);
|
||||
return;
|
||||
}
|
||||
|
||||
TextDefinition message = null;
|
||||
if (!ConsumeAttributes(from, ref message, false))
|
||||
{
|
||||
from.EndAction<CraftSystem>();
|
||||
ShowCraftMenu(from, craftSystem, tool, message);
|
||||
return;
|
||||
}
|
||||
|
||||
var context = craftSystem.GetContext(from);
|
||||
context?.OnMade(this);
|
||||
|
||||
var iMin = craftSystem.MinCraftEffect;
|
||||
var iMax = craftSystem.MaxCraftEffect - iMin + 1;
|
||||
var iRandom = Utility.Random(iMax);
|
||||
iRandom += iMin + 1;
|
||||
new InternalTimer(from, craftSystem, this, typeRes, tool, iRandom, resHue).Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the backpack has enough of the specified resource type matching the target hue.
|
||||
/// Used as a dry-run check before starting the craft timer.
|
||||
/// </summary>
|
||||
private bool CheckHuedRes(Mobile from, Type typeRes, CraftSystem craftSystem, int targetHue)
|
||||
{
|
||||
var ourPack = from.Backpack;
|
||||
if (ourPack == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var resCol = UseSubRes2 ? craftSystem.CraftSubRes2 : craftSystem.CraftSubRes;
|
||||
|
||||
for (var i = 0; i < Resources.Count; i++)
|
||||
{
|
||||
var craftRes = Resources[i];
|
||||
var baseType = craftRes.ItemType;
|
||||
|
||||
// Resource mutation
|
||||
if (baseType == resCol.ResType && typeRes != null)
|
||||
{
|
||||
baseType = typeRes;
|
||||
}
|
||||
|
||||
// For the primary resource, count only items matching the target hue
|
||||
if (targetHue >= 0 && i == 0)
|
||||
{
|
||||
var amount = GetHuedAmount(ourPack, baseType, targetHue);
|
||||
if (amount < craftRes.Amount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ourPack.GetAmount(baseType) < craftRes.Amount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Consumes resources from the backpack, restricting the primary resource to items matching the target hue.
|
||||
/// Returns true on success. On success, the exact targetHue is used as the resHue output.
|
||||
/// </summary>
|
||||
private bool ConsumeHuedRes(
|
||||
Mobile from, Type typeRes, CraftSystem craftSystem, int targetHue,
|
||||
ref int resHue, ConsumeType consumeType
|
||||
)
|
||||
{
|
||||
var ourPack = from.Backpack;
|
||||
if (ourPack == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (NeedHeat && !Find(from, m_HeatSources))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (NeedOven && !Find(from, m_Ovens))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (NeedMill && !Find(from, m_Mills))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var resCol = UseSubRes2 ? craftSystem.CraftSubRes2 : craftSystem.CraftSubRes;
|
||||
|
||||
for (var i = 0; i < Resources.Count; i++)
|
||||
{
|
||||
var craftRes = Resources[i];
|
||||
var baseType = craftRes.ItemType;
|
||||
var amount = craftRes.Amount;
|
||||
|
||||
// Resource mutation
|
||||
if (baseType == resCol.ResType && typeRes != null)
|
||||
{
|
||||
baseType = typeRes;
|
||||
|
||||
var subResource = resCol.SearchFor(baseType);
|
||||
if (subResource != null && from.Skills[craftSystem.MainSkill].Base < subResource.RequiredSkill)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (consumeType == ConsumeType.Half)
|
||||
{
|
||||
amount = Math.Max(1, amount / 2);
|
||||
}
|
||||
|
||||
// For the primary resource, filter by hue
|
||||
if (targetHue >= 0 && i == 0)
|
||||
{
|
||||
if (consumeType == ConsumeType.None)
|
||||
{
|
||||
// Dry run: just check amount
|
||||
if (GetHuedAmount(ourPack, baseType, targetHue) < amount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Actual consumption: consume only matching-hue items
|
||||
if (!ConsumeHuedAmount(ourPack, baseType, targetHue, amount))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Non-hued resource: normal consumption
|
||||
if (consumeType == ConsumeType.None)
|
||||
{
|
||||
if (ourPack.GetAmount(baseType) < amount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!ourPack.ConsumeTotal(baseType, amount))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resHue = targetHue;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int GetHuedAmount(Container pack, Type type, int hue)
|
||||
{
|
||||
var total = 0;
|
||||
|
||||
foreach (var item in pack.FindItems(true))
|
||||
{
|
||||
if (item.Hue == hue && type.IsInstanceOfType(item))
|
||||
{
|
||||
total += item.Amount;
|
||||
}
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
private static bool ConsumeHuedAmount(Container pack, Type type, int hue, int amount)
|
||||
{
|
||||
var remaining = amount;
|
||||
using var toDelete = PooledRefList<Item>.Create();
|
||||
|
||||
foreach (var item in pack.FindItems(true))
|
||||
{
|
||||
if (remaining <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (item.Hue != hue || !type.IsInstanceOfType(item))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.Amount <= remaining)
|
||||
{
|
||||
remaining -= item.Amount;
|
||||
toDelete.Add(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
item.Amount -= remaining;
|
||||
remaining = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (remaining > 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < toDelete.Count; i++)
|
||||
{
|
||||
toDelete[i].Delete();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static TextDefinition RequiredExpansionMessage(Expansion expansion)
|
||||
{
|
||||
return expansion switch
|
||||
|
|
@ -1007,7 +1320,7 @@ namespace Server.Engines.Craft
|
|||
{
|
||||
if (tool?.Deleted == false && tool.UsesRemaining > 0)
|
||||
{
|
||||
from.SendGump(new CraftGump(from, craftSystem, tool, badCraft));
|
||||
ShowCraftMenu(from, craftSystem, tool, badCraft);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -1036,7 +1349,7 @@ namespace Server.Engines.Craft
|
|||
{
|
||||
if (tool?.Deleted == false && tool.UsesRemaining > 0)
|
||||
{
|
||||
from.SendGump(new CraftGump(from, craftSystem, tool, checkMessage));
|
||||
ShowCraftMenu(from, craftSystem, tool, checkMessage);
|
||||
}
|
||||
else if (checkMessage.Number > 0)
|
||||
{
|
||||
|
|
@ -1076,7 +1389,7 @@ namespace Server.Engines.Craft
|
|||
{
|
||||
if (tool?.Deleted == false && tool.UsesRemaining > 0)
|
||||
{
|
||||
from.SendGump(new CraftGump(from, craftSystem, tool, message));
|
||||
ShowCraftMenu(from, craftSystem, tool, message);
|
||||
}
|
||||
else if (message != null)
|
||||
{
|
||||
|
|
@ -1093,29 +1406,32 @@ namespace Server.Engines.Craft
|
|||
return;
|
||||
}
|
||||
|
||||
tool.UsesRemaining--;
|
||||
|
||||
if (craftSystem is DefBlacksmithy)
|
||||
if (tool != null)
|
||||
{
|
||||
var hammer = from.FindItemOnLayer<AncientSmithyHammer>(Layer.OneHanded);
|
||||
if (hammer != null && hammer != tool)
|
||||
tool.UsesRemaining--;
|
||||
|
||||
if (craftSystem is DefBlacksmithy)
|
||||
{
|
||||
hammer.UsesRemaining--;
|
||||
if (hammer.UsesRemaining < 1)
|
||||
var hammer = from.FindItemOnLayer<AncientSmithyHammer>(Layer.OneHanded);
|
||||
if (hammer != null && hammer != tool)
|
||||
{
|
||||
hammer.Delete();
|
||||
hammer.UsesRemaining--;
|
||||
if (hammer.UsesRemaining < 1)
|
||||
{
|
||||
hammer.Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tool.UsesRemaining < 1 && tool.BreakOnDepletion)
|
||||
{
|
||||
toolBroken = true;
|
||||
}
|
||||
if (tool.UsesRemaining < 1 && tool.BreakOnDepletion)
|
||||
{
|
||||
toolBroken = true;
|
||||
}
|
||||
|
||||
if (toolBroken)
|
||||
{
|
||||
tool.Delete();
|
||||
if (toolBroken)
|
||||
{
|
||||
tool.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
Item item;
|
||||
|
|
@ -1229,7 +1545,18 @@ namespace Server.Engines.Craft
|
|||
}
|
||||
else if (tool?.Deleted == false && tool.UsesRemaining > 0)
|
||||
{
|
||||
from.SendGump(new CraftGump(from, craftSystem, tool, num));
|
||||
if (T2ACraftSystem.Enabled)
|
||||
{
|
||||
if (num > 0)
|
||||
{
|
||||
from.SendLocalizedMessage(num);
|
||||
}
|
||||
ShowCraftMenu(from, craftSystem, tool);
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowCraftMenu(from, craftSystem, tool, num);
|
||||
}
|
||||
}
|
||||
else if (num > 0)
|
||||
{
|
||||
|
|
@ -1243,13 +1570,12 @@ namespace Server.Engines.Craft
|
|||
{
|
||||
if (tool?.Deleted == false && tool.UsesRemaining > 0)
|
||||
{
|
||||
from.SendGump(new CraftGump(from, craftSystem, tool, 1044153));
|
||||
from.SendAsciiMessage("You lack the required skill to craft this item.");
|
||||
}
|
||||
else
|
||||
{
|
||||
from.SendLocalizedMessage(1044153); // You don't have the required skills to attempt this item.
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1260,7 +1586,7 @@ namespace Server.Engines.Craft
|
|||
{
|
||||
if (tool?.Deleted == false && tool.UsesRemaining > 0)
|
||||
{
|
||||
from.SendGump(new CraftGump(from, craftSystem, tool, message));
|
||||
ShowCraftMenu(from, craftSystem, tool, message);
|
||||
}
|
||||
else if (message != null)
|
||||
{
|
||||
|
|
@ -1277,24 +1603,226 @@ namespace Server.Engines.Craft
|
|||
return;
|
||||
}
|
||||
|
||||
tool.UsesRemaining--;
|
||||
|
||||
if (tool.UsesRemaining < 1 && tool.BreakOnDepletion)
|
||||
if (tool != null)
|
||||
{
|
||||
toolBroken = true;
|
||||
}
|
||||
tool.UsesRemaining--;
|
||||
|
||||
if (toolBroken)
|
||||
{
|
||||
tool.Delete();
|
||||
if (tool.UsesRemaining < 1 && tool.BreakOnDepletion)
|
||||
{
|
||||
toolBroken = true;
|
||||
}
|
||||
|
||||
if (toolBroken)
|
||||
{
|
||||
tool.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
// SkillCheck failed.
|
||||
num = craftSystem.PlayEndingEffect(from, true, true, toolBroken, endquality, false, this);
|
||||
|
||||
if (!tool.Deleted && tool.UsesRemaining > 0)
|
||||
if (tool?.Deleted == false && tool.UsesRemaining > 0)
|
||||
{
|
||||
from.SendGump(new CraftGump(from, craftSystem, tool, num));
|
||||
ShowCraftMenu(from, craftSystem, tool, num);
|
||||
}
|
||||
else if (num > 0)
|
||||
{
|
||||
from.SendLocalizedMessage(num);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hue-aware CompleteCraft. Uses ConsumeHuedRes to consume only resources matching
|
||||
/// the target hue, and applies that hue to the crafted item.
|
||||
/// </summary>
|
||||
public void CompleteCraft(
|
||||
int quality, bool makersMark, Mobile from, CraftSystem craftSystem, Type typeRes,
|
||||
BaseTool tool, CustomCraft customCraft, int targetHue
|
||||
)
|
||||
{
|
||||
var badCraft = craftSystem.CanCraft(from, tool, ItemType);
|
||||
|
||||
if (badCraft > 0)
|
||||
{
|
||||
if (tool?.Deleted == false && tool.UsesRemaining > 0)
|
||||
{
|
||||
ShowCraftMenu(from, craftSystem, tool, badCraft);
|
||||
}
|
||||
else
|
||||
{
|
||||
from.SendLocalizedMessage(badCraft);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Dry-run check with hue filtering
|
||||
var checkResHue = 0;
|
||||
if (!ConsumeHuedRes(from, typeRes, craftSystem, targetHue, ref checkResHue, ConsumeType.None))
|
||||
{
|
||||
if (tool?.Deleted == false && tool.UsesRemaining > 0)
|
||||
{
|
||||
// You don't have the resources required to make that item.
|
||||
ShowCraftMenu(from, craftSystem, tool, 502925);
|
||||
}
|
||||
else
|
||||
{
|
||||
from.SendLocalizedMessage(502925);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
TextDefinition checkMessage = null;
|
||||
if (!ConsumeAttributes(from, ref checkMessage, false))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var toolBroken = false;
|
||||
var endquality = 1;
|
||||
var resHue = 0;
|
||||
var num = 0;
|
||||
|
||||
if (CheckSkills(from, typeRes, craftSystem, ref quality, out var allRequiredSkills))
|
||||
{
|
||||
var consumeType = UseAllRes ? ConsumeType.Half : ConsumeType.All;
|
||||
|
||||
if (!ConsumeHuedRes(from, typeRes, craftSystem, targetHue, ref resHue, consumeType))
|
||||
{
|
||||
if (tool?.Deleted == false && tool.UsesRemaining > 0)
|
||||
{
|
||||
ShowCraftMenu(from, craftSystem, tool, 502925);
|
||||
}
|
||||
else
|
||||
{
|
||||
from.SendLocalizedMessage(502925);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (tool != null)
|
||||
{
|
||||
tool.UsesRemaining--;
|
||||
|
||||
if (craftSystem is DefBlacksmithy)
|
||||
{
|
||||
var hammer = from.FindItemOnLayer<AncientSmithyHammer>(Layer.OneHanded);
|
||||
if (hammer != null && hammer != tool)
|
||||
{
|
||||
hammer.UsesRemaining--;
|
||||
if (hammer.UsesRemaining < 1)
|
||||
{
|
||||
hammer.Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tool.UsesRemaining < 1 && tool.BreakOnDepletion)
|
||||
{
|
||||
toolBroken = true;
|
||||
}
|
||||
|
||||
if (toolBroken)
|
||||
{
|
||||
tool.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
Item item;
|
||||
if (customCraft != null)
|
||||
{
|
||||
item = customCraft.CompleteCraft(out num);
|
||||
}
|
||||
else
|
||||
{
|
||||
item = ItemType.CreateInstance<Item>();
|
||||
}
|
||||
|
||||
if (item != null)
|
||||
{
|
||||
if (item is ICraftable craftable)
|
||||
{
|
||||
endquality = craftable.OnCraft(quality, makersMark, from, craftSystem, typeRes, tool, this, resHue);
|
||||
}
|
||||
else if (item.Hue == 0)
|
||||
{
|
||||
item.Hue = resHue;
|
||||
}
|
||||
|
||||
from.AddToBackpack(item);
|
||||
|
||||
num = craftSystem.PlayEndingEffect(from, false, true, toolBroken, endquality, false, this);
|
||||
|
||||
if (T2ACraftSystem.Enabled)
|
||||
{
|
||||
if (tool?.Deleted == false && tool.UsesRemaining > 0)
|
||||
{
|
||||
if (num > 0)
|
||||
{
|
||||
from.SendLocalizedMessage(num);
|
||||
}
|
||||
|
||||
ShowCraftMenu(from, craftSystem, tool);
|
||||
}
|
||||
else if (num > 0)
|
||||
{
|
||||
from.SendLocalizedMessage(num);
|
||||
}
|
||||
}
|
||||
else if (tool?.Deleted == false && tool.UsesRemaining > 0)
|
||||
{
|
||||
ShowCraftMenu(from, craftSystem, tool, num);
|
||||
}
|
||||
else if (num > 0)
|
||||
{
|
||||
from.SendLocalizedMessage(num);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!allRequiredSkills)
|
||||
{
|
||||
if (tool?.Deleted == false && tool.UsesRemaining > 0)
|
||||
{
|
||||
from.SendAsciiMessage("You lack the required skill to craft this item.");
|
||||
}
|
||||
else
|
||||
{
|
||||
from.SendLocalizedMessage(1044153);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var failConsumeType = UseAllRes ? ConsumeType.Half : ConsumeType.All;
|
||||
|
||||
// Failure: consume resources (half on failure)
|
||||
ConsumeHuedRes(from, typeRes, craftSystem, targetHue, ref resHue, failConsumeType);
|
||||
|
||||
if (tool != null)
|
||||
{
|
||||
tool.UsesRemaining--;
|
||||
|
||||
if (tool.UsesRemaining < 1 && tool.BreakOnDepletion)
|
||||
{
|
||||
toolBroken = true;
|
||||
}
|
||||
|
||||
if (toolBroken)
|
||||
{
|
||||
tool.Delete();
|
||||
}
|
||||
}
|
||||
|
||||
num = craftSystem.PlayEndingEffect(from, true, true, toolBroken, endquality, false, this);
|
||||
|
||||
if (tool?.Deleted == false && tool.UsesRemaining > 0)
|
||||
{
|
||||
ShowCraftMenu(from, craftSystem, tool, num);
|
||||
}
|
||||
else if (num > 0)
|
||||
{
|
||||
|
|
@ -1310,11 +1838,12 @@ namespace Server.Engines.Craft
|
|||
private readonly int m_iCountMax;
|
||||
private readonly BaseTool m_Tool;
|
||||
private readonly Type m_TypeRes;
|
||||
private readonly int m_ResHue;
|
||||
private int m_iCount;
|
||||
|
||||
public InternalTimer(
|
||||
Mobile from, CraftSystem craftSystem, CraftItem craftItem, Type typeRes, BaseTool tool,
|
||||
int iCountMax
|
||||
int iCountMax, int resHue = -1
|
||||
) : base(TimeSpan.Zero, TimeSpan.FromSeconds(craftSystem.Delay), iCountMax)
|
||||
{
|
||||
m_From = from;
|
||||
|
|
@ -1324,6 +1853,7 @@ namespace Server.Engines.Craft
|
|||
m_CraftSystem = craftSystem;
|
||||
m_TypeRes = typeRes;
|
||||
m_Tool = tool;
|
||||
m_ResHue = resHue;
|
||||
}
|
||||
|
||||
protected override void OnTick()
|
||||
|
|
@ -1346,7 +1876,7 @@ namespace Server.Engines.Craft
|
|||
{
|
||||
if (m_Tool?.Deleted == false && m_Tool.UsesRemaining > 0)
|
||||
{
|
||||
m_From.SendGump(new CraftGump(m_From, m_CraftSystem, m_Tool, badCraft));
|
||||
ShowCraftMenu(m_From, m_CraftSystem, m_Tool, badCraft);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -1395,7 +1925,9 @@ namespace Server.Engines.Craft
|
|||
makersMark = m_CraftItem.IsMarkable(m_CraftItem.ItemType);
|
||||
}
|
||||
|
||||
if (makersMark && context.MarkOption == CraftMarkOption.PromptForMark)
|
||||
// T2A menus always prompt for maker's mark (no auto-mark/don't-mark options)
|
||||
if (makersMark &&
|
||||
(T2ACraftSystem.Enabled || context.MarkOption == CraftMarkOption.PromptForMark))
|
||||
{
|
||||
m_From.SendGump(
|
||||
new QueryMakersMarkGump(
|
||||
|
|
@ -1403,7 +1935,8 @@ namespace Server.Engines.Craft
|
|||
m_CraftItem,
|
||||
m_CraftSystem,
|
||||
m_TypeRes,
|
||||
m_Tool
|
||||
m_Tool,
|
||||
m_ResHue
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
@ -1414,9 +1947,41 @@ namespace Server.Engines.Craft
|
|||
makersMark = false;
|
||||
}
|
||||
|
||||
m_CraftItem.CompleteCraft(quality, makersMark, m_From, m_CraftSystem, m_TypeRes, m_Tool, null);
|
||||
if (m_ResHue >= 0)
|
||||
{
|
||||
m_CraftItem.CompleteCraft(
|
||||
quality, makersMark, m_From, m_CraftSystem, m_TypeRes, m_Tool, null, m_ResHue
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_CraftItem.CompleteCraft(quality, makersMark, m_From, m_CraftSystem, m_TypeRes, m_Tool, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void ShowCraftMenu(Mobile from, CraftSystem system, BaseTool tool, TextDefinition message = null)
|
||||
{
|
||||
if (T2ACraftSystem.Enabled)
|
||||
{
|
||||
// T2A: Don't reopen menu. Player double-clicks tool to restart.
|
||||
if (message != null)
|
||||
{
|
||||
if (message.Number > 0)
|
||||
{
|
||||
from.SendLocalizedMessage(message.Number);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(message.String))
|
||||
{
|
||||
from.SendMessage(message.String);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
from.SendGump(new CraftGump(from, system, tool, message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ namespace Server.Engines.Craft
|
|||
|
||||
public virtual CraftECA ECA => CraftECA.ChanceMinusSixty;
|
||||
|
||||
public virtual bool RequiresTool => true;
|
||||
|
||||
public bool Resmelt { get; set; }
|
||||
|
||||
public bool Repair { get; set; }
|
||||
|
|
@ -103,6 +105,17 @@ namespace Server.Engines.Craft
|
|||
}
|
||||
}
|
||||
|
||||
public void CreateItem(
|
||||
Mobile from, Type type, Type typeRes, BaseTool tool, CraftItem realCraftItem, int hue
|
||||
)
|
||||
{
|
||||
// Verify if the type is in the list of the craftable item
|
||||
if (CraftItems.SearchFor(type) != null)
|
||||
{
|
||||
realCraftItem.Craft(from, this, typeRes, tool, hue);
|
||||
}
|
||||
}
|
||||
|
||||
public int RandomRecipe()
|
||||
{
|
||||
if (m_Recipes.Count == 0)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using Server.Gumps;
|
||||
using Server.Items;
|
||||
using Server.Targeting;
|
||||
|
||||
|
|
@ -348,7 +347,7 @@ namespace Server.Engines.Craft
|
|||
|
||||
if (from.Skills[craftSystem.MainSkill].Value < res.RequiredSkill)
|
||||
{
|
||||
from.SendGump(new CraftGump(from, craftSystem, tool, res.Message));
|
||||
CraftItem.ShowCraftMenu(from, craftSystem, tool, res.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -362,29 +361,13 @@ namespace Server.Engines.Craft
|
|||
}
|
||||
else
|
||||
{
|
||||
from.SendGump(
|
||||
new CraftGump(
|
||||
from,
|
||||
craftSystem,
|
||||
tool,
|
||||
// You must select a special material in order to enhance an item with its properties.
|
||||
1061010
|
||||
)
|
||||
);
|
||||
CraftItem.ShowCraftMenu(from, craftSystem, tool, 1061010);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
from.SendGump(
|
||||
new CraftGump(
|
||||
from,
|
||||
craftSystem,
|
||||
tool,
|
||||
// You must select a special material in order to enhance an item with its properties.
|
||||
1061010
|
||||
)
|
||||
);
|
||||
CraftItem.ShowCraftMenu(from, craftSystem, tool, 1061010);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -435,7 +418,7 @@ namespace Server.Engines.Craft
|
|||
_ => message
|
||||
};
|
||||
|
||||
from.SendGump(new CraftGump(from, m_CraftSystem, m_Tool, message));
|
||||
CraftItem.ShowCraftMenu(from, m_CraftSystem, m_Tool, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,17 +12,20 @@ public class QueryMakersMarkGump : StaticGump<QueryMakersMarkGump>
|
|||
private readonly int _quality;
|
||||
private readonly BaseTool _tool;
|
||||
private readonly Type _typeRes;
|
||||
private readonly int _resHue;
|
||||
|
||||
public override bool Singleton => true;
|
||||
|
||||
public QueryMakersMarkGump(int quality, CraftItem craftItem, CraftSystem craftSystem, Type typeRes, BaseTool tool)
|
||||
: base(100, 200)
|
||||
public QueryMakersMarkGump(
|
||||
int quality, CraftItem craftItem, CraftSystem craftSystem, Type typeRes, BaseTool tool, int resHue = -1
|
||||
) : base(100, 200)
|
||||
{
|
||||
_quality = quality;
|
||||
_craftItem = craftItem;
|
||||
_craftSystem = craftSystem;
|
||||
_typeRes = typeRes;
|
||||
_tool = tool;
|
||||
_resHue = resHue;
|
||||
}
|
||||
|
||||
protected override void BuildLayout(ref StaticGumpBuilder builder)
|
||||
|
|
@ -55,6 +58,13 @@ public class QueryMakersMarkGump : StaticGump<QueryMakersMarkGump>
|
|||
from.SendLocalizedMessage(501809); // Cancelled mark.
|
||||
}
|
||||
|
||||
_craftItem.CompleteCraft(_quality, makersMark, from, _craftSystem, _typeRes, _tool, null);
|
||||
if (_resHue >= 0)
|
||||
{
|
||||
_craftItem.CompleteCraft(_quality, makersMark, from, _craftSystem, _typeRes, _tool, null, _resHue);
|
||||
}
|
||||
else
|
||||
{
|
||||
_craftItem.CompleteCraft(_quality, makersMark, from, _craftSystem, _typeRes, _tool, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using System;
|
||||
using Server.Gumps;
|
||||
using Server.Engines.Craft.T2A;
|
||||
using Server.Items;
|
||||
using Server.Mobiles;
|
||||
using Server.Targeting;
|
||||
|
|
@ -482,8 +482,15 @@ namespace Server.Engines.Craft
|
|||
|
||||
if (!usingDeed)
|
||||
{
|
||||
var context = m_CraftSystem.GetContext(from);
|
||||
from.SendGump(new CraftGump(from, m_CraftSystem, m_Tool, number));
|
||||
if (T2ACraftSystem.Enabled)
|
||||
{
|
||||
from.SendLocalizedMessage(number);
|
||||
CraftItem.ShowCraftMenu(from, m_CraftSystem, m_Tool);
|
||||
}
|
||||
else
|
||||
{
|
||||
CraftItem.ShowCraftMenu(from, m_CraftSystem, m_Tool, number);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using Server.Ethics;
|
||||
using Server.Gumps;
|
||||
using Server.Items;
|
||||
using Server.Targeting;
|
||||
|
||||
|
|
@ -20,7 +19,7 @@ namespace Server.Engines.Craft
|
|||
|
||||
if (num > 0 && num != 1044267)
|
||||
{
|
||||
from.SendGump(new CraftGump(from, craftSystem, tool, num));
|
||||
CraftItem.ShowCraftMenu(from, craftSystem, tool, num);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -142,7 +141,7 @@ namespace Server.Engines.Craft
|
|||
}
|
||||
}
|
||||
|
||||
from.SendGump(new CraftGump(from, m_CraftSystem, m_Tool, num));
|
||||
CraftItem.ShowCraftMenu(from, m_CraftSystem, m_Tool, num);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -173,7 +172,7 @@ namespace Server.Engines.Craft
|
|||
_ => 1044272
|
||||
};
|
||||
|
||||
from.SendGump(new CraftGump(from, m_CraftSystem, m_Tool, message));
|
||||
CraftItem.ShowCraftMenu(from, m_CraftSystem, m_Tool, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ public class DefAlchemy : CraftSystem
|
|||
|
||||
public static void Initialize()
|
||||
{
|
||||
if (CraftSystem != null)
|
||||
{
|
||||
return; // Already initialized
|
||||
}
|
||||
|
||||
CraftSystem = new DefAlchemy();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using Server.Engines.Craft.T2A;
|
||||
using Server.Items;
|
||||
|
||||
namespace Server.Engines.Craft;
|
||||
|
|
@ -20,18 +21,23 @@ public class DefCartography : CraftSystem
|
|||
|
||||
public static CraftSystem CraftSystem { get; private set; }
|
||||
|
||||
public override bool RequiresTool => !T2ACraftSystem.Enabled;
|
||||
|
||||
public override double GetChanceAtMin(CraftItem item) => 0.0;
|
||||
|
||||
public override int CanCraft(Mobile from, BaseTool tool, Type itemType)
|
||||
{
|
||||
if (tool?.Deleted != false || tool.UsesRemaining < 0)
|
||||
if (RequiresTool)
|
||||
{
|
||||
return 1044038; // You have worn out your tool!
|
||||
}
|
||||
if (tool?.Deleted != false || tool.UsesRemaining < 0)
|
||||
{
|
||||
return 1044038; // You have worn out your tool!
|
||||
}
|
||||
|
||||
if (!BaseTool.CheckAccessible(tool, from))
|
||||
{
|
||||
return 1044263; // The tool must be on your person to use.
|
||||
if (!BaseTool.CheckAccessible(tool, from))
|
||||
{
|
||||
return 1044263; // The tool must be on your person to use.
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using Server.Engines.BulkOrders;
|
||||
using Server.Engines.Craft.T2A;
|
||||
using Server.Items;
|
||||
using Server.Spells;
|
||||
|
||||
|
|
@ -39,18 +40,23 @@ public class DefInscription : CraftSystem
|
|||
|
||||
public static CraftSystem CraftSystem { get; private set; }
|
||||
|
||||
public override bool RequiresTool => !T2ACraftSystem.Enabled;
|
||||
|
||||
public override double GetChanceAtMin(CraftItem item) => 0.0;
|
||||
|
||||
public override int CanCraft(Mobile from, BaseTool tool, Type typeItem)
|
||||
{
|
||||
if (tool?.Deleted != false || tool.UsesRemaining < 0)
|
||||
if (RequiresTool)
|
||||
{
|
||||
return 1044038; // You have worn out your tool!
|
||||
}
|
||||
if (tool?.Deleted != false || tool.UsesRemaining < 0)
|
||||
{
|
||||
return 1044038; // You have worn out your tool!
|
||||
}
|
||||
|
||||
if (!BaseTool.CheckAccessible(tool, from))
|
||||
{
|
||||
return 1044263; // The tool must be on your person to use.
|
||||
if (!BaseTool.CheckAccessible(tool, from))
|
||||
{
|
||||
return 1044263; // The tool must be on your person to use.
|
||||
}
|
||||
}
|
||||
|
||||
var scroll = typeItem?.CreateEntityInstance<SpellScroll>();
|
||||
|
|
|
|||
|
|
@ -664,6 +664,13 @@ public class DefTailoring : CraftSystem
|
|||
AddSubRes(typeof(HornedLeather), 1049152, 80.0, 1044462, 1049311);
|
||||
AddSubRes(typeof(BarbedLeather), 1049153, 99.0, 1044462, 1049311);
|
||||
|
||||
// Add Bolt of Cloth for pre-AOS expansions only
|
||||
if (!Core.AOS)
|
||||
{
|
||||
index = AddCraft(typeof(BoltOfCloth), 1015283, 1044286, 0.0, 25.0, typeof(Cloth), 1044286, 50, 1044287);
|
||||
// 1015283: group (Sashes & Aprons), 1044286: name (Cloth), 0.0-25.0: skill, 50: amount, 1044287: message
|
||||
}
|
||||
|
||||
MarkOption = true;
|
||||
Repair = Core.AOS;
|
||||
CanEnhance = Core.AOS;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using System;
|
||||
using Server.Engines.Craft.T2A;
|
||||
using Server.Factions;
|
||||
using Server.Gumps;
|
||||
using Server.Items;
|
||||
using Server.Targeting;
|
||||
|
||||
|
|
@ -71,7 +71,7 @@ public class DefTinkering : CraftSystem
|
|||
|
||||
public override bool RetainsColorFrom(CraftItem item, Type type)
|
||||
{
|
||||
if (!type.IsSubclassOf(typeof(BaseIngot)))
|
||||
if (!Core.UOTD || !type.IsSubclassOf(typeof(BaseIngot)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -377,15 +377,30 @@ public class DefTinkering : CraftSystem
|
|||
SetNeededExpansion(index, Expansion.SE);
|
||||
}
|
||||
|
||||
AddJewelrySet(GemType.StarSapphire, typeof(StarSapphire));
|
||||
AddJewelrySet(GemType.Emerald, typeof(Emerald));
|
||||
AddJewelrySet(GemType.Sapphire, typeof(Sapphire));
|
||||
AddJewelrySet(GemType.Ruby, typeof(Ruby));
|
||||
AddJewelrySet(GemType.Citrine, typeof(Citrine));
|
||||
AddJewelrySet(GemType.Amethyst, typeof(Amethyst));
|
||||
AddJewelrySet(GemType.Tourmaline, typeof(Tourmaline));
|
||||
AddJewelrySet(GemType.Amber, typeof(Amber));
|
||||
AddJewelrySet(GemType.Diamond, typeof(Diamond));
|
||||
if (!T2ACraftSystem.Enabled)
|
||||
{
|
||||
// AOS+ jewelry with gem resources
|
||||
AddJewelrySet(GemType.StarSapphire, typeof(StarSapphire));
|
||||
AddJewelrySet(GemType.Emerald, typeof(Emerald));
|
||||
AddJewelrySet(GemType.Sapphire, typeof(Sapphire));
|
||||
AddJewelrySet(GemType.Ruby, typeof(Ruby));
|
||||
AddJewelrySet(GemType.Citrine, typeof(Citrine));
|
||||
AddJewelrySet(GemType.Amethyst, typeof(Amethyst));
|
||||
AddJewelrySet(GemType.Tourmaline, typeof(Tourmaline));
|
||||
AddJewelrySet(GemType.Amber, typeof(Amber));
|
||||
AddJewelrySet(GemType.Diamond, typeof(Diamond));
|
||||
}
|
||||
else
|
||||
{
|
||||
// T2A jewelry — ingot-only resources, gem selected via targeting
|
||||
AddCraft(typeof(GoldNecklace), 1044049, "gold necklace", 40.0, 90.0, typeof(IronIngot), 1044036, 2, 1044037);
|
||||
AddCraft(typeof(SilverNecklace), 1044049, "silver necklace", 40.0, 90.0, typeof(IronIngot), 1044036, 2, 1044037);
|
||||
AddCraft(typeof(GoldEarrings), 1044049, "gold earrings", 40.0, 90.0, typeof(IronIngot), 1044036, 2, 1044037);
|
||||
AddCraft(typeof(SilverEarrings), 1044049, "silver earrings", 40.0, 90.0, typeof(IronIngot), 1044036, 2, 1044037);
|
||||
AddCraft(typeof(GoldRing), 1044049, "gold ring", 40.0, 90.0, typeof(IronIngot), 1044036, 2, 1044037);
|
||||
AddCraft(typeof(SilverRing), 1044049, "silver ring", 40.0, 90.0, typeof(IronIngot), 1044036, 2, 1044037);
|
||||
AddCraft(typeof(WeddingRing), 1044049, "wedding ring", 40.0, 90.0, typeof(IronIngot), 1044036, 2, 1044037);
|
||||
}
|
||||
|
||||
index = AddCraft(typeof(AxleGears), 1044051, 1024177, 0.0, 0.0, typeof(Axle), 1044169, 1, 1044253);
|
||||
AddRes(index, typeof(Gears), 1044254, 1, 1044253);
|
||||
|
|
@ -714,7 +729,7 @@ public abstract class TrapCraft : CustomCraft
|
|||
|
||||
if (tool?.Deleted == false && tool.UsesRemaining > 0)
|
||||
{
|
||||
from.SendGump(new CraftGump(from, m_TrapCraft.CraftSystem, tool, message));
|
||||
CraftItem.ShowCraftMenu(from, m_TrapCraft.CraftSystem, tool, message);
|
||||
}
|
||||
else if (message > 0)
|
||||
{
|
||||
|
|
|
|||
262
Projects/UOContent/Engines/Craft/T2A/AlchemyMenu.cs
Normal file
262
Projects/UOContent/Engines/Craft/T2A/AlchemyMenu.cs
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
using System;
|
||||
using Server.Items;
|
||||
using Server.Menus.ItemLists;
|
||||
using Server.Network;
|
||||
|
||||
namespace Server.Engines.Craft.T2A;
|
||||
|
||||
public class AlchemyMenu : ItemListMenu
|
||||
{
|
||||
private enum Category
|
||||
{
|
||||
Main,
|
||||
Refresh,
|
||||
Agility,
|
||||
NightSight,
|
||||
Heal,
|
||||
Strength,
|
||||
Poison,
|
||||
Cure,
|
||||
Explosion
|
||||
}
|
||||
|
||||
private static readonly Type[] RefreshTypes = [typeof(RefreshPotion), typeof(TotalRefreshPotion)];
|
||||
|
||||
private static readonly Type[] AgilityTypes = [typeof(AgilityPotion), typeof(GreaterAgilityPotion)];
|
||||
|
||||
private static readonly Type[] NightSightTypes = [typeof(NightSightPotion)];
|
||||
|
||||
private static readonly Type[] HealTypes = [typeof(LesserHealPotion), typeof(HealPotion), typeof(GreaterHealPotion)];
|
||||
|
||||
private static readonly Type[] StrengthTypes = [typeof(StrengthPotion), typeof(GreaterStrengthPotion)];
|
||||
|
||||
private static readonly Type[] PoisonTypes =
|
||||
[
|
||||
typeof(LesserPoisonPotion), typeof(PoisonPotion), typeof(GreaterPoisonPotion), typeof(DeadlyPoisonPotion)
|
||||
];
|
||||
|
||||
private static readonly Type[] CureTypes = [typeof(LesserCurePotion), typeof(CurePotion), typeof(GreaterCurePotion)];
|
||||
|
||||
private static readonly Type[] ExplosionTypes =
|
||||
[
|
||||
typeof(LesserExplosionPotion), typeof(ExplosionPotion), typeof(GreaterExplosionPotion)
|
||||
];
|
||||
|
||||
private static ItemListEntry[] _mainEntries;
|
||||
private static ItemListEntry[] _refreshEntries;
|
||||
private static ItemListEntry[] _agilityEntries;
|
||||
private static ItemListEntry[] _nightSightEntries;
|
||||
private static ItemListEntry[] _healEntries;
|
||||
private static ItemListEntry[] _strengthEntries;
|
||||
private static ItemListEntry[] _poisonEntries;
|
||||
private static ItemListEntry[] _cureEntries;
|
||||
private static ItemListEntry[] _explosionEntries;
|
||||
|
||||
private readonly Category _category;
|
||||
private readonly BaseTool _tool;
|
||||
|
||||
public AlchemyMenu(Mobile from, BaseTool tool) : this(from, tool, Category.Main)
|
||||
{
|
||||
}
|
||||
|
||||
private static string GetQuestion(Category category) => category switch
|
||||
{
|
||||
Category.Main => "What kind of potion?",
|
||||
_ => "Which potion would you like to make?"
|
||||
};
|
||||
|
||||
private AlchemyMenu(Mobile from, BaseTool tool, Category category)
|
||||
: base(GetQuestion(category), BuildFilteredEntries(from, category))
|
||||
{
|
||||
_tool = tool;
|
||||
_category = category;
|
||||
}
|
||||
|
||||
private static string FormatItemName(Type type)
|
||||
{
|
||||
var name = type.Name;
|
||||
|
||||
if (name.EndsWith("Potion"))
|
||||
{
|
||||
name = name[..^6];
|
||||
}
|
||||
|
||||
Span<char> buffer = stackalloc char[name.Length * 2];
|
||||
var pos = 0;
|
||||
|
||||
for (var i = 0; i < name.Length; i++)
|
||||
{
|
||||
if (i > 0 && char.IsUpper(name[i]))
|
||||
{
|
||||
buffer[pos++] = ' ';
|
||||
}
|
||||
|
||||
buffer[pos++] = char.ToLower(name[i]);
|
||||
}
|
||||
|
||||
return new string(buffer[..pos]);
|
||||
}
|
||||
|
||||
private static ItemListEntry[] BuildStaticEntries(Type[] types)
|
||||
{
|
||||
var entries = new ItemListEntry[types.Length];
|
||||
var count = 0;
|
||||
var craftItems = DefAlchemy.CraftSystem.CraftItems;
|
||||
|
||||
for (var i = 0; i < types.Length; i++)
|
||||
{
|
||||
var itemDef = craftItems.SearchFor(types[i]);
|
||||
if (itemDef == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
entries[count++] = new ItemListEntry(FormatItemName(types[i]), itemDef.ItemId, 0, i);
|
||||
}
|
||||
|
||||
if (count < entries.Length)
|
||||
{
|
||||
Array.Resize(ref entries, count);
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static ItemListEntry[] GetStaticEntries(Category category) => category switch
|
||||
{
|
||||
Category.Main => Main(),
|
||||
Category.Refresh => _refreshEntries ??= BuildStaticEntries(RefreshTypes),
|
||||
Category.Agility => _agilityEntries ??= BuildStaticEntries(AgilityTypes),
|
||||
Category.NightSight => _nightSightEntries ??= BuildStaticEntries(NightSightTypes),
|
||||
Category.Heal => _healEntries ??= BuildStaticEntries(HealTypes),
|
||||
Category.Strength => _strengthEntries ??= BuildStaticEntries(StrengthTypes),
|
||||
Category.Poison => _poisonEntries ??= BuildStaticEntries(PoisonTypes),
|
||||
Category.Cure => _cureEntries ??= BuildStaticEntries(CureTypes),
|
||||
Category.Explosion => _explosionEntries ??= BuildStaticEntries(ExplosionTypes),
|
||||
_ => null
|
||||
};
|
||||
|
||||
private static Type[] GetTypes(Category category) => category switch
|
||||
{
|
||||
Category.Refresh => RefreshTypes,
|
||||
Category.Agility => AgilityTypes,
|
||||
Category.NightSight => NightSightTypes,
|
||||
Category.Heal => HealTypes,
|
||||
Category.Strength => StrengthTypes,
|
||||
Category.Poison => PoisonTypes,
|
||||
Category.Cure => CureTypes,
|
||||
Category.Explosion => ExplosionTypes,
|
||||
_ => null
|
||||
};
|
||||
|
||||
public static ItemListEntry[] Main() => _mainEntries ??=
|
||||
[
|
||||
new ItemListEntry("Refresh", 0xF0B, 0, (int)Category.Refresh),
|
||||
new ItemListEntry("Agility", 0xF08, 0, (int)Category.Agility),
|
||||
new ItemListEntry("Night Sight", 0xF06, 0, (int)Category.NightSight),
|
||||
new ItemListEntry("Heal", 0xF0C, 0, (int)Category.Heal),
|
||||
new ItemListEntry("Strength", 0xF09, 0, (int)Category.Strength),
|
||||
new ItemListEntry("Poison", 0xF0A, 0, (int)Category.Poison),
|
||||
new ItemListEntry("Cure", 0xF07, 0, (int)Category.Cure),
|
||||
new ItemListEntry("Explosion", 0xF0D, 0, (int)Category.Explosion)
|
||||
];
|
||||
|
||||
private static ItemListEntry[] BuildFilteredEntries(Mobile from, Category category)
|
||||
{
|
||||
if (category == Category.Main)
|
||||
{
|
||||
return BuildFilteredMainEntries(from);
|
||||
}
|
||||
|
||||
var types = GetTypes(category);
|
||||
var staticEntries = GetStaticEntries(category);
|
||||
if (types == null || staticEntries == null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return T2ACraftSystem.FilterEntries(from, staticEntries, types, DefAlchemy.CraftSystem);
|
||||
}
|
||||
|
||||
private static ItemListEntry[] BuildFilteredMainEntries(Mobile from)
|
||||
{
|
||||
var system = DefAlchemy.CraftSystem;
|
||||
var mainStatic = Main();
|
||||
var filtered = new ItemListEntry[mainStatic.Length];
|
||||
var count = 0;
|
||||
|
||||
for (var i = 0; i < mainStatic.Length; i++)
|
||||
{
|
||||
var entry = mainStatic[i];
|
||||
var types = GetTypes((Category)entry.CraftIndex);
|
||||
if (types != null && T2ACraftSystem.AnyCraftableInCategory(from, types, system))
|
||||
{
|
||||
filtered[count++] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (count < filtered.Length)
|
||||
{
|
||||
Array.Resize(ref filtered, count);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
private void CraftPotion(Mobile from, Type potionType)
|
||||
{
|
||||
if ((from.Backpack?.GetAmount(typeof(Bottle)) ?? 0) == 0)
|
||||
{
|
||||
from.SendAsciiMessage("You need an empty bottle to make a potion.");
|
||||
return;
|
||||
}
|
||||
|
||||
var itemDef = DefAlchemy.CraftSystem.CraftItems.SearchFor(potionType);
|
||||
|
||||
if (itemDef == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var num = DefAlchemy.CraftSystem.CanCraft(from, _tool, itemDef.ItemType);
|
||||
|
||||
if (num > 0)
|
||||
{
|
||||
from.SendLocalizedMessage(num);
|
||||
return;
|
||||
}
|
||||
|
||||
var res = itemDef.Resources[0];
|
||||
DefAlchemy.CraftSystem.CreateItem(from, itemDef.ItemType, res.ItemType, _tool, itemDef);
|
||||
}
|
||||
|
||||
public override void OnResponse(NetState state, int index)
|
||||
{
|
||||
var from = state.Mobile;
|
||||
var craftIndex = Entries[index].CraftIndex;
|
||||
|
||||
if (_category == Category.Main)
|
||||
{
|
||||
var menu = new AlchemyMenu(from, _tool, (Category)craftIndex);
|
||||
if (menu.Entries.Length == 0)
|
||||
{
|
||||
from.SendAsciiMessage("You lack the skill and materials to craft anything in that category.");
|
||||
return;
|
||||
}
|
||||
|
||||
from.SendMenu(menu);
|
||||
return;
|
||||
}
|
||||
|
||||
var types = GetTypes(_category);
|
||||
if (types != null && craftIndex >= 0 && craftIndex < types.Length)
|
||||
{
|
||||
CraftPotion(from, types[craftIndex]);
|
||||
}
|
||||
}
|
||||
}
|
||||
624
Projects/UOContent/Engines/Craft/T2A/BlacksmithMenu.cs
Normal file
624
Projects/UOContent/Engines/Craft/T2A/BlacksmithMenu.cs
Normal file
|
|
@ -0,0 +1,624 @@
|
|||
using System;
|
||||
using Server.Items;
|
||||
using Server.Menus.ItemLists;
|
||||
using Server.Network;
|
||||
using Server.Targeting;
|
||||
|
||||
namespace Server.Engines.Craft.T2A;
|
||||
|
||||
public class BlacksmithMenu : ItemListMenu
|
||||
{
|
||||
private enum Category
|
||||
{
|
||||
Main,
|
||||
Shields,
|
||||
Weapons,
|
||||
Armor,
|
||||
Blades,
|
||||
Axes,
|
||||
Maces,
|
||||
Polearms,
|
||||
Platemail,
|
||||
Chainmail,
|
||||
Ringmail,
|
||||
Helmets
|
||||
}
|
||||
|
||||
private static readonly Type[] RingmailTypes =
|
||||
[
|
||||
typeof(RingmailGloves), typeof(RingmailLegs), typeof(RingmailArms), typeof(RingmailChest)
|
||||
];
|
||||
|
||||
private static readonly Type[] ChainmailTypes =
|
||||
[
|
||||
typeof(ChainCoif), typeof(ChainLegs), typeof(ChainChest)
|
||||
];
|
||||
|
||||
private static readonly Type[] PlatemailTypes =
|
||||
[
|
||||
typeof(Bascinet), typeof(CloseHelm), typeof(Helmet), typeof(NorseHelm), typeof(PlateHelm),
|
||||
typeof(PlateArms), typeof(PlateGloves), typeof(PlateGorget), typeof(PlateLegs),
|
||||
typeof(PlateChest), typeof(FemalePlateChest)
|
||||
];
|
||||
|
||||
private static readonly Type[] ShieldTypes =
|
||||
[
|
||||
typeof(Buckler), typeof(BronzeShield), typeof(HeaterShield), typeof(MetalShield),
|
||||
typeof(MetalKiteShield), typeof(WoodenKiteShield)
|
||||
];
|
||||
|
||||
private static readonly Type[] BladeTypes =
|
||||
[
|
||||
typeof(Broadsword), typeof(Cutlass), typeof(Dagger), typeof(Katana),
|
||||
typeof(Kryss), typeof(Longsword), typeof(Scimitar), typeof(VikingSword)
|
||||
];
|
||||
|
||||
private static readonly Type[] AxeTypes =
|
||||
[
|
||||
typeof(Axe), typeof(BattleAxe), typeof(DoubleAxe), typeof(ExecutionersAxe),
|
||||
typeof(LargeBattleAxe), typeof(TwoHandedAxe), typeof(WarAxe)
|
||||
];
|
||||
|
||||
private static readonly Type[] PolearmTypes =
|
||||
[
|
||||
typeof(Bardiche), typeof(Halberd), typeof(WarFork), typeof(ShortSpear), typeof(Spear)
|
||||
];
|
||||
|
||||
private static readonly Type[] MaceTypes =
|
||||
[
|
||||
typeof(HammerPick), typeof(Mace), typeof(Maul), typeof(WarMace), typeof(WarHammer)
|
||||
];
|
||||
|
||||
// Non-category actions at the top of the main menu (Repair, Smelt).
|
||||
// Category CraftIndex values are offset by this count.
|
||||
private const int MainActionCount = 2;
|
||||
|
||||
private static ItemListEntry[] _mainEntries;
|
||||
private static ItemListEntry[] _weaponEntries;
|
||||
private static ItemListEntry[] _armorEntries;
|
||||
private static ItemListEntry[] _shieldEntries;
|
||||
private static ItemListEntry[] _bladeEntries;
|
||||
private static ItemListEntry[] _axeEntries;
|
||||
private static ItemListEntry[] _maceEntries;
|
||||
private static ItemListEntry[] _polearmEntries;
|
||||
private static ItemListEntry[] _platemailEntries;
|
||||
private static ItemListEntry[] _chainmailEntries;
|
||||
private static ItemListEntry[] _ringmailEntries;
|
||||
|
||||
private readonly Category _category;
|
||||
private readonly BaseTool _tool;
|
||||
|
||||
// Resource is always selected before any menu is shown (Lost Lands flow)
|
||||
public BlacksmithMenu(Mobile from, BaseTool tool) : this(from, tool, Category.Main)
|
||||
{
|
||||
}
|
||||
|
||||
private static string GetQuestion(Category category) => category switch
|
||||
{
|
||||
Category.Main => "What would you like to do?",
|
||||
Category.Armor => "What kind of armor?",
|
||||
Category.Shields => "What kind of shield?",
|
||||
Category.Weapons => "What kind of weapon?",
|
||||
Category.Blades => "What kind of blade?",
|
||||
Category.Axes => "What kind of axe?",
|
||||
Category.Polearms => "What kind of pole arm?",
|
||||
Category.Maces => "What kind of bludgeoning weapon?",
|
||||
Category.Ringmail => "What kind of ring armor?",
|
||||
Category.Chainmail => "What kind of chain armor?",
|
||||
Category.Platemail => "What kind of plate armor?",
|
||||
_ => "What would you like to make?"
|
||||
};
|
||||
|
||||
private BlacksmithMenu(Mobile from, BaseTool tool, Category category)
|
||||
: base(GetQuestion(category), BuildFilteredEntries(from, category))
|
||||
{
|
||||
_tool = tool;
|
||||
_category = category;
|
||||
}
|
||||
|
||||
private static string FormatItemName(Type type)
|
||||
{
|
||||
var name = type.Name;
|
||||
Span<char> buffer = stackalloc char[name.Length * 2];
|
||||
var pos = 0;
|
||||
for (var i = 0; i < name.Length; i++)
|
||||
{
|
||||
if (i > 0 && char.IsUpper(name[i]))
|
||||
{
|
||||
buffer[pos++] = ' ';
|
||||
}
|
||||
|
||||
buffer[pos++] = char.ToLower(name[i]);
|
||||
}
|
||||
|
||||
return new string(buffer[..pos]);
|
||||
}
|
||||
|
||||
private static ItemListEntry[] BuildStaticEntries(Type[] types, string resourceName)
|
||||
{
|
||||
var entries = new ItemListEntry[types.Length];
|
||||
var count = 0;
|
||||
var craftItems = DefBlacksmithy.CraftSystem.CraftItems;
|
||||
|
||||
for (var i = 0; i < types.Length; i++)
|
||||
{
|
||||
var itemDef = craftItems.SearchFor(types[i]);
|
||||
if (itemDef == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var name = FormatItemName(types[i]);
|
||||
var res = itemDef.Resources[0];
|
||||
var itemId = itemDef.ItemId;
|
||||
|
||||
if (itemId == 7033)
|
||||
{
|
||||
itemId = 7032;
|
||||
}
|
||||
|
||||
entries[count++] = new ItemListEntry($"{name} ({res.Amount} {resourceName})", itemId, 0, i);
|
||||
}
|
||||
|
||||
if (count < entries.Length)
|
||||
{
|
||||
Array.Resize(ref entries, count);
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static ItemListEntry[] GetStaticEntries(Category category) => category switch
|
||||
{
|
||||
Category.Main => Main(),
|
||||
Category.Weapons => Weapons(),
|
||||
Category.Armor => Armor(),
|
||||
Category.Shields => Shields(),
|
||||
Category.Blades => Blades(),
|
||||
Category.Axes => Axes(),
|
||||
Category.Maces => Maces(),
|
||||
Category.Polearms => Polearms(),
|
||||
Category.Platemail => Platemail(),
|
||||
Category.Chainmail => Chainmail(),
|
||||
Category.Ringmail => Ringmail(),
|
||||
_ => null
|
||||
};
|
||||
|
||||
private static Type[] GetTypes(Category category) => category switch
|
||||
{
|
||||
Category.Shields => ShieldTypes,
|
||||
Category.Blades => BladeTypes,
|
||||
Category.Axes => AxeTypes,
|
||||
Category.Maces => MaceTypes,
|
||||
Category.Polearms => PolearmTypes,
|
||||
Category.Platemail => PlatemailTypes,
|
||||
Category.Chainmail => ChainmailTypes,
|
||||
Category.Ringmail => RingmailTypes,
|
||||
_ => null
|
||||
};
|
||||
|
||||
// Leaf categories under Weapons
|
||||
private static readonly Category[] WeaponLeafCategories =
|
||||
[
|
||||
Category.Blades, Category.Axes, Category.Maces, Category.Polearms
|
||||
];
|
||||
|
||||
// Leaf categories under Armor
|
||||
private static readonly Category[] ArmorLeafCategories =
|
||||
[
|
||||
Category.Ringmail, Category.Chainmail, Category.Platemail
|
||||
];
|
||||
|
||||
public static ItemListEntry[] Main() => _mainEntries ??=
|
||||
[
|
||||
new ItemListEntry("Repair", 0x0FAF, 0, 0),
|
||||
new ItemListEntry("Smelt", 0x0FB1, 0, 1),
|
||||
new ItemListEntry("Build Armor", 0x13EC, 0, (int)Category.Armor + MainActionCount),
|
||||
new ItemListEntry("Build Shield", 0x1B74, 0, (int)Category.Shields + MainActionCount),
|
||||
new ItemListEntry("Build Weapons", 0xF45, 0, (int)Category.Weapons + MainActionCount)
|
||||
];
|
||||
|
||||
public static ItemListEntry[] Weapons() => _weaponEntries ??=
|
||||
[
|
||||
new ItemListEntry("Build Blades", 0xF61, 0, (int)Category.Blades),
|
||||
new ItemListEntry("Build Axes", 0x13FB, 0, (int)Category.Axes),
|
||||
new ItemListEntry("Build Pole Arms", 0xF4D, 0, (int)Category.Polearms),
|
||||
new ItemListEntry("Build Bludgeoning Weapons", 0x1407, 0, (int)Category.Maces)
|
||||
];
|
||||
|
||||
public static ItemListEntry[] Armor() => _armorEntries ??=
|
||||
[
|
||||
new ItemListEntry("Build Ring Armor", 0x13EC, 0, (int)Category.Ringmail),
|
||||
new ItemListEntry("Build Chain Armor", 0x13BF, 0, (int)Category.Chainmail),
|
||||
new ItemListEntry("Build Plate Armor", 0x1415, 0, (int)Category.Platemail)
|
||||
];
|
||||
|
||||
public static ItemListEntry[] Shields() => _shieldEntries ??= BuildStaticEntries(ShieldTypes, "ingots");
|
||||
public static ItemListEntry[] Blades() => _bladeEntries ??= BuildStaticEntries(BladeTypes, "ingots");
|
||||
public static ItemListEntry[] Axes() => _axeEntries ??= BuildStaticEntries(AxeTypes, "ingots");
|
||||
public static ItemListEntry[] Maces() => _maceEntries ??= BuildStaticEntries(MaceTypes, "ingots");
|
||||
public static ItemListEntry[] Polearms() => _polearmEntries ??= BuildStaticEntries(PolearmTypes, "ingots");
|
||||
public static ItemListEntry[] Platemail() => _platemailEntries ??= BuildStaticEntries(PlatemailTypes, "ingots");
|
||||
public static ItemListEntry[] Chainmail() => _chainmailEntries ??= BuildStaticEntries(ChainmailTypes, "ingots");
|
||||
public static ItemListEntry[] Ringmail() => _ringmailEntries ??= BuildStaticEntries(RingmailTypes, "ingots");
|
||||
|
||||
private static Type GetSelectedResourceType(Mobile from)
|
||||
{
|
||||
var context = DefBlacksmithy.CraftSystem.GetContext(from);
|
||||
if (context?.LastResourceIndex >= 0)
|
||||
{
|
||||
var res = DefBlacksmithy.CraftSystem.CraftSubRes;
|
||||
if (context.LastResourceIndex < res.Count)
|
||||
{
|
||||
return res[context.LastResourceIndex].ItemType;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static ItemListEntry[] BuildFilteredEntries(Mobile from, Category category)
|
||||
{
|
||||
// Resource is always selected before any menu is shown
|
||||
var selectedResType = GetSelectedResourceType(from);
|
||||
|
||||
if (category == Category.Main)
|
||||
{
|
||||
return BuildFilteredMainEntries(from, selectedResType);
|
||||
}
|
||||
|
||||
if (category == Category.Weapons)
|
||||
{
|
||||
return BuildFilteredMidEntries(from, Weapons(), WeaponLeafCategories, selectedResType);
|
||||
}
|
||||
|
||||
if (category == Category.Armor)
|
||||
{
|
||||
return BuildFilteredMidEntries(from, Armor(), ArmorLeafCategories, selectedResType);
|
||||
}
|
||||
|
||||
// Leaf category: filter individual items
|
||||
var types = GetTypes(category);
|
||||
var staticEntries = GetStaticEntries(category);
|
||||
if (types == null || staticEntries == null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return T2ACraftSystem.FilterEntries(from, staticEntries, types, DefBlacksmithy.CraftSystem, selectedResType);
|
||||
}
|
||||
|
||||
private static ItemListEntry[] BuildFilteredMainEntries(Mobile from, Type selectedResType)
|
||||
{
|
||||
var system = DefBlacksmithy.CraftSystem;
|
||||
var mainStatic = Main();
|
||||
var filtered = new ItemListEntry[mainStatic.Length];
|
||||
var count = 0;
|
||||
|
||||
for (var i = 0; i < mainStatic.Length; i++)
|
||||
{
|
||||
var entry = mainStatic[i];
|
||||
|
||||
// Repair and Smelt are always shown
|
||||
if (entry.CraftIndex < MainActionCount)
|
||||
{
|
||||
filtered[count++] = entry;
|
||||
continue;
|
||||
}
|
||||
|
||||
var cat = (Category)(entry.CraftIndex - MainActionCount);
|
||||
|
||||
if (cat == Category.Shields)
|
||||
{
|
||||
if (T2ACraftSystem.AnyCraftableInCategory(from, ShieldTypes, system, selectedResType))
|
||||
{
|
||||
filtered[count++] = entry;
|
||||
}
|
||||
}
|
||||
else if (cat == Category.Weapons)
|
||||
{
|
||||
if (AnyLeafCraftable(from, system, WeaponLeafCategories, selectedResType))
|
||||
{
|
||||
filtered[count++] = entry;
|
||||
}
|
||||
}
|
||||
else if (cat == Category.Armor)
|
||||
{
|
||||
if (AnyLeafCraftable(from, system, ArmorLeafCategories, selectedResType))
|
||||
{
|
||||
filtered[count++] = entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (count < filtered.Length)
|
||||
{
|
||||
Array.Resize(ref filtered, count);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
private static ItemListEntry[] BuildFilteredMidEntries(
|
||||
Mobile from, ItemListEntry[] staticEntries, Category[] leafCategories, Type selectedResType
|
||||
)
|
||||
{
|
||||
var system = DefBlacksmithy.CraftSystem;
|
||||
var filtered = new ItemListEntry[staticEntries.Length];
|
||||
var count = 0;
|
||||
|
||||
for (var i = 0; i < staticEntries.Length; i++)
|
||||
{
|
||||
var entry = staticEntries[i];
|
||||
var leafCat = (Category)entry.CraftIndex;
|
||||
var types = GetTypes(leafCat);
|
||||
if (types != null && T2ACraftSystem.AnyCraftableInCategory(from, types, system, selectedResType))
|
||||
{
|
||||
filtered[count++] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (count < filtered.Length)
|
||||
{
|
||||
Array.Resize(ref filtered, count);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
private static bool AnyLeafCraftable(
|
||||
Mobile from, CraftSystem system, Category[] leafCategories, Type selectedResType = null
|
||||
)
|
||||
{
|
||||
for (var i = 0; i < leafCategories.Length; i++)
|
||||
{
|
||||
var types = GetTypes(leafCategories[i]);
|
||||
if (types != null && T2ACraftSystem.AnyCraftableInCategory(from, types, system, selectedResType))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void CraftItemFromType(Mobile from, Type itemType)
|
||||
{
|
||||
var itemDef = DefBlacksmithy.CraftSystem.CraftItems.SearchFor(itemType);
|
||||
|
||||
if (itemDef == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var num = DefBlacksmithy.CraftSystem.CanCraft(from, _tool, itemDef.ItemType);
|
||||
if (num > 0)
|
||||
{
|
||||
from.SendLocalizedMessage(num);
|
||||
return;
|
||||
}
|
||||
|
||||
var context = DefBlacksmithy.CraftSystem.GetContext(from);
|
||||
var res = itemDef.UseSubRes2
|
||||
? DefBlacksmithy.CraftSystem.CraftSubRes2
|
||||
: DefBlacksmithy.CraftSystem.CraftSubRes;
|
||||
var resIndex = itemDef.UseSubRes2 ? context.LastResourceIndex2 : context.LastResourceIndex;
|
||||
var type = resIndex > -1 ? res[resIndex].ItemType : null;
|
||||
|
||||
DefBlacksmithy.CraftSystem.CreateItem(from, itemDef.ItemType, type, _tool, itemDef);
|
||||
}
|
||||
|
||||
public override void OnResponse(NetState state, int index)
|
||||
{
|
||||
var from = state.Mobile;
|
||||
var craftIndex = Entries[index].CraftIndex;
|
||||
|
||||
if (_category == Category.Main)
|
||||
{
|
||||
switch (craftIndex)
|
||||
{
|
||||
case 0:
|
||||
Repair.Do(from, DefBlacksmithy.CraftSystem, _tool);
|
||||
return;
|
||||
case 1:
|
||||
Resmelt.Do(from, DefBlacksmithy.CraftSystem, _tool);
|
||||
return;
|
||||
}
|
||||
|
||||
var menu = new BlacksmithMenu(from, _tool, (Category)(craftIndex - MainActionCount));
|
||||
if (menu.Entries.Length == 0)
|
||||
{
|
||||
from.SendAsciiMessage("You lack the skill and materials to craft anything in that category.");
|
||||
return;
|
||||
}
|
||||
|
||||
from.SendMenu(menu);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_category is Category.Weapons or Category.Armor)
|
||||
{
|
||||
var menu = new BlacksmithMenu(from, _tool, (Category)craftIndex);
|
||||
if (menu.Entries.Length == 0)
|
||||
{
|
||||
from.SendAsciiMessage("You lack the skill and materials to craft anything in that category.");
|
||||
return;
|
||||
}
|
||||
|
||||
from.SendMenu(menu);
|
||||
return;
|
||||
}
|
||||
|
||||
var types = GetTypes(_category);
|
||||
if (types != null && craftIndex >= 0 && craftIndex < types.Length)
|
||||
{
|
||||
CraftItemFromType(from, types[craftIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnCancel(NetState state)
|
||||
{
|
||||
base.OnCancel(state);
|
||||
}
|
||||
|
||||
public static void ResourceSelection(
|
||||
Mobile from, BaseTool tool, Action<Mobile, BaseTool> afterSelect, Item preTarget = null
|
||||
)
|
||||
{
|
||||
var res = DefBlacksmithy.CraftSystem.CraftSubRes;
|
||||
|
||||
// Validate preTarget first — reject invalid targets before any auto-selection
|
||||
if (preTarget != null)
|
||||
{
|
||||
if (TrySelectResource(from, preTarget, res, afterSelect, tool))
|
||||
{
|
||||
// Valid ingot — resource selected, menu will open
|
||||
return;
|
||||
}
|
||||
|
||||
if (preTarget is BaseArmor or BaseWeapon
|
||||
&& DefBlacksmithy.CraftSystem.CraftItems.SearchForSubclass(preTarget.GetType()) != null)
|
||||
{
|
||||
// Repairable/smeltable item — auto-select default resource and open menu
|
||||
SelectDefaultResource(from, res);
|
||||
afterSelect(from, tool);
|
||||
return;
|
||||
}
|
||||
|
||||
// Invalid target — prompt for ingots
|
||||
from.SendMessage("Target the ingots you wish to use.");
|
||||
from.Target = new BlacksmithResourceTarget(tool, afterSelect);
|
||||
return;
|
||||
}
|
||||
|
||||
// No target (make-last failure path) — auto-select if only one ingot type available
|
||||
var availableCount = 0;
|
||||
var lastAvailable = -1;
|
||||
|
||||
for (var i = 0; i < res.Count; ++i)
|
||||
{
|
||||
if ((from.Backpack?.GetAmount(res[i].ItemType) ?? 0) > 0)
|
||||
{
|
||||
availableCount++;
|
||||
lastAvailable = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (availableCount <= 1)
|
||||
{
|
||||
var context = DefBlacksmithy.CraftSystem.GetContext(from);
|
||||
if (context != null && lastAvailable != -1)
|
||||
{
|
||||
context.LastResourceIndex = lastAvailable;
|
||||
}
|
||||
|
||||
afterSelect(from, tool);
|
||||
}
|
||||
else
|
||||
{
|
||||
from.SendMessage("Target the ingots you wish to use.");
|
||||
from.Target = new BlacksmithResourceTarget(tool, afterSelect);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SelectDefaultResource(Mobile from, CraftSubResCol res)
|
||||
{
|
||||
var context = DefBlacksmithy.CraftSystem.GetContext(from);
|
||||
if (context == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var pack = from.Backpack;
|
||||
var firstAvailable = -1;
|
||||
|
||||
for (var i = 0; i < res.Count; ++i)
|
||||
{
|
||||
if ((pack?.GetAmount(res[i].ItemType) ?? 0) > 0)
|
||||
{
|
||||
if (res[i].ItemType == typeof(IronIngot))
|
||||
{
|
||||
context.LastResourceIndex = i;
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstAvailable == -1)
|
||||
{
|
||||
firstAvailable = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (firstAvailable != -1)
|
||||
{
|
||||
context.LastResourceIndex = firstAvailable;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TrySelectResource(
|
||||
Mobile from, Item item, CraftSubResCol res, Action<Mobile, BaseTool> afterSelect, BaseTool tool
|
||||
)
|
||||
{
|
||||
for (var i = 0; i < res.Count; ++i)
|
||||
{
|
||||
if (item.GetType() == res[i].ItemType)
|
||||
{
|
||||
var context = DefBlacksmithy.CraftSystem.GetContext(from);
|
||||
if (context != null)
|
||||
{
|
||||
context.LastResourceIndex = i;
|
||||
}
|
||||
|
||||
afterSelect(from, tool);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public class BlacksmithResourceTarget : Target
|
||||
{
|
||||
private readonly BaseTool _tool;
|
||||
private readonly Action<Mobile, BaseTool> _afterSelect;
|
||||
|
||||
public BlacksmithResourceTarget(BaseTool tool, Action<Mobile, BaseTool> afterSelect)
|
||||
: base(2, false, TargetFlags.None)
|
||||
{
|
||||
_tool = tool;
|
||||
_afterSelect = afterSelect;
|
||||
}
|
||||
|
||||
protected override void OnTarget(Mobile from, object targeted)
|
||||
{
|
||||
if (targeted is Item item)
|
||||
{
|
||||
var res = DefBlacksmithy.CraftSystem.CraftSubRes;
|
||||
for (var i = 0; i < res.Count; ++i)
|
||||
{
|
||||
if (item.GetType() == res[i].ItemType)
|
||||
{
|
||||
var context = DefBlacksmithy.CraftSystem.GetContext(from);
|
||||
context?.LastResourceIndex = i;
|
||||
|
||||
_afterSelect(from, _tool);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
from.SendMessage("That is not a valid ingot.");
|
||||
from.Target = new BlacksmithResourceTarget(_tool, _afterSelect);
|
||||
}
|
||||
}
|
||||
95
Projects/UOContent/Engines/Craft/T2A/BowFletchingMenu.cs
Normal file
95
Projects/UOContent/Engines/Craft/T2A/BowFletchingMenu.cs
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
using System;
|
||||
using Server.Items;
|
||||
using Server.Menus.ItemLists;
|
||||
using Server.Network;
|
||||
|
||||
namespace Server.Engines.Craft.T2A;
|
||||
|
||||
public class BowFletchingMenu : ItemListMenu
|
||||
{
|
||||
private static readonly Type[] ItemTypes =
|
||||
[
|
||||
typeof(Kindling), typeof(Shaft), typeof(Arrow), typeof(Bolt),
|
||||
typeof(Bow), typeof(Crossbow), typeof(HeavyCrossbow)
|
||||
];
|
||||
|
||||
private static readonly string[] ItemNames =
|
||||
[
|
||||
"Kindling", "Shafts", "Arrows", "Bolts", "Bow", "Crossbow", "Heavy Crossbow"
|
||||
];
|
||||
|
||||
private static readonly int[] ItemGraphics = [0xDE1, 0x1BD4, 0xF3F, 0x1BFB, 0x13B2, 0xF50, 0x13FD];
|
||||
|
||||
private static ItemListEntry[] _cachedEntries;
|
||||
|
||||
private readonly BaseTool _tool;
|
||||
|
||||
public BowFletchingMenu(Mobile from, BaseTool tool)
|
||||
: base("What would you like to make?", BuildFilteredEntries(from))
|
||||
{
|
||||
_tool = tool;
|
||||
}
|
||||
|
||||
public static ItemListEntry[] Main()
|
||||
{
|
||||
if (_cachedEntries != null)
|
||||
{
|
||||
return _cachedEntries;
|
||||
}
|
||||
|
||||
var entries = new ItemListEntry[ItemTypes.Length];
|
||||
var count = 0;
|
||||
var craftItems = DefBowFletching.CraftSystem.CraftItems;
|
||||
|
||||
for (var i = 0; i < ItemTypes.Length; i++)
|
||||
{
|
||||
if (craftItems.SearchFor(ItemTypes[i]) != null)
|
||||
{
|
||||
entries[count++] = new ItemListEntry(ItemNames[i], ItemGraphics[i], 0, i);
|
||||
}
|
||||
}
|
||||
|
||||
if (count < entries.Length)
|
||||
{
|
||||
Array.Resize(ref entries, count);
|
||||
}
|
||||
|
||||
_cachedEntries = entries;
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static ItemListEntry[] BuildFilteredEntries(Mobile from)
|
||||
{
|
||||
return T2ACraftSystem.FilterEntries(from, Main(), ItemTypes, DefBowFletching.CraftSystem);
|
||||
}
|
||||
|
||||
public override void OnResponse(NetState state, int index)
|
||||
{
|
||||
var from = state.Mobile;
|
||||
var craftIndex = Entries[index].CraftIndex;
|
||||
|
||||
if (craftIndex < 0 || craftIndex >= ItemTypes.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var itemDef = DefBowFletching.CraftSystem.CraftItems.SearchFor(ItemTypes[craftIndex]);
|
||||
if (itemDef == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var num = DefBowFletching.CraftSystem.CanCraft(from, _tool, itemDef.ItemType);
|
||||
if (num > 0)
|
||||
{
|
||||
from.SendLocalizedMessage(num);
|
||||
return;
|
||||
}
|
||||
|
||||
var context = DefBowFletching.CraftSystem.GetContext(from);
|
||||
var res = itemDef.UseSubRes2 ? DefBowFletching.CraftSystem.CraftSubRes2 : DefBowFletching.CraftSystem.CraftSubRes;
|
||||
var resIndex = itemDef.UseSubRes2 ? context.LastResourceIndex2 : context.LastResourceIndex;
|
||||
var type = resIndex > -1 ? res[resIndex].ItemType : null;
|
||||
DefBowFletching.CraftSystem.CreateItem(from, itemDef.ItemType, type, _tool, itemDef);
|
||||
}
|
||||
}
|
||||
298
Projects/UOContent/Engines/Craft/T2A/CarpentryMenu.cs
Normal file
298
Projects/UOContent/Engines/Craft/T2A/CarpentryMenu.cs
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
using System;
|
||||
using Server.Items;
|
||||
using Server.Menus.ItemLists;
|
||||
using Server.Network;
|
||||
|
||||
namespace Server.Engines.Craft.T2A;
|
||||
|
||||
public class CarpentryMenu : ItemListMenu
|
||||
{
|
||||
private enum Category
|
||||
{
|
||||
Main,
|
||||
Furniture,
|
||||
Containers,
|
||||
Weapons,
|
||||
Instruments,
|
||||
Misc,
|
||||
Addons
|
||||
}
|
||||
|
||||
private static readonly Type[] FurnitureTypes =
|
||||
[
|
||||
typeof(FootStool), typeof(Stool), typeof(BambooChair), typeof(WoodenChair),
|
||||
typeof(FancyWoodenChairCushion), typeof(WoodenChairCushion),
|
||||
typeof(WoodenBench), typeof(WoodenThrone), typeof(Throne),
|
||||
typeof(Nightstand), typeof(WritingTable), typeof(YewWoodTable), typeof(LargeTable)
|
||||
];
|
||||
|
||||
private static readonly Type[] ContainerTypes =
|
||||
[
|
||||
typeof(WoodenBox), typeof(SmallCrate), typeof(MediumCrate), typeof(LargeCrate),
|
||||
typeof(WoodenChest), typeof(EmptyBookcase), typeof(FancyArmoire), typeof(Armoire),
|
||||
typeof(Keg)
|
||||
];
|
||||
|
||||
private static readonly Type[] WeaponTypes =
|
||||
[
|
||||
typeof(ShepherdsCrook), typeof(QuarterStaff), typeof(GnarledStaff), typeof(WoodenShield)
|
||||
];
|
||||
|
||||
private static readonly Type[] InstrumentTypes =
|
||||
[
|
||||
typeof(LapHarp), typeof(Harp), typeof(Drums), typeof(Lute),
|
||||
typeof(Tambourine), typeof(TambourineTassel)
|
||||
];
|
||||
|
||||
private static readonly Type[] MiscItemTypes =
|
||||
[
|
||||
typeof(FishingPole), typeof(BarrelStaves), typeof(BarrelLid),
|
||||
typeof(ShortMusicStand), typeof(TallMusicStand), typeof(Easel)
|
||||
];
|
||||
|
||||
private static readonly Type[] AddonTypes =
|
||||
[
|
||||
typeof(SmallBedSouthDeed), typeof(SmallBedEastDeed),
|
||||
typeof(LargeBedSouthDeed), typeof(LargeBedEastDeed),
|
||||
typeof(DartBoardSouthDeed), typeof(DartBoardEastDeed),
|
||||
typeof(BallotBoxDeed),
|
||||
typeof(PentagramDeed), typeof(AbbatoirDeed),
|
||||
typeof(SmallForgeDeed), typeof(LargeForgeEastDeed), typeof(LargeForgeSouthDeed),
|
||||
typeof(AnvilEastDeed), typeof(AnvilSouthDeed),
|
||||
typeof(TrainingDummyEastDeed), typeof(TrainingDummySouthDeed),
|
||||
typeof(PickpocketDipEastDeed), typeof(PickpocketDipSouthDeed),
|
||||
typeof(Dressform),
|
||||
typeof(SpinningWheelEastDeed), typeof(SpinningWheelSouthDeed),
|
||||
typeof(LoomEastDeed), typeof(LoomSouthDeed),
|
||||
typeof(StoneOvenEastDeed), typeof(StoneOvenSouthDeed),
|
||||
typeof(FlourMillEastDeed), typeof(FlourMillSouthDeed),
|
||||
typeof(WaterTroughEastDeed), typeof(WaterTroughSouthDeed)
|
||||
];
|
||||
|
||||
private static ItemListEntry[] _mainEntries;
|
||||
private static ItemListEntry[] _furnitureEntries;
|
||||
private static ItemListEntry[] _containerEntries;
|
||||
private static ItemListEntry[] _weaponEntries;
|
||||
private static ItemListEntry[] _instrumentEntries;
|
||||
private static ItemListEntry[] _miscEntries;
|
||||
private static ItemListEntry[] _addonEntries;
|
||||
|
||||
private readonly Category _category;
|
||||
private readonly BaseTool _tool;
|
||||
|
||||
public CarpentryMenu(Mobile from, BaseTool tool) : this(from, tool, Category.Main)
|
||||
{
|
||||
}
|
||||
|
||||
private static string GetQuestion(Category category) => category switch
|
||||
{
|
||||
Category.Main => "What would you like to make?",
|
||||
Category.Furniture => "What kind of furniture?",
|
||||
Category.Containers => "What kind of container?",
|
||||
Category.Weapons => "What kind of weapon?",
|
||||
Category.Instruments => "What kind of instrument?",
|
||||
Category.Addons => "What kind of add-on?",
|
||||
_ => "What would you like to make?"
|
||||
};
|
||||
|
||||
private CarpentryMenu(Mobile from, BaseTool tool, Category category)
|
||||
: base(GetQuestion(category), BuildFilteredEntries(from, category))
|
||||
{
|
||||
_tool = tool;
|
||||
_category = category;
|
||||
}
|
||||
|
||||
private static string FormatItemName(Type type)
|
||||
{
|
||||
var name = type.Name;
|
||||
Span<char> buffer = stackalloc char[name.Length * 2];
|
||||
var pos = 0;
|
||||
|
||||
for (var i = 0; i < name.Length; i++)
|
||||
{
|
||||
if (i > 0 && char.IsUpper(name[i]))
|
||||
{
|
||||
buffer[pos++] = ' ';
|
||||
}
|
||||
|
||||
buffer[pos++] = char.ToLower(name[i]);
|
||||
}
|
||||
|
||||
return new string(buffer[..pos]);
|
||||
}
|
||||
|
||||
private static ItemListEntry[] BuildStaticEntries(Type[] types, string resourceName)
|
||||
{
|
||||
var entries = new ItemListEntry[types.Length];
|
||||
var count = 0;
|
||||
var craftItems = DefCarpentry.CraftSystem.CraftItems;
|
||||
|
||||
for (var i = 0; i < types.Length; i++)
|
||||
{
|
||||
var itemDef = craftItems.SearchFor(types[i]);
|
||||
if (itemDef == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var name = FormatItemName(types[i]);
|
||||
var res = itemDef.Resources;
|
||||
|
||||
string label;
|
||||
if (res.Count > 1)
|
||||
{
|
||||
var secondName = res[1].ItemType == typeof(IronIngot) ? "ingots" : "cloth";
|
||||
label = $"{name} ({res[0].Amount} {resourceName}, {res[1].Amount} {secondName})";
|
||||
}
|
||||
else
|
||||
{
|
||||
label = $"{name} ({res[0].Amount} {resourceName})";
|
||||
}
|
||||
|
||||
entries[count++] = new ItemListEntry(label, itemDef.ItemId, 0, i);
|
||||
}
|
||||
|
||||
if (count < entries.Length)
|
||||
{
|
||||
Array.Resize(ref entries, count);
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static ItemListEntry[] GetStaticEntries(Category category) => category switch
|
||||
{
|
||||
Category.Main => Main(),
|
||||
Category.Furniture => Furniture(),
|
||||
Category.Containers => Containers(),
|
||||
Category.Weapons => Weapons(),
|
||||
Category.Instruments => Instruments(),
|
||||
Category.Misc => Misc(),
|
||||
Category.Addons => Addons(),
|
||||
_ => null
|
||||
};
|
||||
|
||||
private static Type[] GetTypes(Category category) => category switch
|
||||
{
|
||||
Category.Furniture => FurnitureTypes,
|
||||
Category.Containers => ContainerTypes,
|
||||
Category.Weapons => WeaponTypes,
|
||||
Category.Instruments => InstrumentTypes,
|
||||
Category.Misc => MiscItemTypes,
|
||||
Category.Addons => AddonTypes,
|
||||
_ => null
|
||||
};
|
||||
|
||||
public static ItemListEntry[] Main() => _mainEntries ??=
|
||||
[
|
||||
new ItemListEntry("Furniture", 0xB57, 0, (int)Category.Furniture),
|
||||
new ItemListEntry("Containers", 0x9AA, 0, (int)Category.Containers),
|
||||
new ItemListEntry("Weapons", 0xE89, 0, (int)Category.Weapons),
|
||||
new ItemListEntry("Instruments", 0xEB3, 0, (int)Category.Instruments),
|
||||
new ItemListEntry("Miscellaneous", 0xDC0, 0, (int)Category.Misc),
|
||||
new ItemListEntry("Add-Ons", 0x14F0, 0, (int)Category.Addons)
|
||||
];
|
||||
|
||||
public static ItemListEntry[] Furniture() => _furnitureEntries ??= BuildStaticEntries(FurnitureTypes, "wood");
|
||||
public static ItemListEntry[] Containers() => _containerEntries ??= BuildStaticEntries(ContainerTypes, "wood");
|
||||
public static ItemListEntry[] Weapons() => _weaponEntries ??= BuildStaticEntries(WeaponTypes, "wood");
|
||||
public static ItemListEntry[] Instruments() => _instrumentEntries ??= BuildStaticEntries(InstrumentTypes, "wood");
|
||||
public static ItemListEntry[] Misc() => _miscEntries ??= BuildStaticEntries(MiscItemTypes, "wood");
|
||||
public static ItemListEntry[] Addons() => _addonEntries ??= BuildStaticEntries(AddonTypes, "wood");
|
||||
|
||||
private static ItemListEntry[] BuildFilteredEntries(Mobile from, Category category)
|
||||
{
|
||||
if (category == Category.Main)
|
||||
{
|
||||
return BuildFilteredMainEntries(from);
|
||||
}
|
||||
|
||||
var types = GetTypes(category);
|
||||
var staticEntries = GetStaticEntries(category);
|
||||
if (types == null || staticEntries == null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return T2ACraftSystem.FilterEntries(from, staticEntries, types, DefCarpentry.CraftSystem);
|
||||
}
|
||||
|
||||
private static ItemListEntry[] BuildFilteredMainEntries(Mobile from)
|
||||
{
|
||||
var system = DefCarpentry.CraftSystem;
|
||||
var mainStatic = Main();
|
||||
var filtered = new ItemListEntry[mainStatic.Length];
|
||||
var count = 0;
|
||||
|
||||
for (var i = 0; i < mainStatic.Length; i++)
|
||||
{
|
||||
var entry = mainStatic[i];
|
||||
var types = GetTypes((Category)entry.CraftIndex);
|
||||
if (types != null && T2ACraftSystem.AnyCraftableInCategory(from, types, system))
|
||||
{
|
||||
filtered[count++] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (count < filtered.Length)
|
||||
{
|
||||
Array.Resize(ref filtered, count);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
private void CraftSelectedItem(Mobile from, Type itemType)
|
||||
{
|
||||
var itemDef = DefCarpentry.CraftSystem.CraftItems.SearchFor(itemType);
|
||||
|
||||
if (itemDef == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var num = DefCarpentry.CraftSystem.CanCraft(from, _tool, itemDef.ItemType);
|
||||
|
||||
if (num > 0)
|
||||
{
|
||||
from.SendLocalizedMessage(num);
|
||||
return;
|
||||
}
|
||||
|
||||
var context = DefCarpentry.CraftSystem.GetContext(from);
|
||||
var res = itemDef.UseSubRes2 ? DefCarpentry.CraftSystem.CraftSubRes2 : DefCarpentry.CraftSystem.CraftSubRes;
|
||||
var resIndex = itemDef.UseSubRes2 ? context.LastResourceIndex2 : context.LastResourceIndex;
|
||||
var type = resIndex > -1 ? res[resIndex].ItemType : null;
|
||||
DefCarpentry.CraftSystem.CreateItem(from, itemDef.ItemType, type, _tool, itemDef);
|
||||
}
|
||||
|
||||
public override void OnResponse(NetState state, int index)
|
||||
{
|
||||
var from = state.Mobile;
|
||||
var craftIndex = Entries[index].CraftIndex;
|
||||
|
||||
if (_category == Category.Main)
|
||||
{
|
||||
var menu = new CarpentryMenu(from, _tool, (Category)craftIndex);
|
||||
if (menu.Entries.Length == 0)
|
||||
{
|
||||
from.SendAsciiMessage("You lack the skill and materials to craft anything in that category.");
|
||||
return;
|
||||
}
|
||||
|
||||
from.SendMenu(menu);
|
||||
return;
|
||||
}
|
||||
|
||||
var types = GetTypes(_category);
|
||||
if (types != null && craftIndex >= 0 && craftIndex < types.Length)
|
||||
{
|
||||
CraftSelectedItem(from, types[craftIndex]);
|
||||
}
|
||||
}
|
||||
}
|
||||
110
Projects/UOContent/Engines/Craft/T2A/CartographyMenu.cs
Normal file
110
Projects/UOContent/Engines/Craft/T2A/CartographyMenu.cs
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
using System;
|
||||
using Server.Items;
|
||||
using Server.Menus.ItemLists;
|
||||
using Server.Network;
|
||||
|
||||
namespace Server.Engines.Craft.T2A;
|
||||
|
||||
public class CartographyMenu : ItemListMenu
|
||||
{
|
||||
private static ItemListEntry[] _cachedEntries;
|
||||
|
||||
private readonly BaseTool _tool;
|
||||
|
||||
public CartographyMenu(Mobile from, BaseTool tool)
|
||||
: base("What kind of map?", BuildFilteredEntries(from))
|
||||
{
|
||||
_tool = tool;
|
||||
}
|
||||
|
||||
public static ItemListEntry[] Main()
|
||||
{
|
||||
if (_cachedEntries != null)
|
||||
{
|
||||
return _cachedEntries;
|
||||
}
|
||||
|
||||
var craftItems = DefCartography.CraftSystem.CraftItems;
|
||||
var entries = new ItemListEntry[craftItems.Count];
|
||||
var count = 0;
|
||||
|
||||
for (var i = 0; i < craftItems.Count; i++)
|
||||
{
|
||||
var name = i switch
|
||||
{
|
||||
0 => "A map of the local environs.",
|
||||
1 => "A map suitable for cities.",
|
||||
2 => "A moderately sized sea chart.",
|
||||
3 => "A map of the world.",
|
||||
_ => craftItems[i].ItemType.Name
|
||||
};
|
||||
|
||||
entries[count++] = new ItemListEntry(name, 6511 + i, 0, i);
|
||||
}
|
||||
|
||||
if (count < entries.Length)
|
||||
{
|
||||
Array.Resize(ref entries, count);
|
||||
}
|
||||
|
||||
_cachedEntries = entries;
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static ItemListEntry[] BuildFilteredEntries(Mobile from)
|
||||
{
|
||||
var staticEntries = Main();
|
||||
var craftItems = DefCartography.CraftSystem.CraftItems;
|
||||
|
||||
var filtered = new ItemListEntry[staticEntries.Length];
|
||||
var count = 0;
|
||||
|
||||
for (var i = 0; i < staticEntries.Length; i++)
|
||||
{
|
||||
var entry = staticEntries[i];
|
||||
var craftIndex = entry.CraftIndex;
|
||||
if (craftIndex >= 0 && craftIndex < craftItems.Count)
|
||||
{
|
||||
var itemDef = craftItems[craftIndex];
|
||||
if (T2ACraftSystem.CanCraftItem(from, itemDef, DefCartography.CraftSystem))
|
||||
{
|
||||
filtered[count++] = entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (count < filtered.Length)
|
||||
{
|
||||
Array.Resize(ref filtered, count);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
public override void OnResponse(NetState state, int index)
|
||||
{
|
||||
var from = state.Mobile;
|
||||
var craftIndex = Entries[index].CraftIndex;
|
||||
var craftItems = DefCartography.CraftSystem.CraftItems;
|
||||
|
||||
if (craftIndex < 0 || craftIndex >= craftItems.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var itemDef = craftItems[craftIndex];
|
||||
var num = DefCartography.CraftSystem.CanCraft(from, _tool, itemDef.ItemType);
|
||||
if (num > 0)
|
||||
{
|
||||
from.SendLocalizedMessage(num);
|
||||
return;
|
||||
}
|
||||
|
||||
DefCartography.CraftSystem.CreateItem(from, itemDef.ItemType, typeof(BlankMap), _tool, itemDef);
|
||||
}
|
||||
}
|
||||
334
Projects/UOContent/Engines/Craft/T2A/InscriptionMenu.cs
Normal file
334
Projects/UOContent/Engines/Craft/T2A/InscriptionMenu.cs
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Server.Items;
|
||||
using Server.Menus.ItemLists;
|
||||
using Server.Network;
|
||||
|
||||
namespace Server.Engines.Craft.T2A;
|
||||
|
||||
public class InscriptionMenu : ItemListMenu
|
||||
{
|
||||
private enum Category
|
||||
{
|
||||
Main,
|
||||
Circle1,
|
||||
Circle2,
|
||||
Circle3,
|
||||
Circle4,
|
||||
Circle5,
|
||||
Circle6,
|
||||
Circle7,
|
||||
Circle8,
|
||||
Runebook
|
||||
}
|
||||
|
||||
private static readonly ItemListEntry[][] _circleEntries = new ItemListEntry[8][];
|
||||
|
||||
private static ItemListEntry[] _mainEntries;
|
||||
|
||||
private readonly Category _category;
|
||||
private readonly BaseTool _tool;
|
||||
|
||||
public InscriptionMenu(Mobile from, BaseTool tool) : this(from, tool, Category.Main)
|
||||
{
|
||||
}
|
||||
|
||||
private static string GetQuestion(Category category) => category switch
|
||||
{
|
||||
Category.Main => "Which circle of spells?",
|
||||
_ => "Which spell would you like to scribe?"
|
||||
};
|
||||
|
||||
private InscriptionMenu(Mobile from, BaseTool tool, Category category)
|
||||
: base(GetQuestion(category), BuildFilteredEntries(from, category))
|
||||
{
|
||||
_tool = tool;
|
||||
_category = category;
|
||||
}
|
||||
|
||||
private static string FormatScrollName(Type type)
|
||||
{
|
||||
var name = type.Name;
|
||||
if (name.EndsWith("Scroll"))
|
||||
{
|
||||
name = name[..^6];
|
||||
}
|
||||
|
||||
Span<char> buffer = stackalloc char[name.Length * 2];
|
||||
var pos = 0;
|
||||
for (var i = 0; i < name.Length; i++)
|
||||
{
|
||||
if (i > 0 && char.IsUpper(name[i]))
|
||||
{
|
||||
buffer[pos++] = ' ';
|
||||
}
|
||||
|
||||
buffer[pos++] = char.ToLower(name[i]);
|
||||
}
|
||||
|
||||
return new string(buffer[..pos]);
|
||||
}
|
||||
|
||||
public static ItemListEntry[] Main() => _mainEntries ??=
|
||||
[
|
||||
new ItemListEntry("Runebook", 0xEFA, 0, (int)Category.Runebook),
|
||||
new ItemListEntry("First Circle", 8384, 0, (int)Category.Circle1),
|
||||
new ItemListEntry("Second Circle", 8385, 0, (int)Category.Circle2),
|
||||
new ItemListEntry("Third Circle", 8386, 0, (int)Category.Circle3),
|
||||
new ItemListEntry("Fourth Circle", 8387, 0, (int)Category.Circle4),
|
||||
new ItemListEntry("Fifth Circle", 8388, 0, (int)Category.Circle5),
|
||||
new ItemListEntry("Sixth Circle", 8389, 0, (int)Category.Circle6),
|
||||
new ItemListEntry("Seventh Circle", 8390, 0, (int)Category.Circle7),
|
||||
new ItemListEntry("Eighth Circle", 8391, 0, (int)Category.Circle8)
|
||||
];
|
||||
|
||||
private static ItemListEntry[] BuildCircleEntries(int circleIndex)
|
||||
{
|
||||
var offset = circleIndex * 8;
|
||||
var craftItems = DefInscription.CraftSystem.CraftItems;
|
||||
var entries = new ItemListEntry[8];
|
||||
var count = 0;
|
||||
|
||||
for (var i = 0; i < 8; i++)
|
||||
{
|
||||
var itemDef = craftItems[offset + i];
|
||||
var name = FormatScrollName(itemDef.ItemType);
|
||||
entries[count++] = new ItemListEntry(name, 8320 + offset + i, 0, i);
|
||||
}
|
||||
|
||||
if (count < entries.Length)
|
||||
{
|
||||
Array.Resize(ref entries, count);
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static ItemListEntry[] GetCircleEntries(int circleIndex) =>
|
||||
_circleEntries[circleIndex] ??= BuildCircleEntries(circleIndex);
|
||||
|
||||
private static Dictionary<Type, int> _spellIds;
|
||||
private static object[] _args;
|
||||
|
||||
private static bool HasSpellInBook(Mobile from, Type scrollType)
|
||||
{
|
||||
if (scrollType == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_spellIds ??= [];
|
||||
if (!_spellIds.TryGetValue(scrollType, out var spellId))
|
||||
{
|
||||
try
|
||||
{
|
||||
_args ??= [1];
|
||||
var scroll = scrollType.CreateInstance<SpellScroll>(_args);
|
||||
if (scroll != null)
|
||||
{
|
||||
spellId = _spellIds[scrollType] = scroll.SpellID;
|
||||
scroll.Delete();
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var book = Spellbook.Find(from, spellId);
|
||||
return book?.HasSpell(spellId) ?? false;
|
||||
}
|
||||
|
||||
private static bool CanScribeScroll(Mobile from, CraftItem itemDef)
|
||||
{
|
||||
// Skill check
|
||||
if (!T2ACraftSystem.CanCraftItem(from, itemDef, DefInscription.CraftSystem))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must have blank scrolls
|
||||
if ((from.Backpack?.GetAmount(typeof(BlankScroll)) ?? 0) == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must have the spell in spellbook
|
||||
return HasSpellInBook(from, itemDef.ItemType);
|
||||
}
|
||||
|
||||
private static ItemListEntry[] BuildFilteredEntries(Mobile from, Category category)
|
||||
{
|
||||
if (category == Category.Main)
|
||||
{
|
||||
return BuildFilteredMainEntries(from);
|
||||
}
|
||||
|
||||
var circleIndex = (int)category - 1;
|
||||
var staticEntries = GetCircleEntries(circleIndex);
|
||||
var craftItems = DefInscription.CraftSystem.CraftItems;
|
||||
var offset = circleIndex * 8;
|
||||
|
||||
var filtered = new ItemListEntry[staticEntries.Length];
|
||||
var count = 0;
|
||||
|
||||
for (var i = 0; i < staticEntries.Length; i++)
|
||||
{
|
||||
var entry = staticEntries[i];
|
||||
var scrollIndex = entry.CraftIndex;
|
||||
var itemDef = craftItems[offset + scrollIndex];
|
||||
if (CanScribeScroll(from, itemDef))
|
||||
{
|
||||
filtered[count++] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (count < filtered.Length)
|
||||
{
|
||||
Array.Resize(ref filtered, count);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
private static ItemListEntry[] BuildFilteredMainEntries(Mobile from)
|
||||
{
|
||||
var craftItems = DefInscription.CraftSystem.CraftItems;
|
||||
var system = DefInscription.CraftSystem;
|
||||
var mainStatic = Main();
|
||||
var filtered = new ItemListEntry[mainStatic.Length];
|
||||
var count = 0;
|
||||
|
||||
for (var i = 0; i < mainStatic.Length; i++)
|
||||
{
|
||||
var entry = mainStatic[i];
|
||||
var category = (Category)entry.CraftIndex;
|
||||
|
||||
if (category == Category.Runebook)
|
||||
{
|
||||
if (T2ACraftSystem.CanCraftItem(from, typeof(Runebook), system))
|
||||
{
|
||||
filtered[count++] = entry;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var circleIndex = (int)category - 1;
|
||||
var offset = circleIndex * 8;
|
||||
|
||||
var hasAny = false;
|
||||
for (var j = 0; j < 8; j++)
|
||||
{
|
||||
var itemDef = craftItems[offset + j];
|
||||
if (CanScribeScroll(from, itemDef))
|
||||
{
|
||||
hasAny = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasAny)
|
||||
{
|
||||
filtered[count++] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (count < filtered.Length)
|
||||
{
|
||||
Array.Resize(ref filtered, count);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
private void CraftScroll(Mobile from, Category circle, int scrollIndex)
|
||||
{
|
||||
var itemIndex = ((int)circle - 1) * 8 + scrollIndex;
|
||||
var craftItems = DefInscription.CraftSystem.CraftItems;
|
||||
var itemDef = craftItems[itemIndex];
|
||||
|
||||
if (!HasSpellInBook(from, itemDef.ItemType))
|
||||
{
|
||||
from.SendAsciiMessage("You do not have that spell in your spellbook.");
|
||||
return;
|
||||
}
|
||||
|
||||
var context = DefInscription.CraftSystem.GetContext(from);
|
||||
var res = itemDef.UseSubRes2 ? DefInscription.CraftSystem.CraftSubRes2 : DefInscription.CraftSystem.CraftSubRes;
|
||||
var resIndex = itemDef.UseSubRes2 ? context.LastResourceIndex2 : context.LastResourceIndex;
|
||||
var type = resIndex > -1 ? res[resIndex].ItemType : null;
|
||||
DefInscription.CraftSystem.CreateItem(from, itemDef.ItemType, type, _tool, itemDef);
|
||||
}
|
||||
|
||||
public override void OnResponse(NetState state, int index)
|
||||
{
|
||||
var from = state.Mobile;
|
||||
|
||||
if ((from.Backpack?.GetAmount(typeof(BlankScroll)) ?? 0) == 0)
|
||||
{
|
||||
from.SendAsciiMessage("You do not have enough blank scrolls to make that.");
|
||||
return;
|
||||
}
|
||||
|
||||
var craftIndex = Entries[index].CraftIndex;
|
||||
if (_category == Category.Main)
|
||||
{
|
||||
var category = (Category)craftIndex;
|
||||
|
||||
if (category == Category.Runebook)
|
||||
{
|
||||
CraftRunebook(from);
|
||||
return;
|
||||
}
|
||||
|
||||
var menu = new InscriptionMenu(from, _tool, category);
|
||||
if (menu.Entries.Length == 0)
|
||||
{
|
||||
from.SendAsciiMessage("You lack the skill and materials to scribe anything in that circle.");
|
||||
return;
|
||||
}
|
||||
|
||||
from.SendMenu(menu);
|
||||
return;
|
||||
}
|
||||
|
||||
CraftScroll(from, _category, craftIndex);
|
||||
}
|
||||
|
||||
private void CraftRunebook(Mobile from)
|
||||
{
|
||||
var system = DefInscription.CraftSystem;
|
||||
var itemDef = system.CraftItems.SearchFor(typeof(Runebook));
|
||||
if (itemDef == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var num = system.CanCraft(from, _tool, itemDef.ItemType);
|
||||
if (num > 0)
|
||||
{
|
||||
from.SendLocalizedMessage(num);
|
||||
return;
|
||||
}
|
||||
|
||||
system.CreateItem(from, itemDef.ItemType, null, _tool, itemDef);
|
||||
}
|
||||
}
|
||||
351
Projects/UOContent/Engines/Craft/T2A/T2ACraftSystem.cs
Normal file
351
Projects/UOContent/Engines/Craft/T2A/T2ACraftSystem.cs
Normal file
|
|
@ -0,0 +1,351 @@
|
|||
// T2A crafting system: packet-based, not gump-based
|
||||
|
||||
using System;
|
||||
using Server.Items;
|
||||
using Server.Menus.ItemLists;
|
||||
using Server.Targeting;
|
||||
|
||||
namespace Server.Engines.Craft.T2A;
|
||||
|
||||
public static class T2ACraftSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether T2A packet-based crafting menus are active. Set once at startup from the
|
||||
/// "t2aCraftMenus" server setting (default: !Core.UOTD). Not flippable at runtime.
|
||||
/// </summary>
|
||||
public static bool Enabled { get; set; }
|
||||
|
||||
public static void ShowMenu(Mobile from, CraftSystem craftSystem, BaseTool tool, Item preTarget = null)
|
||||
{
|
||||
if (!Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (craftSystem == DefBlacksmithy.CraftSystem)
|
||||
{
|
||||
// Lost Lands flow: resource selection first, then menu
|
||||
BlacksmithMenu.ResourceSelection(from, tool, (mob, t) =>
|
||||
{
|
||||
var menu = new BlacksmithMenu(mob, t);
|
||||
if (menu.Entries.Length == 0)
|
||||
{
|
||||
mob.SendAsciiMessage("You lack the skill and materials to craft anything.");
|
||||
return;
|
||||
}
|
||||
|
||||
mob.SendMenu(menu);
|
||||
}, preTarget);
|
||||
}
|
||||
else if (craftSystem == DefAlchemy.CraftSystem)
|
||||
{
|
||||
if (preTarget is BaseReagent or Bottle || preTarget == null)
|
||||
{
|
||||
ShowMenuDirect<AlchemyMenu>(from, tool);
|
||||
}
|
||||
else
|
||||
{
|
||||
PromptForResource(from, tool, craftSystem, "Target a reagent or empty bottle.",
|
||||
item => item is BaseReagent or Bottle);
|
||||
}
|
||||
}
|
||||
else if (craftSystem == DefBowFletching.CraftSystem)
|
||||
{
|
||||
if (preTarget is Log or Board or Feather or Shaft || preTarget == null)
|
||||
{
|
||||
ShowMenuDirect<BowFletchingMenu>(from, tool);
|
||||
}
|
||||
else
|
||||
{
|
||||
PromptForResource(from, tool, craftSystem, "Target the wood or feathers you wish to use.",
|
||||
item => item is Log or Board or Feather or Shaft);
|
||||
}
|
||||
}
|
||||
else if (craftSystem == DefCarpentry.CraftSystem)
|
||||
{
|
||||
if (preTarget is Log or Board || preTarget == null)
|
||||
{
|
||||
ShowMenuDirect<CarpentryMenu>(from, tool);
|
||||
}
|
||||
else
|
||||
{
|
||||
PromptForResource(from, tool, craftSystem, "Target the wood you wish to use.",
|
||||
item => item is Log or Board);
|
||||
}
|
||||
}
|
||||
else if (craftSystem == DefCartography.CraftSystem)
|
||||
{
|
||||
if (preTarget is BlankMap || preTarget == null)
|
||||
{
|
||||
ShowMenuDirect<CartographyMenu>(from, tool);
|
||||
}
|
||||
else
|
||||
{
|
||||
PromptForResource(from, tool, craftSystem, "Target a blank map.",
|
||||
item => item is BlankMap);
|
||||
}
|
||||
}
|
||||
else if (craftSystem == DefInscription.CraftSystem)
|
||||
{
|
||||
if (preTarget is BlankScroll or BaseReagent or RecallRune || preTarget == null)
|
||||
{
|
||||
ShowMenuDirect<InscriptionMenu>(from, tool);
|
||||
}
|
||||
else
|
||||
{
|
||||
PromptForResource(from, tool, craftSystem, "Target the blank scrolls you wish to use.",
|
||||
item => item is BlankScroll or BaseReagent or RecallRune);
|
||||
}
|
||||
}
|
||||
else if (craftSystem == DefTailoring.CraftSystem)
|
||||
{
|
||||
TailoringMenu.ResourceSelection(from, tool, preTarget);
|
||||
}
|
||||
else if (craftSystem == DefTinkering.CraftSystem)
|
||||
{
|
||||
TinkeringMenu.ResourceSelection(from, tool, preTarget);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ShowMenuDirect<T>(Mobile from, BaseTool tool) where T : ItemListMenu
|
||||
{
|
||||
var menu = (T)Activator.CreateInstance(typeof(T), from, tool);
|
||||
if (menu.Entries.Length == 0)
|
||||
{
|
||||
from.SendAsciiMessage("You lack the skill and materials to craft anything.");
|
||||
return;
|
||||
}
|
||||
|
||||
from.SendMenu(menu);
|
||||
}
|
||||
|
||||
private static void PromptForResource(
|
||||
Mobile from, BaseTool tool, CraftSystem system, string message, Func<Item, bool> isValid
|
||||
)
|
||||
{
|
||||
from.SendAsciiMessage(message);
|
||||
from.Target = new CraftResourceTarget(tool, system, message, isValid);
|
||||
}
|
||||
|
||||
private class CraftResourceTarget : Target
|
||||
{
|
||||
private readonly BaseTool _tool;
|
||||
private readonly CraftSystem _system;
|
||||
private readonly string _message;
|
||||
private readonly Func<Item, bool> _isValid;
|
||||
|
||||
public CraftResourceTarget(
|
||||
BaseTool tool, CraftSystem system, string message, Func<Item, bool> isValid
|
||||
) : base(12, false, TargetFlags.None)
|
||||
{
|
||||
_tool = tool;
|
||||
_system = system;
|
||||
_message = message;
|
||||
_isValid = isValid;
|
||||
}
|
||||
|
||||
protected override void OnTarget(Mobile from, object targeted)
|
||||
{
|
||||
if (targeted is Item item && _isValid(item))
|
||||
{
|
||||
ShowMenu(from, _system, _tool, item);
|
||||
return;
|
||||
}
|
||||
|
||||
from.SendAsciiMessage(_message);
|
||||
from.Target = new CraftResourceTarget(_tool, _system, _message, _isValid);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Persists the selected resource type as a LastResourceIndex on the craft context,
|
||||
/// so that make-last can recall which resource was used.
|
||||
/// </summary>
|
||||
public static void SetLastResourceIndex(Mobile from, CraftSystem system, Type selectedResourceType)
|
||||
{
|
||||
if (selectedResourceType == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var context = system.GetContext(from);
|
||||
var resCol = system.CraftSubRes;
|
||||
|
||||
if (context == null || !resCol.Init)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < resCol.Count; i++)
|
||||
{
|
||||
if (resCol[i].ItemType == selectedResourceType)
|
||||
{
|
||||
context.LastResourceIndex = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a player can craft a specific item, accounting for sub-resource types.
|
||||
/// When <paramref name="selectedResourceType"/> is non-null, checks against that specific sub-resource.
|
||||
/// When null, checks against ANY available sub-resource the player has sufficient skill and materials for.
|
||||
/// </summary>
|
||||
public static bool CanCraftItem(
|
||||
Mobile from, CraftItem itemDef, CraftSystem system, Type selectedResourceType = null
|
||||
)
|
||||
{
|
||||
var pack = from.Backpack;
|
||||
if (pack == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var chance = itemDef.GetSuccessChance(from, selectedResourceType, system, false, out var allRequiredSkills);
|
||||
if (!allRequiredSkills || chance <= 0.0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var resCol = system.CraftSubRes;
|
||||
|
||||
for (var i = 0; i < itemDef.Resources.Count; i++)
|
||||
{
|
||||
var res = itemDef.Resources[i];
|
||||
var resType = res.ItemType;
|
||||
|
||||
// If this resource is the base sub-resource type (e.g. IronIngot for blacksmithing),
|
||||
// handle sub-resource substitution
|
||||
if (resCol.Init && resType == resCol.ResType)
|
||||
{
|
||||
if (selectedResourceType != null)
|
||||
{
|
||||
// Specific resource selected — check skill gate for this sub-resource
|
||||
var subRes = resCol.SearchFor(selectedResourceType);
|
||||
if (subRes != null && from.Skills[system.MainSkill].Value < subRes.RequiredSkill)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if player has enough of the selected resource
|
||||
if (GetResourceAmount(pack, selectedResourceType) < res.Amount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (!HasAnySufficientSubResource(from, pack, res.Amount, system, resCol))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (GetResourceAmount(pack, resType) < res.Amount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convenience overload that resolves a Type to a CraftItem first.
|
||||
/// </summary>
|
||||
public static bool CanCraftItem(Mobile from, Type itemType, CraftSystem system, Type selectedResourceType = null)
|
||||
{
|
||||
var itemDef = system.CraftItems.SearchFor(itemType);
|
||||
return itemDef != null && CanCraftItem(from, itemDef, system, selectedResourceType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters static template entries to only those the player can craft.
|
||||
/// </summary>
|
||||
public static ItemListEntry[] FilterEntries(
|
||||
Mobile from, ItemListEntry[] staticEntries, Type[] types, CraftSystem system, Type selectedResourceType = null
|
||||
)
|
||||
{
|
||||
var filtered = new ItemListEntry[staticEntries.Length];
|
||||
var count = 0;
|
||||
|
||||
for (var i = 0; i < staticEntries.Length; i++)
|
||||
{
|
||||
var entry = staticEntries[i];
|
||||
var typeIndex = entry.CraftIndex;
|
||||
if (typeIndex >= 0 && typeIndex < types.Length &&
|
||||
CanCraftItem(from, types[typeIndex], system, selectedResourceType))
|
||||
{
|
||||
filtered[count++] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (count < filtered.Length)
|
||||
{
|
||||
Array.Resize(ref filtered, count);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if at least one item in the type array is craftable.
|
||||
/// </summary>
|
||||
public static bool AnyCraftableInCategory(
|
||||
Mobile from, Type[] types, CraftSystem system, Type selectedResourceType = null
|
||||
)
|
||||
{
|
||||
for (var i = 0; i < types.Length; i++)
|
||||
{
|
||||
if (CanCraftItem(from, types[i], system, selectedResourceType))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Equivalent type pairs mirroring CraftItem.m_TypesTable — used so that menu filtering
|
||||
/// counts boards when checking for logs, hides when checking for leather, etc.
|
||||
/// </summary>
|
||||
private static readonly Type[][] _equivalentTypes =
|
||||
[
|
||||
[typeof(Log), typeof(Board)],
|
||||
[typeof(Cloth), typeof(UncutCloth)],
|
||||
[typeof(Items.Leather), typeof(Hides)]
|
||||
];
|
||||
|
||||
private static int GetResourceAmount(Container pack, Type type)
|
||||
{
|
||||
for (var i = 0; i < _equivalentTypes.Length; i++)
|
||||
{
|
||||
if (_equivalentTypes[i][0] == type)
|
||||
{
|
||||
return pack.GetAmount(_equivalentTypes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return pack.GetAmount(type);
|
||||
}
|
||||
|
||||
private static bool HasAnySufficientSubResource(
|
||||
Mobile from, Container pack, int amountNeeded, CraftSystem system, CraftSubResCol resCol
|
||||
)
|
||||
{
|
||||
for (var j = 0; j < resCol.Count; j++)
|
||||
{
|
||||
var subRes = resCol[j];
|
||||
if (from.Skills[system.MainSkill].Value >= subRes.RequiredSkill &&
|
||||
GetResourceAmount(pack, subRes.ItemType) >= amountNeeded)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
71
Projects/UOContent/Engines/Craft/T2A/T2ACraftToolTarget.cs
Normal file
71
Projects/UOContent/Engines/Craft/T2A/T2ACraftToolTarget.cs
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
using Server.Items;
|
||||
using Server.Targeting;
|
||||
|
||||
namespace Server.Engines.Craft.T2A;
|
||||
|
||||
public class T2ACraftToolTarget : Target
|
||||
{
|
||||
private readonly BaseTool _tool;
|
||||
private readonly CraftSystem _system;
|
||||
|
||||
public T2ACraftToolTarget(BaseTool tool, CraftSystem system) : base(2, false, TargetFlags.None)
|
||||
{
|
||||
_tool = tool;
|
||||
_system = system;
|
||||
}
|
||||
|
||||
protected override void OnTarget(Mobile from, object targeted)
|
||||
{
|
||||
if (targeted == _tool)
|
||||
{
|
||||
// Make Last: repeat last crafted item
|
||||
var context = _system.GetContext(from);
|
||||
var lastMade = context?.LastMade;
|
||||
|
||||
if (lastMade != null)
|
||||
{
|
||||
var num = _system.CanCraft(from, _tool, lastMade.ItemType);
|
||||
|
||||
if (num > 0)
|
||||
{
|
||||
from.SendLocalizedMessage(num);
|
||||
return;
|
||||
}
|
||||
|
||||
var res = lastMade.UseSubRes2 ? _system.CraftSubRes2 : _system.CraftSubRes;
|
||||
var resIndex = lastMade.UseSubRes2 ? context.LastResourceIndex2 : context.LastResourceIndex;
|
||||
var type = resIndex > -1 ? res[resIndex].ItemType : null;
|
||||
|
||||
// Jewelry requires gem targeting — re-prompt instead of crafting directly
|
||||
if (typeof(BaseJewel).IsAssignableFrom(lastMade.ItemType))
|
||||
{
|
||||
context.PendingGemType = GemType.None;
|
||||
context.PendingGemCount = 0;
|
||||
from.SendAsciiMessage("Target the gemstone you wish to use.");
|
||||
from.Target = new TinkeringMenu.GemSelectTarget(from, _tool, lastMade.ItemType, type);
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.LastHue >= 0)
|
||||
{
|
||||
_system.CreateItem(from, lastMade.ItemType, type, _tool, lastMade, context.LastHue);
|
||||
}
|
||||
else
|
||||
{
|
||||
_system.CreateItem(from, lastMade.ItemType, type, _tool, lastMade);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
from.SendAsciiMessage("You have not yet crafted anything.");
|
||||
T2ACraftSystem.ShowMenu(from, _system, _tool);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Normal flow: pass the targeted object through so resource selection
|
||||
// can use it directly instead of requiring a second target.
|
||||
T2ACraftSystem.ShowMenu(from, _system, _tool, targeted as Item);
|
||||
}
|
||||
}
|
||||
}
|
||||
375
Projects/UOContent/Engines/Craft/T2A/TailoringMenu.cs
Normal file
375
Projects/UOContent/Engines/Craft/T2A/TailoringMenu.cs
Normal file
|
|
@ -0,0 +1,375 @@
|
|||
using System;
|
||||
using Server.Items;
|
||||
using Server.Menus.ItemLists;
|
||||
using Server.Network;
|
||||
using Server.Targeting;
|
||||
|
||||
namespace Server.Engines.Craft.T2A;
|
||||
|
||||
public class TailoringMenu : ItemListMenu
|
||||
{
|
||||
private enum Category
|
||||
{
|
||||
Main,
|
||||
LeatherMain,
|
||||
Hats,
|
||||
Shirts,
|
||||
Pants,
|
||||
Misc,
|
||||
Footwear,
|
||||
Leather,
|
||||
Studded,
|
||||
Female
|
||||
}
|
||||
|
||||
private static readonly Type[] HatsTypes =
|
||||
[
|
||||
typeof(SkullCap), typeof(Bandana), typeof(FloppyHat), typeof(Cap), typeof(WideBrimHat),
|
||||
typeof(StrawHat), typeof(TallStrawHat), typeof(WizardsHat), typeof(Bonnet),
|
||||
typeof(FeatheredHat), typeof(TricorneHat), typeof(JesterHat)
|
||||
];
|
||||
|
||||
private static readonly Type[] ShirtsTypes =
|
||||
[
|
||||
typeof(Doublet), typeof(Shirt), typeof(FancyShirt), typeof(Tunic), typeof(Surcoat), typeof(PlainDress)
|
||||
];
|
||||
|
||||
private static readonly Type[] PantsTypes = [typeof(ShortPants), typeof(LongPants), typeof(Kilt) ];
|
||||
|
||||
private static readonly Type[] MiscTypes =
|
||||
[
|
||||
typeof(Skirt), typeof(Cloak), typeof(Robe), typeof(JesterSuit), typeof(FancyDress),
|
||||
typeof(BodySash), typeof(HalfApron), typeof(FullApron)
|
||||
];
|
||||
|
||||
private static readonly Type[] FootwearTypes = [typeof(Sandals), typeof(Shoes), typeof(Boots), typeof(ThighBoots)];
|
||||
|
||||
private static readonly Type[] LeatherArmorTypes =
|
||||
[
|
||||
typeof(LeatherChest), typeof(LeatherGorget), typeof(LeatherGloves), typeof(LeatherCap),
|
||||
typeof(LeatherArms), typeof(LeatherLegs)
|
||||
];
|
||||
|
||||
private static readonly Type[] StuddedArmorTypes =
|
||||
[
|
||||
typeof(StuddedChest), typeof(StuddedGorget), typeof(StuddedGloves), typeof(StuddedArms), typeof(StuddedLegs)
|
||||
];
|
||||
|
||||
private static readonly Type[] FemaleArmorTypes =
|
||||
[
|
||||
typeof(FemaleLeatherChest), typeof(FemaleStuddedChest), typeof(LeatherBustierArms), typeof(StuddedBustierArms),
|
||||
typeof(FemalePlateChest), typeof(LeatherShorts), typeof(LeatherSkirt)
|
||||
];
|
||||
|
||||
private static ItemListEntry[] _mainEntries;
|
||||
private static ItemListEntry[] _leatherMainEntries;
|
||||
private static ItemListEntry[] _hatsEntries;
|
||||
private static ItemListEntry[] _shirtsEntries;
|
||||
private static ItemListEntry[] _pantsEntries;
|
||||
private static ItemListEntry[] _miscEntries;
|
||||
private static ItemListEntry[] _footwearEntries;
|
||||
private static ItemListEntry[] _leatherEntries;
|
||||
private static ItemListEntry[] _studdedEntries;
|
||||
private static ItemListEntry[] _femaleEntries;
|
||||
|
||||
private readonly Category _category;
|
||||
private readonly BaseTool _tool;
|
||||
private readonly int _hue;
|
||||
|
||||
private static string GetQuestion(Category category) => category switch
|
||||
{
|
||||
Category.Main => "What would you like to make?",
|
||||
Category.LeatherMain => "What would you like to make?",
|
||||
Category.Hats => "What kind of hat?",
|
||||
Category.Shirts => "What kind of shirt?",
|
||||
Category.Pants => "What kind of pants?",
|
||||
Category.Misc => "What would you like to make?",
|
||||
Category.Footwear => "What kind of footwear?",
|
||||
Category.Leather => "What kind of leather armor?",
|
||||
Category.Studded => "What kind of studded armor?",
|
||||
Category.Female => "What kind of female leather?",
|
||||
_ => "What would you like to make?"
|
||||
};
|
||||
|
||||
private TailoringMenu(Mobile from, BaseTool tool, Category category, int hue = -1)
|
||||
: base(GetQuestion(category), BuildFilteredEntries(from, category))
|
||||
{
|
||||
_tool = tool;
|
||||
_category = category;
|
||||
_hue = hue;
|
||||
}
|
||||
|
||||
private static string FormatItemName(Type type)
|
||||
{
|
||||
var name = type.Name;
|
||||
Span<char> buffer = stackalloc char[name.Length * 2];
|
||||
var pos = 0;
|
||||
|
||||
for (var i = 0; i < name.Length; i++)
|
||||
{
|
||||
if (i > 0 && char.IsUpper(name[i]))
|
||||
{
|
||||
buffer[pos++] = ' ';
|
||||
}
|
||||
|
||||
buffer[pos++] = char.ToLower(name[i]);
|
||||
}
|
||||
|
||||
return new string(buffer[..pos]);
|
||||
}
|
||||
|
||||
private static ItemListEntry[] BuildStaticEntries(Type[] types, string resourceName)
|
||||
{
|
||||
var entries = new ItemListEntry[types.Length];
|
||||
var count = 0;
|
||||
var craftItems = DefTailoring.CraftSystem.CraftItems;
|
||||
|
||||
for (var i = 0; i < types.Length; i++)
|
||||
{
|
||||
var itemDef = craftItems.SearchFor(types[i]);
|
||||
if (itemDef == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var name = FormatItemName(types[i]);
|
||||
var res = itemDef.Resources[0];
|
||||
entries[count++] = new ItemListEntry($"{name} ({res.Amount} {resourceName})", itemDef.ItemId, 0, i);
|
||||
}
|
||||
|
||||
if (count < entries.Length)
|
||||
{
|
||||
Array.Resize(ref entries, count);
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static ItemListEntry[] GetStaticEntries(Category category) => category switch
|
||||
{
|
||||
Category.Main => Main(),
|
||||
Category.LeatherMain => LeatherMain(),
|
||||
Category.Hats => Hats(),
|
||||
Category.Shirts => Shirts(),
|
||||
Category.Pants => Pants(),
|
||||
Category.Misc => Misc(),
|
||||
Category.Footwear => Footwear(),
|
||||
Category.Leather => Leather(),
|
||||
Category.Studded => Studded(),
|
||||
Category.Female => Female(),
|
||||
_ => null
|
||||
};
|
||||
|
||||
private static Type[] GetTypes(Category category) => category switch
|
||||
{
|
||||
Category.Hats => HatsTypes,
|
||||
Category.Shirts => ShirtsTypes,
|
||||
Category.Pants => PantsTypes,
|
||||
Category.Misc => MiscTypes,
|
||||
Category.Footwear => FootwearTypes,
|
||||
Category.Leather => LeatherArmorTypes,
|
||||
Category.Studded => StuddedArmorTypes,
|
||||
Category.Female => FemaleArmorTypes,
|
||||
_ => null
|
||||
};
|
||||
|
||||
public static ItemListEntry[] Main() => _mainEntries ??=
|
||||
[
|
||||
new ItemListEntry("Build Hats", 0x1718, 0, (int)Category.Hats),
|
||||
new ItemListEntry("Build Shirts", 0x1517, 0, (int)Category.Shirts),
|
||||
new ItemListEntry("Build Pants", 0x1539, 0, (int)Category.Pants),
|
||||
new ItemListEntry("Build Misc", 0x153D, 0, (int)Category.Misc)
|
||||
];
|
||||
|
||||
public static ItemListEntry[] LeatherMain() => _leatherMainEntries ??=
|
||||
[
|
||||
new ItemListEntry("Build Shoes", 0x170f, 0, (int)Category.Footwear),
|
||||
new ItemListEntry("Build Leather Armor", 0x13cc, 0, (int)Category.Leather),
|
||||
new ItemListEntry("Build Studded Armor", 0x13db, 0, (int)Category.Studded),
|
||||
new ItemListEntry("Build Female Armor", 0x1c06, 0, (int)Category.Female)
|
||||
];
|
||||
|
||||
public static ItemListEntry[] Hats() => _hatsEntries ??= BuildStaticEntries(HatsTypes, "cloth");
|
||||
public static ItemListEntry[] Shirts() => _shirtsEntries ??= BuildStaticEntries(ShirtsTypes, "cloth");
|
||||
public static ItemListEntry[] Pants() => _pantsEntries ??= BuildStaticEntries(PantsTypes, "cloth");
|
||||
public static ItemListEntry[] Misc() => _miscEntries ??= BuildStaticEntries(MiscTypes, "cloth");
|
||||
public static ItemListEntry[] Footwear() => _footwearEntries ??= BuildStaticEntries(FootwearTypes, "leather");
|
||||
public static ItemListEntry[] Leather() => _leatherEntries ??= BuildStaticEntries(LeatherArmorTypes, "leather");
|
||||
public static ItemListEntry[] Studded() => _studdedEntries ??= BuildStaticEntries(StuddedArmorTypes, "leather");
|
||||
public static ItemListEntry[] Female() => _femaleEntries ??= BuildStaticEntries(FemaleArmorTypes, "leather");
|
||||
|
||||
private static ItemListEntry[] BuildFilteredEntries(Mobile from, Category category)
|
||||
{
|
||||
if (category is Category.Main or Category.LeatherMain)
|
||||
{
|
||||
return BuildFilteredMainEntries(from, category);
|
||||
}
|
||||
|
||||
var types = GetTypes(category);
|
||||
var staticEntries = GetStaticEntries(category);
|
||||
if (types == null || staticEntries == null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return T2ACraftSystem.FilterEntries(from, staticEntries, types, DefTailoring.CraftSystem);
|
||||
}
|
||||
|
||||
private static ItemListEntry[] BuildFilteredMainEntries(Mobile from, Category mainCategory)
|
||||
{
|
||||
var system = DefTailoring.CraftSystem;
|
||||
var mainStatic = GetStaticEntries(mainCategory);
|
||||
if (mainStatic == null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var filtered = new ItemListEntry[mainStatic.Length];
|
||||
var count = 0;
|
||||
|
||||
for (var i = 0; i < mainStatic.Length; i++)
|
||||
{
|
||||
var entry = mainStatic[i];
|
||||
var types = GetTypes((Category)entry.CraftIndex);
|
||||
if (types != null && T2ACraftSystem.AnyCraftableInCategory(from, types, system))
|
||||
{
|
||||
filtered[count++] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (count < filtered.Length)
|
||||
{
|
||||
Array.Resize(ref filtered, count);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
private void CraftItem(Mobile from, Type itemType)
|
||||
{
|
||||
var itemDef = DefTailoring.CraftSystem.CraftItems.SearchFor(itemType);
|
||||
if (itemDef == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var num = DefTailoring.CraftSystem.CanCraft(from, _tool, itemDef.ItemType);
|
||||
if (num > 0)
|
||||
{
|
||||
from.SendLocalizedMessage(num);
|
||||
return;
|
||||
}
|
||||
|
||||
var context = DefTailoring.CraftSystem.GetContext(from);
|
||||
var res = itemDef.UseSubRes2 ? DefTailoring.CraftSystem.CraftSubRes2 : DefTailoring.CraftSystem.CraftSubRes;
|
||||
var resIndex = itemDef.UseSubRes2 ? context.LastResourceIndex2 : context.LastResourceIndex;
|
||||
var resourceType = resIndex > -1 ? res[resIndex].ItemType : null;
|
||||
|
||||
if (_hue >= 0)
|
||||
{
|
||||
// Pipeline B: hue-aware crafting — only consumes resources matching this hue
|
||||
context.LastHue = _hue;
|
||||
DefTailoring.CraftSystem.CreateItem(from, itemDef.ItemType, resourceType, _tool, itemDef, _hue);
|
||||
}
|
||||
else
|
||||
{
|
||||
DefTailoring.CraftSystem.CreateItem(from, itemDef.ItemType, resourceType, _tool, itemDef);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnResponse(NetState state, int index)
|
||||
{
|
||||
var from = state.Mobile;
|
||||
var craftIndex = Entries[index].CraftIndex;
|
||||
|
||||
if (_category is Category.Main or Category.LeatherMain)
|
||||
{
|
||||
// Carry hue through to child menus
|
||||
var menu = new TailoringMenu(from, _tool, (Category)craftIndex, _hue);
|
||||
if (menu.Entries.Length == 0)
|
||||
{
|
||||
from.SendAsciiMessage("You lack the skill and materials to craft anything in that category.");
|
||||
return;
|
||||
}
|
||||
|
||||
from.SendMenu(menu);
|
||||
return;
|
||||
}
|
||||
|
||||
var types = GetTypes(_category);
|
||||
if (types != null && craftIndex >= 0 && craftIndex < types.Length)
|
||||
{
|
||||
CraftItem(from, types[craftIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
public static void ResourceSelection(Mobile from, BaseTool tool, Item preTarget = null)
|
||||
{
|
||||
if (preTarget != null && TrySelectResource(from, tool, preTarget))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
from.SendAsciiMessage("Select the resource you wish to use (cloth, leather, or hides).");
|
||||
from.Target = new ResourceSelectTarget(from, tool);
|
||||
}
|
||||
|
||||
private static bool TrySelectResource(Mobile from, BaseTool tool, Item targeted)
|
||||
{
|
||||
if (targeted is Cloth or UncutCloth)
|
||||
{
|
||||
var menu = new TailoringMenu(from, tool, Category.Main, targeted.Hue);
|
||||
if (menu.Entries.Length == 0)
|
||||
{
|
||||
from.SendAsciiMessage("You lack the skill and materials to craft anything.");
|
||||
return true;
|
||||
}
|
||||
|
||||
from.SendMenu(menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (targeted is Items.Leather or Hides)
|
||||
{
|
||||
var menu = new TailoringMenu(from, tool, Category.LeatherMain);
|
||||
if (menu.Entries.Length == 0)
|
||||
{
|
||||
from.SendAsciiMessage("You lack the skill and materials to craft anything.");
|
||||
return true;
|
||||
}
|
||||
|
||||
from.SendMenu(menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private class ResourceSelectTarget : Target
|
||||
{
|
||||
private readonly Mobile _from;
|
||||
private readonly BaseTool _tool;
|
||||
|
||||
public ResourceSelectTarget(Mobile from, BaseTool tool) : base(12, false, TargetFlags.None)
|
||||
{
|
||||
_from = from;
|
||||
_tool = tool;
|
||||
}
|
||||
|
||||
protected override void OnTarget(Mobile from, object targeted)
|
||||
{
|
||||
if (targeted is Item item && TrySelectResource(from, _tool, item))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
from.SendAsciiMessage("That is not a valid resource. Please select cloth, leather, or hides.");
|
||||
from.Target = new ResourceSelectTarget(_from, _tool);
|
||||
}
|
||||
}
|
||||
}
|
||||
529
Projects/UOContent/Engines/Craft/T2A/TinkeringMenu.cs
Normal file
529
Projects/UOContent/Engines/Craft/T2A/TinkeringMenu.cs
Normal file
|
|
@ -0,0 +1,529 @@
|
|||
using System;
|
||||
using Server.Items;
|
||||
using Server.Menus.ItemLists;
|
||||
using Server.Network;
|
||||
using Server.Targeting;
|
||||
|
||||
namespace Server.Engines.Craft.T2A;
|
||||
|
||||
public class TinkeringMenu : ItemListMenu
|
||||
{
|
||||
private enum Category
|
||||
{
|
||||
Main,
|
||||
Wood,
|
||||
Tools,
|
||||
Parts,
|
||||
Utensils,
|
||||
Traps,
|
||||
Misc,
|
||||
Jewelry,
|
||||
Necklaces,
|
||||
Earrings,
|
||||
Rings,
|
||||
Keg
|
||||
}
|
||||
|
||||
private static readonly Type[] WoodItemTypes =
|
||||
[
|
||||
typeof(JointingPlane), typeof(MouldingPlane), typeof(SmoothingPlane),
|
||||
typeof(ClockFrame), typeof(Axle), typeof(RollingPin)
|
||||
];
|
||||
|
||||
private static readonly Type[] ToolTypes =
|
||||
[
|
||||
typeof(SewingKit), typeof(TinkerTools),
|
||||
typeof(DrawKnife), typeof(Froe), typeof(Inshave), typeof(Scorp),
|
||||
typeof(Scissors), typeof(Tongs),
|
||||
typeof(DovetailSaw), typeof(Saw), typeof(Hammer),
|
||||
typeof(SmithHammer), typeof(SledgeHammer), typeof(Shovel),
|
||||
typeof(MortarPestle), typeof(Hatchet), typeof(Pickaxe), typeof(Lockpick)
|
||||
];
|
||||
|
||||
private static readonly Type[] PartTypes =
|
||||
[
|
||||
typeof(Gears), typeof(Springs), typeof(Hinge),
|
||||
typeof(ClockParts), typeof(SextantParts),
|
||||
typeof(BarrelTap), typeof(BarrelHoops), typeof(AxleGears)
|
||||
];
|
||||
|
||||
private static readonly Type[] UtensilTypes =
|
||||
[
|
||||
typeof(ButcherKnife), typeof(Plate), typeof(Cleaver),
|
||||
typeof(KnifeLeft), typeof(KnifeRight), typeof(SkinningKnife),
|
||||
typeof(ForkLeft), typeof(ForkRight),
|
||||
typeof(SpoonLeft), typeof(SpoonRight),
|
||||
typeof(Goblet), typeof(PewterMug)
|
||||
];
|
||||
|
||||
private static readonly Type[] TrapTypes =
|
||||
[
|
||||
typeof(DartTrapCraft), typeof(ExplosionTrapCraft), typeof(PoisonTrapCraft)
|
||||
];
|
||||
|
||||
private static readonly Type[] MiscTypes =
|
||||
[
|
||||
typeof(KeyRing), typeof(Key),
|
||||
typeof(Scales), typeof(Spyglass), typeof(Lantern), typeof(HeatingStand),
|
||||
typeof(Globe), typeof(Candelabra), typeof(Sextant),
|
||||
typeof(ClockRight), typeof(ClockLeft)
|
||||
];
|
||||
|
||||
private static readonly Type[] NecklaceTypes = [typeof(GoldNecklace), typeof(SilverNecklace)];
|
||||
private static readonly Type[] EarringTypes = [typeof(GoldEarrings), typeof(SilverEarrings)];
|
||||
private static readonly Type[] RingTypes = [typeof(GoldRing), typeof(SilverRing), typeof(WeddingRing)];
|
||||
|
||||
// Combined for AnyCraftableInCategory check on the Jewelry parent entry
|
||||
private static readonly Type[] AllJewelryTypes =
|
||||
[
|
||||
typeof(GoldNecklace), typeof(SilverNecklace),
|
||||
typeof(GoldEarrings), typeof(SilverEarrings),
|
||||
typeof(GoldRing), typeof(SilverRing), typeof(WeddingRing)
|
||||
];
|
||||
|
||||
private static readonly Type[] KegItemTypes = [typeof(PotionKeg)];
|
||||
|
||||
private static ItemListEntry[] _mainEntries;
|
||||
private static ItemListEntry[] _woodEntries;
|
||||
private static ItemListEntry[] _toolEntries;
|
||||
private static ItemListEntry[] _partEntries;
|
||||
private static ItemListEntry[] _utensilEntries;
|
||||
private static ItemListEntry[] _trapEntries;
|
||||
private static ItemListEntry[] _miscEntries;
|
||||
private static ItemListEntry[] _necklaceEntries;
|
||||
private static ItemListEntry[] _earringEntries;
|
||||
private static ItemListEntry[] _ringEntries;
|
||||
private static ItemListEntry[] _kegEntries;
|
||||
|
||||
private readonly Category _category;
|
||||
private readonly BaseTool _tool;
|
||||
private readonly Type _selectedResourceType;
|
||||
|
||||
private static string GetQuestion(Category category) => category switch
|
||||
{
|
||||
Category.Main => "What would you like to make?",
|
||||
Category.Wood => "What kind of wooden item?",
|
||||
Category.Tools => "What kind of tool?",
|
||||
Category.Parts => "What kind of part?",
|
||||
Category.Utensils => "What kind of utensil?",
|
||||
Category.Traps => "What kind of trap?",
|
||||
Category.Misc => "What would you like to make?",
|
||||
Category.Jewelry => "What kind of jewelry?",
|
||||
Category.Necklaces => "What kind of necklace?",
|
||||
Category.Earrings => "What kind of earrings?",
|
||||
Category.Rings => "What kind of ring?",
|
||||
Category.Keg => "What would you like to make?",
|
||||
_ => "What would you like to make?"
|
||||
};
|
||||
|
||||
private TinkeringMenu(Mobile from, BaseTool tool, Category category, Type selectedResourceType)
|
||||
: base(GetQuestion(category), BuildFilteredEntries(from, category))
|
||||
{
|
||||
_tool = tool;
|
||||
_category = category;
|
||||
_selectedResourceType = selectedResourceType;
|
||||
}
|
||||
|
||||
private static string FormatItemName(Type type)
|
||||
{
|
||||
var name = type.Name;
|
||||
Span<char> buffer = stackalloc char[name.Length * 2];
|
||||
var pos = 0;
|
||||
|
||||
for (var i = 0; i < name.Length; i++)
|
||||
{
|
||||
if (i > 0 && char.IsUpper(name[i]))
|
||||
{
|
||||
buffer[pos++] = ' ';
|
||||
}
|
||||
|
||||
buffer[pos++] = char.ToLower(name[i]);
|
||||
}
|
||||
|
||||
return new string(buffer[..pos]);
|
||||
}
|
||||
|
||||
private static ItemListEntry[] BuildStaticEntries(Type[] types, string resourceName)
|
||||
{
|
||||
var entries = new ItemListEntry[types.Length];
|
||||
var count = 0;
|
||||
var craftItems = DefTinkering.CraftSystem.CraftItems;
|
||||
|
||||
for (var i = 0; i < types.Length; i++)
|
||||
{
|
||||
var itemDef = craftItems.SearchFor(types[i]);
|
||||
if (itemDef == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var name = FormatItemName(types[i]);
|
||||
var res = itemDef.Resources[0];
|
||||
entries[count++] = new ItemListEntry($"{name} ({res.Amount} {resourceName})", itemDef.ItemId, 0, i);
|
||||
}
|
||||
|
||||
if (count < entries.Length)
|
||||
{
|
||||
Array.Resize(ref entries, count);
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static ItemListEntry[] GetStaticEntries(Category category) => category switch
|
||||
{
|
||||
Category.Main => Main(),
|
||||
Category.Wood => Wood(),
|
||||
Category.Tools => Tools(),
|
||||
Category.Parts => Parts(),
|
||||
Category.Utensils => Utensils(),
|
||||
Category.Traps => Traps(),
|
||||
Category.Misc => Misc(),
|
||||
Category.Necklaces => Necklaces(),
|
||||
Category.Earrings => Earrings(),
|
||||
Category.Rings => Rings(),
|
||||
Category.Keg => KegItems(),
|
||||
_ => null
|
||||
};
|
||||
|
||||
private static Type[] GetTypes(Category category) => category switch
|
||||
{
|
||||
Category.Wood => WoodItemTypes,
|
||||
Category.Tools => ToolTypes,
|
||||
Category.Parts => PartTypes,
|
||||
Category.Utensils => UtensilTypes,
|
||||
Category.Traps => TrapTypes,
|
||||
Category.Misc => MiscTypes,
|
||||
Category.Jewelry => AllJewelryTypes,
|
||||
Category.Necklaces => NecklaceTypes,
|
||||
Category.Earrings => EarringTypes,
|
||||
Category.Rings => RingTypes,
|
||||
Category.Keg => KegItemTypes,
|
||||
_ => null
|
||||
};
|
||||
|
||||
public static ItemListEntry[] Main() => _mainEntries ??=
|
||||
[
|
||||
new ItemListEntry("Wooden Items", 0x1BDD, 0, (int)Category.Wood),
|
||||
new ItemListEntry("Tools", 0x1EB8, 0, (int)Category.Tools),
|
||||
new ItemListEntry("Parts", 0x1053, 0, (int)Category.Parts),
|
||||
new ItemListEntry("Utensils", 0x9D7, 0, (int)Category.Utensils),
|
||||
new ItemListEntry("Traps", 0x1BFC, 0, (int)Category.Traps),
|
||||
new ItemListEntry("Miscellaneous", 0xA25, 0, (int)Category.Misc),
|
||||
new ItemListEntry("Jewelry", 0x1088, 0, (int)Category.Jewelry)
|
||||
];
|
||||
|
||||
public static ItemListEntry[] Wood() => _woodEntries ??= BuildStaticEntries(WoodItemTypes, "logs");
|
||||
public static ItemListEntry[] Tools() => _toolEntries ??= BuildStaticEntries(ToolTypes, "ingots");
|
||||
public static ItemListEntry[] Parts() => _partEntries ??= BuildStaticEntries(PartTypes, "ingots");
|
||||
public static ItemListEntry[] Utensils() => _utensilEntries ??= BuildStaticEntries(UtensilTypes, "ingots");
|
||||
public static ItemListEntry[] Traps() => _trapEntries ??= BuildStaticEntries(TrapTypes, "ingots");
|
||||
public static ItemListEntry[] Misc() => _miscEntries ??= BuildStaticEntries(MiscTypes, "ingots");
|
||||
public static ItemListEntry[] Necklaces() => _necklaceEntries ??= BuildStaticEntries(NecklaceTypes, "ingots");
|
||||
public static ItemListEntry[] Earrings() => _earringEntries ??= BuildStaticEntries(EarringTypes, "ingots");
|
||||
public static ItemListEntry[] Rings() => _ringEntries ??= BuildStaticEntries(RingTypes, "ingots");
|
||||
public static ItemListEntry[] KegItems() => _kegEntries ??= BuildStaticEntries(KegItemTypes, "kegs");
|
||||
|
||||
private static ItemListEntry[] BuildFilteredEntries(Mobile from, Category category)
|
||||
{
|
||||
if (category == Category.Main)
|
||||
{
|
||||
return BuildFilteredMainEntries(from);
|
||||
}
|
||||
|
||||
if (category == Category.Jewelry)
|
||||
{
|
||||
return BuildFilteredJewelryEntries(from);
|
||||
}
|
||||
|
||||
var types = GetTypes(category);
|
||||
var staticEntries = GetStaticEntries(category);
|
||||
if (types == null || staticEntries == null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return T2ACraftSystem.FilterEntries(from, staticEntries, types, DefTinkering.CraftSystem);
|
||||
}
|
||||
|
||||
private static readonly ItemListEntry[] JewelrySubcategoryEntries =
|
||||
[
|
||||
new("Necklaces", 0x1088, 0, (int)Category.Necklaces),
|
||||
new("Earrings", 0x1087, 0, (int)Category.Earrings),
|
||||
new("Rings", 0x108a, 0, (int)Category.Rings)
|
||||
];
|
||||
|
||||
private static ItemListEntry[] BuildFilteredJewelryEntries(Mobile from)
|
||||
{
|
||||
var system = DefTinkering.CraftSystem;
|
||||
var filtered = new ItemListEntry[JewelrySubcategoryEntries.Length];
|
||||
var count = 0;
|
||||
|
||||
for (var i = 0; i < JewelrySubcategoryEntries.Length; i++)
|
||||
{
|
||||
var entry = JewelrySubcategoryEntries[i];
|
||||
var types = GetTypes((Category)entry.CraftIndex);
|
||||
if (types != null && T2ACraftSystem.AnyCraftableInCategory(from, types, system))
|
||||
{
|
||||
filtered[count++] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (count < filtered.Length)
|
||||
{
|
||||
Array.Resize(ref filtered, count);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
private static ItemListEntry[] BuildFilteredMainEntries(Mobile from)
|
||||
{
|
||||
var system = DefTinkering.CraftSystem;
|
||||
var mainStatic = Main();
|
||||
var filtered = new ItemListEntry[mainStatic.Length];
|
||||
var count = 0;
|
||||
|
||||
for (var i = 0; i < mainStatic.Length; i++)
|
||||
{
|
||||
var entry = mainStatic[i];
|
||||
var types = GetTypes((Category)entry.CraftIndex);
|
||||
if (types != null && T2ACraftSystem.AnyCraftableInCategory(from, types, system))
|
||||
{
|
||||
filtered[count++] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if (count < filtered.Length)
|
||||
{
|
||||
Array.Resize(ref filtered, count);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
public override void OnResponse(NetState state, int index)
|
||||
{
|
||||
var from = state.Mobile;
|
||||
var craftIndex = Entries[index].CraftIndex;
|
||||
|
||||
// Navigation categories: Main → subcategory, Jewelry → subcategory
|
||||
if (_category is Category.Main or Category.Jewelry)
|
||||
{
|
||||
var childCategory = (Category)craftIndex;
|
||||
var menu = new TinkeringMenu(from, _tool, childCategory, _selectedResourceType);
|
||||
if (menu.Entries.Length == 0)
|
||||
{
|
||||
from.SendAsciiMessage("You lack the skill and materials to craft anything in that category.");
|
||||
return;
|
||||
}
|
||||
|
||||
from.SendMenu(menu);
|
||||
return;
|
||||
}
|
||||
|
||||
// Jewelry leaf categories: prompt for gem targeting
|
||||
if (_category is Category.Necklaces or Category.Earrings or Category.Rings)
|
||||
{
|
||||
var types = GetTypes(_category);
|
||||
if (types == null || craftIndex < 0 || craftIndex >= types.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var itemType = types[craftIndex];
|
||||
if (DefTinkering.CraftSystem.CraftItems.SearchFor(itemType) == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear stale gem state from any previous craft attempt (e.g. failed skill check)
|
||||
var ctx = DefTinkering.CraftSystem.GetContext(from);
|
||||
if (ctx != null)
|
||||
{
|
||||
ctx.PendingGemType = GemType.None;
|
||||
ctx.PendingGemCount = 0;
|
||||
}
|
||||
|
||||
from.SendAsciiMessage("Target the gemstone you wish to use.");
|
||||
from.Target = new GemSelectTarget(from, _tool, itemType, _selectedResourceType);
|
||||
return;
|
||||
}
|
||||
|
||||
// Leaf categories: craft directly
|
||||
var leafTypes = GetTypes(_category);
|
||||
if (leafTypes == null || craftIndex < 0 || craftIndex >= leafTypes.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var leafItemType = leafTypes[craftIndex];
|
||||
var system = DefTinkering.CraftSystem;
|
||||
var itemDef = system.CraftItems.SearchFor(leafItemType);
|
||||
if (itemDef == null || _selectedResourceType == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Persist selected resource index so make-last remembers it
|
||||
T2ACraftSystem.SetLastResourceIndex(from, system, _selectedResourceType);
|
||||
|
||||
itemDef.Craft(from, system, _selectedResourceType, _tool);
|
||||
}
|
||||
|
||||
public static void ResourceSelection(Mobile from, BaseTool tool, Item preTarget = null)
|
||||
{
|
||||
if (preTarget != null && TrySelectResource(from, tool, preTarget))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
from.SendAsciiMessage("Select the resource you wish to use (wood or ingots).");
|
||||
from.Target = new ResourceSelectTarget(from, tool);
|
||||
}
|
||||
|
||||
private static bool TrySelectResource(Mobile from, BaseTool tool, Item targeted)
|
||||
{
|
||||
if (targeted is Log or Board)
|
||||
{
|
||||
var menu = new TinkeringMenu(from, tool, Category.Wood, typeof(Log));
|
||||
if (menu.Entries.Length == 0)
|
||||
{
|
||||
from.SendAsciiMessage("You lack the skill and materials to craft anything.");
|
||||
return true;
|
||||
}
|
||||
|
||||
from.SendMenu(menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (targeted is BaseIngot)
|
||||
{
|
||||
var menu = new TinkeringMenu(from, tool, Category.Main, targeted.GetType());
|
||||
if (menu.Entries.Length == 0)
|
||||
{
|
||||
from.SendAsciiMessage("You lack the skill and materials to craft anything.");
|
||||
return true;
|
||||
}
|
||||
|
||||
from.SendMenu(menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (targeted is Keg)
|
||||
{
|
||||
var menu = new TinkeringMenu(from, tool, Category.Keg, typeof(Keg));
|
||||
if (menu.Entries.Length == 0)
|
||||
{
|
||||
from.SendAsciiMessage("You lack the skill and materials to craft anything.");
|
||||
return true;
|
||||
}
|
||||
|
||||
from.SendMenu(menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private class ResourceSelectTarget : Target
|
||||
{
|
||||
private readonly Mobile _from;
|
||||
private readonly BaseTool _tool;
|
||||
|
||||
public ResourceSelectTarget(Mobile from, BaseTool tool) : base(12, false, TargetFlags.None)
|
||||
{
|
||||
_from = from;
|
||||
_tool = tool;
|
||||
}
|
||||
|
||||
protected override void OnTarget(Mobile from, object targeted)
|
||||
{
|
||||
if (targeted is Item item && TrySelectResource(from, _tool, item))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
from.SendAsciiMessage("That is not a valid resource. Please select wood or ingots.");
|
||||
from.Target = new ResourceSelectTarget(_from, _tool);
|
||||
}
|
||||
}
|
||||
|
||||
internal class GemSelectTarget : Target
|
||||
{
|
||||
private readonly Mobile _from;
|
||||
private readonly BaseTool _tool;
|
||||
private readonly Type _itemType;
|
||||
private readonly Type _selectedResourceType;
|
||||
|
||||
public GemSelectTarget(Mobile from, BaseTool tool, Type itemType, Type selectedResourceType)
|
||||
: base(12, false, TargetFlags.None)
|
||||
{
|
||||
_from = from;
|
||||
_tool = tool;
|
||||
_itemType = itemType;
|
||||
_selectedResourceType = selectedResourceType;
|
||||
}
|
||||
|
||||
protected override void OnTarget(Mobile from, object targeted)
|
||||
{
|
||||
if (targeted is not Item gemItem)
|
||||
{
|
||||
from.SendAsciiMessage("That is not a gemstone.");
|
||||
return;
|
||||
}
|
||||
|
||||
var gemType = BaseJewel.GetGemType(gemItem);
|
||||
if (gemType == GemType.None)
|
||||
{
|
||||
from.SendAsciiMessage("That is not a gemstone.");
|
||||
return;
|
||||
}
|
||||
|
||||
var amount = gemItem.Amount;
|
||||
if (amount < 1)
|
||||
{
|
||||
from.SendAsciiMessage("That gemstone stack is empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
var system = DefTinkering.CraftSystem;
|
||||
var itemDef = system.CraftItems.SearchFor(_itemType);
|
||||
if (itemDef == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Store pending gem info in craft context
|
||||
var ctx = system.GetContext(from);
|
||||
if (ctx == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.PendingGemType = gemType;
|
||||
ctx.PendingGemCount = amount;
|
||||
|
||||
T2ACraftSystem.SetLastResourceIndex(from, system, _selectedResourceType);
|
||||
itemDef.Craft(from, system, _selectedResourceType, _tool);
|
||||
}
|
||||
|
||||
protected override void OnTargetCancel(Mobile from, TargetCancelType cancelType)
|
||||
{
|
||||
if (cancelType == TargetCancelType.Canceled)
|
||||
{
|
||||
CraftItem.ShowCraftMenu(from, DefTinkering.CraftSystem, _tool);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -96,7 +96,7 @@ public class FactionImbueGump : FactionGump
|
|||
|
||||
if (m_Tool?.Deleted == false && m_Tool.UsesRemaining > 0)
|
||||
{
|
||||
m_Mobile.SendGump(new CraftGump(m_Mobile, m_CraftSystem, m_Tool, m_Notice));
|
||||
CraftItem.ShowCraftMenu(m_Mobile, m_CraftSystem, m_Tool, m_Notice);
|
||||
}
|
||||
else if (m_Notice != null)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ using Server.Engines.MLQuests.Gumps;
|
|||
using Server.Engines.MLQuests.Objectives;
|
||||
using Server.Engines.MLQuests.Rewards;
|
||||
using Server.Engines.Spawners;
|
||||
using Server.Gumps;
|
||||
using Server.Mobiles;
|
||||
|
||||
namespace Server.Engines.MLQuests
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using Server.Engines.MLQuests.Gumps;
|
||||
using Server.Engines.MLQuests.Objectives;
|
||||
using Server.Gumps;
|
||||
using Server.Mobiles;
|
||||
|
||||
namespace Server.Engines.MLQuests
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using Server.Gumps;
|
||||
using Server.Targeting;
|
||||
|
||||
namespace Server.Engines.Plants
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using Server.Gumps;
|
||||
using Server.Targeting;
|
||||
|
||||
namespace Server.Engines.Plants
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Server.Gumps;
|
||||
using Server.Items;
|
||||
using Server.Mobiles;
|
||||
using Server.Network;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
using System;
|
||||
using ModernUO.CodeGeneratedEvents;
|
||||
using Server.Accounting;
|
||||
using Server.Gumps;
|
||||
using Server.Items;
|
||||
using Server.Mobiles;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Server.Mobiles;
|
||||
using Server.Multis;
|
||||
using Server.Network;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ public enum GemType
|
|||
Diamond
|
||||
}
|
||||
|
||||
[SerializationGenerator(4, false)]
|
||||
[SerializationGenerator(5, false)]
|
||||
public abstract partial class BaseJewel : Item, ICraftable, IAosItem
|
||||
{
|
||||
[EncodedInt]
|
||||
|
|
@ -47,6 +47,11 @@ public abstract partial class BaseJewel : Item, ICraftable, IAosItem
|
|||
[SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)]
|
||||
private AosSkillBonuses _skillBonuses;
|
||||
|
||||
[EncodedInt]
|
||||
[SerializableField(7)]
|
||||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||||
private int _gemCount;
|
||||
|
||||
public BaseJewel(int itemID, Layer layer) : base(itemID)
|
||||
{
|
||||
_attributes = new AosAttributes(this);
|
||||
|
|
@ -183,9 +188,129 @@ public abstract partial class BaseJewel : Item, ICraftable, IAosItem
|
|||
}
|
||||
}
|
||||
|
||||
// T2A jewelry: read gem info from craft context (set by GemSelectTarget).
|
||||
// The entire targeted gem stack is consumed and the piece is named by that
|
||||
// count (e.g. "a 1000 diamond ring").
|
||||
if (context is { PendingGemType: not GemType.None, PendingGemCount: > 0 })
|
||||
{
|
||||
var gemItemType = GetGemItemType(context.PendingGemType);
|
||||
var gemCount = context.PendingGemCount;
|
||||
|
||||
if (gemItemType != null && from.Backpack?.ConsumeTotal(gemItemType, gemCount) == true)
|
||||
{
|
||||
GemType = context.PendingGemType;
|
||||
GemCount = gemCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Gems were no longer available (or unknown type): craft a plain piece
|
||||
// rather than naming it for gems that were never consumed.
|
||||
from.SendAsciiMessage("You lack the gemstones to set into this piece.");
|
||||
}
|
||||
|
||||
context.PendingGemType = GemType.None;
|
||||
context.PendingGemCount = 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
public override void OnSingleClick(Mobile from)
|
||||
{
|
||||
if (!Core.UOTD)
|
||||
{
|
||||
OnSingleClickPreUOTD(from);
|
||||
return;
|
||||
}
|
||||
|
||||
base.OnSingleClick(from);
|
||||
}
|
||||
|
||||
public virtual void OnSingleClickPreUOTD(Mobile from)
|
||||
{
|
||||
var plural = _gemCount > 1;
|
||||
string name;
|
||||
if (this is WeddingRing)
|
||||
{
|
||||
name = $"a {Name}";
|
||||
}
|
||||
else
|
||||
{
|
||||
name = Name;
|
||||
|
||||
if (name == null)
|
||||
{
|
||||
var articleAnName = (TileData.ItemTable[ItemID].Flags & TileFlag.ArticleAn) != 0;
|
||||
name = $"{(articleAnName ? "an" : "a")} {Localization.GetText(LabelNumber).ToLowerInvariant()}";
|
||||
}
|
||||
}
|
||||
|
||||
if (_gemType != GemType.None && _gemCount > 0)
|
||||
{
|
||||
var gemName = GetGemName(_gemType, plural);
|
||||
LabelTo(from, plural
|
||||
? $"{name} with {_gemCount} {gemName}"
|
||||
: $"{name} with {gemName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
LabelTo(from, name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static string GetGemName(GemType type, bool plural = false) => type switch
|
||||
{
|
||||
GemType.StarSapphire when plural => "star sapphires",
|
||||
GemType.StarSapphire => "a star sapphire",
|
||||
GemType.Emerald when plural => "emeralds",
|
||||
GemType.Emerald => "an emerald",
|
||||
GemType.Sapphire when plural => "sapphires",
|
||||
GemType.Sapphire => "a sapphire",
|
||||
GemType.Ruby when plural => "rubies",
|
||||
GemType.Ruby => "a ruby",
|
||||
GemType.Citrine when plural => "citrines",
|
||||
GemType.Citrine => "a citrine",
|
||||
GemType.Amethyst when plural => "amethysts",
|
||||
GemType.Amethyst => "an amethyst",
|
||||
GemType.Tourmaline when plural => "tourmalines",
|
||||
GemType.Tourmaline => "a tourmaline",
|
||||
GemType.Amber when plural => "ambers",
|
||||
GemType.Amber => "an amber",
|
||||
GemType.Diamond when plural => "diamonds",
|
||||
GemType.Diamond => "a diamond",
|
||||
_ when plural => "gems",
|
||||
_ => "a gem"
|
||||
};
|
||||
|
||||
internal static GemType GetGemType(Item item) => item switch
|
||||
{
|
||||
StarSapphire => GemType.StarSapphire,
|
||||
Emerald => GemType.Emerald,
|
||||
Sapphire => GemType.Sapphire,
|
||||
Ruby => GemType.Ruby,
|
||||
Citrine => GemType.Citrine,
|
||||
Amethyst => GemType.Amethyst,
|
||||
Tourmaline => GemType.Tourmaline,
|
||||
Amber => GemType.Amber,
|
||||
Diamond => GemType.Diamond,
|
||||
_ => GemType.None
|
||||
};
|
||||
|
||||
internal static Type GetGemItemType(GemType type) => type switch
|
||||
{
|
||||
GemType.StarSapphire => typeof(StarSapphire),
|
||||
GemType.Emerald => typeof(Emerald),
|
||||
GemType.Sapphire => typeof(Sapphire),
|
||||
GemType.Ruby => typeof(Ruby),
|
||||
GemType.Citrine => typeof(Citrine),
|
||||
GemType.Amethyst => typeof(Amethyst),
|
||||
GemType.Tourmaline => typeof(Tourmaline),
|
||||
GemType.Amber => typeof(Amber),
|
||||
GemType.Diamond => typeof(Diamond),
|
||||
_ => null
|
||||
};
|
||||
|
||||
public override void OnAfterDuped(Item newItem)
|
||||
{
|
||||
if (newItem is not BaseJewel jewel)
|
||||
|
|
@ -406,6 +531,18 @@ public abstract partial class BaseJewel : Item, ICraftable, IAosItem
|
|||
_skillBonuses.Deserialize(reader);
|
||||
}
|
||||
|
||||
private void MigrateFrom(V4Content content)
|
||||
{
|
||||
_maxHitPoints = content.MaxHitPoints;
|
||||
_hitPoints = content.HitPoints;
|
||||
_resource = content.Resource;
|
||||
_gemType = content.GemType;
|
||||
_attributes = content.Attributes;
|
||||
_resistances = content.Resistances;
|
||||
_skillBonuses = content.SkillBonuses;
|
||||
// _gemCount defaults to 0
|
||||
}
|
||||
|
||||
[AfterDeserialization]
|
||||
private void AfterDeserialization()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -33,3 +33,16 @@ public partial class SilverRing : BaseRing
|
|||
|
||||
public override double DefaultWeight => 0.1;
|
||||
}
|
||||
|
||||
[SerializationGenerator(0, false)]
|
||||
public partial class WeddingRing : BaseRing
|
||||
{
|
||||
public override string DefaultName => "wedding ring";
|
||||
|
||||
[Constructible]
|
||||
public WeddingRing() : base(0x108a)
|
||||
{
|
||||
}
|
||||
|
||||
public override double DefaultWeight => 0.1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using System;
|
||||
using ModernUO.Serialization;
|
||||
using Server.Engines.Craft;
|
||||
using Server.Gumps;
|
||||
using Server.Engines.Craft.T2A;
|
||||
using Server.Network;
|
||||
|
||||
namespace Server.Items;
|
||||
|
|
@ -159,9 +159,14 @@ public abstract partial class BaseTool : Item, IUsesRemaining, ICraftable
|
|||
{
|
||||
from.SendLocalizedMessage(num);
|
||||
}
|
||||
else if (T2ACraftSystem.Enabled)
|
||||
{
|
||||
from.Target = new T2ACraftToolTarget(this, system);
|
||||
from.SendAsciiMessage("Target this tool to make last item, or any other target to begin crafting.");
|
||||
}
|
||||
else
|
||||
{
|
||||
from.SendGump(new CraftGump(from, system, this, null));
|
||||
CraftItem.ShowCraftMenu(from, system, this, null);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
|
|||
64
Projects/UOContent/Migrations/Server.Items.BaseJewel.v5.json
generated
Normal file
64
Projects/UOContent/Migrations/Server.Items.BaseJewel.v5.json
generated
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
"version": 5,
|
||||
"type": "Server.Items.BaseJewel",
|
||||
"properties": [
|
||||
{
|
||||
"name": "MaxHitPoints",
|
||||
"type": "int",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
"EncodedInt"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "HitPoints",
|
||||
"type": "int",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
"EncodedInt"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Resource",
|
||||
"type": "Server.Items.CraftResource",
|
||||
"rule": "EnumMigrationRule"
|
||||
},
|
||||
{
|
||||
"name": "GemType",
|
||||
"type": "Server.Items.GemType",
|
||||
"rule": "EnumMigrationRule"
|
||||
},
|
||||
{
|
||||
"name": "Attributes",
|
||||
"type": "Server.AosAttributes",
|
||||
"rule": "RawSerializableMigrationRule",
|
||||
"ruleArguments": [
|
||||
"DeserializationRequiresParent"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Resistances",
|
||||
"type": "Server.AosElementAttributes",
|
||||
"rule": "RawSerializableMigrationRule",
|
||||
"ruleArguments": [
|
||||
"DeserializationRequiresParent"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SkillBonuses",
|
||||
"type": "Server.AosSkillBonuses",
|
||||
"rule": "RawSerializableMigrationRule",
|
||||
"ruleArguments": [
|
||||
"DeserializationRequiresParent"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "GemCount",
|
||||
"type": "int",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
"EncodedInt"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
4
Projects/UOContent/Migrations/Server.Items.WeddingRing.v0.json
generated
Normal file
4
Projects/UOContent/Migrations/Server.Items.WeddingRing.v0.json
generated
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"version": 0,
|
||||
"type": "Server.Items.WeddingRing"
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
using ModernUO.Serialization;
|
||||
using Server.Collections;
|
||||
using Server.ContextMenus;
|
||||
using Server.Gumps;
|
||||
using Server.Items;
|
||||
|
||||
namespace Server.Mobiles
|
||||
|
|
|
|||
|
|
@ -287,30 +287,32 @@ public static class IncomingPlayerPackets
|
|||
public static void MenuResponse(NetState state, SpanReader reader)
|
||||
{
|
||||
var serial = reader.ReadUInt32();
|
||||
int menuID = reader.ReadInt16(); // unused in our implementation
|
||||
int menuID = reader.ReadInt16();
|
||||
int index = reader.ReadInt16();
|
||||
int itemID = reader.ReadInt16();
|
||||
int hue = reader.ReadInt16();
|
||||
|
||||
index -= 1; // convert from 1-based to 0-based
|
||||
|
||||
foreach (var menu in state.Menus)
|
||||
for (var i = 0; i < state.Menus.Count; i++)
|
||||
{
|
||||
if (menu.Serial == serial)
|
||||
var menu = state.Menus[i];
|
||||
if ((uint)menu.Serial != serial)
|
||||
{
|
||||
state.RemoveMenu(menu);
|
||||
|
||||
if (index >= 0 && index < menu.EntryLength)
|
||||
{
|
||||
menu.OnResponse(state, index);
|
||||
}
|
||||
else
|
||||
{
|
||||
menu.OnCancel(state);
|
||||
}
|
||||
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
|
||||
state.RemoveMenu(menu);
|
||||
|
||||
if (index >= 0 && index < menu.EntryLength)
|
||||
{
|
||||
menu.OnResponse(state, index);
|
||||
}
|
||||
else
|
||||
{
|
||||
menu.OnCancel(state);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
25
Projects/UOContent/Skills/Cartography.cs
Normal file
25
Projects/UOContent/Skills/Cartography.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
using System;
|
||||
using Server.Engines.Craft;
|
||||
using Server.Engines.Craft.T2A;
|
||||
|
||||
namespace Server.SkillHandlers;
|
||||
|
||||
public static class Cartography
|
||||
{
|
||||
public static void Initialize()
|
||||
{
|
||||
SkillInfo.Table[(int)SkillName.Cartography].Callback = OnUse;
|
||||
}
|
||||
|
||||
public static TimeSpan OnUse(Mobile m)
|
||||
{
|
||||
if (!T2ACraftSystem.Enabled)
|
||||
{
|
||||
m.SendLocalizedMessage(1046444); // Use a mapmaker's pen to draw maps.
|
||||
return TimeSpan.Zero;
|
||||
}
|
||||
|
||||
T2ACraftSystem.ShowMenu(m, DefCartography.CraftSystem, null);
|
||||
return TimeSpan.FromSeconds(1.0);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Server.Engines.Craft;
|
||||
using Server.Engines.Craft.T2A;
|
||||
using Server.Items;
|
||||
using Server.Targeting;
|
||||
|
||||
|
|
@ -16,10 +18,19 @@ namespace Server.SkillHandlers
|
|||
|
||||
public static TimeSpan OnUse(Mobile m)
|
||||
{
|
||||
Target target = new InternalTargetSrc();
|
||||
m.Target = target;
|
||||
if (T2ACraftSystem.Enabled)
|
||||
{
|
||||
var target = new T2AInscribeTarget();
|
||||
m.Target = target;
|
||||
m.SendAsciiMessage("Target the book you wish to copy or scroll you want to use.");
|
||||
target.BeginTimeout(m, 60000); // 1 minute
|
||||
return TimeSpan.FromSeconds(1.0);
|
||||
}
|
||||
|
||||
Target uotdTarget = new InternalTargetSrc();
|
||||
m.Target = uotdTarget;
|
||||
m.SendLocalizedMessage(1046295); // Target the book you wish to copy.
|
||||
target.BeginTimeout(m, 60000); // 1 minute
|
||||
uotdTarget.BeginTimeout(m, 60000); // 1 minute
|
||||
|
||||
return TimeSpan.FromSeconds(1.0);
|
||||
}
|
||||
|
|
@ -42,10 +53,12 @@ namespace Server.SkillHandlers
|
|||
|
||||
public static bool IsEmpty(BaseBook book)
|
||||
{
|
||||
foreach (var page in book.Pages)
|
||||
for (var i = 0; i < book.Pages.Length; i++)
|
||||
{
|
||||
foreach (var line in page.Lines)
|
||||
var page = book.Pages[i];
|
||||
for (var j = 0; j < page.Lines.Length; j++)
|
||||
{
|
||||
var line = page.Lines[j];
|
||||
if (!string.IsNullOrEmpty(line))
|
||||
{
|
||||
return false;
|
||||
|
|
@ -78,6 +91,76 @@ namespace Server.SkillHandlers
|
|||
}
|
||||
}
|
||||
|
||||
private class T2AInscribeTarget : Target
|
||||
{
|
||||
public T2AInscribeTarget() : base(3, false, TargetFlags.None)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void OnTarget(Mobile from, object targeted)
|
||||
{
|
||||
if (targeted is BlankScroll scroll)
|
||||
{
|
||||
if (!scroll.IsChildOf(from.Backpack))
|
||||
{
|
||||
from.SendAsciiMessage("That must be in your pack for you to use it.");
|
||||
}
|
||||
else
|
||||
{
|
||||
T2ACraftSystem.ShowMenu(from, DefInscription.CraftSystem, null);
|
||||
}
|
||||
}
|
||||
else if (targeted is RecallRune)
|
||||
{
|
||||
var system = DefInscription.CraftSystem;
|
||||
var itemDef = system.CraftItems.SearchFor(typeof(Runebook));
|
||||
if (itemDef != null)
|
||||
{
|
||||
var num = system.CanCraft(from, null, itemDef.ItemType);
|
||||
if (num > 0)
|
||||
{
|
||||
from.SendLocalizedMessage(num);
|
||||
}
|
||||
else
|
||||
{
|
||||
system.CreateItem(from, itemDef.ItemType, null, null, itemDef);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (targeted is BaseBook book)
|
||||
{
|
||||
if (IsEmpty(book))
|
||||
{
|
||||
from.SendLocalizedMessage(501611); // Can't copy an empty book.
|
||||
}
|
||||
else if (GetUser(book) != null)
|
||||
{
|
||||
from.SendLocalizedMessage(501621); // Someone else is inscribing that item.
|
||||
}
|
||||
else
|
||||
{
|
||||
Target target = new InternalTargetDst(book);
|
||||
from.Target = target;
|
||||
from.SendLocalizedMessage(501612); // Select a book to copy this to.
|
||||
target.BeginTimeout(from, 60000);
|
||||
SetUser(book, from);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
from.SendLocalizedMessage(1046296); // That is not a book
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnTargetCancel(Mobile from, TargetCancelType cancelType)
|
||||
{
|
||||
if (cancelType == TargetCancelType.Timeout)
|
||||
{
|
||||
from.SendLocalizedMessage(501619);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class InternalTargetSrc : Target
|
||||
{
|
||||
public InternalTargetSrc() : base(3, false, TargetFlags.None)
|
||||
|
|
@ -151,19 +234,16 @@ namespace Server.SkillHandlers
|
|||
{
|
||||
from.SendLocalizedMessage(501621); // Someone else is inscribing that item.
|
||||
}
|
||||
else if (from.CheckTargetSkill(SkillName.Inscribe, bookDst, 0, 50))
|
||||
{
|
||||
Copy(m_BookSrc, bookDst);
|
||||
|
||||
from.SendLocalizedMessage(501618); // You make a copy of the book.
|
||||
from.PlaySound(0x249);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (from.CheckTargetSkill(SkillName.Inscribe, bookDst, 0, 50))
|
||||
{
|
||||
Copy(m_BookSrc, bookDst);
|
||||
|
||||
from.SendLocalizedMessage(501618); // You make a copy of the book.
|
||||
from.PlaySound(0x249);
|
||||
}
|
||||
else
|
||||
{
|
||||
from.SendLocalizedMessage(501617); // You fail to make a copy of the book.
|
||||
}
|
||||
from.SendLocalizedMessage(501617); // You fail to make a copy of the book.
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
141
dev-docs/t2a-crafting.md
Normal file
141
dev-docs/t2a-crafting.md
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
# T2A Packet-Based Crafting Menus
|
||||
|
||||
This document covers ModernUO's **T2A-era crafting menus** — the pre-UO:Third-Dawn, packet-based item-list crafting UI that replaces the modern gump crafting interface when enabled. It is the developer/AI reference for how the system is wired, how to extend it, and how it deviates from authentic T2A behavior.
|
||||
|
||||
## Overview
|
||||
|
||||
In the T2A era (≈1998–2001, before Publish 14 on 2001-11-30), UO crafting did not use gumps. The server sent the generic `0x7C` "Open Dialog" menu packet and the client replied with the 13-byte `0x7D` response. Double-clicking a crafting tool opened a **skill-and-material-filtered item-list menu**; the player picked a category/item and targeted a resource, and the item was made.
|
||||
|
||||
ModernUO reproduces this behind a single startup-read toggle. When `T2ACraftSystem.Enabled` is `false`, crafting uses the normal `CraftGump`. When `true`, the same `CraftSystem`/`CraftItem` definitions are presented through packet menus instead. The value is read once at startup from the `t2aCraftMenus` server setting (default `!Core.UOTD`), so a pre-UO:TD shard gets the T2A menus automatically and a UO:TD-or-later shard gets gumps — with no runtime/admin toggle.
|
||||
|
||||
The wire-level menu packets (`0x7C`/`0x7D`) already exist in the engine (`Projects/Server/Network/Packets/OutgoingMenuPackets.cs`, `Projects/UOContent/Network/Packets/IncomingPlayerPackets.cs`) and in `Server.Menus.ItemLists.ItemListMenu` / `Server.Menus.Questions.QuestionMenu`. The T2A feature is a *consumer* of that existing infrastructure, not a new protocol.
|
||||
|
||||
## Activation
|
||||
|
||||
Toggle: `T2ACraftSystem.Enabled` (static). It is set once in `ExpansionConfiguration.Configure()` from `ServerConfiguration.GetSetting("t2aCraftMenus", !Core.UOTD)` — a read-only setting (the default is **not** written back to the config file). It is intentionally **not** a runtime feature flag and cannot be flipped in-game by admins; change it via the `t2aCraftMenus` server setting and restart.
|
||||
|
||||
**Intended deployment:** because the default is `!Core.UOTD`, a pre-UO:TD shard gets T2A menus and the matching era mechanics automatically. The toggle controls the *UI system*; the expansion/era (`Core.UOTD`) controls *mechanics* (see [Gating model](#gating-model-toggle-vs-era)). Since the default tracks the era and there is no runtime override, the two cannot drift into an incoherent combination.
|
||||
|
||||
## Architecture
|
||||
|
||||
All T2A-specific code lives under `Projects/UOContent/Engines/Craft/T2A/`.
|
||||
|
||||
| Type | File | Responsibility |
|
||||
|---|---|---|
|
||||
| `T2ACraftSystem` (static) | `T2ACraftSystem.cs` | Central router. `ShowMenu(from, craftSystem, tool, preTarget)` dispatches per craft system to the right resource-selection / menu flow. Hosts shared filtering helpers. |
|
||||
| `T2ACraftToolTarget` (Target) | `T2ACraftToolTarget.cs` | The first target after double-clicking a tool: target the **tool** → make-last; target **anything else** → begin crafting with that item as `preTarget`. |
|
||||
| `*Menu : ItemListMenu` | `AlchemyMenu.cs`, `BlacksmithMenu.cs`, `BowFletchingMenu.cs`, `CarpentryMenu.cs`, `CartographyMenu.cs`, `InscriptionMenu.cs`, `TailoringMenu.cs`, `TinkeringMenu.cs` | One menu per skill. Builds filtered entries, drives category submenus, and on response either descends a category or crafts. |
|
||||
|
||||
**Separation of concerns:**
|
||||
- Each `*Menu` owns only its category tree and entry formatting.
|
||||
- `T2ACraftSystem.CanCraftItem` / `FilterEntries` / `AnyCraftableInCategory` own *craft-eligibility* (skill gate + material count, sub-resource aware, with resource-equivalence: `Log↔Board`, `Cloth↔UncutCloth`, `Leather↔Hides`).
|
||||
- `CraftItem` / `CraftSystem` own *consumption and item creation*.
|
||||
|
||||
### Control flow
|
||||
|
||||
```
|
||||
BaseTool.OnDoubleClick
|
||||
└─ if T2ACraftSystem.Enabled:
|
||||
from.Target = new T2ACraftToolTarget(tool, system)
|
||||
"Target this tool to make last item, or any other target to begin crafting."
|
||||
├─ target == tool → make-last (repeat context.LastMade; jewelry re-prompts the gem)
|
||||
└─ target == item/null → T2ACraftSystem.ShowMenu(from, system, tool, preTarget)
|
||||
├─ resource selected/targeted (per skill)
|
||||
├─ build filtered menu; empty → "You lack the skill and materials…"
|
||||
└─ ItemListMenu sent (0x7C) → player picks → 0x7D → OnResponse
|
||||
├─ category → open submenu
|
||||
└─ leaf → CraftItem.Craft(...)
|
||||
```
|
||||
|
||||
Tool-less skills (Inscription, Cartography) enter `ShowMenu` from their **skill handler** (`Skills/Inscribe.cs`, `Skills/Cartography.cs`) instead of a tool double-click — see [Tool-less skills](#tool-less-skills).
|
||||
|
||||
### How a selection maps back to a craftable
|
||||
|
||||
`ItemListEntry` carries a `CraftIndex` (a 4th constructor arg added for this feature) — an index into the menu's parallel `Type[]`. When the `0x7D` response arrives, `OnResponse(state, index)` uses the entry's `CraftIndex` to resolve the chosen category or `CraftItem` type. Menus build their entries through `T2ACraftSystem.FilterEntries(from, staticEntries, types, system, selectedResourceType)` so only craftable rows appear.
|
||||
|
||||
## Key mechanics
|
||||
|
||||
### Resource pre-selection
|
||||
Tool skills select the working resource **before** the menu opens. `T2ACraftToolTarget` passes whatever the player targeted as `preTarget`; `T2ACraftSystem.ShowMenu` validates it per skill (e.g. ingots for smithing, cloth/leather for tailoring, wood for carpentry/fletching, blank map for cartography, blank scroll/reagent/rune for inscription) and otherwise prompts for a valid resource. The selected sub-resource index is stored via `T2ACraftSystem.SetLastResourceIndex` (`context.LastResourceIndex` / `LastResourceIndex2`) for make-last.
|
||||
|
||||
### Make-last (QoL — see deviations)
|
||||
`T2ACraftToolTarget`: targeting the tool repeats `context.LastMade` with the remembered resource (and hue). Jewelry re-prompts for a gem target (you cannot silently re-consume gems). **Not historically part of T2A packet menus** ("Make Last" was a Publish 14 gump feature) — kept as a quality-of-life convenience.
|
||||
|
||||
### Hue-aware tailoring
|
||||
Targeting hued cloth/leather makes the craft consume **only matching-hue** material for the primary resource. Implemented by the `CraftItem.Craft(..., resHue)` overload and `CheckHuedRes`/`ConsumeHuedRes`/`GetHuedAmount`/`ConsumeHuedAmount`; the hue rides the `InternalTimer` (`m_ResHue`) into the hue-aware `CompleteCraft` overload, which sets `context.LastHue`. Secondary resources (e.g. ingots in mixed items) are consumed normally. This affects **consumption only**.
|
||||
|
||||
### How crafted items get their color
|
||||
A crafted item's color comes from its **`CraftResource`**, not from the consumed item's (dyed) hue: `OnCraft` sets `Resource = CraftResources.GetFromType(resourceType)` and armor/clothing then take `Hue = CraftResources.GetHue(Resource)`. So dyeing raw leather/cloth does **not** tint the crafted piece (plain `Leather` maps to `RegularLeather`, hue 0) — **leather, cloth, and wood never produce colored items in T2A**. The only color-bearing resource in T2A is **colored ingots/ore** (→ colored metal armor and shields). Colored/special leather, hides, and scales that convey color via `CraftResource` are an **AOS+** addition and don't exist in the T2A era.
|
||||
|
||||
Era gating differs by item:
|
||||
- **`BaseArmor`** / **`BaseClothing`**: set `Resource` (and thus color) in **all eras** — colored-ore armor is colored even in T2A (authentic).
|
||||
- **`BaseWeapon`**: sets `Resource`/color **only when `Core.AOS`** — pre-AOS weapons are uncolored (and unnamed by resource). This is intended; weapons did not retain resource color until AOS/runic.
|
||||
|
||||
### Stacked-gem jewelry
|
||||
Tinkered jewelry consumes ingots + a targeted gem **stack**. The player targets a stack of N gems; `TinkeringMenu.GemSelectTarget` captures `gemItem.Amount` into `context.PendingGemCount` and the gem type into `context.PendingGemType`. `BaseJewel.OnCraft` consumes the **entire** stack (`ConsumeTotal(gemItemType, PendingGemCount)`) and names the piece by count ("a 1000 diamond ring"). The count persists via `_gemCount` (`[SerializableField(7)]`, jewel serialization **v5**) and is shown in `OnSingleClickPreUOTD`. If the gems are unavailable at craft time the piece is left plain and the player is messaged.
|
||||
|
||||
### Half-resources on failure (era mechanic)
|
||||
`CraftItem.ConsumeRes` reduces each resource by half on a failed craft when `!Core.UOTD` (`amounts[i] -= amounts[i] / 2`). Note integer division: amount-1 resources (e.g. each inscription reagent + the single blank scroll) are fully consumed, matching the confirmed scroll-scribing rule; multi-unit resources (e.g. runebook's 8 blank scrolls) lose half.
|
||||
|
||||
### Tool-less skills
|
||||
`DefInscription` and `DefCartography` override `RequiresTool => !T2ACraftSystem.Enabled`, and `CanCraft` wraps tool validation in `if (RequiresTool)`. Inscription is invoked from the skill list (`Inscribe.cs` → `T2AInscribeTarget`: blank scroll opens the menu, recall rune crafts a runebook, a book enters the copy flow); cartography from `Cartography.cs`. `CraftItem` tool-null guards prevent `UsesRemaining` decrement when there is no tool.
|
||||
|
||||
### Maker's mark
|
||||
Under T2A the system **always prompts** for the maker's mark (no auto/never toggle): `CompleteCraft` gates on `makersMark && (T2ACraftMenus || context.MarkOption == PromptForMark)`, using the shared `QueryMakersMarkGump` (the old `QueryMakersMarkMenu` was removed). Exceptional + mark are tied to GM/near-GM skill, as in the era.
|
||||
|
||||
## Gating model (toggle vs era)
|
||||
|
||||
| Switch | Meaning | Governs |
|
||||
|---|---|---|
|
||||
| `T2ACraftSystem.Enabled` (from `t2aCraftMenus` setting, default `!Core.UOTD`) | "Use packet menus instead of gumps." | Menu routing, `ShowCraftMenu` (message vs gump), tool-less inscription/cartography, jewelry gem-targeting flow, always-prompt maker's mark, `BlankMap`/`BlankScroll` equivalence suppression. |
|
||||
| `Core.UOTD` (expansion/era) | T2A↔UO:TD era boundary (`false` = T2A or earlier). | Era mechanics: half-on-failure, tinkering metal-color suppression, pre-AOS recipe availability. |
|
||||
|
||||
Because the toggle's default **is** `!Core.UOTD` and there is no runtime override, the two move together by construction — a pre-UO:TD shard gets both the menus and the era mechanics, and there is no incoherent "menus on / UO:TD era" combination to guard against. An operator can still force the setting explicitly (e.g. menus on a later era) via `t2aCraftMenus`, but that is a deliberate, restart-time choice.
|
||||
|
||||
## Extending: add a craftable to a T2A menu
|
||||
|
||||
1. Ensure the item has a `CraftItem` in the relevant `Def*.cs` (`AddCraft(...)`), as for gump crafting — the T2A menus read the same `CraftSystem.CraftItems`.
|
||||
2. Add the item's `Type` to the appropriate category `Type[]` in the skill's `*Menu.cs` and a matching static `ItemListEntry` (name + `ItemID` + `CraftIndex`). Entries are filtered at build time by `T2ACraftSystem`, so you don't repeat skill/material checks.
|
||||
3. For a new **category**, add a `Category` enum value, a `GetQuestion` arm, a static entries array, and the navigation case in `OnResponse`. `BlacksmithMenu.cs` is the canonical template.
|
||||
4. Jewelry: gem-bearing pieces flow through `TinkeringMenu.GemSelectTarget` and `BaseJewel.OnCraft`; ensure `BaseJewel.GetGemType`/`GetGemItemType` cover any new gem.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Resource equivalence is era-gated.** `CraftItem.InitTypesTable()` only treats `BlankMap`/`BlankScroll` as interchangeable when `!T2ACraftMenus` (the gump clilocs reference both). Under T2A they are distinct, so cartography consumes blank *maps*, not scroll s.
|
||||
- **Transient context fields are not serialized.** `CraftContext.PendingGemType`, `PendingGemCount`, and `LastHue` are plain properties (no `[SerializableField]`) — they exist only during a craft.
|
||||
- **`BaseJewel` is at serialization v5.** Bumping it again requires `MigrateFrom(V5Content)` per the serialization rules.
|
||||
- **Menu entry creation uses reflection in one spot.** `T2ACraftSystem.ShowMenuDirect<T>` uses `Activator.CreateInstance` (once per tool double-click). Fine for now; convert to a compiled factory if it ever shows up hot.
|
||||
|
||||
## Files
|
||||
|
||||
- T2A UI: `Projects/UOContent/Engines/Craft/T2A/*.cs`
|
||||
- Engine glue: `Projects/UOContent/Engines/Craft/Core/{CraftItem,CraftContext,CraftSystem,Enhance,Repair,Resmelt,CraftGumpItem,QueryMakersMarkGump}.cs`
|
||||
- Defs: `Projects/UOContent/Engines/Craft/Def{Alchemy,Cartography,Inscription,Tailoring,Tinkering}.cs`
|
||||
- Skills: `Projects/UOContent/Skills/{Inscribe,Cartography}.cs`
|
||||
- Items: `Projects/UOContent/Items/Jewels/{BaseJewel,Ring}.cs` (+ `Migrations/Server.Items.BaseJewel.v5.json`)
|
||||
- Tool entry: `Projects/UOContent/Items/Skill Items/Tools/BaseTool.cs`
|
||||
- Toggle: `T2ACraftSystem.Enabled` (in `Engines/Craft/T2A/T2ACraftSystem.cs`), set from `Projects/UOContent/Configuration/ExpansionConfiguration.cs` via `ServerConfiguration.GetSetting("t2aCraftMenus", !Core.UOTD)`
|
||||
- Engine menus (additive): `Projects/Server/Menus/{BaseMenu,ItemListMenu,QuestionMenu}.cs`; response: `Projects/UOContent/Network/Packets/IncomingPlayerPackets.cs`
|
||||
- Tests: `Projects/UOContent.Tests/Tests/Items/Jewels/T2AJewelGemCraftTests.cs`
|
||||
|
||||
## Testing
|
||||
|
||||
`BaseJewel.OnCraft`'s gem block is unit-testable directly (it keys off `CraftContext.PendingGem*`, not the flag): see `T2AJewelGemCraftTests.cs`. The packet-menu UX (double-click → window → target → craft) requires a running shard + T2A client and is covered by the manual checklist in the design spec (§12.1).
|
||||
|
||||
## Deviations from authentic T2A (summary)
|
||||
|
||||
Decided in the design spec §4; faithful to Jack's research except where shard authority overrode:
|
||||
- **Make-last** — kept as QoL though it post-dates the T2A packet menus.
|
||||
- **Half-on-failure** for non-scroll crafts — best-known reconstruction, not OSI-confirmed.
|
||||
- **Hue-aware tailoring** — matching-hue *consumption* only (does **not** color the product); reconstruction, unverified by primary sources.
|
||||
- **Stacked-gem jewelry** — the full targeted stack is consumed and named by count (shard-authoritative; overrides both the "single gem" reconstruction and Jack's deliberate "consume 1, name by stack").
|
||||
- **Cooking** — out of scope (no T2A crafting menu existed for it).
|
||||
|
||||
## Related docs
|
||||
|
||||
| Topic | File |
|
||||
|---|---|
|
||||
| Serialization | `dev-docs/serialization.md` |
|
||||
| Networking & packets | `dev-docs/networking-packets.md` |
|
||||
| Era & expansion handling | `dev-docs/era-expansion.md` |
|
||||
| Gumps (the non-T2A path) | `dev-docs/gump-system.md` |
|
||||
Loading…
Add table
Add a link
Reference in a new issue