Big changes 2
- Split into several projects - All dalamud/lumina deps are in the plugin - Crafty/craftingway sim implemented! - optimizations to follow
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
using Craftimizer.Simulator.Actions;
|
||||
|
||||
namespace Craftimizer.Simulator;
|
||||
|
||||
public class Simulator
|
||||
{
|
||||
public SimulationInput Input { get; private set; }
|
||||
public int StepCount { get; private set; }
|
||||
public int Progress { get; private set; }
|
||||
public int Quality { get; private set; }
|
||||
public int Durability { get; private set; }
|
||||
public int CP { get; private set; }
|
||||
public Condition Condition { get; private set; }
|
||||
public List<Effect> ActiveEffects { get; private set; }
|
||||
public List<ActionType> ActionHistory { get; private set; }
|
||||
|
||||
public bool IsFirstStep => StepCount == 0;
|
||||
|
||||
public CompletionState CompletionState
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Progress >= Input.Recipe.MaxProgress)
|
||||
return CompletionState.ProgressComplete;
|
||||
if (Durability <= 0)
|
||||
return CompletionState.NoMoreDurability;
|
||||
return CompletionState.Incomplete;
|
||||
}
|
||||
}
|
||||
public virtual bool IsComplete => CompletionState != CompletionState.Incomplete;
|
||||
|
||||
public IEnumerable<ActionType> AvailableActions => ActionUtils.AvailableActions(this);
|
||||
|
||||
#pragma warning disable CS8618 // Emplace sets all the fields already
|
||||
public Simulator(SimulationState state)
|
||||
#pragma warning restore CS8618
|
||||
{
|
||||
Emplace(state);
|
||||
}
|
||||
|
||||
private void Emplace(SimulationState state)
|
||||
{
|
||||
Input = state.Input;
|
||||
StepCount = state.StepCount;
|
||||
Progress = state.Progress;
|
||||
Quality = state.Quality;
|
||||
Durability = state.Durability;
|
||||
CP = state.CP;
|
||||
Condition = state.Condition;
|
||||
ActiveEffects = new(state.ActiveEffects);
|
||||
ActionHistory = new(state.ActionHistory);
|
||||
}
|
||||
|
||||
private SimulationState Displace() => new()
|
||||
{
|
||||
Input = Input,
|
||||
StepCount = StepCount,
|
||||
Progress = Progress,
|
||||
Quality = Quality,
|
||||
Durability = Durability,
|
||||
CP = CP,
|
||||
Condition = Condition,
|
||||
ActiveEffects = ActiveEffects!,
|
||||
ActionHistory = ActionHistory!,
|
||||
};
|
||||
|
||||
public (ActionResponse Response, SimulationState NewState) Execute(SimulationState state, ActionType action)
|
||||
{
|
||||
Emplace(state);
|
||||
return (Execute(action), Displace());
|
||||
}
|
||||
|
||||
private ActionResponse Execute(ActionType action)
|
||||
{
|
||||
if (IsComplete)
|
||||
return ActionResponse.SimulationComplete;
|
||||
|
||||
var baseAction = action.With(this);
|
||||
if (!baseAction.CanUse)
|
||||
{
|
||||
if (baseAction.Level > Input.Stats.Level)
|
||||
return ActionResponse.ActionNotUnlocked;
|
||||
if (baseAction.CPCost > CP)
|
||||
return ActionResponse.NotEnoughCP;
|
||||
return ActionResponse.CannotUseAction;
|
||||
}
|
||||
|
||||
baseAction.Use();
|
||||
ActionHistory!.Add(action);
|
||||
|
||||
for (var i = 0; i < ActiveEffects!.Count; ++i)
|
||||
{
|
||||
var effect = ActiveEffects[i].DecrementDuration();
|
||||
if (effect.Duration == 0)
|
||||
{
|
||||
ActiveEffects.RemoveAt(i);
|
||||
--i;
|
||||
}
|
||||
else
|
||||
ActiveEffects[i] = effect;
|
||||
}
|
||||
|
||||
return ActionResponse.UsedAction;
|
||||
}
|
||||
|
||||
private int GetEffectIdx(EffectType effect) =>
|
||||
ActiveEffects!.FindIndex(e => e.Type == effect);
|
||||
|
||||
public Effect? GetEffect(EffectType effect)
|
||||
{
|
||||
var idx = GetEffectIdx(effect);
|
||||
return idx == -1 ? null : ActiveEffects![idx];
|
||||
}
|
||||
|
||||
public void AddEffect(EffectType effect, int? duration = null, int? strength = null)
|
||||
{
|
||||
if (Condition == Condition.Primed && duration != null)
|
||||
duration += 2;
|
||||
|
||||
// Duration will be decreased in the next step, so we need to add 1
|
||||
if (duration != null)
|
||||
duration++;
|
||||
|
||||
var newEffect = new Effect { Type = effect, Duration = duration, Strength = strength };
|
||||
|
||||
var effectIdx = GetEffectIdx(effect);
|
||||
if (effectIdx != -1)
|
||||
ActiveEffects![effectIdx] = newEffect;
|
||||
else
|
||||
ActiveEffects!.Add(newEffect);
|
||||
}
|
||||
|
||||
public void StrengthenEffect(EffectType effect, int? duration = null)
|
||||
{
|
||||
if (duration != null)
|
||||
duration += 1;
|
||||
|
||||
var effectIdx = GetEffectIdx(effect);
|
||||
if (effectIdx != -1)
|
||||
{
|
||||
if (effect == EffectType.InnerQuiet && ActiveEffects![effectIdx].Strength < 10)
|
||||
ActiveEffects[effectIdx] = ActiveEffects[effectIdx].IncrementStrength();
|
||||
}
|
||||
else
|
||||
ActiveEffects!.Add(new Effect { Type = effect, Duration = duration, Strength = 1 });
|
||||
}
|
||||
|
||||
public void RemoveEffect(EffectType effect) =>
|
||||
ActiveEffects!.RemoveAll(e => e.Type == effect);
|
||||
|
||||
public bool HasEffect(EffectType effect) =>
|
||||
ActiveEffects!.Any(e => e.Type == effect);
|
||||
|
||||
public bool IsPreviousAction(ActionType action, int stepsBack = 1) =>
|
||||
ActionHistory!.Count >= stepsBack && ActionHistory[^stepsBack] == action;
|
||||
|
||||
public int CountPreviousAction(ActionType action) =>
|
||||
ActionHistory!.Count(a => a == action);
|
||||
|
||||
public virtual bool RollSuccessRaw(float successRate) =>
|
||||
successRate >= Input.Random.NextSingle();
|
||||
|
||||
public bool RollSuccess(float successRate) =>
|
||||
RollSuccessRaw(CalculateSuccessRate(successRate));
|
||||
|
||||
public void IncreaseStepCount()
|
||||
{
|
||||
StepCount++;
|
||||
StepCondition();
|
||||
}
|
||||
|
||||
private static float GetConditionChance(SimulationInput input, Condition condition) =>
|
||||
condition switch
|
||||
{
|
||||
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,
|
||||
Condition.Pliant => 0.10f,
|
||||
Condition.Malleable => 0.13f,
|
||||
Condition.Primed => 0.15f,
|
||||
Condition.GoodOmen => 0.12f, // https://github.com/ffxiv-teamcraft/simulator/issues/77
|
||||
_ => 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 virtual void StepCondition()
|
||||
{
|
||||
Condition = Condition switch
|
||||
{
|
||||
Condition.Poor => Condition.Normal,
|
||||
Condition.Good => Condition.Normal,
|
||||
Condition.Excellent => Condition.Poor,
|
||||
Condition.GoodOmen => Condition.Good,
|
||||
_ => GetNextRandomCondition()
|
||||
};
|
||||
}
|
||||
|
||||
public void RestoreDurability(int amount)
|
||||
{
|
||||
Durability += amount;
|
||||
|
||||
if (Durability > Input.Recipe.MaxDurability)
|
||||
Durability = Input.Recipe.MaxDurability;
|
||||
}
|
||||
|
||||
public void RestoreCP(int amount)
|
||||
{
|
||||
CP += amount;
|
||||
|
||||
if (CP > Input.Stats.CP)
|
||||
CP = Input.Stats.CP;
|
||||
}
|
||||
|
||||
public float CalculateSuccessRate(float successRate)
|
||||
{
|
||||
if (Condition == Condition.Centered)
|
||||
successRate += 0.25f;
|
||||
return Math.Clamp(successRate, 0, 1);
|
||||
}
|
||||
|
||||
public int CalculateDurabilityCost(int amount)
|
||||
{
|
||||
var amt = (double)amount;
|
||||
if (HasEffect(EffectType.WasteNot) || HasEffect(EffectType.WasteNot2))
|
||||
amt /= 2;
|
||||
if (Condition == Condition.Sturdy)
|
||||
amt /= 2;
|
||||
return (int)Math.Ceiling(amt);
|
||||
}
|
||||
|
||||
public int CalculateCPCost(int amount)
|
||||
{
|
||||
var amt = (double)amount;
|
||||
if (Condition == Condition.Pliant)
|
||||
amt /= 2;
|
||||
return (int)Math.Ceiling(amt);
|
||||
}
|
||||
|
||||
public int CalculateProgressGain(float efficiency, bool dryRun = true)
|
||||
{
|
||||
var buffModifier = 1.00f;
|
||||
if (HasEffect(EffectType.MuscleMemory))
|
||||
{
|
||||
buffModifier += 1.00f;
|
||||
if (!dryRun)
|
||||
RemoveEffect(EffectType.MuscleMemory);
|
||||
}
|
||||
if (HasEffect(EffectType.Veneration))
|
||||
buffModifier += 0.50f;
|
||||
|
||||
var conditionModifier = Condition switch
|
||||
{
|
||||
Condition.Malleable => 1.50f,
|
||||
_ => 1.00f
|
||||
};
|
||||
|
||||
// https://github.com/NotRanged/NotRanged.github.io/blob/0f4aee074f969fb05aad34feaba605057c08ffd1/app/js/ffxivcraftmodel.js#L88
|
||||
var baseIncrease = (Input.Stats.Craftsmanship * 10f / Input.Recipe.ProgressDivider) + 2;
|
||||
if (Input.Stats.CLvl <= Input.Recipe.RLvl)
|
||||
baseIncrease *= Input.Recipe.ProgressModifier / 100f;
|
||||
baseIncrease = MathF.Floor(baseIncrease);
|
||||
|
||||
var progressGain = (int)(baseIncrease * efficiency * conditionModifier * buffModifier);
|
||||
return progressGain;
|
||||
}
|
||||
|
||||
public int CalculateQualityGain(float efficiency, bool dryRun = true)
|
||||
{
|
||||
var buffModifier = 1.00f;
|
||||
if (HasEffect(EffectType.GreatStrides))
|
||||
{
|
||||
buffModifier += 1.00f;
|
||||
if (!dryRun)
|
||||
RemoveEffect(EffectType.GreatStrides);
|
||||
}
|
||||
if (HasEffect(EffectType.Innovation))
|
||||
buffModifier += 0.50f;
|
||||
|
||||
buffModifier *= 1 + ((GetEffect(EffectType.InnerQuiet)?.Strength ?? 0) * 0.10f);
|
||||
|
||||
var conditionModifier = Condition switch
|
||||
{
|
||||
Condition.Poor => 0.50f,
|
||||
Condition.Good => Input.Stats.HasRelic ? 1.75f : 1.50f,
|
||||
Condition.Excellent => 4.00f,
|
||||
_ => 1.00f,
|
||||
};
|
||||
|
||||
var baseIncrease = (Input.Stats.Control * 10f / Input.Recipe.QualityDivider) + 35;
|
||||
if (Input.Stats.CLvl <= Input.Recipe.RLvl)
|
||||
baseIncrease *= Input.Recipe.QualityModifier / 100f;
|
||||
baseIncrease = MathF.Floor(baseIncrease);
|
||||
|
||||
var qualityGain = (int)(baseIncrease * efficiency * conditionModifier * buffModifier);
|
||||
return qualityGain;
|
||||
}
|
||||
|
||||
public void ReduceDurabilityRaw(int amount) =>
|
||||
Durability -= amount;
|
||||
|
||||
public void ReduceCPRaw(int amount) =>
|
||||
CP -= amount;
|
||||
|
||||
public void IncreaseProgressRaw(int progressGain)
|
||||
{
|
||||
Progress += progressGain;
|
||||
|
||||
if (HasEffect(EffectType.FinalAppraisal) && Progress >= Input.Recipe.MaxProgress)
|
||||
{
|
||||
Progress = Input.Recipe.MaxProgress - 1;
|
||||
RemoveEffect(EffectType.FinalAppraisal);
|
||||
}
|
||||
}
|
||||
|
||||
public void IncreaseQualityRaw(int qualityGain)
|
||||
{
|
||||
Quality += qualityGain;
|
||||
|
||||
if (Input.Stats.Level >= 11)
|
||||
StrengthenEffect(EffectType.InnerQuiet);
|
||||
}
|
||||
|
||||
public void ReduceDurability(int amount) =>
|
||||
ReduceDurabilityRaw(CalculateDurabilityCost(amount));
|
||||
|
||||
public void ReduceCP(int amount) =>
|
||||
ReduceCPRaw(CalculateCPCost(amount));
|
||||
|
||||
public void IncreaseProgress(float efficiency) =>
|
||||
IncreaseProgressRaw(CalculateProgressGain(efficiency, false));
|
||||
|
||||
public void IncreaseQuality(float efficiency) =>
|
||||
IncreaseQualityRaw(CalculateQualityGain(efficiency, false));
|
||||
}
|
||||
Reference in New Issue
Block a user