namespace Aeshnidae.Inspect;
///
/// /inspect - the numbers the appraisal window does not show for a creature: its
/// effective attack skill and its three defenses, as the combat formulas will use
/// them right now (attributes, tier scaling, buffs and debuffs all applied), with
/// the unbuffed base beside any that differ.
///
public static class Commands
{
[CommandHandler("inspect", AccessLevel.Player, CommandHandlerFlag.RequiresWorld, -1,
"The effective attack skill and melee, missile and magic defense of the creature you have selected.",
"/inspect the creature you have selected\n" +
"/inspect the nearest creature of that name instead")]
public static void HandleInspect(Session session, params string[] parameters)
{
var player = session.Player;
if (player is null)
return;
try
{
var target = parameters.Length > 0
? Nearest(player, string.Join(" ", parameters))
: Selected(player);
if (target is null)
{
Reply(session, parameters.Length > 0
? $"No creature called '{string.Join(" ", parameters)}' within {Mod.Settings.NameSearchRange:0} metres."
: "Select a creature first, or give a name: /inspect .");
return;
}
if (target is Player && !Mod.Settings.AllowInspectingPlayers && session.AccessLevel < AccessLevel.Admin)
{
Reply(session, "Players keep their numbers to themselves.");
return;
}
Reply(session, Describe(target));
}
catch (Exception ex)
{
ModManager.Log($"[{Mod.Name}] /inspect failed for {player.Name}: {ex}", ModManager.LogLevel.Error);
Reply(session, "Could not read that creature.");
}
}
[CommandHandler("inspect-reload", AccessLevel.Admin, CommandHandlerFlag.None, 0,
"Restart the Aeshnidae.Inspect mod, re-reading Settings.json.",
"/inspect-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 sends an appraisal for whatever the player selects, so the last
/// appraisal REQUEST is the selection. Not CurrentAppraisalTarget: ACE only sets
/// that when the Assess Creature roll against the monster's Deception succeeds,
/// and a failed assess would leave /inspect claiming nothing was selected.
/// Looked up through the player's landblock so an instanced copy answers for its
/// own creatures.
///
private static Creature? Selected(Player player)
{
var guid = player.RequestedAppraisalTarget ?? player.CurrentAppraisalTarget;
if (guid is null)
return null;
return player.FindObject(guid.Value, Player.SearchLocations.Landblock | Player.SearchLocations.ObjectsKnownByMe) as Creature;
}
private static Creature? Nearest(Player player, string name)
{
var range = Mod.Settings.NameSearchRange;
return player.GetKnownObjects()
.OfType()
.Where(c => c != player && c.Name.Equals(name, StringComparison.OrdinalIgnoreCase))
.Select(c => (creature: c, distance: player.Location.DistanceTo(c.Location)))
.Where(t => t.distance <= range)
.OrderBy(t => t.distance)
.Select(t => t.creature)
.FirstOrDefault();
}
private static string Describe(Creature c)
{
var sb = new StringBuilder();
sb.AppendLine($"{c.Name} - level {c.Level ?? 0}, {c.Health.Current:N0}/{c.Health.MaxValue:N0} health");
// The skill the next swing will roll. A caster's bolts roll War Magic instead,
// so that is shown too whenever the creature has it.
var attack = c.GetCurrentAttackSkill();
sb.AppendLine($" attack : {attack.ToSentence()} {Number(c, attack)}");
var war = c.GetCreatureSkill(Skill.WarMagic);
if (war.AdvancementClass >= SkillAdvancementClass.Trained && attack != Skill.WarMagic)
sb.AppendLine($" war magic : {Number(c, Skill.WarMagic)}");
sb.AppendLine($" melee defense : {Number(c, Skill.MeleeDefense)}");
sb.AppendLine($" missile defense : {Number(c, Skill.MissileDefense)}");
sb.AppendLine($" magic defense : {Number(c, Skill.MagicDefense)}");
var ratings = new List();
Rating(ratings, "damage", c.GetDamageRating());
Rating(ratings, "damage resist", c.GetDamageResistRating());
Rating(ratings, "crit", c.GetCritRating());
Rating(ratings, "crit damage", c.GetCritDamageRating());
if (ratings.Count > 0)
sb.AppendLine($" ratings : {string.Join(", ", ratings)}");
return sb.ToString().TrimEnd();
}
/// "150", or "135 (base 150)" when spells have moved it.
private static string Number(Creature c, Skill skill)
{
var cs = c.GetCreatureSkill(skill);
var current = cs.Current;
var baseValue = cs.Base;
return current == baseValue ? $"{current}" : $"{current} (base {baseValue})";
}
private static void Rating(List into, string name, int value)
{
if (value != 0)
into.Add($"{name} {(value > 0 ? "+" : "")}{value}");
}
///
/// 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);
}
}
}