namespace Aeshnidae.AllegianceBounty;
public static class Commands
{
[CommandHandler("bounty", AccessLevel.Player, CommandHandlerFlag.RequiresWorld, 0,
"Your allegiance's bounty: the quests completed in the last day and the kill-experience bonus they add up to.",
"/bounty the active entries, the total, the size band, the settings\n" +
"/bounty quests the quest list and what each is worth")]
public static void HandleBounty(Session session, params string[] parameters)
{
var player = session.Player;
if (player is null)
return;
var s = Mod.Settings;
if (parameters.Length > 0 && parameters[0].StartsWith("quest", StringComparison.OrdinalIgnoreCase))
{
var sb = new StringBuilder();
sb.AppendLine($"Bounty quests ({QuestList.Detectable} of {QuestList.Entries.Count} detectable):");
foreach (var group in QuestList.Entries.Where(e => e.Flags.Count > 0).GroupBy(e => e.Kind).OrderBy(g => g.Key))
sb.AppendLine($" {group.Key} (+{group.First().EffectivePercent(s):0.##}%): {string.Join(", ", group.Select(e => e.Name).OrderBy(n => n))}");
Reply(session, sb.ToString());
return;
}
if (!s.Enabled)
{
Reply(session, "The bounty is switched off.");
return;
}
var monarch = Bounty.MonarchOf(player);
var head = new StringBuilder();
head.AppendLine($"Allegiance bounty - every listed quest a member completes adds to the kill experience of the whole allegiance for {s.HoursActive:0.#} hours: " +
$"+{s.QuestPercent:0.##}% per quest, +{s.KillTaskPercent:0.##}% per kill task, once per quest per allegiance per day" +
(s.MaxPercent > 0 ? $", capped at {s.MaxPercent:0.##}%" : ", no cap") +
$"; the bigger the allegiance the smaller each one counts ({string.Join(", ", s.SizeBands.Select(b => b.UpToMembers > 0 ? $"to {b.UpToMembers}: x{b.Multiplier:0.##}" : $"above: x{b.Multiplier:0.##}"))}).");
if (monarch is null)
{
head.AppendLine(" you : not sworn to an allegiance - swear to one and its bounty applies to your kills too.");
head.Append(PersonalLine(player));
Reply(session, head.ToString());
return;
}
var summary = Bounty.Summary(player);
var entries = Bounty.EntriesOf(monarch.Value);
var now = DateTime.UtcNow;
head.AppendLine($" your allegiance: {summary.Members} member{(summary.Members == 1 ? "" : "s")} ({summary.Band}: x{summary.Multiplier:0.##}), {entries.Count} quest{(entries.Count == 1 ? "" : "s")} active");
head.AppendLine($" bounty : +{summary.Raw:0.##}% raw x{summary.Multiplier:0.##} = +{summary.Effective:0.##}% on every member's kill experience" +
(s.ApplyToFellowshipShares ? " (fellowship shares of kills too)" : "") +
(s.ApplyToQuestExperience ? " and quest experience" : ""));
foreach (var e in entries)
{
var left = e.ExpiresAt - now;
var when = left.TotalHours >= 1 ? $"{(int)left.TotalHours}h {left.Minutes:00}m" : $"{Math.Max(0, left.Minutes)}m";
head.AppendLine($" +{e.Percent:0.##}% {e.QuestName} - {e.CompletedBy}, {when} left");
}
if (entries.Count == 0)
head.AppendLine(" nothing active - complete a listed quest (/bounty quests) and the allegiance's kills are up for a day.");
head.Append(PersonalLine(player));
Reply(session, head.ToString());
}
/// "Today: N distinct quest stamps, +X% (cap 20%), resets at 17:00".
private static string PersonalLine(Player player)
{
var s = Mod.Settings;
if (!s.PersonalEnabled)
return " today : personal bounty off";
var stamps = Personal.StampsToday(player);
var pct = Personal.PercentOf(stamps);
var cap = s.PersonalMaxPercent > 0 ? $" (cap {s.PersonalMaxPercent:0.##}%)" : " (no cap)";
return $" today : {stamps} distinct quest stamp{(stamps == 1 ? "" : "s")}, +{pct:0.##}%{cap}, resets at {Personal.NextReset():HH:mm} - " +
$"+{s.PersonalPercentPerStamp:0.##}% per {(s.PersonalAnyQuest ? "quest flag" : "listed quest")} you stamp, each once a day, on your own kills, on top of the allegiance's";
}
[CommandHandler("bounty-reload", AccessLevel.Admin, CommandHandlerFlag.None, 0,
"Re-read Settings.json and Quests.json for Aeshnidae.AllegianceBounty without restarting it. Active entries are kept.",
"/bounty-reload")]
public static void HandleReload(Session session, params string[] parameters)
{
try
{
Mod.ReloadFiles();
Reply(session, $"{Mod.Name}: settings and quest list re-read - {QuestList.Entries.Count} quests, {QuestList.Detectable} detectable, " +
$"{Bounty.ActiveEntries} entries active across {Bounty.ActiveAllegiances} allegiances.");
}
catch (Exception ex)
{
Reply(session, $"{Mod.Name}: reload failed: {ex.Message}");
}
}
[CommandHandler("bounty-grant", AccessLevel.Admin, CommandHandlerFlag.None, 2,
"Record a quest completion for a player's allegiance as if they had just turned it in. Testing.",
"/bounty-grant the quest name may be a prefix; use quotes or just type it")]
public static void HandleGrant(Session session, params string[] parameters)
{
if (parameters.Length < 2)
{
Reply(session, "/bounty-grant ");
return;
}
// The player name is the first word (or the first quoted run); the rest is the quest.
var (playerName, questName) = SplitNameAndQuest(parameters);
var target = PlayerManager.GetOnlinePlayer(playerName);
if (target is null)
{
Reply(session, $"'{playerName}' is not online.");
return;
}
var quest = QuestList.ByName(questName);
if (quest is null)
{
Reply(session, $"No listed quest matches '{questName}'. /bounty quests lists them.");
return;
}
var monarch = Bounty.MonarchOf(target);
if (monarch is null)
{
Reply(session, $"{target.Name} is not sworn to an allegiance; a bounty needs one.");
return;
}
if (!BountyDb.Ready)
{
Reply(session, "The bounty table is not open; nothing recorded.");
return;
}
var flag = quest.Flags.FirstOrDefault() ?? "(granted)";
var added = Bounty.Gain(target, monarch.Value, quest, flag, "was granted");
var summary = Bounty.Summary(target);
Reply(session, added
? $"Recorded '{quest.Name}' (+{quest.EffectivePercent(Mod.Settings):0.##}%) for {target.Name}'s allegiance: now +{summary.Raw:0.##}% raw x{summary.Multiplier:0.##} ({summary.Members} members) = +{summary.Effective:0.##}%, {summary.Count} active."
: $"{target.Name}'s allegiance already has '{quest.Name}' active; nothing added (+{summary.Effective:0.##}% stands).");
ModManager.Log($"[{Mod.Name}] {session?.Player?.Name ?? "console"} granted '{quest.Name}' to {target.Name}'s allegiance: {(added ? "added" : "already active")}");
}
private static (string player, string quest) SplitNameAndQuest(string[] parameters)
{
var joined = string.Join(" ", parameters);
if (joined.StartsWith('"'))
{
var end = joined.IndexOf('"', 1);
if (end > 0)
return (joined[1..end], joined[(end + 1)..].Trim().Trim('"'));
}
return (parameters[0], string.Join(" ", parameters.Skip(1)).Trim('"'));
}
/// The client renders an embedded newline as a music note: one SendMessage per line.
private static void Reply(Session? session, string message)
{
if (session?.Player is null)
{
ModManager.Log(message);
return;
}
foreach (var line in (message ?? "").Split('\n'))
{
var text = line.TrimEnd('\r');
if (!string.IsNullOrWhiteSpace(text))
session.Player.SendMessage(text);
}
}
}