acdream/src/AcDream.Plugins.MossTank/CombatSettings.cs
Erik 8fd3999616 fix(vt): I widen tDouble-declared distance fields from float to double
Item I (slice-1 fix round, finishing item 1). CaptureMatchesDeclaredSettingTypeAndValue's
~1.5e-7 float-round-trip tolerance was masking a real cause, not a real
value change: every tDouble-declared distance setting whose live field
was actually a float (CombatSettings.MaximumRange/MinimumRange/
ApproachDistance/RingDistance/ArcRange/TargetSelectAngleRange/
PetCustomRange/CollisionProjectileRadius/CollisionStepDistance/
SpellRangeFudge, Looting.CorpseApproachRange/CorpseMinimumApproachRange,
VitalPlan.HelperHealthDistance/HelperStaminaDistance/HelperManaDistance)
lost precision below float's ~7-significant-digit guarantee on every
load, since a loaded .usd's real value is a double.

- All 15 fields widened from float to double, matching their declared
  tDouble type. Every physics/combat call site that genuinely needs a
  float (IProjectileAutomation.EvaluatePath/EvaluatePathWithDiagnostics,
  ICombatAutomation.CaptureHostileTargets/CaptureCorpses, the
  fellow-distance Lowest() helper) now casts explicitly at that one use
  site (CombatController.cs, Looting.cs, VitalRecharge.cs,
  MossTankCommands.cs, PetAutomation.cs) instead of the field itself
  being narrowed everywhere it's stored.
- MossTankProfileStore's JSON DTOs (InventoryProfileDocument.
  CorpseApproachRange, CombatProfileDocument.MaximumRange/
  ApproachDistance/TargetSelectAngleRange/ArcRange/RingDistance/
  PetCustomRange) widened to match, so MossTank's own persisted profiles
  keep full precision too — their Apply()-side Math.Clamp calls needed no
  changes (the float literal bounds like 2f/100f already widen to the
  double overload implicitly).
- VtankSettingsProfileSerializer.Apply's `(float)(cell.AsDouble() * 240d)`
  casts and `cell.AsFloat()` calls for these 15 settings are now plain
  `cell.AsDouble()` / `cell.AsDouble() * 240d` — no narrowing at all.
- ValuesEqual's "d"/"f" branch dropped DoublesEqual/FloatRoundTripTolerance
  entirely: `a.AsDouble() == b.AsDouble()`, exact, matching every other
  tag. CaptureMatchesDeclaredSettingTypeAndValue's own test-side tolerance
  (a second, independently-tolerant comparison) removed the same way —
  all 135 catalog names now pass under Assert.Equal(exact) with zero
  special-casing.

Full MossTank suite: 581/581 (no count change — this is a precision fix,
not new coverage; CaptureMatchesDeclaredSettingTypeAndValue's own 135
cases already existed and now pass exactly instead of within tolerance).
App.Tests (Plugin|LaunchOptions filter): 84/84.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 23:27:09 +02:00

173 lines
6.6 KiB
C#

