#W# Bounty: The bounty system is working. Its a standalone system not MLQuest. Need to fill it in and add rewards.

This commit is contained in:
WarrentyExpired 2026-10-09 20:00:00 -04:00
parent b4e25a2713
commit 4a20c34c0d
8 changed files with 532 additions and 110 deletions

View file

@ -0,0 +1,177 @@
using System;
using ModernUO.Serialization;
using Server.Engines.Bounties;
using Server.Gumps;
using Server.Items;
using Server.Mobiles;
using Server.Network;
namespace Server.Items;
[SerializationGenerator(0, false)]
public partial class DungeonBountyBoard : Item
{
[Constructible]
public DungeonBountyBoard() : base(0x1E5E)
{
Name = "Dungeon Bounty Board";
Movable = false;
}
public override void OnDoubleClick(Mobile from)
{
if (from is PlayerMobile pm)
{
if (from.InRange(Location, 3))
pm.SendGump(new BountyBoardGump(pm));
else
from.SendLocalizedMessage(500446);
}
}
}
public class BountyBoardGump : Gump
{
private readonly PlayerMobile _player;
public BountyBoardGump(PlayerMobile player) : base(50, 50)
{
_player = player;
BountyProfile profile = BountySystem.GetProfile(player);
AddPage(0);
AddBackground(0, 0, 550, 480, 9270);
AddAlphaRegion(10, 10, 530, 460);
AddLabel(180, 15, 1152, "DUNGEON BOUNTY BOARD");
// --- ACTIVE BOUNTY SECTION ---
AddLabel(20, 50, 0x3F, "--- Your Active Bounty ---");
if (profile.HasActiveBounty && BountyRegistry.AllBounties.TryGetValue(profile.ActiveBountyID, out var activeBounty))
{
AddLabel(20, 75, 1152, activeBounty.Title);
AddLabel(350, 75, 2100, $"{profile.CurrentKills} / {activeBounty.RequiredKills} Slain");
if (profile.CurrentKills >= activeBounty.RequiredKills)
{
AddButton(20, 100, 4005, 4007, 1000, GumpButtonType.Reply, 0);
AddLabel(55, 100, 0x44, "CLAIM REWARD");
}
else
{
int penaltyCost = activeBounty.RewardGold; // Cancellation costs the reward amount
AddButton(20, 100, 4017, 4019, 1001, GumpButtonType.Reply, 0);
AddLabel(55, 100, 33, $"Refuse Bounty (Cost: {penaltyCost} Gold)");
}
}
else
{
AddLabel(20, 75, 1152, "You have no active bounties.");
}
// --- AVAILABLE BOUNTIES SECTION ---
AddLabel(20, 150, 0x3F, "--- Available Bounties ---");
if (profile.IsOnCooldown)
{
TimeSpan wait = profile.NextAvailableTime - DateTime.UtcNow;
AddLabel(20, 180, 33, $"The board is empty. Next bounties arrive in {(int)wait.TotalMinutes} minutes.");
}
else
{
AddLabel(20, 170, 2100, $"Your Current Fame: {player.Fame}");
int y = 200;
for (int tier = 1; tier <= 5; tier++)
{
// Grab an example from the tier to check fame requirements and rewards
var example = BountyRegistry.GetRandomBounty(tier);
if (example == null) continue;
if (player.Fame >= example.RequiredFame)
{
if (!profile.HasActiveBounty)
{
// Assigning Button IDs 101 to 105 for Tiers 1-5
AddButton(20, y, 4005, 4007, 100 + tier, GumpButtonType.Reply, 0);
}
AddLabel(55, y, 1152, $"Request Tier {tier} Bounty");
AddLabel(320, y, 0x3F, $"Earn {example.RewardTokens} Tokens");
}
else
{
AddLabel(55, y, 33, $"Tier {tier} Bounty (Requires {example.RequiredFame} Fame)");
}
y += 35;
}
}
AddButton(20, 440, 4017, 4019, 0, GumpButtonType.Reply, 0);
AddLabel(55, 440, 1152, "Close Board");
}
public override void OnResponse(NetState sender, in RelayInfo info)
{
if (info.ButtonID == 0) return;
BountyProfile profile = BountySystem.GetProfile(_player);
if (info.ButtonID == 1000) // CLAIM REWARD
{
if (profile.HasActiveBounty && BountyRegistry.AllBounties.TryGetValue(profile.ActiveBountyID, out var activeBounty))
{
if (profile.CurrentKills >= activeBounty.RequiredKills)
{
_player.AddToBackpack(new BountyToken(activeBounty.RewardTokens));
_player.AddToBackpack(new Gold(activeBounty.RewardGold));
_player.SendMessage(0x44, $"Complete! Received {activeBounty.RewardTokens} Tokens and {activeBounty.RewardGold} Gold.");
_player.PlaySound(0x2E6);
// Reset Profile and trigger the 60 minute cooldown
profile.ActiveBountyID = -1;
profile.CurrentKills = 0;
profile.NextAvailableTime = DateTime.UtcNow + TimeSpan.FromMinutes(60);
}
}
}
else if (info.ButtonID == 1001) // CANCEL BOUNTY (Refusal Fee)
{
if (BountyRegistry.AllBounties.TryGetValue(profile.ActiveBountyID, out var activeBounty))
{
int cost = activeBounty.RewardGold;
// Attempt to consume gold from their backpack
if (_player.Backpack != null && _player.Backpack.ConsumeTotal(typeof(Gold), cost))
{
profile.ActiveBountyID = -1;
profile.CurrentKills = 0;
profile.NextAvailableTime = DateTime.UtcNow + TimeSpan.FromMinutes(60);
_player.SendMessage(33, $"You paid the {cost} gold refusal fee and abandoned the bounty.");
}
else
{
_player.SendMessage(33, $"You need {cost} gold in your backpack to refuse this bounty!");
}
}
}
else if (info.ButtonID > 100 && info.ButtonID <= 105) // ACCEPT RANDOM BOUNTY BY TIER
{
int selectedTier = info.ButtonID - 100;
if (!profile.HasActiveBounty && !profile.IsOnCooldown)
{
var newBounty = BountyRegistry.GetRandomBounty(selectedTier);
if (newBounty != null && _player.Fame >= newBounty.RequiredFame)
{
profile.ActiveBountyID = newBounty.ID;
profile.CurrentKills = 0;
_player.SendMessage(0x3F, $"You have been assigned: {newBounty.Title}");
_player.PlaySound(0x04A);
}
}
}
_player.SendGump(new BountyBoardGump(_player));
}
}

