From e878776b0f117c4a6fad9e49864a3fcc45cf5a1f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Jul 2026 21:04:49 +0000 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E4=B8=89=E5=80=8D=E5=B0=84?= =?UTF-8?q?=E7=A8=8B=E4=B8=8B=E6=8B=8B=E5=B0=84=E7=89=A9=E5=B0=84=E7=9F=AD?= =?UTF-8?q?=E6=89=93=E4=B8=8D=E5=88=B0=E7=9B=AE=E6=A8=99=E7=9A=84=E5=95=8F?= =?UTF-8?q?=E9=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 開啟 RangedRange3x 後弓箭等拋射物「根本射不到」站著不動的敵人。 根因為彈道幾何:水平抵達時間為與距離無關的常數 4/ISF(約 2.67 秒), 垂直飛行落地時間為 2×emit/ysub(弓箭約 2.47 秒),因此每箭固定落在 目標距離的約 92.5%,形成「射短≈7.5%×距離」的系統性偏差。原版射程下 7.5% 尚在傷害半徑內,三倍射程時射短距離同步三倍、遠超傷害半徑。 依使用者要求不更動傷害半徑(w*_drad),改以提高拋射初速解決: RangedRange3x 啟用時 EparaPatcher 將 cl_epara.ini [ProjectileInitSpeedFactor] 乘以 InitSpeedReachMultiplier=1.08 (1.5→1.62),使 4/ISF 對齊弓箭飛行時間、射短比例趨近 0,拋射物 即可覆蓋放大後的射程(並縮短飛行時間降低移動目標閃避)。偵測不受影響 (RangedRange3x 由 objdef 射程欄位回讀,不讀 epara)。1.08 屬靜態逆向 推導,正式倍率仍待實機校準。新增 round-trip 測試並更新既有 epara 測試。 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Brrn8kwp6d2HX3nZzBhGAc --- AI_HANDOFF.md | 4 ++ .../projectile-ballistics.md | 20 ++++++++ src/Core/Patches/EparaPatcher.cs | 46 +++++++++++++------ src/Core/Services/PatchEngine.cs | 5 +- .../PatcherRoundTripTests.cs | 23 ++++++++-- 5 files changed, 79 insertions(+), 19 deletions(-) diff --git a/AI_HANDOFF.md b/AI_HANDOFF.md index 961830a..f7c80b2 100644 --- a/AI_HANDOFF.md +++ b/AI_HANDOFF.md @@ -1,5 +1,9 @@ # AI Handoff - Live Project Memory +## RangedRange3x projectile reach fix (2026-07-13) + +- User report: with 3× range enabled, bows "根本射不到" even stationary enemies that vanilla hits directly. Root cause is ballistic, not the earlier sight/targeting gap (already fixed): horizontal arrival time is the distance-independent constant `4/ISF` (≈2.67 s) while vertical flight-to-ground is `2·emit/ysub` (bows ≈2.47 s), so every arrow lands at ~92.5 % of the target distance — a **fixed ~7.5 %-of-distance short fall**. Vanilla range keeps that within `w_drad` (arrows 45); tripling the range triples the absolute shortfall to far beyond `w_drad`, so arrows land in front of the target. Per user direction the damage radius (`w_drad`) was **not** touched. Fix: `EparaPatcher` now raises `cl_epara.ini [ProjectileInitSpeedFactor]` by `InitSpeedReachMultiplier` = 1.08 (1.5 → 1.62) whenever `RangedRange3x` is enabled, so `4/ISF` drops to ≈ the bow flight time and the land-short fraction → ≈0 at every distance (also reduces mover dodge via shorter flight time). Wired through `PatchEngine` (`EparaPatcher.GetPatchedBytes` now takes `rangedRange3x`); detection is unaffected (RangedRange3x is detected from objdef range columns, epara is not read back, same as the existing variance patch). The 1.08 factor is static-derived from the reverse-engineered estimates (like `ArcEmitMultiplier`) and needs in-game calibration — the 3× hit window is tight (ISF within ~±0.05 of 1.62 for bows). Added `Epara_range3x_raises_init_speed_...` round-trip test and updated the existing epara test's signature. See `docs/reverse-engineering/projectile-ballistics.md`. NOT YET locally built (no .NET SDK in this session) — CI/dev build to confirm. + ## Custom Troop Balance Table (2026-07-13) - Corrected the custom-troop balance-table task after a scope clarification: `TroopConfig.BalancedUnitStats` is the fixed in-modifier source of the selectable `修改器內建平衡` / `Balanced Base Stats` template and the Balance feature. All 42 entries now contain the original-game values extracted once from the repository's read-only original `SYSTEM/DATA_MP/DEFAULTS/objdef.dau` through the production `BackupManager.GetOriginalStats` field mapping. The template remains a static table at runtime; it does not read the selected game directory. The preset editor and localization source paths were restored to `GetDefaultBalancedStats` / their original labels. Because applying the stock table alone no longer changes `objdef.dau`, detection correctly leaves `Balance` false unless custom values produce a difference; affected characterization/integration assertions were updated. Verification: Release build 0 warnings/errors; xUnit 122/122 passed. diff --git a/docs/reverse-engineering/projectile-ballistics.md b/docs/reverse-engineering/projectile-ballistics.md index 839a1c1..97c2b13 100644 --- a/docs/reverse-engineering/projectile-ballistics.md +++ b/docs/reverse-engineering/projectile-ballistics.md @@ -127,3 +127,23 @@ Let `k` = arc-raise factor, `s` = speed-up factor: Do not scale `w_emit` when scaling ranges — it is not a range field; scaling it alone shifts every landing point long (overshoot). + +## Range 3× interaction: arrows land short (fixed 2026-07-13) + +The land-short margin is a **fixed fraction of shot distance**, not an absolute +value: on flat ground the arrow returns to launch height at `T_fall = 2·emit/ysub` +(bows ≈ 2.47 s) while the horizontal arrival time is the distance-independent +constant `T_arrival = 4/ISF` (≈ 2.67 s at ISF 1.5), so every shot lands at +`T_fall/T_arrival ≈ 92.5 %` of the target distance — a ~7.5 %-of-distance short +fall. At vanilla range that ~7.5 % is roughly within `w_drad` (arrows 45) so +stationary targets are hit; with **`RangedRange3x`** the shortfall triples to far +beyond `w_drad`, so arrows physically land in front of the enemy ("根本射不到"). + +Fix (`EparaPatcher.InitSpeedReachMultiplier`, tied to `RangedRange3x`): raise ISF +so `T_arrival = 4/ISF` drops to the bow flight time `2·emit/ysub ≈ 2.47 s`, which +needs ISF ≈ 1.618; shipped multiplier is 1.08 (1.5 → 1.62). This drives the +land-short fraction to ≈ 0 at **every** distance, so projectiles reach the tripled +range. It also shortens flight time, reducing dodge on movers. The exact factor is +static-derived from the estimates above (like `ArcEmitMultiplier`) and still needs +in-game calibration — the hit window at 3× range is tight (`|1 − T_fall·ISF/4| · +range_3x < w_drad`, i.e. ISF within roughly ±0.05 of 1.62 for bows). diff --git a/src/Core/Patches/EparaPatcher.cs b/src/Core/Patches/EparaPatcher.cs index 18d92f1..615cf0a 100644 --- a/src/Core/Patches/EparaPatcher.cs +++ b/src/Core/Patches/EparaPatcher.cs @@ -5,33 +5,51 @@ namespace AgainstRomeModifier.Core.Patches; /// /// cl_epara.ini 補丁:拋射物瞄準參數。 /// [ProjectileVarianceOnMove] 是對移動目標預判提前量的隨機散布倍率(0.5 = 最多 0.5×3×移動速度的偏差); -/// 遠程命中強化將其設為 0.0,預判完全精準。詳見 docs/reverse-engineering/projectile-ballistics.md。 +/// 遠程命中強化將其設為 0.0,預判完全精準。 +/// [ProjectileInitSpeedFactor](ISF)是拋射物水平初速倍率,水平抵達時間為常數 4/ISF 秒(原版 1.5 → 2.67 秒)。 +/// 拋射物的垂直飛行時間為 2×emit/ysub(弓箭約 2.47 秒),與水平抵達時間無關,因此每箭都固定落在目標距離的 +/// T_fall/T_arrival ≈ 92.5%,形成「射短 ≈ 7.5%×距離」的系統性偏差。原版射程下 7.5% 尚在傷害半徑內, +/// 但開啟三倍射程後射短距離同步三倍、遠超傷害半徑,弓箭因此「根本射不到」站著不動的敵人。 +/// RangedRange3x 啟用時將 ISF 乘以 (1.5 → 1.62),使水平抵達時間 +/// 4/ISF ≈ 2.47 秒對齊垂直飛行時間,射短比例趨近 0,拋射物即可覆蓋放大後的射程。 +/// 詳見 docs/reverse-engineering/projectile-ballistics.md。 /// public static class EparaPatcher { internal const string VarianceSection = "[ProjectileVarianceOnMove]"; internal const string AccuracyVarianceValue = "0.0"; + internal const string InitSpeedSection = "[ProjectileInitSpeedFactor]"; + /// 三倍射程時的 ISF 放大倍率。1.5×1.08=1.62;由 4/ISF = 2×emit/ysub(弓箭 2×110/89≈2.47 秒)解得 + /// ISF≈1.618,取 1.08 使射短比例趨近 0。與 ArcEmitMultiplier 相同屬靜態逆向推導,正式倍率仍待實機校準。 + internal const double InitSpeedReachMultiplier = 1.08; - public static byte[] GetPatchedBytes(byte[] original, bool rangedAccuracy) { + public static byte[] GetPatchedBytes(byte[] original, bool rangedAccuracy, bool rangedRange3x) { ArgumentNullException.ThrowIfNull(original); - if (!rangedAccuracy) return original; + if (!rangedAccuracy && !rangedRange3x) return original; var (text, lineEnding, lines) = PatchText.Read(original); - bool patched = false; - for (int i = 0; i < lines.Length && !patched; i++) { - if (!lines[i].Trim().Equals(VarianceSection, StringComparison.OrdinalIgnoreCase)) continue; + if (rangedAccuracy) + PatchSectionValue(lines, VarianceSection, _ => AccuracyVarianceValue); + if (rangedRange3x) + PatchSectionValue(lines, InitSpeedSection, + value => (value * InitSpeedReachMultiplier).ToString("0.###", CultureInfo.InvariantCulture)); + return PatchText.Write(text, lineEnding, lines, original); + } + + /// 找到區段後的第一個值行,以 的結果取代原值(沿用原行的前後文字)。 + /// 找不到區段或值行、或值行無法解析為數字時擲出,中止整次套用。 + private static void PatchSectionValue(string[] lines, string section, Func transform) { + for (int i = 0; i < lines.Length; i++) { + if (!lines[i].Trim().Equals(section, StringComparison.OrdinalIgnoreCase)) continue; for (int j = i + 1; j < lines.Length; j++) { string trimmed = lines[j].Trim(); if (trimmed.Length == 0 || trimmed.StartsWith(";", StringComparison.Ordinal)) continue; if (trimmed.StartsWith("[", StringComparison.Ordinal)) break; // 區段內沒有值行 - if (!double.TryParse(trimmed, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) - throw new InvalidDataException($"cl_epara.ini {VarianceSection} 的值行無法解析: '{trimmed}',已取消整次套用。"); - lines[j] = lines[j].Replace(trimmed, AccuracyVarianceValue); - patched = true; - break; + if (!double.TryParse(trimmed, NumberStyles.Any, CultureInfo.InvariantCulture, out double value)) + throw new InvalidDataException($"cl_epara.ini {section} 的值行無法解析: '{trimmed}',已取消整次套用。"); + lines[j] = lines[j].Replace(trimmed, transform(value)); + return; } } - if (!patched) - throw new InvalidDataException($"cl_epara.ini 找不到 {VarianceSection} 區段的值行,已取消整次套用。"); - return PatchText.Write(text, lineEnding, lines, original); + throw new InvalidDataException($"cl_epara.ini 找不到 {section} 區段的值行,已取消整次套用。"); } } diff --git a/src/Core/Services/PatchEngine.cs b/src/Core/Services/PatchEngine.cs index 183678d..6d66b24 100644 --- a/src/Core/Services/PatchEngine.cs +++ b/src/Core/Services/PatchEngine.cs @@ -104,9 +104,10 @@ public void ApplyPatches(string gamePath, PatchProfile profile, BackupManager ba byte[] clBytes = IniFeaturePatcher.BuildClScript(backupManager, options); patchedFiles[Path.Combine(gamePath, @"SYSTEM\cl_script.ini")] = clBytes; - // C. cl_epara.ini — 遠程命中強化(拋射預判散布歸零);未啟用時還原為原版備份 + // C. cl_epara.ini — 遠程命中強化(拋射預判散布歸零)+三倍射程時提高拋射初速讓箭矢能命中放大後的射程; + // 兩者皆未啟用時還原為原版備份 patchedFiles[Path.Combine(gamePath, @"SYSTEM\cl_epara.ini")] = - EparaPatcher.GetPatchedBytes(backupManager.GetBackupBytes("SYSTEM/cl_epara.ini"), options.RangedAccuracy); + EparaPatcher.GetPatchedBytes(backupManager.GetBackupBytes("SYSTEM/cl_epara.ini"), options.RangedAccuracy, options.RangedRange3x); // C2. partgeo.dau — 拋射彈道增高(重力 ysub 與 objdef w*_emit 同倍率);未啟用時還原為原版備份。 // 舊備份可能沒有 partgeo.dau(不在內嵌 Backup.zip、且自動補齊失敗時): diff --git a/tests/AgainstRomeModifier.Tests/PatcherRoundTripTests.cs b/tests/AgainstRomeModifier.Tests/PatcherRoundTripTests.cs index 6720288..f6e6ce6 100644 --- a/tests/AgainstRomeModifier.Tests/PatcherRoundTripTests.cs +++ b/tests/AgainstRomeModifier.Tests/PatcherRoundTripTests.cs @@ -221,11 +221,28 @@ public void Epara_accuracy_patch_zeros_variance_on_move_and_restores() { const string text = ";comment\r\n[ProjectileInitSpeedFactor]\r\n1.5\r\n\r\n;lead scatter\r\n[ProjectileVarianceOnMove]\r\n0.5\r\n\r\n[ProjectileVarianceMaximumAngle]\r\n45\r\n"; byte[] original = SyntheticFixture.Pfil(text); - string patched = SyntheticFixture.Text(EparaPatcher.GetPatchedBytes(original, rangedAccuracy: true)); + string patched = SyntheticFixture.Text(EparaPatcher.GetPatchedBytes(original, rangedAccuracy: true, rangedRange3x: false)); Assert.Contains("[ProjectileVarianceOnMove]\r\n0.0\r\n", patched); - Assert.Contains("[ProjectileInitSpeedFactor]\r\n1.5\r\n", patched); // 其他區段不可動 + Assert.Contains("[ProjectileInitSpeedFactor]\r\n1.5\r\n", patched); // 未開三倍射程時初速不動 Assert.Contains("[ProjectileVarianceMaximumAngle]\r\n45\r\n", patched); - Assert.Equal(original, EparaPatcher.GetPatchedBytes(original, rangedAccuracy: false)); + Assert.Equal(original, EparaPatcher.GetPatchedBytes(original, rangedAccuracy: false, rangedRange3x: false)); + } + + [Fact] + public void Epara_range3x_raises_init_speed_so_projectiles_reach_extended_range() { + const string text = ";comment\r\n[ProjectileInitSpeedFactor]\r\n1.5\r\n\r\n;lead scatter\r\n[ProjectileVarianceOnMove]\r\n0.5\r\n\r\n[ProjectileVarianceMaximumAngle]\r\n45\r\n"; + byte[] original = SyntheticFixture.Pfil(text); + + // 只開三倍射程:拉高初速讓箭矢能命中放大後的射程,散布不動 + string range3xOnly = SyntheticFixture.Text(EparaPatcher.GetPatchedBytes(original, rangedAccuracy: false, rangedRange3x: true)); + Assert.Contains("[ProjectileInitSpeedFactor]\r\n1.62\r\n", range3xOnly); + Assert.Contains("[ProjectileVarianceOnMove]\r\n0.5\r\n", range3xOnly); // 未開命中強化時散布不動 + Assert.Contains("[ProjectileVarianceMaximumAngle]\r\n45\r\n", range3xOnly); + + // 三倍射程+命中強化:兩個區段都改 + string both = SyntheticFixture.Text(EparaPatcher.GetPatchedBytes(original, rangedAccuracy: true, rangedRange3x: true)); + Assert.Contains("[ProjectileInitSpeedFactor]\r\n1.62\r\n", both); + Assert.Contains("[ProjectileVarianceOnMove]\r\n0.0\r\n", both); } [Fact]