namespace Aeshnidae.CombatWindow;
///
/// Chat / console commands added by this mod.
///
/// ModContainer scans the assembly for [CommandHandler] methods and registers them
/// when the mod is enabled (Meta.json "RegisterCommands": true), then removes them
/// again on Disable. Handlers must be public static and match ACE's CommandHandler
/// delegate exactly: (Session session, params string[] parameters).
///
/// Putting a [CommandCategory("...")] attribute on this class would exclude it from
/// automatic registration, leaving it for ModContainer.RegisterCommandCategory().
///
public static class Commands
{
[CommandHandler("combatwindow", AccessLevel.Player, CommandHandlerFlag.None, -1,
"The evade and resist window in force, and your chance to land at a given skill gap.",
"/combatwindow the terms\n" +
"/combatwindow chance to land and to resist at that gap, e.g. /combatwindow -50")]
public static void HandleStatus(Session session, params string[] parameters)
{
var s = Mod.Settings;
var sb = new StringBuilder();
if (!s.Enabled)
{
Reply(session, "Combat window: off - retail's curve (slope 0.03, no floor, no ceiling).");
return;
}
sb.AppendLine($"Combat window: evade slope {s.EvadeSlope:0.###} (retail 0.03), land chance clamped {s.MinLandChance:P0}-{s.MaxLandChance:P0}; " +
$"resist slope {s.ResistSlope:0.###}, spell land chance clamped {s.MinSpellLandChance:P0}-{s.MaxSpellLandChance:P0}.");
if (parameters.Length > 0 && int.TryParse(parameters[0], out var gap))
{
sb.AppendLine($" attack {gap:+#;-#;0} over defense: melee/missile lands {s.LandChance(gap, 0, false):P1} (retail {Retail(gap):P1}); " +
$"a spell lands {s.LandChance(gap, 0, true):P1} (retail {Retail(gap):P1}).");
}
else
{
foreach (var g in new[] { -150, -100, -50, 0, 50, 100, 150 })
sb.AppendLine($" gap {g,4:+#;-#;0}: lands {s.LandChance(g, 0, false),6:P1} retail {Retail(g),6:P1}");
}
Reply(session, sb.ToString().TrimEnd());
}
private static double Retail(int gap) => Math.Clamp(1.0 - 1.0 / (1.0 + Math.Exp(0.03 * gap)), 0, 1);
[CommandHandler("combatwindow-watch", AccessLevel.Admin, CommandHandlerFlag.None, 0,
"Write every attack on or by an online player to the server journal, with ACE's numbers (Watch.cs). Console-safe.",
"/combatwindow-watch [minutes] watch for that long (default 10)\n" +
"/combatwindow-watch off stop\n" +
"/combatwindow-watch who is being watched")]
public static void HandleWatch(Session session, params string[] parameters)
{
if (parameters.Length == 0)
{
Reply(session, Watch.Status);
return;
}
if (parameters.Length == 1 && parameters[0].Equals("off", StringComparison.OrdinalIgnoreCase))
{
Watch.Stop();
Reply(session, "watch stopped");
return;
}
var minutes = 10.0;
var nameParts = parameters;
if (parameters.Length > 1 && double.TryParse(parameters[^1], out var m) && m > 0)
{
minutes = Math.Min(m, 120);
nameParts = parameters[..^1];
}
var name = string.Join(" ", nameParts).Trim();
var player = PlayerManager.GetOnlinePlayer(name) ?? PlayerManager.GetOnlinePlayer("+" + name);
if (player is null)
{
Reply(session, $"{name} is not online.");
return;
}
Watch.Start(player, minutes);
Reply(session, $"{Watch.Status} - lines go to the server journal, prefixed [{Mod.Name}] watch:");
}
[CommandHandler("combatwindow-reload", AccessLevel.Admin, CommandHandlerFlag.None, 0,
"Restart the Aeshnidae.CombatWindow mod, re-reading Settings.json.",
"/combatwindow-reload")]
public static void HandleReload(Session session, params string[] parameters)
{
var container = Mod.Container;
if (container is null)
{
Reply(session, $"{Mod.Name} is not loaded.");
return;
}
container.Restart();
Reply(session, $"{Mod.Name} restarted.");
}
///
/// The client renders an embedded newline as a music note, so a multi-line
/// message has to go out as 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);
}
}
}