View file

@ -0,0 +1,245 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using Server;
using Server.Commands;
using Server.Gumps;
using Server.Mobiles;
namespace Server.Engines.Bounties;
// Moved BountyDefinition to BountyRegistry.cs
// ==========================================
// 1. PLAYER BOUNTY PROFILE
// ==========================================
public class BountyProfile
{
public int ActiveBountyID { get; set; } = -1;
public int CurrentKills { get; set; } = 0;
// Time-Gate Tracking
public DateTime NextAvailableTime { get; set; } = DateTime.MinValue;
public bool HasActiveBounty => ActiveBountyID != -1;
public bool IsOnCooldown => DateTime.UtcNow < NextAvailableTime;
}
// ==========================================
// 2. JSON SAVE DATA STRUCTURE
// ==========================================
public class BountySaveData
{
public uint MobileSerial { get; set; }
public int ActiveBountyID { get; set; }
public int CurrentKills { get; set; }
public long NextAvailableTicks { get; set; } // Save DateTime as ticks
}
// ==========================================
// 3. BOUNTY SYSTEM ENGINE
// ==========================================
public static class BountySystem
{
private static Dictionary<Mobile, BountyProfile> Profiles = new();
public static void Configure()
{
EventSink.WorldSave += delegate { OnSave(); };
}
public static void Initialize()
{
CommandSystem.Register("Bounties", AccessLevel.Player, new CommandEventHandler(Bounties_OnCommand));
// Initialize the new external registry
BountyRegistry.Initialize();
Load();
}
[Usage("Bounties")]
[Description("Checks your current active dungeon bounty.")]
private static void Bounties_OnCommand(CommandEventArgs e)
{
if (e.Mobile is not PlayerMobile pm) return;
BountyProfile profile = GetProfile(pm);
pm.CloseGump<ActiveBountyGump>();
pm.SendGump(new ActiveBountyGump(pm, profile));
}
public static BountyProfile GetProfile(Mobile pm)
{
if (!Profiles.TryGetValue(pm, out var profile))
{
profile = new BountyProfile();
Profiles[pm] = profile;
}
return profile;
}
// --- BULLETPROOF REGION CHECKER ---
private static bool IsInValidRegion(Mobile creature, Mobile killer, string requiredRegion)
{
if (CheckRegionTree(creature.Region, requiredRegion)) return true;
if (CheckRegionTree(killer.Region, requiredRegion)) return true;
return false;
}
private static bool CheckRegionTree(Region region, string requiredRegion)
{
Region current = region;
while (current != null)
{
if (current.Name != null && current.Name.IndexOf(requiredRegion, StringComparison.OrdinalIgnoreCase) >= 0)
return true;
current = current.Parent;
}
return false;
}
// --- KILL TRACKING HOOK ---
public static void OnCreatureDeath(BaseCreature creature)
{
if (creature == null) return;
Mobile source = creature.LastKiller ?? creature.FindMostRecentDamager(true);
if (source is BaseCreature pet && pet.Controlled && pet.ControlMaster != null)
source = pet.ControlMaster;
if (source is not PlayerMobile killer) return;
BountyProfile profile = GetProfile(killer);
if (profile.HasActiveBounty && BountyRegistry.AllBounties.TryGetValue(profile.ActiveBountyID, out BountyDefinition bounty))
{
if (!string.IsNullOrEmpty(bounty.RequiredRegion))
{
if (!IsInValidRegion(creature, killer, bounty.RequiredRegion)) return;
}
if (profile.CurrentKills < bounty.RequiredKills && bounty.Matches(creature.GetType()))
{
profile.CurrentKills++;
if (profile.CurrentKills >= bounty.RequiredKills)
{
killer.SendMessage(0x3F, $"[Bounty Complete]: You have slain {profile.CurrentKills}/{bounty.RequiredKills} {bounty.Title}!");
killer.PlaySound(0x5B5);
}
else
{
killer.SendMessage(0x35, $"[Bounty Progress]: {creature.Name} slain. ({profile.CurrentKills}/{bounty.RequiredKills})");
}
}
}
}
// --- DATA PERSISTENCE (JSON) ---
private static string FilePath => Path.Combine(Core.BaseDirectory, "Data", "DungeonBounties.json");
private static void OnSave()
{
try
{
string dir = Path.GetDirectoryName(FilePath);
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
var list = new List<BountySaveData>();
foreach (var kvp in Profiles)
{
if (kvp.Key != null && !kvp.Key.Deleted && (kvp.Value.HasActiveBounty || kvp.Value.IsOnCooldown))
{
list.Add(new BountySaveData
{
MobileSerial = kvp.Key.Serial.Value,
ActiveBountyID = kvp.Value.ActiveBountyID,
CurrentKills = kvp.Value.CurrentKills,
NextAvailableTicks = kvp.Value.NextAvailableTime.Ticks
});
}
}
File.WriteAllText(FilePath, JsonSerializer.Serialize(list, new JsonSerializerOptions { WriteIndented = true }));
}
catch (Exception e) { Console.WriteLine($"Error saving Bounties: {e.Message}"); }
}
private static void Load()
{
if (!File.Exists(FilePath)) return;
try
{
var list = JsonSerializer.Deserialize<List<BountySaveData>>(File.ReadAllText(FilePath));
if (list == null) return;
foreach (var data in list)
{
Mobile m = World.FindMobile((Serial)data.MobileSerial);
if (m != null && !m.Deleted)
{
Profiles[m] = new BountyProfile
{
ActiveBountyID = data.ActiveBountyID,
CurrentKills = data.CurrentKills,
NextAvailableTime = new DateTime(data.NextAvailableTicks, DateTimeKind.Utc)
};
}
}
}
catch (Exception e) { Console.WriteLine($"Error loading Bounties: {e.Message}"); }
}
}
// ==========================================
// 4. PLAYER STATUS GUMP ([Bounties Command)
// ==========================================
public class ActiveBountyGump : Gump
{
public ActiveBountyGump(PlayerMobile pm, BountyProfile profile) : base(100, 100)
{
AddPage(0);
AddBackground(0, 0, 350, 220, 9270);
AddAlphaRegion(10, 10, 330, 200);
AddLabel(110, 15, 1152, "ACTIVE BOUNTY STATUS");
if (profile.HasActiveBounty && BountyRegistry.AllBounties.TryGetValue(profile.ActiveBountyID, out BountyDefinition bounty))
{
AddLabel(20, 50, 0x3F, "Target:");
AddLabel(90, 50, 1152, bounty.Title);
if (!string.IsNullOrEmpty(bounty.RequiredRegion))
{
AddLabel(20, 75, 0x3F, "Location:");
AddLabel(90, 75, 1152, bounty.RequiredRegion);
}
AddLabel(20, 100, 0x3F, "Progress:");
if (profile.CurrentKills >= bounty.RequiredKills)
{
AddLabel(90, 100, 0x44, $"{profile.CurrentKills} / {bounty.RequiredKills} - COMPLETE!");
AddLabel(20, 125, 0x44, "Return to a Bounty Board to claim!");
}
else
{
AddLabel(90, 100, 2100, $"{profile.CurrentKills} / {bounty.RequiredKills} Slain");
}
}
else
{
AddLabel(20, 75, 33, "You do not have an active bounty.");
if (profile.IsOnCooldown)
{
TimeSpan wait = profile.NextAvailableTime - DateTime.UtcNow;
AddLabel(20, 95, 2100, $"New bounties in: {(int)wait.TotalMinutes} minutes.");
}
else
{
AddLabel(20, 95, 1152, "Visit a Bounty Board to accept one.");
}
}
AddButton(20, 175, 4017, 4019, 0, GumpButtonType.Reply, 0);
AddLabel(55, 175, 1152, "Close");
}
}

