fix(vt): model UseArcs as VTank's real 3-way enum, not a collapsed bool
Fidelity blocker 2: VTank's UseArcs is 1=No / 2=AtRange / 3=Yes
(refs/vtank/decompiled/hi.cs:515-538, switch on f3.f("UseArcs")): No
always picks the direct-shape spell, AtRange picks arc only once the
target reaches ArcRange, and Yes always picks arc regardless of
distance. The prior port collapsed this onto CombatSettings.UseArcs
(bool), which can represent No and (an approximation of) AtRange but
has no way to express Yes — a profile with UseArcs=3 could never
actually always-arc, and Capture() had to fudge 1<->3 on save,
which is why UntouchedRoundTripIsByteIdentical needed a NormalizeUseArcs
special case.
- CombatSettings: new UseArcsMode enum (No=1, AtRange=2, Yes=3);
UseArcs is now UseArcsMode (default AtRange, matching the previous
bool default's runtime behavior).
- AttackSpellCatalog.ShouldUseArc implements the real 3-way switch,
replacing the `settings.UseArcs && target.Distance >= settings.ArcRange`
expression at both call sites (Yes now genuinely always arcs).
- VtankSettingsProfileSerializer Apply/Capture "usearcs" cases now
cast directly to/from UseArcsMode instead of the `!= 0` / `? 3 : 1`
bool collapse.
- MossTankPanel GetMetaOption/SetMetaOption "usearcs" now exposes the
raw 1-3 value (ExpressionValue.Number / AsInt32 clamped 1-3),
matching the existing pattern for DebuffEachFirst/DebuffSelectionMethod,
instead of ExpressionValue.Boolean/IsTruthy.
- MossTankProfileStore's JSON DTO field type follows suit (System.Text.Json
already serializes CombatSettings' other enum settings the same way).
- Deleted NormalizeUseArcs from UntouchedRoundTripIsByteIdentical —
the test now asserts full byte-identity with no special case, and
CaptureMatchesDeclaredSettingTypeAndValue (added in the previous
commit) no longer needs to skip "UseArcs".
Verification: reverting to the bool model reproduces exactly one
theory failure (CaptureMatchesDeclaredSettingTypeAndValue("UseArcs"):
expected 1, actual 3) confirming this is the only affected setting;
after this change the full 559-test suite passes with zero special
cases.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
9c34b6074d
commit
74283e1116
7 changed files with 45 additions and 32 deletions
|
|
@ -174,11 +174,11 @@ internal sealed class AttackSpellCatalog
|
|||
{
|
||||
if (shape == AttackSpellShape.Streak)
|
||||
return 1;
|
||||
if (settings.UseArcs && target.Distance >= settings.ArcRange)
|
||||
if (ShouldUseArc(settings, target))
|
||||
return shape == AttackSpellShape.Arc ? 2 : 3;
|
||||
return shape == AttackSpellShape.Direct ? 2 : 3;
|
||||
}
|
||||
if (settings.UseArcs && target.Distance >= settings.ArcRange)
|
||||
if (ShouldUseArc(settings, target))
|
||||
{
|
||||
if (shape == AttackSpellShape.Arc)
|
||||
return 1;
|
||||
|
|
@ -203,6 +203,19 @@ internal sealed class AttackSpellCatalog
|
|||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// VTank's real 3-way UseArcs switch (<c>refs/vtank/decompiled/hi.cs:515-538</c>):
|
||||
/// <c>No</c> never arcs, <c>Yes</c> always arcs, <c>AtRange</c> arcs only
|
||||
/// once the target is at or beyond <see cref="CombatSettings.ArcRange"/>.
|
||||
/// </summary>
|
||||
private static bool ShouldUseArc(CombatSettings settings, PluginCombatTarget target) =>
|
||||
settings.UseArcs switch
|
||||
{
|
||||
UseArcsMode.Yes => true,
|
||||
UseArcsMode.AtRange => target.Distance >= settings.ArcRange,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
private static bool MatchesPrimaryShape(
|
||||
AttackSpellShape shape,
|
||||
MonsterRuleActions actions,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,23 @@ internal enum DebuffSelectionMethod
|
|||
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,
|
||||
|
|
@ -83,7 +100,7 @@ internal sealed class CombatSettings
|
|||
DebuffSelectionMethod.Skill;
|
||||
public double DebuffPrecastSeconds { get; set; } = 5d;
|
||||
public bool SwitchWandsToDebuff { get; set; }
|
||||
public bool UseArcs { get; set; } = true;
|
||||
public UseArcsMode UseArcs { get; set; } = UseArcsMode.AtRange;
|
||||
public float SpellRangeFudge { get; set; } = 1f;
|
||||
public bool UseBreakableTurnTo { get; set; } = true;
|
||||
public bool UseProjectileAwareness { get; set; } = true;
|
||||
|
|
|
|||
|
|
@ -2528,7 +2528,7 @@ internal sealed partial class MossTankPanel
|
|||
_combatSettings.DebuffPrecastSeconds),
|
||||
"switchwandstodebuff" => ExpressionValue.Boolean(
|
||||
_combatSettings.SwitchWandsToDebuff),
|
||||
"usearcs" => ExpressionValue.Boolean(_combatSettings.UseArcs),
|
||||
"usearcs" => ExpressionValue.Number((int)_combatSettings.UseArcs),
|
||||
"deleteghostmonsters" => ExpressionValue.Boolean(
|
||||
_combatSettings.DeleteGhostMonsters),
|
||||
"ghostmonsterspellattemptcount" => ExpressionValue.Number(
|
||||
|
|
@ -2998,7 +2998,8 @@ internal sealed partial class MossTankPanel
|
|||
_combatSettings.SwitchWandsToDebuff = value.IsTruthy;
|
||||
break;
|
||||
case "usearcs":
|
||||
_combatSettings.UseArcs = value.IsTruthy;
|
||||
_combatSettings.UseArcs = (UseArcsMode)Math.Clamp(
|
||||
value.AsInt32("UseArcs"), 1, 3);
|
||||
break;
|
||||
case "deleteghostmonsters":
|
||||
_combatSettings.DeleteGhostMonsters = value.IsTruthy;
|
||||
|
|
|
|||
|
|
@ -615,7 +615,7 @@ internal sealed class MossTankProfileStore
|
|||
DebuffSelectionMethod.Skill;
|
||||
public double DebuffPrecastSeconds { get; set; } = 5d;
|
||||
public bool SwitchWandsToDebuff { get; set; }
|
||||
public bool UseArcs { get; set; } = true;
|
||||
public UseArcsMode UseArcs { get; set; } = UseArcsMode.AtRange;
|
||||
public float ArcRange { get; set; } = 5f;
|
||||
public float RingDistance { get; set; } = 5f;
|
||||
public int MinimumRingTargets { get; set; } = 4;
|
||||
|
|
|
|||
|
|
@ -261,7 +261,7 @@ internal static class VtankSettingsProfileSerializer
|
|||
case "idlepeacemode": c.IdlePeaceMode = cell.AsBool(); break;
|
||||
case "targetlock": c.TargetLock = cell.AsBool(); break;
|
||||
case "stopmacroondeath": c.StopMacroOnDeath = cell.AsBool(); break;
|
||||
case "usearcs": c.UseArcs = cell.AsInt() != 0; break;
|
||||
case "usearcs": c.UseArcs = (UseArcsMode)cell.AsInt(); break;
|
||||
case "arcrange": c.ArcRange = (float)(cell.AsDouble() * 240d); break;
|
||||
case "targetselectmethod": c.SelectionMethod = (TargetSelectionMethod)(cell.AsInt() - 1); break;
|
||||
case "targetselectanglerange": c.TargetSelectAngleRange = (float)(cell.AsDouble() * 240d); break;
|
||||
|
|
@ -427,7 +427,7 @@ internal static class VtankSettingsProfileSerializer
|
|||
"idlepeacemode" => VtankCell.Bool(c.IdlePeaceMode),
|
||||
"targetlock" => VtankCell.Bool(c.TargetLock),
|
||||
"stopmacroondeath" => VtankCell.Bool(c.StopMacroOnDeath),
|
||||
"usearcs" => Num(name, c.UseArcs ? 3 : 1),
|
||||
"usearcs" => Num(name, (int)c.UseArcs),
|
||||
"arcrange" => Num(name, c.ArcRange / 240d),
|
||||
"targetselectmethod" => Num(name, (int)c.SelectionMethod + 1),
|
||||
"targetselectanglerange" => Num(name, c.TargetSelectAngleRange / 240d),
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ public sealed class AttackSpellCatalogTests
|
|||
IsProjectile = true,
|
||||
},
|
||||
]);
|
||||
var settings = new CombatSettings { UseArcs = true, ArcRange = 15 };
|
||||
var settings = new CombatSettings { UseArcs = UseArcsMode.AtRange, ArcRange = 15 };
|
||||
var actions = new MonsterRuleActions
|
||||
{
|
||||
DamageType = MonsterDamageType.Fire,
|
||||
|
|
@ -215,7 +215,7 @@ public sealed class AttackSpellCatalogTests
|
|||
|
||||
IReadOnlyList<AttackSpellChoice> choices = catalog.Candidates(
|
||||
new MonsterRuleActions { DamageType = MonsterDamageType.Auto },
|
||||
new CombatSettings { UseArcs = false },
|
||||
new CombatSettings { UseArcs = UseArcsMode.No },
|
||||
target,
|
||||
0,
|
||||
Character.Instance);
|
||||
|
|
|
|||
|
|
@ -40,20 +40,9 @@ public sealed class VtankSettingsProfileSerializerTests
|
|||
// VTank's own value range (Save() only overwrites a Settings row's
|
||||
// Value cell when the live value actually differs from what was
|
||||
// parsed, using a tolerance that absorbs float<->double formatting
|
||||
// noise but not a real value change).
|
||||
//
|
||||
// One documented exception: "UseArcs" is a real, pre-existing,
|
||||
// already-shipped simplification (MossTankPanel.cs:3000-3002,
|
||||
// "usearcs" case) that collapses VTank's 3-way enum (1=No, 2=AtRange,
|
||||
// 3=Yes) onto a single bool, matching ExpressionValue.IsTruthy's
|
||||
// "!= 0" semantics for the *live* /vt-style setting path. A profile
|
||||
// whose original value is exactly 1 (No) genuinely cannot round-trip
|
||||
// byte-for-byte through that bool — Capture() must produce either 1 or
|
||||
// 3, and both this serializer and the live SetMetaOption path treat 1
|
||||
// and 3 identically on load (both truthy). This is intentionally
|
||||
// reproduced here rather than diverging from the already-accepted
|
||||
// live-path behavior; the assertion below normalizes UseArcs before
|
||||
// comparing so the test still proves everything else is untouched.
|
||||
// noise but not a real value change). UseArcs is now modeled as
|
||||
// VTank's real 3-way enum (CombatSettings.UseArcsMode), so no
|
||||
// normalization is needed — every setting round-trips untouched.
|
||||
[Theory]
|
||||
[MemberData(nameof(UsdFixtureData))]
|
||||
public void UntouchedRoundTripIsByteIdentical(string path)
|
||||
|
|
@ -62,14 +51,9 @@ public sealed class VtankSettingsProfileSerializerTests
|
|||
VtankSettingsProfileSerializer.AllSettings settings = NewSettings();
|
||||
VtankDatabase document = VtankSettingsProfileSerializer.Load(original, settings);
|
||||
string rewritten = VtankSettingsProfileSerializer.Save(document, settings);
|
||||
Assert.Equal(NormalizeUseArcs(original), NormalizeUseArcs(rewritten));
|
||||
Assert.Equal(original, rewritten);
|
||||
}
|
||||
|
||||
private static string NormalizeUseArcs(string usd) => usd.Replace(
|
||||
"UseArcs\r\ni\r\n1\r\n",
|
||||
"UseArcs\r\ni\r\n3\r\n",
|
||||
StringComparison.Ordinal);
|
||||
|
||||
[Fact]
|
||||
public void DefaultSettingsUsdMatchesEveryCatalogDefault()
|
||||
{
|
||||
|
|
@ -147,8 +131,6 @@ public sealed class VtankSettingsProfileSerializerTests
|
|||
{
|
||||
if (name is "EnableMeta" or "RechargeHandlerSet")
|
||||
continue; // no live write path (Every137CatalogNameMapsToExactlyOneField).
|
||||
if (name == "UseArcs")
|
||||
continue; // pre-existing bool-collapse simplification; see UntouchedRoundTripIsByteIdentical.
|
||||
data.Add(name);
|
||||
}
|
||||
return data;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue