diff --git a/Info/MyChanges.txt b/Info/MyChanges.txt index 2b3909af5..bb44a35fc 100644 --- a/Info/MyChanges.txt +++ b/Info/MyChanges.txt @@ -3,8 +3,8 @@ Citizens - They only spawn on specific tiles (dirt/gravel/stone) to keep them from spawning in houses and businesses. They also spawn slower so the town fills up slowly. - They also say unique things. Come false some true. -Town Gathering Spots - - Same as citizens but the stand at a camp fire. + - Tradesman Citizens are npc that look like they are working. Standing at an + anvil making armor/weapons etc. CrystalOfKnowlege/Fortitude/Insight - These act as power scrolls, but only raise skill/skill total/stat cap 1 point at a time. Portal Gates diff --git a/Info/NeedsFinished.txt b/Info/NeedsFinished.txt index 8dbf003ec..ba2309be5 100644 --- a/Info/NeedsFinished.txt +++ b/Info/NeedsFinished.txt @@ -13,7 +13,8 @@ Spawned Treasure Chests The new DungeonLootCrates fill the void for now. Citizens - - Town citizens that have there own personalitys + - Need to add the Tradesmen spawn nodes to towns. + - Need to add more Tradesmen. Hoard Piles - Treausre hoard pile. diff --git a/Projects/UOContent/Commands/Object Creation/Decorate.cs b/Projects/UOContent/Commands/Object Creation/Decorate.cs index 19a11ce3f..23ed99c8a 100644 --- a/Projects/UOContent/Commands/Object Creation/Decorate.cs +++ b/Projects/UOContent/Commands/Object Creation/Decorate.cs @@ -512,7 +512,7 @@ namespace Server.Commands light.ItemID = m_ItemID; } } - else if (item is TradesmanNode node) + else if (item is CitizenNode node) { for (var i = 0; i < m_Params.Length; ++i) { @@ -522,8 +522,8 @@ namespace Server.Commands if (indexOf >= 0) { - node.NodeType = (TradesmanNodeType)Enum.Parse( - typeof(TradesmanNodeType), + node.NodeType = (CitizenNodeType)Enum.Parse( + typeof(CitizenNodeType), m_Params[i][++indexOf..], true ); diff --git a/Projects/UOContent/Mobiles/Townfolk/Citizens/CitizenNode.cs b/Projects/UOContent/Mobiles/Townfolk/Citizens/CitizenNode.cs new file mode 100644 index 000000000..1166099ea --- /dev/null +++ b/Projects/UOContent/Mobiles/Townfolk/Citizens/CitizenNode.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections.Generic; +using Server; +using Server.Mobiles; +using ModernUO.Serialization; + +namespace Server.Items +{ + public enum CitizenNodeType + { + Smith, + Smelter, + MeetingSpot + } + + [SerializationGenerator(0, false)] + public partial class CitizenNode : Item + { + [SerializableField(0, fieldChanged: nameof(OnNodeTypeChanged))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private CitizenNodeType _nodeType; + + [SerializableField(1)] + private List _workers = new(); + + [SerializableField(2)] + private Item _workstation; + + [SerializableField(3)] + private DateTime _nextRespawn; + + public override bool HandlesOnMovement => true; + + private void OnNodeTypeChanged(CitizenNodeType oldValue, CitizenNodeType newValue) + { + ClearNode(); + } + + [Constructible] + public CitizenNode() : this(CitizenNodeType.Smith) + { + } + + [Constructible] + public CitizenNode(CitizenNodeType type) : base(0x1BCE) + { + Name = "citizen node"; + Movable = false; + Visible = false; + _nodeType = type; + _nextRespawn = DateTime.UtcNow; + } + + public override void OnMovement(Mobile m, Point3D oldLocation) + { + if (Deleted || m == null || !m.Player || m.Hidden || m.Map != Map) + return; + + if (Utility.InRange(m.Location, Location, 15)) + { + CheckAndSpawn(); + } + } + + private void CheckAndSpawn() + { + if (Deleted || Map == null || Map == Map.Internal) + return; + + _workers.RemoveAll(w => w == null || w.Deleted || !w.Alive); + + bool workersMissing = _workers.Count == 0; + bool stationMissing = (_workstation == null || _workstation.Deleted); + + if ((workersMissing || stationMissing) && DateTime.UtcNow >= _nextRespawn) + { + SpawnNode(); + _nextRespawn = DateTime.UtcNow + TimeSpan.FromMinutes(10); + } + } + + public void SpawnNode() + { + ClearNode(); + + switch (_nodeType) + { + case CitizenNodeType.Smith: + _workstation = new AnvilHit(); + AddWorker(new TradesmanSmith(), GetSingleWorkerSpot()); + break; + + case CitizenNodeType.Smelter: + _workstation = new ForgeHit(); + AddWorker(new TradesmanSmelter(), GetSingleWorkerSpot()); + break; + + case CitizenNodeType.MeetingSpot: + _workstation = new CampfireHit(); + SpawnMeetingSpotCitizens(); + break; + } + + if (_workstation != null) + { + _workstation.MoveToWorld(Location, Map); + } + } + + private (Point3D loc, Direction dir) GetSingleWorkerSpot() + { + if (Utility.RandomBool()) + { + return (new Point3D(X - 1, Y, Z), Direction.East); + } + return (new Point3D(X, Y - 1, Z), Direction.South); + } + + private void AddWorker(Mobile worker, (Point3D loc, Direction dir) spawnData) + { + if (worker == null) return; + + worker.MoveToWorld(spawnData.loc, Map); + worker.Direction = spawnData.dir; + _workers.Add(worker); + } + + private void SpawnMeetingSpotCitizens() + { + (Point3D loc, Direction dir)[] potentialSpots = new[] + { + (new Point3D(X - 2, Y, Z), Direction.East), // West spot -> Face East + (new Point3D(X + 2, Y, Z), Direction.West), // East spot -> Face West + (new Point3D(X, Y - 2, Z), Direction.South), // North spot -> Face South + (new Point3D(X, Y + 2, Z), Direction.North) // South spot -> Face North + }; + + Utility.Shuffle(potentialSpots); + + int countToSpawn = Utility.RandomMinMax(2, 4); + + for (int i = 0; i < countToSpawn && i < potentialSpots.Length; i++) + { + var spot = potentialSpots[i]; + Mobile attendee; + + if (Utility.RandomBool()) + { + var gc = new GatheringCitizen(); + gc.GatherDirection = spot.dir; + attendee = gc; + } + else + { + var gm = new GatheringMonster(); + gm.GatherDirection = spot.dir; + attendee = gm; + } + + AddWorker(attendee, spot); + } + } + + public void ClearNode() + { + foreach (var worker in _workers) + { + if (worker != null && !worker.Deleted) + { + worker.Delete(); + } + } + _workers.Clear(); + + 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."); + } + } + + public override void OnDelete() + { + ClearNode(); + base.OnDelete(); + } + + public CitizenNode(Serial serial) : base(serial) + { + } + } +} diff --git a/Projects/UOContent/Mobiles/Townfolk/Citizens/Citizens/MonsterCitizen.cs b/Projects/UOContent/Mobiles/Townfolk/Citizens/Citizens/MonsterCitizen.cs index 76e6d3204..6f6f8a9bc 100644 --- a/Projects/UOContent/Mobiles/Townfolk/Citizens/Citizens/MonsterCitizen.cs +++ b/Projects/UOContent/Mobiles/Townfolk/Citizens/Citizens/MonsterCitizen.cs @@ -32,7 +32,7 @@ namespace Server.Mobiles BaseSoundID = profile.SoundID; Karma = 1000; Fame = 1000; - _NextChatter = DateTime.UtcNow + TimeSpan.FromSeconds(Utility.RandomMinMax(10, 20)); + _NextChatter = DateTime.UtcNow + TimeSpan.FromSeconds(Utility.RandomMinMax(5, 15)); } public override void OnThink() diff --git a/Projects/UOContent/Mobiles/Townfolk/Citizens/MeetingSpots/CampfireHit.cs b/Projects/UOContent/Mobiles/Townfolk/Citizens/MeetingSpots/CampfireHit.cs new file mode 100644 index 000000000..b9bd99144 --- /dev/null +++ b/Projects/UOContent/Mobiles/Townfolk/Citizens/MeetingSpots/CampfireHit.cs @@ -0,0 +1,22 @@ +using System; +using Server; +using ModernUO.Serialization; + +namespace Server.Items +{ + [SerializationGenerator(0, false)] + public partial class CampfireHit : Item + { + [Constructible] + public CampfireHit() : base(0xFF8D) + { + Name = "a campfire"; + Movable = false; + Light = LightType.Circle225; + } + + public CampfireHit(Serial serial) : base(serial) + { + } + } +} diff --git a/Projects/UOContent/Mobiles/Townfolk/Citizens/MeetingSpots/MeetingSpotHuman.cs b/Projects/UOContent/Mobiles/Townfolk/Citizens/MeetingSpots/MeetingSpotHuman.cs new file mode 100644 index 000000000..78859c69f --- /dev/null +++ b/Projects/UOContent/Mobiles/Townfolk/Citizens/MeetingSpots/MeetingSpotHuman.cs @@ -0,0 +1,53 @@ +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/MeetingSpots/MeetingSpotMonser.cs b/Projects/UOContent/Mobiles/Townfolk/Citizens/MeetingSpots/MeetingSpotMonser.cs new file mode 100644 index 000000000..e2f6a4b98 --- /dev/null +++ b/Projects/UOContent/Mobiles/Townfolk/Citizens/MeetingSpots/MeetingSpotMonser.cs @@ -0,0 +1,53 @@ +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/Tradesmen/TradesmanNode.cs b/Projects/UOContent/Mobiles/Townfolk/Citizens/Tradesmen/TradesmanNode.cs deleted file mode 100644 index 7f2152ed8..000000000 --- a/Projects/UOContent/Mobiles/Townfolk/Citizens/Tradesmen/TradesmanNode.cs +++ /dev/null @@ -1,159 +0,0 @@ -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/TradesmanCitizen.cs b/Projects/UOContent/Mobiles/Townfolk/Citizens/Tradesmen/TradesmenCitizen.cs similarity index 100% rename from Projects/UOContent/Mobiles/Townfolk/Citizens/Tradesmen/TradesmanCitizen.cs rename to Projects/UOContent/Mobiles/Townfolk/Citizens/Tradesmen/TradesmenCitizen.cs