using AcDream.Plugin.Abstractions;
namespace AcDream.Plugins.MossTank;
internal enum TargetSelectionMethod
{
Range,
Angle,
Both,
}
internal enum DebuffEachFirst
{
One = 1,
Priority = 2,
All = 3,
}
internal enum DebuffSelectionMethod
{
SpellLevel = 1,
Skill = 2,
}
/// <summary>
/// VTank's real 3-way "UseArcs" enum
/// (<c>refs/vtank/decompiled/hi.cs:515-538</c>, switch on
/// <c>f3.f("UseArcs")</c>): <c>No</c> always picks the direct-shape spell,
/// <c>AtRange</c> picks the arc shape only once the target is at or beyond
/// <c>ArcRange</c>, and <c>Yes</c> always picks the arc shape regardless of
/// distance. A prior port collapsed this onto a single bool (effectively
/// only distinguishing "No" from "AtRange"), which cannot represent "Yes"
/// at all — see <see cref="AttackSpellCatalog"/>.
/// </summary>
internal enum UseArcsMode
{
No = 1,
AtRange = 2,
Yes = 3,
}
internal enum PetRangeMode
{
AttackDistance = 0,
Custom = 1,
}
internal enum ConsumableCategory
{
Other,
HealthKit,
HealthFood,
StaminaKit,
StaminaFood,
ManaKit,
ManaFood,
Pea,
AllPeas,
Lockpick,
}
internal sealed class CombatSettings
{
/// <summary>
/// VTank's EnableCombat profile option. This is deliberately separate
/// from the panel's Run Macro state: a running macro may navigate, loot,
/// buff, or execute Meta rules while combat itself is disabled.
/// </summary>
public bool Enabled { get; set; } = true;
/// <summary>VTank's hunt-cast skill margin.</summary>
public int HuntSkillExcessOverDifficulty { get; set; } = 25;
// Declared tDouble in VTank's own Settings table (VtankOptionCatalog):
// double, not float, so a loaded .usd round-trips this setting exactly
// instead of losing precision below float's ~7-digit guarantee (item I,
// slice-1 fix round). Physics/combat consumers that want a float cast
// at their own use site.
public double MaximumRange { get; set; } = 5d;
/// <summary>
/// Monsters nearer than this are not valid attack targets. VTank applies
/// this before priority and angle/range ranking.
/// </summary>
public double MinimumRange { get; set; }
/// <summary>
/// VTank's Approach Distance. Zero disables monster approach; otherwise
/// navigation may close a selected target from this range down to
/// <see cref="MaximumRange"/>.
/// </summary>
public double ApproachDistance { get; set; }
public bool IdlePeaceMode { get; set; }
public bool StopMacroOnDeath { get; set; } = true;
public bool JumpOutWandCasting { get; set; }
public bool DoJiggle { get; set; }
public TargetSelectionMethod SelectionMethod { get; set; } =
TargetSelectionMethod.Both;
public double TargetSelectAngleRange { get; set; } = 5d;
public bool TargetLock { get; set; }
public PluginAttackHeight AttackHeight { get; set; } =
PluginAttackHeight.Medium;
public float AttackPower { get; set; } = 0.5f;
public bool AutoAttackPower { get; set; } = true;
public bool UseRecklessness { get; set; } = true;
public double ScanIntervalSeconds { get; set; } = 0.25;
public DebuffEachFirst DebuffEachFirst { get; set; } = DebuffEachFirst.One;
public DebuffSelectionMethod DebuffSelectionMethod { get; set; } =
DebuffSelectionMethod.Skill;
public double DebuffPrecastSeconds { get; set; } = 5d;
public bool SwitchWandsToDebuff { get; set; }
public UseArcsMode UseArcs { get; set; } = UseArcsMode.AtRange;
public double SpellRangeFudge { get; set; } = 1d;
public bool UseBreakableTurnTo { get; set; } = true;
public bool UseProjectileAwareness { get; set; } = true;
public double CollisionProjectileRadius { get; set; } = 0.4d;
public double CollisionStepDistance { get; set; } = 0.7d;
public bool ShowCollisionDebug { get; set; }
public int MaximumCollisionChecksPerTick { get; set; } = 500;
public double ArcRange { get; set; } = 5d;
public double RingDistance { get; set; } = 5d;
public int MinimumRingTargets { get; set; } = 4;
public bool DeleteGhostMonsters { get; set; } = true;
public int GhostMonsterSpellAttemptCount { get; set; } = 200;
public int BlacklistMonsterAttemptCount { get; set; } = 4;
public double BlacklistMonsterTimeoutSeconds { get; set; } = 120d;
public bool DeleteGhostMonstersByHealthTracker { get; set; } = true;
public double GhostDeleteHealthTrackerSeconds { get; set; } = 30d;
public bool SummonPets { get; set; } = true;
public PetRangeMode PetRangeMode { get; set; } = PetRangeMode.AttackDistance;
public double PetCustomRange { get; set; } = 5d;
public int PetMonsterDensity { get; set; } = 1;
public int PetRefillCountIdle { get; set; } = 3;
public int PetRefillCountNormal { get; set; } = 1;
public bool AllowDebuffFallback { get; set; }
public int UseSpecialAmmo { get; set; }
public bool WhoYouGonnaCall { get; set; } = true;
public bool AutoFellowManagement { get; set; } = true;
public string BlacklistedSpellComponents { get; set; } = string.Empty;
/// <summary>
/// Runtime object ids resolved from VTank's Items profile. Debuff lenses
/// and cast-on-strike weapons are never taken from arbitrary inventory.
/// </summary>
public ISet<uint> CombatItemObjectIds { get; } = new HashSet<uint>();
public ISet<string> CombatItemNames { get; } =
new HashSet<string>(StringComparer.Ordinal);
/// <summary>Exact names enabled in VTank's Consumables profile.</summary>
public ISet<string> ConsumableNames { get; } =
new HashSet<string>(StringComparer.Ordinal);
public IDictionary<string, ConsumableCategory> ConsumableCategories { get; } =
new Dictionary<string, ConsumableCategory>(StringComparer.Ordinal);
public IList<MonsterRule> Rules { get; } =
new List<MonsterRule> { new("DEFAULT", 0) };
public ResolvedMonsterRule ResolveRule(PluginCombatTarget target)
{
var context = new MonsterExpressionContext(
target.Name,
target.WeenieClassId,
target.SpeciesName,
target.MaximumHealth,
target.Distance,
target.HasShield,
MetaState,
ResolveSetting);
return MonsterRuleResolver.Resolve(Rules, context);
}
public string MetaState { get; set; } = "Default";
public IDictionary<string, MonsterValue> DynamicSettings { get; } =
new Dictionary<string, MonsterValue>(StringComparer.OrdinalIgnoreCase);
private MonsterValue? ResolveSetting(string name) =>
DynamicSettings.TryGetValue(name, out MonsterValue value)
? value
: null;
}