From 740d06a524442ed5d467d4ae6ded3ab605bd7e91 Mon Sep 17 00:00:00 2001 From: WarrentyExpired Date: Wed, 4 Nov 2026 10:08:06 -0500 Subject: [PATCH] #W# Tradesman: Added citizens that stand round and 'work' --- .../Overworld/Bastion_GatheringSpots.cfg | 14 -- .../Overworld/Coldbarrow_GatheringSpots.cfg | 17 -- .../Overworld/Cresthaven_GatheringSpots.cfg | 29 ---- .../EastRivermouth_GatheringSpots.cfg | 23 --- .../Overworld/Fairwinds_GatheringSpots.cfg | 17 -- .../Overworld/Landow_GatheringSpots.cfg | 8 - .../Overworld/Maerid_GatheringSpots.cfg | 17 -- .../Overworld/Mireport_GatheringSpots.cfg | 17 -- .../Overworld/Myrish_GatheringSpots.cfg | 20 --- .../Overworld/Stonehollow_GatheringSpots.cfg | 8 - .../Overworld/Teareek_GatheringSpots.cfg | 11 -- .../WestRivermouth_GatheringSpots.cfg | 17 -- .../Overworld/WharfsEnd_GatheringSpots.cfg | 11 -- .../Commands/Object Creation/Decorate.cs | 19 +++ .../Citizens/Citizens}/CitizensRef.cs | 0 .../Citizens/Citizens/CitizensSpawner.cs} | 0 .../Citizens/{ => Citizens}/HumanCitizen.cs | 0 .../Citizens/{ => Citizens}/MonsterCitizen.cs | 0 .../Townfolk/Citizens/GatheringCitizen.cs | 53 ------ .../Townfolk/Citizens/GatheringMonster.cs | 53 ------ .../Townfolk/Citizens/TownGatheringSpot.cs | 144 ---------------- .../Citizens/Tradesmen/SmelterForge.cs | 92 ++++++++++ .../Townfolk/Citizens/Tradesmen/SmithAnvil.cs | 96 +++++++++++ .../Citizens/Tradesmen/TradesmanCitizen.cs | 87 ++++++++++ .../Citizens/Tradesmen/TradesmanNode.cs | 159 ++++++++++++++++++ .../Citizens/Tradesmen/TradesmanSmelter.cs | 80 +++++++++ .../Citizens/Tradesmen/TradesmanSmith.cs | 77 +++++++++ 27 files changed, 610 insertions(+), 459 deletions(-) delete mode 100644 Distribution/Data/Decoration/Overworld/Bastion_GatheringSpots.cfg delete mode 100644 Distribution/Data/Decoration/Overworld/Coldbarrow_GatheringSpots.cfg delete mode 100644 Distribution/Data/Decoration/Overworld/Cresthaven_GatheringSpots.cfg delete mode 100644 Distribution/Data/Decoration/Overworld/EastRivermouth_GatheringSpots.cfg delete mode 100644 Distribution/Data/Decoration/Overworld/Fairwinds_GatheringSpots.cfg delete mode 100644 Distribution/Data/Decoration/Overworld/Landow_GatheringSpots.cfg delete mode 100644 Distribution/Data/Decoration/Overworld/Maerid_GatheringSpots.cfg delete mode 100644 Distribution/Data/Decoration/Overworld/Mireport_GatheringSpots.cfg delete mode 100644 Distribution/Data/Decoration/Overworld/Myrish_GatheringSpots.cfg delete mode 100644 Distribution/Data/Decoration/Overworld/Stonehollow_GatheringSpots.cfg delete mode 100644 Distribution/Data/Decoration/Overworld/Teareek_GatheringSpots.cfg delete mode 100644 Distribution/Data/Decoration/Overworld/WestRivermouth_GatheringSpots.cfg delete mode 100644 Distribution/Data/Decoration/Overworld/WharfsEnd_GatheringSpots.cfg rename Projects/UOContent/{Engines/Spawners/DynamicSpawner => Mobiles/Townfolk/Citizens/Citizens}/CitizensRef.cs (100%) rename Projects/UOContent/{Engines/Spawners/DynamicSpawner/Citizens.cs => Mobiles/Townfolk/Citizens/Citizens/CitizensSpawner.cs} (100%) rename Projects/UOContent/Mobiles/Townfolk/Citizens/{ => Citizens}/HumanCitizen.cs (100%) rename Projects/UOContent/Mobiles/Townfolk/Citizens/{ => Citizens}/MonsterCitizen.cs (100%) delete mode 100644 Projects/UOContent/Mobiles/Townfolk/Citizens/GatheringCitizen.cs delete mode 100644 Projects/UOContent/Mobiles/Townfolk/Citizens/GatheringMonster.cs delete mode 100644 Projects/UOContent/Mobiles/Townfolk/Citizens/TownGatheringSpot.cs create mode 100644 Projects/UOContent/Mobiles/Townfolk/Citizens/Tradesmen/SmelterForge.cs create mode 100644 Projects/UOContent/Mobiles/Townfolk/Citizens/Tradesmen/SmithAnvil.cs create mode 100644 Projects/UOContent/Mobiles/Townfolk/Citizens/Tradesmen/TradesmanCitizen.cs create mode 100644 Projects/UOContent/Mobiles/Townfolk/Citizens/Tradesmen/TradesmanNode.cs create mode 100644 Projects/UOContent/Mobiles/Townfolk/Citizens/Tradesmen/TradesmanSmelter.cs create mode 100644 Projects/UOContent/Mobiles/Townfolk/Citizens/Tradesmen/TradesmanSmith.cs diff --git a/Distribution/Data/Decoration/Overworld/Bastion_GatheringSpots.cfg b/Distribution/Data/Decoration/Overworld/Bastion_GatheringSpots.cfg deleted file mode 100644 index 4b895edbb..000000000 --- a/Distribution/Data/Decoration/Overworld/Bastion_GatheringSpots.cfg +++ /dev/null @@ -1,14 +0,0 @@ -TownGatheringSpot 0xFF8D -1855 2013 0 - -TownGatheringSpot 0xFF8D -1823 1981 0 - -TownGatheringSpot 0xFF8D -1798 2045 0 - -TownGatheringSpot 0xFF8D -1854 1967 0 - -TownGatheringSpot 0xFF8D -1771 2001 0 diff --git a/Distribution/Data/Decoration/Overworld/Coldbarrow_GatheringSpots.cfg b/Distribution/Data/Decoration/Overworld/Coldbarrow_GatheringSpots.cfg deleted file mode 100644 index 13ea7280d..000000000 --- a/Distribution/Data/Decoration/Overworld/Coldbarrow_GatheringSpots.cfg +++ /dev/null @@ -1,17 +0,0 @@ -TownGatheringSpot 0xFF8D -3352 2402 1 - -TownGatheringSpot 0xFF8D -3374 2390 2 - -TownGatheringSpot 0xFF8D -3398 2372 2 - -TownGatheringSpot 0xFF8D -3374 2345 5 - -TownGatheringSpot 0xFF8D -3357 2347 1 - -TownGatheringSpot 0xFF8D -3334 2370 1 diff --git a/Distribution/Data/Decoration/Overworld/Cresthaven_GatheringSpots.cfg b/Distribution/Data/Decoration/Overworld/Cresthaven_GatheringSpots.cfg deleted file mode 100644 index 5bf9066cf..000000000 --- a/Distribution/Data/Decoration/Overworld/Cresthaven_GatheringSpots.cfg +++ /dev/null @@ -1,29 +0,0 @@ -TownGatheringSpot 0xFF8D -1421 1188 2 - -TownGatheringSpot 0xFF8D -1449 1191 2 - -TownGatheringSpot 0xFF8D -1482 1175 2 - -TownGatheringSpot 0xFF8D -1527 1170 2 - -TownGatheringSpot 0xFF8D -1559 1158 2 - -TownGatheringSpot 0xFF8D -1551 1139 2 - -TownGatheringSpot 0xFF8D -1572 1123 2 - -TownGatheringSpot 0xFF8D -1612 1147 2 - -TownGatheringSpot 0xFF8D -1653 1146 2 - -TownGatheringSpot 0xFF8D -1612 1167 2 diff --git a/Distribution/Data/Decoration/Overworld/EastRivermouth_GatheringSpots.cfg b/Distribution/Data/Decoration/Overworld/EastRivermouth_GatheringSpots.cfg deleted file mode 100644 index 6f39bb023..000000000 --- a/Distribution/Data/Decoration/Overworld/EastRivermouth_GatheringSpots.cfg +++ /dev/null @@ -1,23 +0,0 @@ -TownGatheringSpot 0xFF8D -1312 1758 2 - -TownGatheringSpot 0xFF8D -1340 1718 5 - -TownGatheringSpot 0xFF8D -1333 1682 0 - -TownGatheringSpot 0xFF8D -1416 1697 0 - -TownGatheringSpot 0xFF8D -1445 1689 0 - -TownGatheringSpot 0xFF8D -1388 1731 0 - -TownGatheringSpot 0xFF8D -1411 1754 0 - -TownGatheringSpot 0xFF8D -1445 1766 0 diff --git a/Distribution/Data/Decoration/Overworld/Fairwinds_GatheringSpots.cfg b/Distribution/Data/Decoration/Overworld/Fairwinds_GatheringSpots.cfg deleted file mode 100644 index 88c1e0b02..000000000 --- a/Distribution/Data/Decoration/Overworld/Fairwinds_GatheringSpots.cfg +++ /dev/null @@ -1,17 +0,0 @@ -TownGatheringSpot 0xFF8D -2249 2835 0 - -TownGatheringSpot 0xFF8D -2258 2809 0 - -TownGatheringSpot 0xFF8D -2294 2805 0 - -TownGatheringSpot 0xFF8D -2331 2779 0 - -TownGatheringSpot 0xFF8D -2354 2797 0 - -TownGatheringSpot 0xFF8D -2352 2826 0 diff --git a/Distribution/Data/Decoration/Overworld/Landow_GatheringSpots.cfg b/Distribution/Data/Decoration/Overworld/Landow_GatheringSpots.cfg deleted file mode 100644 index e15151c38..000000000 --- a/Distribution/Data/Decoration/Overworld/Landow_GatheringSpots.cfg +++ /dev/null @@ -1,8 +0,0 @@ -TownGatheringSpot 0xFF8D -1797 1408 2 - -TownGatheringSpot 0xFF8D -1779 1400 2 - -TownGatheringSpot 0xFF8D -1813 1370 2 diff --git a/Distribution/Data/Decoration/Overworld/Maerid_GatheringSpots.cfg b/Distribution/Data/Decoration/Overworld/Maerid_GatheringSpots.cfg deleted file mode 100644 index 336f70079..000000000 --- a/Distribution/Data/Decoration/Overworld/Maerid_GatheringSpots.cfg +++ /dev/null @@ -1,17 +0,0 @@ -TownGatheringSpot 0xFF8D -3351 3633 1 - -TownGatheringSpot 0xFF8D -3378 3614 0 - -TownGatheringSpot 0xFF8D -3362 3686 0 - -TownGatheringSpot 0xFF8D -3444 3650 0 - -TownGatheringSpot 0xFF8D -3453 3694 0 - -TownGatheringSpot 0xFF8D -3419 3670 0 diff --git a/Distribution/Data/Decoration/Overworld/Mireport_GatheringSpots.cfg b/Distribution/Data/Decoration/Overworld/Mireport_GatheringSpots.cfg deleted file mode 100644 index 9da227806..000000000 --- a/Distribution/Data/Decoration/Overworld/Mireport_GatheringSpots.cfg +++ /dev/null @@ -1,17 +0,0 @@ -TownGatheringSpot 0xFF8D -358 1264 0 - -TownGatheringSpot 0xFF8D -362 1231 1 - -TownGatheringSpot 0xFF8D -340 1291 0 - -TownGatheringSpot 0xFF8D -392 1283 0 - -TownGatheringSpot 0xFF8D -450 1302 0 - -TownGatheringSpot 0xFF8D -425 1304 0 diff --git a/Distribution/Data/Decoration/Overworld/Myrish_GatheringSpots.cfg b/Distribution/Data/Decoration/Overworld/Myrish_GatheringSpots.cfg deleted file mode 100644 index ee3e57546..000000000 --- a/Distribution/Data/Decoration/Overworld/Myrish_GatheringSpots.cfg +++ /dev/null @@ -1,20 +0,0 @@ -TownGatheringSpot 0xFF8D -1575 2895 2 - -TownGatheringSpot 0xFF8D -1574 2920 2 - -TownGatheringSpot 0xFF8D -1530 2930 2 - -TownGatheringSpot 0xFF8D -1545 2997 2 - -TownGatheringSpot 0xFF8D -1574 2976 2 - -TownGatheringSpot 0xFF8D -1599 2968 2 - -TownGatheringSpot 0xFF8D -1549 2927 2 diff --git a/Distribution/Data/Decoration/Overworld/Stonehollow_GatheringSpots.cfg b/Distribution/Data/Decoration/Overworld/Stonehollow_GatheringSpots.cfg deleted file mode 100644 index 49ba991b9..000000000 --- a/Distribution/Data/Decoration/Overworld/Stonehollow_GatheringSpots.cfg +++ /dev/null @@ -1,8 +0,0 @@ -TownGatheringSpot 0xFF8D -756 1225 0 - -TownGatheringSpot 0xFF8D -775 1269 0 - -TownGatheringSpot 0xFF8D -806 1304 0 diff --git a/Distribution/Data/Decoration/Overworld/Teareek_GatheringSpots.cfg b/Distribution/Data/Decoration/Overworld/Teareek_GatheringSpots.cfg deleted file mode 100644 index 2f171420f..000000000 --- a/Distribution/Data/Decoration/Overworld/Teareek_GatheringSpots.cfg +++ /dev/null @@ -1,11 +0,0 @@ -TownGatheringSpot 0xFF8D -412 1856 1 - -TownGatheringSpot 0xFF8D -454 1835 1 - -TownGatheringSpot 0xFF8D -393 1899 1 - -TownGatheringSpot 0xFF8D -381 1886 1 diff --git a/Distribution/Data/Decoration/Overworld/WestRivermouth_GatheringSpots.cfg b/Distribution/Data/Decoration/Overworld/WestRivermouth_GatheringSpots.cfg deleted file mode 100644 index b34c25a54..000000000 --- a/Distribution/Data/Decoration/Overworld/WestRivermouth_GatheringSpots.cfg +++ /dev/null @@ -1,17 +0,0 @@ -TownGatheringSpot 0xFF8D -1211 1691 0 - -TownGatheringSpot 0xFF8D -1244 1758 0 - -TownGatheringSpot 0xFF8D -1138 1751 0 - -TownGatheringSpot 0xFF8D -1271 1707 2 - -TownGatheringSpot 0xFF8D -1270 1770 2 - -TownGatheringSpot 0xFF8D -1275 1747 1 diff --git a/Distribution/Data/Decoration/Overworld/WharfsEnd_GatheringSpots.cfg b/Distribution/Data/Decoration/Overworld/WharfsEnd_GatheringSpots.cfg deleted file mode 100644 index 738e706d0..000000000 --- a/Distribution/Data/Decoration/Overworld/WharfsEnd_GatheringSpots.cfg +++ /dev/null @@ -1,11 +0,0 @@ -TownGatheringSpot 0xFF8D -3501 403 2 - -TownGatheringSpot 0xFF8D -3521 393 2 - -TownGatheringSpot 0xFF8D -3510 358 2 - -TownGatheringSpot 0xFF8D -3485 369 2 diff --git a/Projects/UOContent/Commands/Object Creation/Decorate.cs b/Projects/UOContent/Commands/Object Creation/Decorate.cs index 876daa47b..19a11ce3f 100644 --- a/Projects/UOContent/Commands/Object Creation/Decorate.cs +++ b/Projects/UOContent/Commands/Object Creation/Decorate.cs @@ -512,6 +512,25 @@ namespace Server.Commands light.ItemID = m_ItemID; } } + else if (item is TradesmanNode node) + { + for (var i = 0; i < m_Params.Length; ++i) + { + if (m_Params[i].StartsWithOrdinal("NodeType")) + { + var indexOf = m_Params[i].IndexOfOrdinal('='); + + if (indexOf >= 0) + { + node.NodeType = (TradesmanNodeType)Enum.Parse( + typeof(TradesmanNodeType), + m_Params[i][++indexOf..], + true + ); + } + } + } + } else if (item is BaseSpawner sp) { sp.NextSpawn = TimeSpan.Zero; diff --git a/Projects/UOContent/Engines/Spawners/DynamicSpawner/CitizensRef.cs b/Projects/UOContent/Mobiles/Townfolk/Citizens/Citizens/CitizensRef.cs similarity index 100% rename from Projects/UOContent/Engines/Spawners/DynamicSpawner/CitizensRef.cs rename to Projects/UOContent/Mobiles/Townfolk/Citizens/Citizens/CitizensRef.cs diff --git a/Projects/UOContent/Engines/Spawners/DynamicSpawner/Citizens.cs b/Projects/UOContent/Mobiles/Townfolk/Citizens/Citizens/CitizensSpawner.cs similarity index 100% rename from Projects/UOContent/Engines/Spawners/DynamicSpawner/Citizens.cs rename to Projects/UOContent/Mobiles/Townfolk/Citizens/Citizens/CitizensSpawner.cs diff --git a/Projects/UOContent/Mobiles/Townfolk/Citizens/HumanCitizen.cs b/Projects/UOContent/Mobiles/Townfolk/Citizens/Citizens/HumanCitizen.cs similarity index 100% rename from Projects/UOContent/Mobiles/Townfolk/Citizens/HumanCitizen.cs rename to Projects/UOContent/Mobiles/Townfolk/Citizens/Citizens/HumanCitizen.cs diff --git a/Projects/UOContent/Mobiles/Townfolk/Citizens/MonsterCitizen.cs b/Projects/UOContent/Mobiles/Townfolk/Citizens/Citizens/MonsterCitizen.cs similarity index 100% rename from Projects/UOContent/Mobiles/Townfolk/Citizens/MonsterCitizen.cs rename to Projects/UOContent/Mobiles/Townfolk/Citizens/Citizens/MonsterCitizen.cs diff --git a/Projects/UOContent/Mobiles/Townfolk/Citizens/GatheringCitizen.cs b/Projects/UOContent/Mobiles/Townfolk/Citizens/GatheringCitizen.cs deleted file mode 100644 index 78859c69f..000000000 --- a/Projects/UOContent/Mobiles/Townfolk/Citizens/GatheringCitizen.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using Server; - -namespace Server.Mobiles -{ - public class GatheringCitizen : Citizen - { - private Direction _GatherDirection; - - [CommandProperty(AccessLevel.GameMaster)] - public Direction GatherDirection - { - get => _GatherDirection; - set => _GatherDirection = value; - } - - [Constructible] - public GatheringCitizen() : base() - { - CantWalk = true; - } - - public override void OnThink() - { - base.OnThink(); - - if (Direction != _GatherDirection) - { - Direction = _GatherDirection; - } - } - - public GatheringCitizen(Serial serial) : base(serial) - { - } - - public override void Serialize(IGenericWriter writer) - { - base.Serialize(writer); - writer.Write(0); - - writer.Write((int)_GatherDirection); - } - - public override void Deserialize(IGenericReader reader) - { - base.Deserialize(reader); - int version = reader.ReadInt(); - - _GatherDirection = (Direction)reader.ReadInt(); - } - } -} diff --git a/Projects/UOContent/Mobiles/Townfolk/Citizens/GatheringMonster.cs b/Projects/UOContent/Mobiles/Townfolk/Citizens/GatheringMonster.cs deleted file mode 100644 index e2f6a4b98..000000000 --- a/Projects/UOContent/Mobiles/Townfolk/Citizens/GatheringMonster.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using Server; - -namespace Server.Mobiles -{ - public class GatheringMonster : MonsterCitizen - { - private Direction _GatherDirection; - - [CommandProperty(AccessLevel.GameMaster)] - public Direction GatherDirection - { - get => _GatherDirection; - set => _GatherDirection = value; - } - - [Constructible] - public GatheringMonster() : base() - { - CantWalk = true; - } - - public override void OnThink() - { - base.OnThink(); - - if (Direction != _GatherDirection) - { - Direction = _GatherDirection; - } - } - - public GatheringMonster(Serial serial) : base(serial) - { - } - - public override void Serialize(IGenericWriter writer) - { - base.Serialize(writer); - writer.Write(0); - - writer.Write((int)_GatherDirection); - } - - public override void Deserialize(IGenericReader reader) - { - base.Deserialize(reader); - int version = reader.ReadInt(); - - _GatherDirection = (Direction)reader.ReadInt(); - } - } -} diff --git a/Projects/UOContent/Mobiles/Townfolk/Citizens/TownGatheringSpot.cs b/Projects/UOContent/Mobiles/Townfolk/Citizens/TownGatheringSpot.cs deleted file mode 100644 index 3f2cf238b..000000000 --- a/Projects/UOContent/Mobiles/Townfolk/Citizens/TownGatheringSpot.cs +++ /dev/null @@ -1,144 +0,0 @@ -using System; -using Server; -using Server.Mobiles; -using System.Collections.Generic; - -namespace Server.Items -{ - public class TownGatheringSpot : Item - { - private List _Attendees = new List(); - - [Constructible] - public TownGatheringSpot() : base(0xFF8D) - { - Name = "Town Gathering Spot"; - Visible = true; - Movable = false; - - Timer.DelayCall(TimeSpan.FromSeconds(1.0), CheckAndSpawn); - } - - private void CheckAndSpawn() - { - if (Deleted || Map == null || Map == Map.Internal) - return; - - _Attendees.RemoveAll(m => m == null || m.Deleted || !m.Alive); - - if (_Attendees.Count == 0) - { - SpawnGathering(); - } - } - - public override void OnDoubleClick(Mobile from) - { - if (from.AccessLevel >= AccessLevel.GameMaster) - { - SpawnGathering(); - from.SendMessage("Gathering forcefully respawned and reset!"); - } - } - - public void SpawnGathering() - { - foreach(Mobile m in _Attendees) - { - if (m != null && !m.Deleted) - m.Delete(); - } - _Attendees.Clear(); - - Mobile east = RandomAttendee(); - Mobile south = RandomAttendee(); - Mobile west = RandomAttendee(); - Mobile north = RandomAttendee(); - - PlaceAndFace(east, Location.X + 2, Location.Y, Location.Z, Direction.West); - PlaceAndFace(west, Location.X - 2, Location.Y, Location.Z, Direction.East); - PlaceAndFace(south, Location.X, Location.Y + 2, Location.Z, Direction.North); - PlaceAndFace(north, Location.X, Location.Y - 2, Location.Z, Direction.South); - } - - private Mobile RandomAttendee() - { - if (Utility.RandomBool()) - { - return new GatheringCitizen(); - } - else - { - return new GatheringMonster(); - } - } - - private void PlaceAndFace(Mobile m, int x, int y, int z, Direction facing) - { - m.MoveToWorld(new Point3D(x, y, z), Map); - m.CantWalk = true; - - if (m is GatheringCitizen gc) - { - gc.GatherDirection = facing; - } - else if (m is GatheringMonster gm) - { - gm.GatherDirection = facing; - } - - m.Direction = facing; - - _Attendees.Add(m); - } - - public override void OnDelete() - { - foreach (Mobile m in _Attendees) - { - if (m != null && !m.Deleted) - { - m.Delete(); - } - } - - _Attendees.Clear(); - base.OnDelete(); - } - - public TownGatheringSpot(Serial serial) : base(serial) - { - } - - public override void Serialize(IGenericWriter writer) - { - base.Serialize(writer); - writer.Write(0); - - writer.Write(_Attendees.Count); - foreach (Mobile m in _Attendees) - { - writer.Write(m); - } - } - - public override void Deserialize(IGenericReader reader) - { - base.Deserialize(reader); - int version = reader.ReadInt(); - - int count = reader.ReadInt(); - for (int i = 0; i < count; i++) - { - Mobile m = reader.ReadEntity(); - - if (m != null && !m.Deleted) - { - _Attendees.Add(m); - } - } - - Timer.DelayCall(TimeSpan.FromSeconds(1.0), CheckAndSpawn); - } - } -} diff --git a/Projects/UOContent/Mobiles/Townfolk/Citizens/Tradesmen/SmelterForge.cs b/Projects/UOContent/Mobiles/Townfolk/Citizens/Tradesmen/SmelterForge.cs new file mode 100644 index 000000000..25911872a --- /dev/null +++ b/Projects/UOContent/Mobiles/Townfolk/Citizens/Tradesmen/SmelterForge.cs @@ -0,0 +1,92 @@ +using System; +using Server; +using ModernUO.Serialization; + +namespace Server.Items +{ + [SerializationGenerator(0, false)] + public partial class ForgeHit : Item + { + [SerializableField(0)] + private Item _smeltedItem; + + [Constructible] + public ForgeHit() : base(0xFB1) + { + Name = "forge"; + Movable = false; + } + + public void Smelt(Mobile from) + { + if (from == null || from.Deleted) return; + + // Snap the NPC to face the forge directly + from.Direction = from.GetDirectionTo(Location); + + // Play the blunt swing animation + from.Animate(11, 5, 1, true, false, 0); + + if (_smeltedItem == null || _smeltedItem.Deleted) + { + // Create Ore on the first strike + _smeltedItem = new Item(0x19B8); + _smeltedItem.Movable = false; + _smeltedItem.Name = "smelting ore"; + + // Elevate it so it sits on top of the forge + _smeltedItem.MoveToWorld(new Point3D(X, Y, Z + 5), Map); + from.PlaySound(0x2B); // Metal clink + } + else + { + // Toggle between Ore and Ingots + if (_smeltedItem.ItemID == 0x19B8) + { + _smeltedItem.ItemID = 0x1BF3; + _smeltedItem.Name = "fresh ingots"; + from.PlaySound(0x208); // Fire sizzle/quench sound + } + else + { + _smeltedItem.ItemID = 0x19B8; + _smeltedItem.Name = "smelting ore"; + from.PlaySound(0x2B); // Metal clink + } + } + } + + // Cleanup: If a GM deletes the forge, delete the visual ore + public override void OnDelete() + { + if (_smeltedItem != null && !_smeltedItem.Deleted) + { + _smeltedItem.Delete(); + } + base.OnDelete(); + } + + // QoL: If a GM moves the forge, bring the visual ore with it + public override void OnLocationChange(Point3D oldLocation) + { + base.OnLocationChange(oldLocation); + if (_smeltedItem != null && !_smeltedItem.Deleted) + { + _smeltedItem.Location = new Point3D(X, Y, Z + 5); + } + } + + public override void OnMapChange() + { + base.OnMapChange(); + if (_smeltedItem != null && !_smeltedItem.Deleted) + { + _smeltedItem.Map = Map; + } + } + + public ForgeHit(Serial serial) : base(serial) + { + } + } +} diff --git a/Projects/UOContent/Mobiles/Townfolk/Citizens/Tradesmen/SmithAnvil.cs b/Projects/UOContent/Mobiles/Townfolk/Citizens/Tradesmen/SmithAnvil.cs new file mode 100644 index 000000000..6e83ed677 --- /dev/null +++ b/Projects/UOContent/Mobiles/Townfolk/Citizens/Tradesmen/SmithAnvil.cs @@ -0,0 +1,96 @@ +using System; +using Server; +using ModernUO.Serialization; + +namespace Server.Items +{ + [SerializationGenerator(0, false)] + public partial class AnvilHit : Item + { + // Tells ModernUO to automatically save our forged item reference during server saves + [SerializableField(0)] + private Item _ForgedItem; + + // A registry of item graphics to cycle through on the anvil + private static readonly int[] _ForgedGraphics = + { + 0x1BEF, // Iron Ingot + 0x1B73, // Buckler + 0x1BEF, // Iron Ingot + 0x13B9, // Viking Sword + 0x1BEF, + 0x1401, // Kryss + 0x1BEF, + 0x1B76 // Heater Shield + }; + + [Constructible] + public AnvilHit() : base(0xFAF) + { + Name = "anvil"; + Movable = false; + } + + public void Strike(Mobile from) + { + if (from == null || from.Deleted) return; + + // Snap the NPC to face the anvil directly + from.Direction = from.GetDirectionTo(Location); + + // Action 11 is the human animation for swinging a 1-handed blunt weapon + from.Animate(11, 5, 1, true, false, 0); + from.PlaySound(0x2A); + + // Handle the visual forged item on top of the anvil + if (_ForgedItem == null || _ForgedItem.Deleted) + { + // Create it on the first strike + _ForgedItem = new Item(_ForgedGraphics[Utility.Random(_ForgedGraphics.Length)]); + _ForgedItem.Movable = false; + _ForgedItem.Name = "a forged item"; + + // Elevate it by 5 Z-levels so it sits on top of the anvil mesh + _ForgedItem.MoveToWorld(new Point3D(X, Y, Z + 5), Map); + } + else + { + // Cycle to a new random graphic with each subsequent strike + _ForgedItem.ItemID = _ForgedGraphics[Utility.Random(_ForgedGraphics.Length)]; + } + } + + // Cleanup: If a GM deletes the anvil, delete the forged weapon too + public override void OnDelete() + { + if (_ForgedItem != null && !_ForgedItem.Deleted) + { + _ForgedItem.Delete(); + } + base.OnDelete(); + } + + // QoL: If a GM moves the anvil, bring the forged weapon with it + public override void OnLocationChange(Point3D oldLocation) + { + base.OnLocationChange(oldLocation); + if (_ForgedItem != null && !_ForgedItem.Deleted) + { + _ForgedItem.Location = new Point3D(X, Y, Z + 5); + } + } + + public override void OnMapChange() + { + base.OnMapChange(); + if (_ForgedItem != null && !_ForgedItem.Deleted) + { + _ForgedItem.Map = Map; + } + } + + public AnvilHit(Serial serial) : base(serial) + { + } + } +} diff --git a/Projects/UOContent/Mobiles/Townfolk/Citizens/Tradesmen/TradesmanCitizen.cs b/Projects/UOContent/Mobiles/Townfolk/Citizens/Tradesmen/TradesmanCitizen.cs new file mode 100644 index 000000000..9bf86b61e --- /dev/null +++ b/Projects/UOContent/Mobiles/Townfolk/Citizens/Tradesmen/TradesmanCitizen.cs @@ -0,0 +1,87 @@ +using System; +using Server; +using Server.Items; +using Server.Mobiles; +using ModernUO.Serialization; + +namespace Server.Mobiles +{ + [SerializationGenerator(0, false)] + public abstract partial class TradesmanCitizen : BaseCreature + { + private DateTime _NextChatter; + + public override bool InitialInnocent => true; + public override bool CanTeach => false; + public override bool ClickTitle => true; + + [Constructible] + public TradesmanCitizen() : base(AIType.AI_Animal, FightMode.None) + { + SpeechHue = 48; + Hue = Race.Human.RandomSkinHue(); + + if (Utility.RandomBool()) + { + Body = 0x191; // Female + Name = NameList.RandomName("female"); + } + else + { + Body = 0x190; // Male + Name = NameList.RandomName("male"); + Utility.AssignRandomFacialHair(this, HairHue); + } + + Utility.AssignRandomHair(this); + Title = "the tradesman"; + + SetStr(100); + SetDex(100); + SetInt(100); + SetHits(200, 300); + + DressTradesman(); + + CantWalk = false; // Allow them to wander + _NextChatter = DateTime.UtcNow + TimeSpan.FromSeconds(Utility.RandomMinMax(10, 25)); + } + + public virtual void DressTradesman() + { + AddItem(new Shirt(Utility.RandomNeutralHue())); + AddItem(new LongPants(Utility.RandomNeutralHue())); + AddItem(new Boots(Utility.RandomNeutralHue())); + } + + public override void OnMovement(Mobile m, Point3D oldLocation) + { + base.OnMovement(m, oldLocation); + + if (m.Player && m.Alive && !m.Hidden && DateTime.UtcNow >= _NextChatter && InRange(m, 8)) + { + if (Utility.RandomBool()) + { + SayRandomChatter(); + } + + _NextChatter = DateTime.UtcNow + TimeSpan.FromSeconds(Utility.RandomMinMax(30, 60)); + } + } + + public virtual void SayRandomChatter() + { + string[] lines = { + "Lovely weather today.", + "Always so much work to do...", + "Have you heard any news from the capital?", + "I need to visit the market later." + }; + Say(lines[Utility.Random(lines.Length)]); + } + + public TradesmanCitizen(Serial serial) : base(serial) + { + } + } +} diff --git a/Projects/UOContent/Mobiles/Townfolk/Citizens/Tradesmen/TradesmanNode.cs b/Projects/UOContent/Mobiles/Townfolk/Citizens/Tradesmen/TradesmanNode.cs new file mode 100644 index 000000000..7f2152ed8 --- /dev/null +++ b/Projects/UOContent/Mobiles/Townfolk/Citizens/Tradesmen/TradesmanNode.cs @@ -0,0 +1,159 @@ +using System; +using Server; +using Server.Mobiles; +using ModernUO.Serialization; + +namespace Server.Items +{ + public enum TradesmanNodeType + { + Smith, + Smelter + } + + [SerializationGenerator(0, false)] + public partial class TradesmanNode : Item + { + // The generator automatically creates 'public TradesmanNodeType NodeType { get; set; }' + // and triggers OnNodeTypeChanged whenever a GM changes it via the [Props gump. + [SerializableField(0, fieldChanged: nameof(OnNodeTypeChanged))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private TradesmanNodeType _nodeType; + + [SerializableField(1)] + private Mobile _worker; + + [SerializableField(2)] + private Item _workstation; + + [SerializableField(3)] + private DateTime _nextRespawn; + + public override bool HandlesOnMovement => true; + + private void OnNodeTypeChanged(TradesmanNodeType oldValue, TradesmanNodeType newValue) + { + ClearNode(); // Instantly reset the scene if a GM changes the type + } + + [Constructible] + public TradesmanNode() : this(TradesmanNodeType.Smith) + { + } + + [Constructible] + public TradesmanNode(TradesmanNodeType type) : base(0x1BCE) + { + Name = "tradesman node"; + Movable = false; + Visible = false; // Completely invisible to players + _nodeType = type; + _nextRespawn = DateTime.UtcNow; + } + + public override void OnMovement(Mobile m, Point3D oldLocation) + { + // Ignore deleted items, NPCs, hidden staff, or players on other maps + if (Deleted || m == null || !m.Player || m.Hidden || m.Map != Map) + return; + + // Trigger the spawn if a player walks within 15 tiles + if (Utility.InRange(m.Location, Location, 15)) + { + CheckAndSpawn(); + } + } + + private void CheckAndSpawn() + { + if (Deleted || Map == null || Map == Map.Internal) + return; + + bool workerMissing = (_worker == null || _worker.Deleted || !_worker.Alive); + bool stationMissing = (_workstation == null || _workstation.Deleted); + + // Only spawn if the scene is missing pieces AND the cooldown is finished + if ((workerMissing || stationMissing) && DateTime.UtcNow >= _nextRespawn) + { + SpawnNode(); + + // 10-minute cooldown so players can't spam-farm the NPC if they kill them + _nextRespawn = DateTime.UtcNow + TimeSpan.FromMinutes(10); + } + } + + public void SpawnNode() + { + ClearNode(); // Clean up any lingering pieces before spawning fresh + + switch (_nodeType) + { + case TradesmanNodeType.Smith: + _workstation = new AnvilHit(); + _worker = new TradesmanSmith(); + break; + case TradesmanNodeType.Smelter: + _workstation = new ForgeHit(); + _worker = new TradesmanSmelter(); + break; + } + + if (_workstation != null) + { + // Place workstation exactly on the node + _workstation.MoveToWorld(Location, Map); + } + + if (_worker != null) + { + if (Utility.RandomBool()) + { + // Place West, face East + _worker.MoveToWorld(new Point3D(X - 1, Y, Z), Map); + _worker.Direction = Direction.East; + } + else + { + // Place North, face South + _worker.MoveToWorld(new Point3D(X, Y - 1, Z), Map); + _worker.Direction = Direction.South; + } + } + } + + public void ClearNode() + { + if (_worker != null && !_worker.Deleted) + { + _worker.Delete(); + } + _worker = null; + + if (_workstation != null && !_workstation.Deleted) + { + _workstation.Delete(); + } + _workstation = null; + } + + public override void OnDoubleClick(Mobile from) + { + if (from.AccessLevel >= AccessLevel.GameMaster) + { + SpawnNode(); + from.SendMessage("Citizen node forcefully respawned."); + } + } + + // Automatic Garbage Collection: Deleting the node deletes the NPC and workstation + public override void OnDelete() + { + ClearNode(); + base.OnDelete(); + } + + public TradesmanNode(Serial serial) : base(serial) + { + } + } +} diff --git a/Projects/UOContent/Mobiles/Townfolk/Citizens/Tradesmen/TradesmanSmelter.cs b/Projects/UOContent/Mobiles/Townfolk/Citizens/Tradesmen/TradesmanSmelter.cs new file mode 100644 index 000000000..114468c56 --- /dev/null +++ b/Projects/UOContent/Mobiles/Townfolk/Citizens/Tradesmen/TradesmanSmelter.cs @@ -0,0 +1,80 @@ +using System; +using Server; +using Server.Items; +using ModernUO.Serialization; + +namespace Server.Mobiles +{ + [SerializationGenerator(0, false)] + public partial class TradesmanSmelter : TradesmanCitizen + { + private DateTime _nextWork; + private ForgeHit _myForge; + + [Constructible] + public TradesmanSmelter() + { + Title = "the smelter"; + CantWalk = true; + _nextWork = DateTime.UtcNow + TimeSpan.FromSeconds(2); + } + + public override void DressTradesman() + { + AddItem(new Shirt(Utility.RandomNeutralHue())); + AddItem(new ShortPants(Utility.RandomNeutralHue())); + AddItem(new Boots()); + + // Full apron for heat protection + AddItem(new FullApron(Utility.RandomList(0x906, 0x8A5, 0x89A))); + + // Tongs instead of a hammer + Item hammer = new SmithHammer(); + hammer.Movable = false; + AddItem(hammer); + } + + public override void SayRandomChatter() + { + string[] lines = { + "Hot enough to melt dragon scales today.", + "More ore, always more ore.", + "Careful, don't burn yourself.", + "Nothing beats the glow of fresh ingots." + }; + Say(lines[Utility.Random(lines.Length)]); + } + + public override void OnThink() + { + base.OnThink(); + + if (DateTime.UtcNow >= _nextWork) + { + if (_myForge == null || _myForge.Deleted) + { + foreach (Item item in GetItemsInRange(1)) + { + if (item is ForgeHit forge) + { + _myForge = forge; + break; + } + } + } + + if (_myForge != null && !_myForge.Deleted) + { + _myForge.Smelt(this); + } + + // Randomize the pause between smelts + _nextWork = DateTime.UtcNow + TimeSpan.FromSeconds(Utility.RandomMinMax(3, 6)); + } + } + + public TradesmanSmelter(Serial serial) : base(serial) + { + } + } +} diff --git a/Projects/UOContent/Mobiles/Townfolk/Citizens/Tradesmen/TradesmanSmith.cs b/Projects/UOContent/Mobiles/Townfolk/Citizens/Tradesmen/TradesmanSmith.cs new file mode 100644 index 000000000..c186483e7 --- /dev/null +++ b/Projects/UOContent/Mobiles/Townfolk/Citizens/Tradesmen/TradesmanSmith.cs @@ -0,0 +1,77 @@ +using System; +using Server; +using Server.Items; +using ModernUO.Serialization; + +namespace Server.Mobiles +{ + [SerializationGenerator(0, false)] + public partial class TradesmanSmith : TradesmanCitizen + { + private DateTime _NextWork; + private AnvilHit _MyAnvil; + + [Constructible] + public TradesmanSmith() + { + Title = "the blacksmith"; + CantWalk = true; + + _NextWork = DateTime.UtcNow + TimeSpan.FromSeconds(2); + } + + public override void DressTradesman() + { + AddItem(new LongPants(Utility.RandomNeutralHue())); + AddItem(new Boots()); + + AddItem(new HalfApron(Utility.RandomList(0x906, 0x8A5, 0x89A))); + + Item hammer = new SmithHammer(); + hammer.Movable = false; + AddItem(hammer); + } + + public override void SayRandomChatter() + { + string[] lines = { + "This iron is stubborn today.", + "A good blade takes time and patience.", + "Hot work, but someone has to do it.", + "Need your armor patched up?" + }; + Say(lines[Utility.Random(lines.Length)]); + } + + public override void OnThink() + { + base.OnThink(); + + if (DateTime.UtcNow >= _NextWork) + { + if (_MyAnvil == null || _MyAnvil.Deleted) + { + foreach (Item item in GetItemsInRange(1)) + { + if (item is AnvilHit anvil) + { + _MyAnvil = anvil; + break; + } + } + } + + if (_MyAnvil != null && !_MyAnvil.Deleted) + { + _MyAnvil.Strike(this); + } + + _NextWork = DateTime.UtcNow + TimeSpan.FromSeconds(Utility.RandomMinMax(2, 5)); + } + } + + public TradesmanSmith(Serial serial) : base(serial) + { + } + } +}