View file

@ -0,0 +1,108 @@
using System;
using System.Collections.Generic;
using Server;
using Server.Mobiles;
namespace Server.Engines.Bounties;
// ==========================================
// 1. BOUNTY DEFINITION
// ==========================================
public class BountyDefinition
{
public int ID { get; }
public string Title { get; }
public Type[] TargetTypes { get; }
public int RequiredKills { get; }
public int RequiredFame { get; }
public int RewardTokens { get; }
public int RewardGold { get; }
public string RequiredRegion { get; }
public BountyDefinition(int id, string title, Type[] targetTypes, int requiredKills, int requiredFame, int rewardTokens, int rewardGold, string requiredRegion = null)
{
ID = id;
Title = title;
TargetTypes = targetTypes;
RequiredKills = requiredKills;
RequiredFame = requiredFame;
RewardTokens = rewardTokens;
RewardGold = rewardGold;
RequiredRegion = requiredRegion;
}
public bool Matches(Type type)
{
foreach (var t in TargetTypes)
{
if (t == type || t.IsAssignableFrom(type))
{
return true;
}
}
return false;
}
}
// ==========================================
// 2. TIERED BOUNTY REGISTRY
// ==========================================
public static class BountyRegistry
{
// Lists mapped by Tier (1 through 5)
public static Dictionary<int, List<BountyDefinition>> Tiers { get; private set; } = new();
// A master dictionary to look up any bounty by ID
public static Dictionary<int, BountyDefinition> AllBounties { get; private set; } = new();
public static void Initialize()
{
// ==========================================
// TIER 1: Local Adventurer (Fame 0) - Rewards: 5 Tokens, 500g
// ==========================================
Add(1, new BountyDefinition(1, "Covetous Harpies", new[] { typeof(Harpy) }, 15, 0, 5, 500, "Covetous"));
Add(1, new BountyDefinition(2, "Shame Earth Elementals", new[] { typeof(EarthElemental) }, 10, 0, 5, 500, "Shame"));
Add(1, new BountyDefinition(3, "Despise Lizardmen", new[] { typeof(Lizardman) }, 15, 0, 5, 500, "Despise"));
// ==========================================
// TIER 2: Estimable Hunter (Fame 1250) - Rewards: 15 Tokens, 1200g
// ==========================================
Add(2, new BountyDefinition(4, "Deceit Skeletal Knights", new[] { typeof(SkeletalKnight) }, 10, 1250, 15, 1200, "Deceit"));
Add(2, new BountyDefinition(5, "Destard Drakes", new[] { typeof(Drake) }, 10, 1250, 15, 1200, "Destard"));
// ==========================================
// TIER 3: Great Slayer (Fame 2500) - Rewards: 30 Tokens, 2500g
// ==========================================
Add(3, new BountyDefinition(6, "Despise Titans", new[] { typeof(Titan) }, 8, 2500, 30, 2500, "Despise"));
// ==========================================
// TIER 4: Illustrious Champion (Fame 5000) - Rewards: 60 Tokens, 5000g
// ==========================================
Add(4, new BountyDefinition(7, "Destard Dragons", new[] { typeof(Dragon) }, 3, 5000, 60, 5000, "Destard"));
// ==========================================
// TIER 5: Glorious Legend (Fame 10000) - Rewards: 100 Tokens, 10000g
// ==========================================
Add(5, new BountyDefinition(8, "Hythloth Balrons", new[] { typeof(Balron) }, 2, 10000, 100, 10000, "Hythloth"));
}
private static void Add(int tier, BountyDefinition bounty)
{
if (!Tiers.ContainsKey(tier))
{
Tiers[tier] = new List<BountyDefinition>();
}
Tiers[tier].Add(bounty);
AllBounties[bounty.ID] = bounty;
}
// Pulls a random bounty from a specific tier pool
public static BountyDefinition GetRandomBounty(int tier)
{
if (!Tiers.ContainsKey(tier)) return null;
var list = Tiers[tier];
return list[Utility.Random(list.Count)];
}
}

