#W# Citizens: Reworked Citizens, working citizens, and meeting spot citizens

This commit is contained in:
WarrentyExpired 2026-11-04 11:30:45 -05:00
parent 740d06a524
commit 54e6d4c978
10 changed files with 337 additions and 166 deletions

View file

@ -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
);

View file

@ -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<Mobile> _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)
{
}
}
}

View file

@ -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()

View file

@ -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)
{
}
}
}

View file

@ -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();
}
}
}

View file

@ -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();
}
}
}

View file

@ -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)
{
}
}
}