diff --git a/src/DamagePerTurnSim.cs b/src/DamagePerTurnSim.cs index 94a5b4d..5fcac04 100644 --- a/src/DamagePerTurnSim.cs +++ b/src/DamagePerTurnSim.cs @@ -53,28 +53,32 @@ internal sealed class DamagePerTurnSim for (int turn = 0; turn < Turns; turn++) { + var hpBefore = harness.Dummy.CurrentHp; + var pcs = harness.Player.PlayerCombatState!; + // Reset PCS.Energy = Energy at turn start. PCS.Energy is the source + // of truth — cards like Offering / Bloodletting modify it via + // PlayerCmd.GainEnergy/LoseEnergy, and we read it back to give + // the policy an accurate budget. + SetEnergy(pcs, Energy); + var played = new List(); + // Capture autoplays (Hellraiser drawn-strike, Havoc, etc.) into + // the same list so the turn log matches reality. The capture must + // be installed BEFORE the draw because Hellraiser fires mid-draw. + GodotShims.StartCapturingPlays(played); + // Use CardPileCmd.Draw so Hook.AfterCardDrawn fires — that's what // powers like Hellraiser hook into to autoplay Strikes mid-draw. - // Manually moving cards between piles silently skips those effects. int needed = HandSize - hand.Cards.Count; if (needed > 0) { await MegaCrit.Sts2.Core.Commands.CardPileCmd.Draw(harness.Ctx, needed, harness.Player); } - // Snapshot hand BEFORE play decisions so the UI can show "you were - // dealt these 5 cards on this turn" alongside what got played. + // Snapshot hand AFTER autoplays have resolved (those cards aren't + // really "drawn into hand" from the player's perspective — they + // were drawn and immediately played out). var handSnapshot = hand.Cards.Select(c => CardLabel(c)).ToList(); - var hpBefore = harness.Dummy.CurrentHp; - var pcs = harness.Player.PlayerCombatState!; - // Reset PCS.Energy = Energy at turn start. PCS.Energy is the source - // of truth — cards like Offering / Bloodletting modify it via - // PlayerCmd.GainEnergy/LoseEnergy, and we read it back to give - // the policy an accurate budget. - SetEnergy(pcs, Energy); - var played = new List(); - while (pcs.Energy > 0) { var card = Policy.ChooseCard(harness, pcs.Energy, policyRng); @@ -102,6 +106,7 @@ internal sealed class DamagePerTurnSim played.Add(CardLabel(card)); } + GodotShims.StopCapturingPlays(); var hpAfter = harness.Dummy.CurrentHp; turnResults.Add(new TurnResult(turn + 1, hpBefore - hpAfter, handSnapshot, played)); diff --git a/src/GodotShims.cs b/src/GodotShims.cs index 3b4fecd..a02d185 100644 --- a/src/GodotShims.cs +++ b/src/GodotShims.cs @@ -47,6 +47,16 @@ internal static class GodotShims methodName: "GetTicksMsec", prefix: typeof(GodotShims).GetMethod(nameof(Time_GetTicksMsec_Prefix), BindingFlags.Static | BindingFlags.NonPublic)!); + // CardCmd.AutoPlay fires for any auto-played card (Hellraiser strikes, + // Havoc top-of-deck plays, etc.). The policy loop only sees plays it + // chose itself, so without this autoplays would go unrecorded in the + // turn log even though they deal real damage. + PatchPrefix( + harmony, + type: typeof(MegaCrit.Sts2.Core.Commands.CardCmd), + methodName: "AutoPlay", + prefix: typeof(GodotShims).GetMethod(nameof(CardCmd_AutoPlay_Prefix), BindingFlags.Static | BindingFlags.NonPublic)!); + // CardPileCmd.Shuffle calls Engine.GetMainLoop() for animation pacing // (sleeps between adding cards back to draw pile). We replace the whole // method with a synchronous shuffle that does the same logical work @@ -94,6 +104,25 @@ internal static class GodotShims return false; } + // ─── Autoplay capture ────────────────────────────────────────────────── + // + // Per-turn list that DamagePerTurnSim populates: it sets the list at + // turn start, the autoplay prefix below appends to it whenever + // CardCmd.AutoPlay runs. The sim's policy loop appends manual plays to + // the same list so the final ordering matches reality. + [System.ThreadStatic] private static System.Collections.Generic.List? _currentTurnPlays; + + public static void StartCapturingPlays(System.Collections.Generic.List sink) => _currentTurnPlays = sink; + public static void StopCapturingPlays() => _currentTurnPlays = null; + + private static void CardCmd_AutoPlay_Prefix(MegaCrit.Sts2.Core.Models.CardModel card) + { + if (_currentTurnPlays == null) return; + var pretty = StS2Sim.CardIdResolver.PrettyName(card.Id.ToString()); + var label = card.IsUpgraded ? pretty + (card.CurrentUpgradeLevel == 1 ? "+" : "+" + card.CurrentUpgradeLevel) : pretty; + _currentTurnPlays.Add(label + " (auto)"); + } + // Replacement for CardPileCmd.Shuffle that skips the per-card animation wait // (which calls Engine.GetMainLoop() and crashes outside Godot). Logic mirrors // the original: pull discards, shuffle by player's RNG, re-add to draw pile.