View file

@ -1,109 +0,0 @@
using System;
using System.Collections.Generic;
using ModernUO.Serialization;
using Server.Engines.MLQuests;
using Server.Engines.MLQuests.Objectives;
using Server.Engines.MLQuests.Rewards;
using Server.Items;
using Server.Mobiles;
namespace Server.Engines.MLQuests.Definitions;
// ==========================================
// 1. BOUNTY BOARD ITEM
// ==========================================
[SerializationGenerator(0, false)]
public partial class DungeonBountyBoard : Item, IQuestGiver
{
// Implementation for IQuestGiver
public List<MLQuest> MLQuests => MLQuestSystem.FindQuestList(GetType());
// Automatically registers all bounties when the server boots up
public static void Initialize()
{
MLQuestSystem.Register(new DestardDrakeBounty(), typeof(DungeonBountyBoard));
MLQuestSystem.Register(new DeceitSkeletalKnightBounty(), typeof(DungeonBountyBoard));
}
[Constructible]
public DungeonBountyBoard() : base(0x1E5E) // Bulletin board Graphic ID
{
Name = "Dungeon Bounty Board";
Movable = false;
}
public override void OnDoubleClick(Mobile from)
{
if (from is PlayerMobile pm)
{
if (from.InRange(GetWorldLocation(), 3))
{
MLQuestSystem.OnDoubleClick(this, pm);
}
else
{
from.SendLocalizedMessage(500446); // That is too far away.
}
}
}
}
// ==========================================
// 2. DESTARD BOUNTY: DRAKES
// ==========================================
public class DestardDrakeBounty : MLQuest
{
public DestardDrakeBounty()
{
Activated = true;
Title = "Bounty: Destard Drakes";
Description = "Notice to all adventurers: Drake activity in the caverns of Destard has surged! Slay 10 Drakes to thin their numbers and keep the surrounding passes safe.";
RefusalMessage = "The bounty remains posted if you change your mind.";
InProgressMessage = "You haven't slain enough Drakes yet. Return once you've dispatched 10 of them in Destard.";
CompletionMessage = "Excellent work! The realm thanks you for your service. Here is your gold reward.";
CompletionNotice = CompletionNoticeShort;
// Objective: Slay 10 Drakes
Objectives.Add(new KillObjective(10, new[] { typeof(Drake) }, "Drakes"));
// Reward: 1,200 Gold
Rewards.Add(new ItemReward("1,200 Bounty Tokens", typeof(BountyToken), 1200));
}
public override void Generate()
{
base.Generate();
MLQuestSystem.Register(this, typeof(DungeonBountyBoard));
}
}
// ==========================================
// 3. DECEIT BOUNTY: SKELETAL KNIGHTS
// ==========================================
public class DeceitSkeletalKnightBounty : MLQuest
{
public DeceitSkeletalKnightBounty()
{
Activated = true;
Title = "Bounty: Deceit Skeletal Knights";
Description = "Undead forces are amassing within the halls of Deceit. Slay 10 Skeletal Knights and report back to claim your reward.";
RefusalMessage = "The bounty remains posted on the board.";
InProgressMessage = "The skeletal knights still roam Deceit. Return when you have destroyed 10 of them.";
CompletionMessage = "The undead threat has been pushed back! Take your gold reward.";
CompletionNotice = CompletionNoticeShort;
// Objective: Slay 10 Skeletal Knights
Objectives.Add(new KillObjective(10, new[] { typeof(SkeletalKnight) }, "Skeletal Knights"));
// Reward: 1,500 Gold
Rewards.Add(new ItemReward("1,500 Bounty Tokens", typeof(BountyToken), 1500));
}
public override void Generate()
{
base.Generate();
MLQuestSystem.Register(this, typeof(DungeonBountyBoard));
}
}

View file

@ -3720,7 +3720,7 @@ namespace Server.Mobiles
{
c.Delete();
}
Server.Engines.Bounties.BountySystem.OnCreatureDeath(this);
CreatureEvents.CreatureDeathEvent(this);
}