diff --git a/Data/Config/Regions.xml b/Data/Config/Regions.xml index d91cfb0..ed56857 100644 --- a/Data/Config/Regions.xml +++ b/Data/Config/Regions.xml @@ -27,7 +27,7 @@ - + @@ -224,5 +224,8 @@ + + + diff --git a/Data/Config/innrooms.cfg b/Data/Config/innrooms.cfg new file mode 100644 index 0000000..354d4d9 --- /dev/null +++ b/Data/Config/innrooms.cfg @@ -0,0 +1,3 @@ +Small Vaelen 7133 3992 0 +Medium Vaelen 7135 4027 0 +Large Vaelen 7137 4066 0 diff --git a/Data/Decoration/Towns/InnRoomDoors.cfg b/Data/Decoration/Towns/InnRoomDoors.cfg new file mode 100644 index 0000000..a230e33 --- /dev/null +++ b/Data/Decoration/Towns/InnRoomDoors.cfg @@ -0,0 +1,8 @@ +InnExitDoor 1765 +7138 4075 0 + +InnExitDoor 1765 +7136 4034 0 + +InnExitDoor 1765 +7134 3997 0 diff --git a/Data/Decoration/inndoors.cfg b/Data/Decoration/inndoors.cfg deleted file mode 100644 index 22a24ed..0000000 --- a/Data/Decoration/inndoors.cfg +++ /dev/null @@ -1,2 +0,0 @@ -#Bastion Inn -Vaelen 835 666 0 Vaelen 7138 4066 0 1779 diff --git a/Scripts/Commands/CheckBalance.cs b/Scripts/Commands/CheckBalance.cs new file mode 100644 index 0000000..e541120 --- /dev/null +++ b/Scripts/Commands/CheckBalance.cs @@ -0,0 +1,49 @@ +using System; +using Server; +using Server.Commands; +using Server.Mobiles; +using Server.Targeting; + +namespace Server.Custom.Commands +{ + public class CheckBalance + { + public static void Initialize() + { + CommandSystem.Register("CheckBalance", AccessLevel.GameMaster, new CommandEventHandler(CheckBalance_OnCommand)); + } + + [Usage("CheckBalance")] + [Description("Checks the true Banker balance (including AccountGold) of a targeted player.")] + public static void CheckBalance_OnCommand(CommandEventArgs e) + { + e.Mobile.Target = new BalanceTarget(); + e.Mobile.SendMessage(68, "Target a player to check their total bank balance."); + } + + private class BalanceTarget : Target + { + // The '-1' means standard range, 'false' means it doesn't allow ground targeting + public BalanceTarget() : base(-1, false, TargetFlags.None) + { + } + + protected override void OnTarget(Mobile from, object targeted) + { + if (targeted is PlayerMobile) + { + PlayerMobile pm = (PlayerMobile)targeted; + + // Banker.GetBalance checks both AccountGold AND physical bank gold + long balance = Banker.GetBalance(pm); + + from.SendMessage(68, $"{pm.Name} has a true bank balance of: {balance} gold."); + } + else + { + from.SendMessage(33, "You must target a player character."); + } + } + } + } +} diff --git a/Scripts/Commands/GenInnDoors.cs b/Scripts/Commands/GenInnDoors.cs deleted file mode 100644 index 8a47ebe..0000000 --- a/Scripts/Commands/GenInnDoors.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System; -using System.IO; -using System.Collections; -using Server; -using Server.Items; -using Server.Custom.InnRooms; // Ensure it can find our InnDoor! - -namespace Server.Commands -{ - public class GenInnDoors - { - public static void Initialize() - { - CommandSystem.Register("InnGen", AccessLevel.Administrator, new CommandEventHandler(GenInnDoors_OnCommand)); - } - - [Usage("InnGen")] - [Description("Generates Inn Doors from a configuration file.")] - public static void GenInnDoors_OnCommand(CommandEventArgs e) - { - e.Mobile.SendMessage("Generating Inn Doors from config, please wait."); - - int count = new InnDoorCreator().CreateDoors(); - - e.Mobile.SendMessage("Inn Door generation complete. {0} doors were generated.", count); - } - - public class InnDoorCreator - { - private int m_Count; - private static Queue m_Queue = new Queue(); - - // Finds and deletes existing InnDoors at the target location to prevent stacking - public static bool FindExistingDoor(Map map, Point3D p) - { - IPooledEnumerable eable = map.GetItemsInRange(p, 0); - - foreach (Item item in eable) - { - if (item is InnDoor) - { - int delta = item.Z - p.Z; - - if (delta >= -12 && delta <= 12) // Uses your existing Z-delta logic - m_Queue.Enqueue(item); - } - } - - eable.Free(); - - while (m_Queue.Count > 0) - ((Item)m_Queue.Dequeue()).Delete(); - - return false; - } - - public void CreateDoor(Point3D loc, Map mapLoc, Point3D roomLoc, Map roomMap, int itemID) - { - if (!FindExistingDoor(mapLoc, loc)) - { - m_Count++; - - // Uses our new parameterized constructor for custom facings! - InnDoor door = new InnDoor(itemID); - - // Wire up the void room coordinates automatically - door.RoomLocation = roomLoc; - door.RoomMap = roomMap; - - door.MoveToWorld(loc, mapLoc); - } - } - - public int CreateDoors() - { - // Reads from the same Decoration directory[cite: 3] - string filePath = Path.Combine(Core.BaseDirectory, "Data", "Decoration", "inndoors.cfg"); - - if (!File.Exists(filePath)) - { - Console.WriteLine("Warning: {0} not found. No Inn Doors generated.", filePath); - return 0; - } - - using (StreamReader ip = new StreamReader(filePath)) - { - string line; - while ((line = ip.ReadLine()) != null) - { - line = line.Trim(); - - if (line.Length == 0 || line.StartsWith("#")) - continue; - - string[] split = line.Split(new char[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); - - // We need at least 8 arguments (Door Map/X/Y/Z and Room Map/X/Y/Z) - if (split.Length >= 8) - { - try - { - Map mapLoc = Map.Parse(split[0]); - int xLoc = int.Parse(split[1]); - int yLoc = int.Parse(split[2]); - int zLoc = int.Parse(split[3]); - - Map mapRoom = Map.Parse(split[4]); - int xRoom = int.Parse(split[5]); - int yRoom = int.Parse(split[6]); - int zRoom = int.Parse(split[7]); - - // Optional 9th argument: ItemID for custom facings! Defaults to 0x6E5 (1765) if missing. - int itemID = 1765; - if (split.Length >= 9) - { - // Handles both hex (0x6F3) and integer (1779) formats - if (split[8].StartsWith("0x", StringComparison.OrdinalIgnoreCase)) - itemID = Convert.ToInt32(split[8], 16); - else - itemID = int.Parse(split[8]); - } - - if (mapLoc != null && mapRoom != null) - { - CreateDoor(new Point3D(xLoc, yLoc, zLoc), mapLoc, new Point3D(xRoom, yRoom, zRoom), mapRoom, itemID); - } - } - catch (Exception ex) - { - Console.WriteLine("Error parsing Inn Door config line: {0}\n{1}", line, ex.Message); - } - } - } - } - - return m_Count; - } - } - } -} diff --git a/Scripts/Engines/InnDoor.cs b/Scripts/Engines/InnDoor.cs deleted file mode 100644 index 5599e33..0000000 --- a/Scripts/Engines/InnDoor.cs +++ /dev/null @@ -1,352 +0,0 @@ -using System; -using Server; -using Server.Items; -using Server.Mobiles; -using Server.Custom.InnRooms; -using Server.Multis; -using System.Collections.Generic; -using Server.ContextMenus; - -namespace Server.Custom.InnRooms -{ - public class InnDoor : Item - { - private Mobile m_Renter; - private InnRoomHouse m_House; - private DateTime m_RentExpires; - - // Where is the void room located? - private Point3D m_RoomLocation; - private Map m_RoomMap; - - [CommandProperty(AccessLevel.GameMaster)] - public bool ForceEviction - { get { return false; } set { if (value == true) { EvictTenant(); } } } - - // Configuration - private int m_RentCost = 5000; // 5,000 gold per week - private TimeSpan m_RentDuration = TimeSpan.FromDays(7); // 1 week - - [CommandProperty(AccessLevel.GameMaster)] - public Mobile Renter { get { return m_Renter; } set { m_Renter = value; InvalidateProperties(); } } - - [CommandProperty(AccessLevel.GameMaster)] - public Point3D RoomLocation { get { return m_RoomLocation; } set { m_RoomLocation = value; } } - - [CommandProperty(AccessLevel.GameMaster)] - public Map RoomMap { get { return m_RoomMap; } set { m_RoomMap = value; } } - - [Constructable] - public InnDoor() : this(0x6E5) - { - } - - [Constructable] - public InnDoor(int itemID) : base(itemID) // Standard wooden door graphic - { - Movable = false; - Name = "A Vacant Inn Room"; - } - - public InnDoor(Serial serial) : base(serial) - { - } - - public override void OnDoubleClick(Mobile from) - { - if (!from.InRange(GetWorldLocation(), 2)) - { - from.LocalOverheadMessage(Server.Network.MessageType.Regular, 0x3B2, 1019045); // I can't reach that. - return; - } - - // SCENARIO A: The room is VACANT - if (m_Renter == null) - { - if (RoomLocation == Point3D.Zero || RoomMap == null) - { - from.SendMessage(33, "This door is out of order (No Room Linked)."); - return; - } - - if (from.Backpack != null && from.Backpack.ConsumeTotal(typeof(Gold), m_RentCost)) - { - // Rent the room! - m_Renter = from; - m_RentExpires = DateTime.UtcNow + m_RentDuration; - Name = from.Name + "'s Inn Room"; - - // Spawn the invisible house controller in the void room - m_House = new InnRoomHouse(from); - //m_House.MoveToWorld(RoomLocation, RoomMap); - // BURY THE STAIRCASE! We spawn the house 20 Z-levels beneath the actual floor. - Point3D buriedLocation = new Point3D(RoomLocation.X, RoomLocation.Y, RoomLocation.Z - 20); - m_House.MoveToWorld(buriedLocation, RoomMap); - - from.PlaySound(0x249); - from.SendMessage(68, $"You have rented this room for {m_RentDuration.Days} days."); - TeleportToRoom(from); - } - else - { - from.SendMessage(33, $"You need {m_RentCost} gold in your backpack to rent this room."); - } - } - // SCENARIO B: The room is RENTED - else - { - // Check if the rent has expired - if (DateTime.UtcNow > m_RentExpires) - { - EvictTenant(); - from.SendMessage(33, "The rent expired and the room has been cleared. It is now vacant."); - return; - } - - // Check Access using your BaseHouse logic! - if (m_House != null && (m_House.IsOwner(from) || m_House.IsFriend(from) || m_House.HasAccess(from))) - { - TeleportToRoom(from); - } - else - { - from.SendMessage(33, "The door is locked and you do not have permission to enter."); - } - } - } - - private void TeleportToRoom(Mobile m) - { - m.PlaySound(0x1EA); - m.MoveToWorld(RoomLocation, RoomMap); - m.SendMessage(0x3B2, "You enter the inn room."); - } - - public void EvictTenant() - { - if (m_Renter != null && m_House != null && !m_House.Deleted) - { - // 1. Locate the player's bank box - BankBox bank = m_Renter.BankBox; - - if (bank != null) - { - // 2. Spawn our heavy-duty moving crate - InnEvictionCrate crate = new InnEvictionCrate(); - - // 3. Pack up all Secured Containers - // We must iterate backward (Count - 1 to 0) when removing things from lists! - for (int i = m_House.Secures.Count - 1; i >= 0; --i) - { - SecureInfo info = m_House.Secures[i] as SecureInfo; - if (info != null && info.Item != null) - { - Container secureContainer = info.Item; - secureContainer.IsSecure = false; - secureContainer.IsLockedDown = false; - secureContainer.Movable = true; - - // Drop the entire secured bag/chest into the crate - crate.DropItem(secureContainer); - } - } - m_House.Secures.Clear(); - - // 4. Pack up all individual Locked Down Items - for (int i = m_House.LockDowns.Count - 1; i >= 0; --i) - { - Item item = m_House.LockDowns[i] as Item; - if (item != null) - { - item.IsLockedDown = false; - item.Movable = true; - - // Drop the loose item into the crate - crate.DropItem(item); - } - } - m_House.LockDowns.Clear(); - - // 5. Deliver the crate to the bank! - if (crate.Items.Count > 0) - { - // Using DropItem forces it into the bank, bypassing standard bank limits - bank.DropItem(crate); - m_Renter.SendMessage(33, "Your inn room rent has expired. Your belongings have been packed into a crate and placed in your bank box."); - } - else - { - // If they didn't have any items in the room, just delete the empty crate - crate.Delete(); - } - } - - // 6. Demolish the invisible house controller - m_House.Delete(); - } - - // 7. Reset the door so a new player can rent it - m_Renter = null; - m_House = null; - Name = "A Vacant Inn Room"; - } - - public override void GetProperties(ObjectPropertyList list) - { - base.GetProperties(list); - - if (m_Renter != null) - { - list.Add(1060658, "Rent Cost\tPaid"); // ~1_val~: ~2_val~ - list.Add(1060659, "Expires\t{0}", m_RentExpires.ToString("g")); // ~1_val~: ~2_val~ - } - else - { - list.Add(1060658, "Rent Cost\t{0} Gold", m_RentCost); // ~1_val~: ~2_val~ - } - } - - public override void Serialize(GenericWriter writer) - { - base.Serialize(writer); - writer.Write((int)0); // version - - writer.Write(m_Renter); - writer.WriteItem(m_House); - writer.Write(m_RentExpires); - writer.Write(m_RoomLocation); - writer.Write(m_RoomMap); - } - - public override void Deserialize(GenericReader reader) - { - base.Deserialize(reader); - int version = reader.ReadInt(); - - m_Renter = reader.ReadMobile(); - m_House = reader.ReadItem() as InnRoomHouse; - m_RentExpires = reader.ReadDateTime(); - m_RoomLocation = reader.ReadPoint3D(); - m_RoomMap = reader.ReadMap(); - } - // --- 1. THE CONTEXT MENU MENU OVERRIDE --- - public override void GetContextMenuEntries(Mobile from, List list) - { - base.GetContextMenuEntries(from, list); - - // Only the active renter gets the option to renew - if (from.Alive && m_Renter == from) - { - // 1062400 is the UO Client Cliloc for "Renew Contract" - list.Add(new RenewRentEntry(from, this)); - } - } - - // --- 2. THE CONTEXT MENU ACTION LOGIC --- - private class RenewRentEntry : ContextMenuEntry - { - private Mobile m_From; - private InnDoor m_Door; - - // The '2' is the interaction range - public RenewRentEntry(Mobile from, InnDoor door) : base(1034309, 2) - { - m_From = from; - m_Door = door; - } - - public override void OnClick() - { - if (m_Door.Deleted || m_Door.Renter != m_From) - return; - - // Check their backpack for the gold - if (m_From.Backpack != null && m_From.Backpack.ConsumeTotal(typeof(Gold), m_Door.m_RentCost)) - { - // Add 7 days to their current expiration time - m_Door.m_RentExpires += m_Door.m_RentDuration; - - m_From.PlaySound(0x249); // Coin jingle sound - m_From.SendMessage(68, $"You have paid {m_Door.m_RentCost} gold to extend your rent."); - m_From.SendMessage(68, $"Your room is now paid until: {m_Door.m_RentExpires.ToString("g")}"); - m_Door.InvalidateProperties(); // Updates the door's mouse-over text - } - else - { - m_From.SendMessage(33, $"You need {m_Door.m_RentCost} gold in your backpack to renew this room."); - } - } - } - - // --- 3. BONUS: DRAG AND DROP RENEWAL --- - public override bool OnDragDrop(Mobile from, Item dropped) - { - // If the renter drops gold directly onto the door... - if (from == m_Renter && dropped is Gold) - { - if (dropped.Amount == m_RentCost) - { - dropped.Delete(); // Consume the gold - m_RentExpires += m_RentDuration; - - from.PlaySound(0x249); - from.SendMessage(68, $"You hand the gold to the landlord and extend your rent."); - from.SendMessage(68, $"Your room is now paid until: {m_RentExpires.ToString("g")}"); - InvalidateProperties(); - - return true; - } - else - { - from.SendMessage(33, $"You must drop exactly {m_RentCost} gold on the door to renew it."); - return false; // Rejects the drop and bounces the gold back to their cursor - } - } - - return base.OnDragDrop(from, dropped); - } - } - - public class InnEvictionCrate : WoodenBox - { - public override int DefaultMaxItems { get { return 500; } } - public override int DefaultMaxWeight { get { return 5000; } } - - [Constructable] - public InnEvictionCrate() - { - Name = "Evicted Inn Room Belongings"; - Hue = 33; // Bright red - } - - public InnEvictionCrate(Serial serial) : base(serial) { } - - // 1. This prevents players from putting anything INTO the crate - public override bool CheckHold(Mobile m, Item item, bool message, bool checkItems, int plusItems, int plusWeight) - { - if (m.AccessLevel < AccessLevel.GameMaster) - { - if (message) - m.SendLocalizedMessage(1061145); // "You cannot place items into a house moving crate." - - return false; - } - - return base.CheckHold(m, item, message, checkItems, plusItems, plusWeight); - } - - // 2. This makes the crate automatically vanish once the player empties it - public override void OnItemRemoved(Item item) - { - base.OnItemRemoved(item); - - if (this.TotalItems == 0) - { - Delete(); - } - } - - public override void Serialize(GenericWriter writer) { base.Serialize(writer); writer.Write((int)0); } - public override void Deserialize(GenericReader reader) { base.Deserialize(reader); int version = reader.ReadInt(); } - } -} diff --git a/Scripts/Engines/InnRoomHouse.cs b/Scripts/Engines/InnRoomHouse.cs deleted file mode 100644 index cd91c9a..0000000 --- a/Scripts/Engines/InnRoomHouse.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using Server; -using Server.Multis; - -namespace Server.Custom.InnRooms -{ - public class InnRoomHouse : BaseHouse - { - public override Rectangle2D[] Area - { - get { return new Rectangle2D[] { new Rectangle2D(-5, -5, 10, 10) }; } - } - - public override Point3D BaseBanLocation - { - get { return new Point3D(this.X, this.Y - 5, this.Z + 20); } - } - - public InnRoomHouse(Mobile owner) : base(0x1DF3, owner, 50, 2) - { - RestrictDecay = true; - } - - public InnRoomHouse(Serial serial) : base(serial) - { - } - - // --- THE LOCKDOWN FIX --- - public override int GetAosMaxLockdowns() - { - return 50; // 50 floor items - } - - public override int GetAosMaxSecures() - { - return 250; // 250 total items in the room - } - // ------------------------ - - public override bool IsInside(Point3D p, int height) - { - if (Deleted) - return false; - - int rx = p.X - this.X; - int ry = p.Y - this.Y; - - bool inArea = false; - foreach (Rectangle2D rect in Area) - { - if (rect.Contains(new Point2D(rx, ry))) - { - inArea = true; - break; - } - } - - if (!inArea) - return false; - - if (p.Z >= this.Z && (p.Z + height) <= this.Z + 40) - return true; - - return false; - } - - public override void Serialize(GenericWriter writer) - { - base.Serialize(writer); - writer.Write((int)0); - } - - public override void Deserialize(GenericReader reader) - { - base.Deserialize(reader); - int version = reader.ReadInt(); - } - } -} diff --git a/Scripts/Engines/InnRooms/InnExitDoor.cs b/Scripts/Engines/InnRooms/InnExitDoor.cs new file mode 100644 index 0000000..4ae47b8 --- /dev/null +++ b/Scripts/Engines/InnRooms/InnExitDoor.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using Server; +using Server.Mobiles; +using Server.Multis; +using Server.ContextMenus; + +namespace Server.Custom.InnRooms +{ + public class InnExitDoor : Item + { + [Constructable] + public InnExitDoor() : this(0x6E5) + { + } + + [Constructable] + public InnExitDoor(int itemID) : base(itemID) + { + Movable = false; + Name = "Room Exit"; + } + + public InnExitDoor(Serial serial) : base(serial) + { + } + + [CommandProperty(AccessLevel.GameMaster)] + public bool ForceEviction + { + get { return false; } + set + { + if (value) + { + foreach (Item item in World.Items.Values) + { + if (item is InnRoomHouse) + { + InnRoomHouse house = (InnRoomHouse)item; + if (house.Map == this.Map && house.IsInside(this.Location, 16)) + { + house.Evict(); + break; + } + } + } + } + } + } + + public override void OnDoubleClick(Mobile from) + { + if (!from.InRange(GetWorldLocation(), 2)) + { + from.LocalOverheadMessage(Server.Network.MessageType.Regular, 0x3B2, 1019045); + return; + } + + if (from is PlayerMobile) + { + PlayerMobile pm = (PlayerMobile)from; + + if (pm.LastInnMap != null && pm.LastInnMap != Map.Internal && pm.LastInnLocation != Point3D.Zero) + { + pm.PlaySound(0x1EA); + pm.MoveToWorld(pm.LastInnLocation, pm.LastInnMap); + pm.SendMessage(68, "You step out of your room and back into the inn."); + + pm.LastInnLocation = Point3D.Zero; + pm.LastInnMap = null; + } + else + { + from.SendMessage(33, "Your return location was lost. Please use a runebook or page a GameMaster."); + } + } + } + + public override void GetContextMenuEntries(Mobile from, List list) + { + base.GetContextMenuEntries(from, list); + + if (from.Alive && from is PlayerMobile) + { + List houses = BaseHouse.GetHouses(from); + foreach (BaseHouse h in houses) + { + if (h is InnRoomHouse && h.IsInside(this.Location, 16)) + { + // Cliloc 6171 displays as "Open House Menu" + list.Add(new ManageRoomEntry(from, (InnRoomHouse)h)); + break; + } + } + } + } + + private class ManageRoomEntry : ContextMenuEntry + { + private Mobile m_From; + private InnRoomHouse m_House; + + public ManageRoomEntry(Mobile from, InnRoomHouse house) : base(1063490, 2) + { + m_From = from; + m_House = house; + } + + public override void OnClick() + { + m_From.SendGump(new InnRoomManagementGump((PlayerMobile)m_From, m_House)); + } + } + + public override void Serialize(GenericWriter writer) + { + base.Serialize(writer); + writer.Write((int)0); + } + + public override void Deserialize(GenericReader reader) + { + base.Deserialize(reader); + int version = reader.ReadInt(); + } + } +} diff --git a/Scripts/Engines/InnRooms/InnRentalGump.cs b/Scripts/Engines/InnRooms/InnRentalGump.cs new file mode 100644 index 0000000..632d56d --- /dev/null +++ b/Scripts/Engines/InnRooms/InnRentalGump.cs @@ -0,0 +1,115 @@ +using System; +using Server; +using Server.Gumps; +using Server.Mobiles; +using Server.Items; +using Server.Network; + +namespace Server.Custom.InnRooms +{ + public class InnRentalGump : Gump + { + private PlayerMobile m_Player; + + public InnRentalGump(PlayerMobile pm) : base(150, 150) + { + m_Player = pm; + + AddPage(0); + + // Widened the background from 400 to 500 + AddBackground(0, 0, 500, 300, 9270); + + // Centered the header to match the new 500 width + AddHtml(0, 15, 500, 20, "
Inn Room Rentals
", false, false); + + DrawRoomOption(1, "Small Room", InnRoomSize.Small, 60); + DrawRoomOption(2, "Medium Room", InnRoomSize.Medium, 120); + DrawRoomOption(3, "Large Room", InnRoomSize.Large, 180); + } + + private void DrawRoomOption(int buttonId, string title, InnRoomSize size, int yPos) + { + int available = InnRoomManager.GetAvailableCount(size); + int cost = InnRoomManager.GetCost(size); + + int total = 0; + foreach (RoomDefinition def in InnRoomManager.Rooms) + { + if (def.Size == size) + total++; + } + + // Pushed the text to X: 70 to clear the button + AddHtml(70, yPos, 150, 20, $"{title}", false, false); + AddHtml(70, yPos + 20, 200, 20, $"Cost: {cost} Gold", false, false); + + // Pushed the availability count to X: 320 to sit nicely on the right side + AddHtml(320, yPos + 10, 150, 20, $"{available} / {total} Available", false, false); + + if (available > 0) + { + // Kept the button firmly at X: 25 + AddButton(25, yPos + 5, 4005, 4007, buttonId, GumpButtonType.Reply, 0); + } + else + { + AddHtml(25, yPos + 5, 20, 20, "X", false, false); + } + } + + public override void OnResponse(NetState sender, RelayInfo info) + { + if (info.ButtonID == 0 || m_Player == null) return; + + InnRoomSize selectedSize = InnRoomSize.Small; + if (info.ButtonID == 2) selectedSize = InnRoomSize.Medium; + if (info.ButtonID == 3) selectedSize = InnRoomSize.Large; + + int slot = InnRoomManager.FindFirstOpenSlot(selectedSize); + + if (slot == -1) + { + m_Player.SendMessage(33, "Those rooms are currently sold out."); + return; + } + + int cost = InnRoomManager.GetCost(selectedSize); + + if (Banker.Withdraw(m_Player, cost)) + { + RentRoom(selectedSize, slot); + } + else if (m_Player.Backpack != null && m_Player.Backpack.ConsumeTotal(typeof(Gold), cost)) + { + RentRoom(selectedSize, slot); + } + else + { + m_Player.SendMessage(33, $"You need {cost} gold in your account or backpack to rent that room."); + } + } + + private void RentRoom(InnRoomSize size, int slot) + { + m_Player.LastInnLocation = m_Player.Location; + m_Player.LastInnMap = m_Player.Map; + + RoomDefinition roomDef = InnRoomManager.Rooms[slot]; + Point3D voidRoomLoc = roomDef.Location; + Map voidRoomMap = roomDef.RoomMap; + + InnRoomHouse newHouse = new InnRoomHouse(m_Player); + newHouse.RoomTier = size; + newHouse.RoomIndex = slot; + newHouse.RentExpires = DateTime.UtcNow + TimeSpan.FromDays(7); + + newHouse.MoveToWorld(new Point3D(voidRoomLoc.X, voidRoomLoc.Y, voidRoomLoc.Z - 20), voidRoomMap); + + m_Player.PlaySound(0x249); + m_Player.SendMessage(68, $"You have rented a {size} room."); + + m_Player.MoveToWorld(voidRoomLoc, voidRoomMap); + } + } +} diff --git a/Scripts/Engines/InnRooms/InnRoomHouse.cs b/Scripts/Engines/InnRooms/InnRoomHouse.cs new file mode 100644 index 0000000..cd76e4f --- /dev/null +++ b/Scripts/Engines/InnRooms/InnRoomHouse.cs @@ -0,0 +1,287 @@ +using System; +using System.Collections.Generic; +using Server; +using Server.Items; +using Server.Mobiles; +using Server.Multis; + +namespace Server.Custom.InnRooms +{ + public class InnRoomHouse : BaseHouse + { + public int RoomIndex; + private InnRoomSize m_RoomTier; + + [CommandProperty(AccessLevel.GameMaster)] + public InnRoomSize RoomTier + { + get { return m_RoomTier; } + set + { + m_RoomTier = value; + MaxLockDowns = GetAosMaxLockdowns(); + MaxSecures = GetAosMaxSecures(); + } + } + + // --- NEW EVICTION VARIABLES --- + public Mobile Renter; + public DateTime RentExpires; + private RentTimer m_Timer; + + public override Rectangle2D[] Area + { + get { return new Rectangle2D[] { new Rectangle2D(-15, -15, 30, 30) }; } + } + + public override Point3D BaseBanLocation + { + get { return new Point3D(this.X, this.Y - 5, this.Z + 20); } + } + + public InnRoomHouse(Mobile owner) : base(0xA28, owner, 50, 2) + { + RestrictDecay = true; + Renter = owner; + + // Start the internal clock! + m_Timer = new RentTimer(this); + m_Timer.Start(); + } + + public InnRoomHouse(Serial serial) : base(serial) + { + } + + public override int GetAosMaxLockdowns() + { + switch (RoomTier) + { + case InnRoomSize.Small: return 350; // 125 Total Items + case InnRoomSize.Medium: return 375; // 250 Total Items + case InnRoomSize.Large: return 500; // 500 Total Items + default: return 125; + } + } + + public override int GetAosMaxSecures() + { + switch (RoomTier) + { + case InnRoomSize.Small: return 2; // 2 Secure Chests + case InnRoomSize.Medium: return 3; // 3 Secure Chests + case InnRoomSize.Large: return 4; // 4 Secure Chests + default: return 2; + } + } + + public override bool IsInside(Point3D p, int height) + { + if (Deleted) return false; + + int rx = p.X - this.X; + int ry = p.Y - this.Y; + + bool inArea = false; + foreach (Rectangle2D rect in Area) + { + if (rect.Contains(new Point2D(rx, ry))) + { + inArea = true; + break; + } + } + + if (!inArea) return false; + if (p.Z >= this.Z && (p.Z + height) <= this.Z + 40) return true; + + return false; + } + + // --- EVICTION LOGIC --- + public void Evict() + { + if (Deleted) return; + + // 1. Pack up items and send to bank + if (Renter != null && Renter.BankBox != null) + { + InnEvictionCrate crate = new InnEvictionCrate(); + + for (int i = Secures.Count - 1; i >= 0; --i) + { + SecureInfo info = Secures[i] as SecureInfo; + if (info != null && info.Item != null) + { + Container sec = info.Item; + sec.IsSecure = false; + sec.IsLockedDown = false; + sec.Movable = true; + crate.DropItem(sec); + } + } + Secures.Clear(); + + for (int i = LockDowns.Count - 1; i >= 0; --i) + { + Item item = LockDowns[i] as Item; + if (item != null) + { + item.IsLockedDown = false; + item.Movable = true; + crate.DropItem(item); + } + } + LockDowns.Clear(); + + if (crate.Items.Count > 0) + { + Renter.BankBox.DropItem(crate); + Renter.SendMessage(33, "Your inn room rent has expired. Your belongings were moved to your bank."); + } + else + { + crate.Delete(); + } + } + + // 2. Kick anyone currently inside the room back to town + List insideMobiles = GetMobiles(); + foreach (Mobile m in insideMobiles) + { + if (m is PlayerMobile) + { + PlayerMobile pm = (PlayerMobile)m; + if (pm.LastInnMap != null && pm.LastInnLocation != Point3D.Zero) + { + pm.MoveToWorld(pm.LastInnLocation, pm.LastInnMap); + pm.LastInnLocation = Point3D.Zero; + pm.LastInnMap = null; + } + else + { + // Safe fallback if they don't have an anchor + pm.MoveToWorld(new Point3D(865, 605, 0), Map.Trammel); // Britain Bank + } + pm.SendMessage(33, "The rent has expired and you have been evicted from the room."); + } + } + + // 3. Destroy the house (Freeing the slot for the Manager!) + Delete(); + } + + public override void OnDelete() + { + if (m_Timer != null) + m_Timer.Stop(); + + base.OnDelete(); + } + + public override void Serialize(GenericWriter writer) + { + base.Serialize(writer); + writer.Write((int)2); // Version bumped to 2! + + writer.Write(Renter); + writer.Write(RentExpires); + + writer.Write((int)RoomTier); + writer.Write(RoomIndex); + } + + public override void Deserialize(GenericReader reader) + { + base.Deserialize(reader); + int version = reader.ReadInt(); + + switch (version) + { + case 2: + { + Renter = reader.ReadMobile(); + RentExpires = reader.ReadDateTime(); + goto case 1; + } + case 1: + { + RoomTier = (InnRoomSize)reader.ReadInt(); + RoomIndex = reader.ReadInt(); + goto case 0; + } + case 0: + { + break; + } + } + + // Restart the internal clock on server boot + m_Timer = new RentTimer(this); + m_Timer.Start(); + } + + // --- INTERNAL CLOCK --- + private class RentTimer : Timer + { + private InnRoomHouse m_House; + + // Checks the expiration date every 1 minute + public RentTimer(InnRoomHouse house) : base(TimeSpan.FromMinutes(1.0), TimeSpan.FromMinutes(1.0)) + { + m_House = house; + Priority = TimerPriority.FiveSeconds; + } + + protected override void OnTick() + { + if (m_House == null || m_House.Deleted) + { + Stop(); + return; + } + + if (DateTime.UtcNow > m_House.RentExpires) + { + m_House.Evict(); + Stop(); + } + } + } + } + + // --- READ-ONLY EVICTION CRATE --- + public class InnEvictionCrate : WoodenBox + { + public override int DefaultMaxItems { get { return 500; } } + public override int DefaultMaxWeight { get { return 5000; } } + + [Constructable] + public InnEvictionCrate() + { + Name = "Evicted Inn Room Belongings"; + Hue = 33; + } + + public InnEvictionCrate(Serial serial) : base(serial) { } + + public override bool CheckHold(Mobile m, Item item, bool message, bool checkItems, int plusItems, int plusWeight) + { + if (m.AccessLevel < AccessLevel.GameMaster) + { + if (message) m.SendLocalizedMessage(1061145); + return false; + } + return base.CheckHold(m, item, message, checkItems, plusItems, plusWeight); + } + + public override void OnItemRemoved(Item item) + { + base.OnItemRemoved(item); + if (this.TotalItems == 0) Delete(); + } + + public override void Serialize(GenericWriter writer) { base.Serialize(writer); writer.Write((int)0); } + public override void Deserialize(GenericReader reader) { base.Deserialize(reader); int version = reader.ReadInt(); } + } +} diff --git a/Scripts/Engines/InnRooms/InnRoomManagementGump.cs b/Scripts/Engines/InnRooms/InnRoomManagementGump.cs new file mode 100644 index 0000000..fadf6c5 --- /dev/null +++ b/Scripts/Engines/InnRooms/InnRoomManagementGump.cs @@ -0,0 +1,82 @@ +using System; +using Server; +using Server.Gumps; +using Server.Mobiles; +using Server.Network; +using Server.Multis; + +namespace Server.Custom.InnRooms +{ + public class InnRoomManagementGump : Gump + { + private PlayerMobile m_Player; + private InnRoomHouse m_House; + + public InnRoomManagementGump(PlayerMobile pm, InnRoomHouse house) : base(150, 150) + { + m_Player = pm; + m_House = house; + + AddPage(0); + AddBackground(0, 0, 400, 350, 9270); + AddHtml(0, 15, 400, 20, "
Room Management
", false, false); + + // --- STORAGE STATS --- + AddHtml(40, 60, 150, 20, "Lockdowns:", false, false); + AddHtml(150, 60, 200, 20, $"{m_House.LockDownCount} / {m_House.MaxLockDowns}", false, false); + + AddHtml(40, 90, 150, 20, "Secures:", false, false); + AddHtml(150, 90, 200, 20, $"{m_House.SecureCount} / {m_House.MaxSecures}", false, false); + + // --- RENT INFO --- + AddHtml(40, 140, 150, 20, "Rent Expires:", false, false); + AddHtml(150, 140, 200, 20, $"{m_House.RentExpires.ToString("g")}", false, false); + + // --- ACTIONS --- + // Auto-Renew Status Placeholder (We will wire this up next!) + AddHtml(40, 190, 250, 20, "Auto-Renew: Disabled", false, false); + + // Cancel Button + AddHtml(75, 280, 250, 20, "Cancel Rental Contract", false, false); + AddButton(40, 280, 4005, 4007, 1, GumpButtonType.Reply, 0); + } + + public override void OnResponse(NetState sender, RelayInfo info) + { + if (info.ButtonID == 1 && m_House != null && !m_House.Deleted) + { + m_Player.SendGump(new CancelRentGump(m_House)); + } + } + } + + // --- CANCELLATION CONFIRMATION GUMP --- + public class CancelRentGump : Gump + { + private InnRoomHouse m_House; + + public CancelRentGump(InnRoomHouse house) : base(200, 200) + { + m_House = house; + + AddPage(0); + AddBackground(0, 0, 300, 150, 9270); + AddHtml(0, 15, 300, 20, "
Cancel Room Rental?
", false, false); + AddHtml(20, 50, 260, 40, "This will instantly pack your items into your bank and evict you.", false, false); + + AddButton(40, 100, 4005, 4007, 1, GumpButtonType.Reply, 0); + AddHtml(75, 100, 100, 20, "Confirm", false, false); + + AddButton(160, 100, 4005, 4007, 0, GumpButtonType.Reply, 0); + AddHtml(195, 100, 100, 20, "Cancel", false, false); + } + + public override void OnResponse(NetState sender, RelayInfo info) + { + if (info.ButtonID == 1 && m_House != null && !m_House.Deleted) + { + m_House.Evict(); + } + } + } +} diff --git a/Scripts/Engines/InnRooms/InnRoomManager.cs b/Scripts/Engines/InnRooms/InnRoomManager.cs new file mode 100644 index 0000000..008fb47 --- /dev/null +++ b/Scripts/Engines/InnRooms/InnRoomManager.cs @@ -0,0 +1,133 @@ +using System; +using System.IO; +using System.Collections.Generic; +using Server; +using Server.Items; +using Server.Custom.InnRooms; + +namespace Server.Custom.InnRooms +{ + public enum InnRoomSize { Small, Medium, Large } + + // This object holds the coordinates for a single room from your config file + public class RoomDefinition + { + public InnRoomSize Size; + public Map RoomMap; + public Point3D Location; + } + + public class InnRoomManager + { + // The master list of all rooms loaded from the config file + public static List Rooms = new List(); + + // This runs automatically when the server starts + public static void Initialize() + { + LoadRooms(); + } + + public static int GetCost(InnRoomSize size) + { + switch (size) + { + case InnRoomSize.Small: return 5000; + case InnRoomSize.Medium: return 10000; + case InnRoomSize.Large: return 20000; + default: return 5000; + } + } + + public static void LoadRooms() + { + Rooms.Clear(); + string filePath = Path.Combine(Core.BaseDirectory, "Data", "Config", "innrooms.cfg"); + + if (!File.Exists(filePath)) + { + Console.WriteLine("InnRoomManager: No innrooms.cfg found!"); + return; + } + + using (StreamReader ip = new StreamReader(filePath)) + { + string line; + while ((line = ip.ReadLine()) != null) + { + line = line.Trim(); + if (line.Length == 0 || line.StartsWith("#")) continue; + + string[] split = line.Split(new char[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); + + // Expected Format: Size Map X Y Z + // Example: Small Felucca 6000 6000 0 + if (split.Length >= 5) + { + try + { + RoomDefinition def = new RoomDefinition(); + def.Size = (InnRoomSize)Enum.Parse(typeof(InnRoomSize), split[0], true); + def.RoomMap = Map.Parse(split[1]); + def.Location = new Point3D(int.Parse(split[2]), int.Parse(split[3]), int.Parse(split[4])); + + Rooms.Add(def); + } + catch (Exception ex) + { + Console.WriteLine($"InnRoomManager Error parsing line: {line}\n{ex.Message}"); + } + } + } + } + Console.WriteLine($"InnRoomManager: Loaded {Rooms.Count} total rooms from config."); + } + + // Checks all active rentals on the server to see which list indexes are currently taken + public static List GetOccupiedIndexes() + { + List occupied = new List(); + + foreach (Item item in World.Items.Values) + { + if (item is InnRoomHouse) + { + InnRoomHouse house = (InnRoomHouse)item; + occupied.Add(house.RoomIndex); + } + } + return occupied; + } + + // Counts how many rooms of a specific size are NOT currently rented + public static int GetAvailableCount(InnRoomSize size) + { + List occupied = GetOccupiedIndexes(); + int count = 0; + + for (int i = 0; i < Rooms.Count; i++) + { + if (Rooms[i].Size == size && !occupied.Contains(i)) + { + count++; + } + } + return count; + } + + // Finds the exact list index of the first available room of the requested size + public static int FindFirstOpenSlot(InnRoomSize size) + { + List occupied = GetOccupiedIndexes(); + + for (int i = 0; i < Rooms.Count; i++) + { + if (Rooms[i].Size == size && !occupied.Contains(i)) + { + return i; // Returns the exact index in the Rooms list + } + } + return -1; // -1 means sold out + } + } +} diff --git a/Scripts/Mobiles/PlayerMobile.cs b/Scripts/Mobiles/PlayerMobile.cs index d37da4f..4e73159 100644 --- a/Scripts/Mobiles/PlayerMobile.cs +++ b/Scripts/Mobiles/PlayerMobile.cs @@ -201,6 +201,11 @@ namespace Server.Mobiles private List m_AllFollowers; private List m_RecentlyReported; + // --- Start Inn Room + private Point3D m_LastInnLocation; + private Map m_LastInnMap; + // -- End Inn Room + #region Getters & Setters public List RecentlyReported @@ -473,6 +478,22 @@ namespace Server.Mobiles get{ return GetFlag( PlayerFlag.RefuseTrades ); } set{ SetFlag( PlayerFlag.RefuseTrades, value ); } } + + // -- Start Inn Room + [CommandProperty(AccessLevel.GameMaster)] + public Point3D LastInnLocation + { + get { return m_LastInnLocation; } + set { m_LastInnLocation = value; } + } + + [CommandProperty(AccessLevel.GameMaster)] + public Map LastInnMap + { + get { return m_LastInnMap; } + set { m_LastInnMap = value; } + } + // -- End Inn Room #endregion #region Auto Arrow Recovery @@ -3358,6 +3379,12 @@ namespace Server.Mobiles switch ( version ) { + case 30: + { + m_LastInnLocation = reader.ReadPoint3D(); + m_LastInnMap = reader.ReadMap(); + goto case 29; + } case 29: { if (reader.ReadBool()) @@ -3667,7 +3694,12 @@ namespace Server.Mobiles base.Serialize( writer ); - writer.Write( (int) 29 ); // version + writer.Write( (int) 30 ); // version + + // -- Start Inn Room + writer.Write( m_LastInnLocation ); + writer.Write( m_LastInnMap ); + // -- End Inn Room if (m_StuckMenuUses != null) { @@ -5175,4 +5207,4 @@ namespace Server.Mobiles m_AutoStabled.Clear(); } } -} \ No newline at end of file +} diff --git a/Scripts/Regions/InnRegion.cs b/Scripts/Regions/InnRegion.cs new file mode 100644 index 0000000..93fbc34 --- /dev/null +++ b/Scripts/Regions/InnRegion.cs @@ -0,0 +1,75 @@ +using System; +using System.Xml; // Required for loading from Regions.xml +using System.Collections.Generic; +using Server; +using Server.Mobiles; +using Server.Regions; +using Server.Multis; + +namespace Server.Custom.InnRooms +{ + public class InnRegion : BaseRegion + { + // Constructor 1: Used if manually generated in a script + public InnRegion(string name, Map map, int priority, params Rectangle2D[] area) + : base(name, map, priority, area) + { + Register(); + } + + // Constructor 2: Used when loading from Regions.xml (This fixes your crash!) + public InnRegion(XmlElement xml, Map map, Region parent) + : base(xml, map, parent) + { + // Note: We don't call Register() here because the XML loader handles it automatically! + } + + public override void OnSpeech(SpeechEventArgs e) + { + base.OnSpeech(e); + + if (!e.Handled && e.Mobile is PlayerMobile && e.Mobile.Alive) + { + PlayerMobile pm = (PlayerMobile)e.Mobile; + + if (e.Speech.ToLower().Contains("inn room")) + { + e.Handled = true; + + InnRoomHouse rentedRoom = GetPlayerRoom(pm); + + if (rentedRoom != null) + { + pm.LastInnLocation = pm.Location; + pm.LastInnMap = pm.Map; + + Point3D roomLoc = new Point3D(rentedRoom.X, rentedRoom.Y, rentedRoom.Z + 20); + + pm.PlaySound(0x1EA); + pm.MoveToWorld(roomLoc, rentedRoom.Map); + pm.SendMessage(68, "You are whisked away to your rented room."); + } + else + { + pm.CloseGump(typeof(InnRentalGump)); + pm.SendGump(new InnRentalGump(pm)); + } + } + } + } + + private InnRoomHouse GetPlayerRoom(Mobile m) + { + List houses = BaseHouse.GetHouses(m); + + foreach (BaseHouse house in houses) + { + if (house is InnRoomHouse && !house.Deleted) + { + return (InnRoomHouse)house; + } + } + return null; + } + } +}