diff --git a/Craftimizer/Plugin/SimulatorWindow.cs b/Craftimizer/Plugin/SimulatorWindow.cs index 8671be3..0f3d9bb 100644 --- a/Craftimizer/Plugin/SimulatorWindow.cs +++ b/Craftimizer/Plugin/SimulatorWindow.cs @@ -11,7 +11,7 @@ namespace Craftimizer.Plugin; public class SimulatorWindow : Window { - public Simulation Simulation { get; } + public SimulationState Simulation { get; } private bool showOnlyGuaranteedActions = true; @@ -23,7 +23,11 @@ public class SimulatorWindow : Window MaximumSize = new Vector2(float.MaxValue, float.MaxValue) }; - Simulation = new(new CharacterStats { Craftsmanship = 4041, Control = 3905, CP = 609, Level = 90 }, LuminaSheets.RecipeSheet.GetRow(35499)!); + Simulation = new(new() + { + Stats = new CharacterStats { Craftsmanship = 4041, Control = 3905, CP = 609, Level = 90 }, + Recipe = LuminaSheets.RecipeSheet.GetRow(35499)! + }); } public override void Draw() @@ -62,19 +66,19 @@ public class SimulatorWindow : Window ImGui.Text($"Step {Simulation.StepCount + 1}"); ImGui.Text(Simulation.Condition.Name()); if (ImGui.IsItemHovered()) - ImGui.SetTooltip(Simulation.Condition.Description(Simulation.Stats.HasRelic)); + ImGui.SetTooltip(Simulation.Condition.Description(Simulation.Input.Stats.HasRelic)); ImGui.Text($"{Simulation.HQPercent}%% HQ"); ImGui.PushStyleColor(ImGuiCol.PlotHistogram, new Vector4(.2f, 1f, .2f, 1f)); - ImGui.ProgressBar(Math.Min((float)Simulation.Progress / Simulation.MaxProgress, 1f), new Vector2(200, 20), $"{Simulation.Progress} / {Simulation.MaxProgress}"); + ImGui.ProgressBar(Math.Min((float)Simulation.Progress / Simulation.Input.MaxProgress, 1f), new Vector2(200, 20), $"{Simulation.Progress} / {Simulation.Input.MaxProgress}"); ImGui.PopStyleColor(); ImGui.PushStyleColor(ImGuiCol.PlotHistogram, new Vector4(.2f, .2f, 1f, 1f)); - ImGui.ProgressBar(Math.Min((float)Simulation.Quality / Simulation.MaxQuality, 1f), new Vector2(200, 20), $"{Simulation.Quality} / {Simulation.MaxQuality}"); + ImGui.ProgressBar(Math.Min((float)Simulation.Quality / Simulation.Input.MaxQuality, 1f), new Vector2(200, 20), $"{Simulation.Quality} / {Simulation.Input.MaxQuality}"); ImGui.PopStyleColor(); ImGui.PushStyleColor(ImGuiCol.PlotHistogram, new Vector4(1f, 1f, .2f, 1f)); - ImGui.ProgressBar(Math.Clamp((float)Simulation.Durability / Simulation.MaxDurability, 0f, 1f), new Vector2(200, 20), $"{Simulation.Durability} / {Simulation.MaxDurability}"); + ImGui.ProgressBar(Math.Clamp((float)Simulation.Durability / Simulation.Input.MaxDurability, 0f, 1f), new Vector2(200, 20), $"{Simulation.Durability} / {Simulation.Input.MaxDurability}"); ImGui.PopStyleColor(); ImGui.PushStyleColor(ImGuiCol.PlotHistogram, new Vector4(1f, .2f, 1f, 1f)); - ImGui.ProgressBar(Math.Clamp((float)Simulation.CP / Simulation.Stats.CP, 0f, 1f), new Vector2(200, 20), $"{Simulation.CP} / {Simulation.Stats.CP}"); + ImGui.ProgressBar(Math.Clamp((float)Simulation.CP / Simulation.Input.Stats.CP, 0f, 1f), new Vector2(200, 20), $"{Simulation.CP} / {Simulation.Input.Stats.CP}"); ImGui.PopStyleColor(); ImGuiHelpers.ScaledDummy(5); ImGui.Text($"Effects:"); diff --git a/Craftimizer/Simulator/Actions/ActionType.cs b/Craftimizer/Simulator/Actions/ActionType.cs index 1af9492..98f619e 100644 --- a/Craftimizer/Simulator/Actions/ActionType.cs +++ b/Craftimizer/Simulator/Actions/ActionType.cs @@ -61,7 +61,7 @@ internal static class ActionUtils private static BaseAction Action(this ActionType me) => Actions[(int)me]; - public static BaseAction With(this ActionType me, Simulation simulation) + public static BaseAction With(this ActionType me, SimulationState simulation) { BaseAction.TLSSimulation.Value = simulation; return Action(me); diff --git a/Craftimizer/Simulator/Actions/BaseAction.cs b/Craftimizer/Simulator/Actions/BaseAction.cs index 9c73a78..05f3cd8 100644 --- a/Craftimizer/Simulator/Actions/BaseAction.cs +++ b/Craftimizer/Simulator/Actions/BaseAction.cs @@ -6,8 +6,8 @@ namespace Craftimizer.Simulator.Actions; internal abstract class BaseAction { - internal static readonly ThreadLocal TLSSimulation = new(false); - protected static Simulation Simulation => TLSSimulation.Value ?? throw new NullReferenceException(); + internal static readonly ThreadLocal TLSSimulation = new(false); + protected static SimulationState Simulation => TLSSimulation.Value ?? throw new NullReferenceException(); public BaseAction() { } @@ -28,7 +28,7 @@ internal abstract class BaseAction public virtual bool IsGuaranteedAction => SuccessRate == 1f; public virtual bool CanUse => - Simulation.Stats.Level >= Level && Simulation.CP >= CPCost; + Simulation.Input.Stats.Level >= Level && Simulation.CP >= CPCost; public virtual void Use() { diff --git a/Craftimizer/Simulator/Actions/BasicSynthesis.cs b/Craftimizer/Simulator/Actions/BasicSynthesis.cs index e06485c..c8440e9 100644 --- a/Craftimizer/Simulator/Actions/BasicSynthesis.cs +++ b/Craftimizer/Simulator/Actions/BasicSynthesis.cs @@ -8,6 +8,6 @@ internal class BasicSynthesis : BaseAction public override int CPCost => 0; // Basic Synthesis Mastery Trait - public override float Efficiency => Simulation.Stats.Level >= 31 ? 1.20f : 1.00f; + public override float Efficiency => Simulation.Input.Stats.Level >= 31 ? 1.20f : 1.00f; public override bool IncreasesProgress => true; } diff --git a/Craftimizer/Simulator/Actions/CarefulObservation.cs b/Craftimizer/Simulator/Actions/CarefulObservation.cs index 536d2ad..224a634 100644 --- a/Craftimizer/Simulator/Actions/CarefulObservation.cs +++ b/Craftimizer/Simulator/Actions/CarefulObservation.cs @@ -10,7 +10,7 @@ internal class CarefulObservation : BaseAction public override int DurabilityCost => 0; public override bool IncreasesStepCount => false; - public override bool CanUse => Simulation.Stats.IsSpecialist && Simulation.CountPreviousAction(ActionType.CarefulObservation) < 3; + public override bool CanUse => Simulation.Input.Stats.IsSpecialist && Simulation.CountPreviousAction(ActionType.CarefulObservation) < 3; public override void UseSuccess() => Simulation.StepCondition(); diff --git a/Craftimizer/Simulator/Actions/CarefulSynthesis.cs b/Craftimizer/Simulator/Actions/CarefulSynthesis.cs index 3e4873c..693520f 100644 --- a/Craftimizer/Simulator/Actions/CarefulSynthesis.cs +++ b/Craftimizer/Simulator/Actions/CarefulSynthesis.cs @@ -8,6 +8,6 @@ internal class CarefulSynthesis : BaseAction public override int CPCost => 7; // Careful Synthesis Mastery Trait - public override float Efficiency => Simulation.Stats.Level >= 82 ? 1.80f : 1.50f; + public override float Efficiency => Simulation.Input.Stats.Level >= 82 ? 1.80f : 1.50f; public override bool IncreasesProgress => true; } diff --git a/Craftimizer/Simulator/Actions/Groundwork.cs b/Craftimizer/Simulator/Actions/Groundwork.cs index 69ca89a..f37e1c1 100644 --- a/Craftimizer/Simulator/Actions/Groundwork.cs +++ b/Craftimizer/Simulator/Actions/Groundwork.cs @@ -12,7 +12,7 @@ internal class Groundwork : BaseAction { get { - var ret = Simulation.Stats.Level >= 86 ? 3.60f : 3.00f; + var ret = Simulation.Input.Stats.Level >= 86 ? 3.60f : 3.00f; // TODO: does not account for waste not return Simulation.Durability < DurabilityCost ? ret / 2 : ret; } diff --git a/Craftimizer/Simulator/Actions/HeartAndSoul.cs b/Craftimizer/Simulator/Actions/HeartAndSoul.cs index d61ed57..45ed5ed 100644 --- a/Craftimizer/Simulator/Actions/HeartAndSoul.cs +++ b/Craftimizer/Simulator/Actions/HeartAndSoul.cs @@ -11,5 +11,5 @@ internal class HeartAndSoul : BaseBuffAction public override Effect Effect => new() { Type = EffectType.HeartAndSoul }; - public override bool CanUse => Simulation.Stats.IsSpecialist && Simulation.CountPreviousAction(ActionType.HeartAndSoul) == 0; + public override bool CanUse => Simulation.Input.Stats.IsSpecialist && Simulation.CountPreviousAction(ActionType.HeartAndSoul) == 0; } diff --git a/Craftimizer/Simulator/Actions/RapidSynthesis.cs b/Craftimizer/Simulator/Actions/RapidSynthesis.cs index 26f4b3b..d82cf79 100644 --- a/Craftimizer/Simulator/Actions/RapidSynthesis.cs +++ b/Craftimizer/Simulator/Actions/RapidSynthesis.cs @@ -8,7 +8,7 @@ internal class RapidSynthesis : BaseAction public override int CPCost => 0; // Rapid Synthesis Mastery Trait - public override float Efficiency => Simulation.Stats.Level >= 63 ? 5.00f : 2.50f; + public override float Efficiency => Simulation.Input.Stats.Level >= 63 ? 5.00f : 2.50f; public override bool IncreasesProgress => true; public override float SuccessRate => 0.50f; } diff --git a/Craftimizer/Simulator/Actions/TrainedEye.cs b/Craftimizer/Simulator/Actions/TrainedEye.cs index 47e7ca0..d2fa8bc 100644 --- a/Craftimizer/Simulator/Actions/TrainedEye.cs +++ b/Craftimizer/Simulator/Actions/TrainedEye.cs @@ -12,5 +12,5 @@ internal class TrainedEye : BaseAction public override bool CanUse => Simulation.IsFirstStep && base.CanUse; public override void UseSuccess() => - Simulation.IncreaseQualityRaw(Simulation.MaxQuality - Simulation.Quality); + Simulation.IncreaseQualityRaw(Simulation.Input.MaxQuality - Simulation.Quality); } diff --git a/Craftimizer/Simulator/SimulationInput.cs b/Craftimizer/Simulator/SimulationInput.cs new file mode 100644 index 0000000..62f7bde --- /dev/null +++ b/Craftimizer/Simulator/SimulationInput.cs @@ -0,0 +1,19 @@ +using Lumina.Excel.GeneratedSheets; +using System; + +namespace Craftimizer.Simulator; + +public readonly record struct SimulationInput +{ + public CharacterStats Stats { get; init; } + public Recipe Recipe { get; init; } + public Random Random { get; init; } + + public RecipeLevelTable RecipeTable => Recipe.RecipeLevelTable.Value!; + public int RLvl => (int)RecipeTable.RowId; + public Condition[] AvailableConditions => ConditionUtils.GetPossibleConditions(RecipeTable.ConditionsFlag); + + public int MaxDurability => RecipeTable.Durability * Recipe.DurabilityFactor / 100; + public int MaxQuality => (int)RecipeTable.Quality * Recipe.QualityFactor / 100; + public int MaxProgress => RecipeTable.Difficulty * Recipe.DifficultyFactor / 100; +} diff --git a/Craftimizer/Simulator/Simulation.cs b/Craftimizer/Simulator/SimulationState.cs similarity index 78% rename from Craftimizer/Simulator/Simulation.cs rename to Craftimizer/Simulator/SimulationState.cs index d4d5475..63b47b3 100644 --- a/Craftimizer/Simulator/Simulation.cs +++ b/Craftimizer/Simulator/SimulationState.cs @@ -1,23 +1,13 @@ using Craftimizer.Simulator.Actions; -using Dalamud.Logging; -using Lumina.Excel.GeneratedSheets; using System; using System.Collections.Generic; using System.Linq; namespace Craftimizer.Simulator; -public class Simulation +public record struct SimulationState { - public CharacterStats Stats { get; } - public Recipe Recipe { get; } - public RecipeLevelTable RecipeTable => Recipe.RecipeLevelTable.Value!; - public int RLvl => (int)RecipeTable.RowId; - public readonly Condition[] AvailableConditions; - - public int MaxDurability => RecipeTable.Durability * Recipe.DurabilityFactor / 100; - public int MaxQuality => (int)RecipeTable.Quality * Recipe.QualityFactor / 100; - public int MaxProgress => RecipeTable.Difficulty * Recipe.DifficultyFactor / 100; + public readonly SimulationInput Input { get; } public bool IsComplete { get; private set; } public int StepCount { get; private set; } @@ -26,8 +16,8 @@ public class Simulation public int Durability { get; private set; } public int CP { get; private set; } public Condition Condition { get; private set; } - public List ActiveEffects { get; } = new(); - public List ActionHistory { get; } = new(); + public List ActiveEffects { get; } + public List ActionHistory { get; } // https://github.com/ffxiv-teamcraft/simulator/blob/0682dfa76043ff4ccb38832c184d046ceaff0733/src/model/tables.ts#L2 private static readonly int[] HQPercentTable = { @@ -36,27 +26,33 @@ public class Simulation 17, 18, 18, 18, 19, 19, 20, 20, 21, 22, 23, 24, 26, 28, 31, 34, 38, 42, 47, 52, 58, 64, 68, 71, 74, 76, 78, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 94, 96, 98, 100 }; - public int HQPercent => HQPercentTable[(int)Math.Clamp((float)Quality / MaxQuality * 100, 0, 100)]; + public int HQPercent => HQPercentTable[(int)Math.Clamp((float)Quality / Input.MaxQuality * 100, 0, 100)]; public bool IsFirstStep => StepCount == 0; - private Random Random { get; } = new(); - - public Simulation(CharacterStats stats, Recipe recipe) + public SimulationState(SimulationInput input) { - Stats = stats; - Recipe = recipe; + Input = input; + IsComplete = false; StepCount = 0; Progress = 0; Quality = 0; - Durability = MaxDurability; - CP = Stats.CP; + Durability = Input.MaxDurability; + CP = Input.Stats.CP; Condition = Condition.Normal; - AvailableConditions = ConditionUtils.GetPossibleConditions(RecipeTable.ConditionsFlag); + ActiveEffects = new(); + ActionHistory = new(); } - public ActionResponse Execute(ActionType action) + public (ActionResponse Response, SimulationState NewState) Execute(ActionType action) + { + var newState = this; + var response = newState.ExecuteSelf(action); + return (response, newState); + } + + public ActionResponse ExecuteSelf(ActionType action) { if (IsComplete) return ActionResponse.SimulationComplete; @@ -64,7 +60,7 @@ public class Simulation var baseAction = action.With(this); if (!baseAction.CanUse) { - if (baseAction.Level > Stats.Level) + if (baseAction.Level > Input.Stats.Level) return ActionResponse.ActionNotUnlocked; if (baseAction.CPCost > CP) return ActionResponse.NotEnoughCP; @@ -85,7 +81,7 @@ public class Simulation } } - if (Progress >= MaxProgress) + if (Progress >= Input.MaxProgress) { IsComplete = true; return ActionResponse.ProgressComplete; @@ -112,7 +108,8 @@ public class Simulation duration++; var currentEffect = GetEffect(effect); - if (currentEffect != null) { + if (currentEffect != null) + { currentEffect.Duration = duration; currentEffect.Strength = strength; } @@ -148,7 +145,7 @@ public class Simulation ActionHistory.Count(a => a == action); public bool RollSuccessRaw(float successRate) => - successRate >= Random.NextSingle(); + successRate >= Input.Random.NextSingle(); public bool RollSuccess(float successRate) => RollSuccessRaw(CalculateSuccessRate(successRate)); @@ -159,10 +156,10 @@ public class Simulation StepCondition(); } - private float GetConditionChance(Condition condition) => + private static float GetConditionChance(SimulationInput input, Condition condition) => condition switch { - Condition.Good => Recipe.IsExpert ? 0.12f : (Stats.Level >= 63 ? 0.15f : 0.18f), + Condition.Good => input.Recipe.IsExpert ? 0.12f : (input.Stats.Level >= 63 ? 0.15f : 0.18f), Condition.Excellent => 0.04f, Condition.Centered => 0.15f, Condition.Sturdy => 0.15f, @@ -173,16 +170,26 @@ public class Simulation _ => 0.00f }; + private Condition GetNextRandomCondition() + { + var conditionChance = Input.Random.NextSingle(); + + foreach (var condition in Input.AvailableConditions) + if ((conditionChance -= GetConditionChance(Input, condition)) < 0) + return condition; + + return Condition.Normal; + } + public void StepCondition() { - var conditionChance = Random.NextSingle(); - - Condition = Condition switch { + Condition = Condition switch + { Condition.Poor => Condition.Normal, Condition.Good => Condition.Normal, Condition.Excellent => Condition.Poor, Condition.GoodOmen => Condition.Good, - _ => AvailableConditions.FirstOrDefault(c => (conditionChance -= GetConditionChance(c)) < 0, Condition.Normal) + _ => GetNextRandomCondition() }; } @@ -190,16 +197,16 @@ public class Simulation { Durability += amount; - if (Durability > MaxDurability) - Durability = MaxDurability; + if (Durability > Input.MaxDurability) + Durability = Input.MaxDurability; } public void RestoreCP(int amount) { CP += amount; - - if (CP > Stats.CP) - CP = Stats.CP; + + if (CP > Input.Stats.CP) + CP = Input.Stats.CP; } public float CalculateSuccessRate(float successRate) @@ -246,9 +253,9 @@ public class Simulation }; // https://github.com/NotRanged/NotRanged.github.io/blob/0f4aee074f969fb05aad34feaba605057c08ffd1/app/js/ffxivcraftmodel.js#L88 - var baseIncrease = (Stats.Craftsmanship * 10f / RecipeTable.ProgressDivider) + 2; - if (Stats.CLvl <= RLvl) - baseIncrease *= RecipeTable.ProgressModifier / 100f; + var baseIncrease = (Input.Stats.Craftsmanship * 10f / Input.RecipeTable.ProgressDivider) + 2; + if (Input.Stats.CLvl <= Input.RLvl) + baseIncrease *= Input.RecipeTable.ProgressModifier / 100f; baseIncrease = MathF.Floor(baseIncrease); var progressGain = (int)(baseIncrease * efficiency * conditionModifier * buffModifier); @@ -272,14 +279,14 @@ public class Simulation var conditionModifier = Condition switch { Condition.Poor => 0.50f, - Condition.Good => Stats.HasRelic ? 1.75f : 1.50f, + Condition.Good => Input.Stats.HasRelic ? 1.75f : 1.50f, Condition.Excellent => 4.00f, _ => 1.00f, }; - var baseIncrease = (Stats.Control * 10f / RecipeTable.QualityDivider) + 35; - if (Stats.CLvl <= RLvl) - baseIncrease *= RecipeTable.QualityModifier / 100f; + var baseIncrease = (Input.Stats.Control * 10f / Input.RecipeTable.QualityDivider) + 35; + if (Input.Stats.CLvl <= Input.RLvl) + baseIncrease *= Input.RecipeTable.QualityModifier / 100f; baseIncrease = MathF.Floor(baseIncrease); var qualityGain = (int)(baseIncrease * efficiency * conditionModifier * buffModifier); @@ -296,9 +303,9 @@ public class Simulation { Progress += progressGain; - if (HasEffect(EffectType.FinalAppraisal) && Progress >= MaxProgress) + if (HasEffect(EffectType.FinalAppraisal) && Progress >= Input.MaxProgress) { - Progress = MaxProgress - 1; + Progress = Input.MaxProgress - 1; RemoveEffect(EffectType.FinalAppraisal); } } @@ -307,7 +314,7 @@ public class Simulation { Quality += qualityGain; - if (Stats.Level >= 11) + if (Input.Stats.Level >= 11) StrengthenEffect(EffectType.InnerQuiet); }