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); } } }