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:
Asriel Camora
2023-06-17 08:50:46 -07:00
parent 15d416ef2a
commit e190368d62
76 changed files with 1284 additions and 435 deletions
+26
View File
@@ -0,0 +1,26 @@
namespace Craftimizer.Simulator;
public enum ActionCategory
{
FirstTurn,
Synthesis,
Quality,
Durability,
Buffs,
Other
}
public static class ActionCategoryUtils
{
public static string GetDisplayName(this ActionCategory category) =>
category switch
{
ActionCategory.FirstTurn => "First Turn",
ActionCategory.Synthesis => "Synthesis",
ActionCategory.Quality => "Quality",
ActionCategory.Durability => "Durability",
ActionCategory.Buffs => "Buffs",
ActionCategory.Other => "Other",
_ => category.ToString()
};
}
+11
View File
@@ -0,0 +1,11 @@
namespace Craftimizer.Simulator;
public enum ActionResponse
{
SimulationComplete,
ActionNotUnlocked,
NotEnoughCP,
NoDurability,
CannotUseAction,
UsedAction,
}
+119
View File
@@ -0,0 +1,119 @@
namespace Craftimizer.Simulator.Actions;
public enum ActionType
{
AdvancedTouch,
BasicSynthesis,
BasicTouch,
ByregotsBlessing,
CarefulObservation,
CarefulSynthesis,
DelicateSynthesis,
FinalAppraisal,
FocusedSynthesis,
FocusedTouch,
GreatStrides,
Groundwork,
HastyTouch,
HeartAndSoul,
Innovation,
IntensiveSynthesis,
Manipulation,
MastersMend,
MuscleMemory,
Observe,
PreciseTouch,
PreparatoryTouch,
PrudentSynthesis,
PrudentTouch,
RapidSynthesis,
Reflect,
StandardTouch,
TrainedEye,
TrainedFinesse,
TricksOfTheTrade,
Veneration,
WasteNot,
WasteNot2,
}
public static class ActionUtils
{
private static readonly BaseAction[] Actions;
static ActionUtils()
{
var types = typeof(BaseAction).Assembly.GetTypes()
.Where(t => t.IsAssignableTo(typeof(BaseAction)) && !t.IsAbstract);
Actions = Enum.GetNames<ActionType>()
.Select(a => types.First(t => t.Name == a))
.Select(t => (Activator.CreateInstance(t) as BaseAction)!)
.ToArray();
}
public static void SetSimulation(Simulator simulation) =>
BaseAction.TLSSimulation.Value = simulation;
public static BaseAction WithUnsafe(this ActionType me) => Actions[(int)me];
public static BaseAction With(this ActionType me, Simulator simulation)
{
SetSimulation(simulation);
return WithUnsafe(me);
}
public static IEnumerable<ActionType> AvailableActions(Simulator simulation)
{
if (simulation.IsComplete)
return Enumerable.Empty<ActionType>();
SetSimulation(simulation);
return Enum.GetValues<ActionType>()
.Where(a => WithUnsafe(a).CanUse);
}
public static int Level(this ActionType me) =>
WithUnsafe(me).Level;
public static ActionCategory Category(this ActionType me) =>
WithUnsafe(me).Category;
public static string IntName(this ActionType me) =>
me switch
{
ActionType.AdvancedTouch => "Advanced Touch",
ActionType.BasicSynthesis => "Basic Synthesis",
ActionType.BasicTouch => "Basic Touch",
ActionType.ByregotsBlessing => "Byregot's Blessing",
ActionType.CarefulObservation => "Careful Observation",
ActionType.CarefulSynthesis => "Careful Synthesis",
ActionType.DelicateSynthesis => "Delicate Synthesis",
ActionType.FinalAppraisal => "Final Appraisal",
ActionType.FocusedSynthesis => "Focused Synthesis",
ActionType.FocusedTouch => "Focused Touch",
ActionType.GreatStrides => "Great Strides",
ActionType.Groundwork => "Groundwork",
ActionType.HastyTouch => "Hasty Touch",
ActionType.HeartAndSoul => "Heart And Soul",
ActionType.Innovation => "Innovation",
ActionType.IntensiveSynthesis => "Intensive Synthesis",
ActionType.Manipulation => "Manipulation",
ActionType.MastersMend => "Master's Mend",
ActionType.MuscleMemory => "Muscle Memory",
ActionType.Observe => "Observe",
ActionType.PreciseTouch => "Precise Touch",
ActionType.PreparatoryTouch => "Preparatory Touch",
ActionType.PrudentSynthesis => "Prudent Synthesis",
ActionType.PrudentTouch => "Prudent Touch",
ActionType.RapidSynthesis => "Rapid Synthesis",
ActionType.Reflect => "Reflect",
ActionType.StandardTouch => "Standard Touch",
ActionType.TrainedEye => "Trained Eye",
ActionType.TrainedFinesse => "Trained Finesse",
ActionType.TricksOfTheTrade => "Tricks Of The Trade",
ActionType.Veneration => "Veneration",
ActionType.WasteNot => "Waste Not",
ActionType.WasteNot2 => "Waste Not II",
_ => me.ToString(),
};
}
+12
View File
@@ -0,0 +1,12 @@
namespace Craftimizer.Simulator.Actions;
internal class AdvancedTouch : BaseAction
{
public override ActionCategory Category => ActionCategory.Quality;
public override int Level => 84;
public override uint ActionId => 100411;
public override int CPCost => Simulation.IsPreviousAction(ActionType.StandardTouch) && Simulation.IsPreviousAction(ActionType.BasicTouch, 2) ? 18 : 46;
public override float Efficiency => 1.50f;
public override bool IncreasesQuality => true;
}
+82
View File
@@ -0,0 +1,82 @@
using System;
using System.Text;
using System.Threading;
namespace Craftimizer.Simulator.Actions;
public abstract class BaseAction
{
internal static readonly ThreadLocal<Simulator?> TLSSimulation = new(false);
protected static Simulator Simulation => TLSSimulation.Value ?? throw new NullReferenceException();
public BaseAction() { }
// Non-instanced properties
public abstract ActionCategory Category { get; }
public abstract int Level { get; }
// Doesn't matter from which class, we'll use the sheet to extrapolate the rest
public abstract uint ActionId { get; }
// Instanced properties
public abstract int CPCost { get; }
public virtual float Efficiency => 0f;
public virtual bool IncreasesProgress => false;
public virtual bool IncreasesQuality => false;
public virtual float SuccessRate => 1f;
public virtual int DurabilityCost => 10;
public virtual bool IncreasesStepCount => true;
public virtual bool IsGuaranteedAction => SuccessRate == 1f;
public virtual bool CanUse =>
Simulation.Input.Stats.Level >= Level && Simulation.CP >= CPCost;
public virtual void Use()
{
Simulation.ReduceCP(CPCost);
Simulation.ReduceDurability(DurabilityCost);
if (Simulation.HasEffect(EffectType.Manipulation))
Simulation.RestoreDurability(5);
if (Simulation.RollSuccess(SuccessRate))
UseSuccess();
if (IncreasesStepCount)
Simulation.IncreaseStepCount();
}
public virtual void UseSuccess()
{
if (Efficiency != 0f)
{
if (IncreasesProgress)
Simulation.IncreaseProgress(Efficiency);
if (IncreasesQuality)
Simulation.IncreaseQuality(Efficiency);
}
}
public virtual string GetTooltip(bool addUsability)
{
var builder = new StringBuilder();
if (addUsability && !CanUse)
builder.AppendLine($"Cannot Use");
builder.AppendLine($"Level {Level}");
if (CPCost != 0)
builder.AppendLine($"-{Simulation.CalculateCPCost(CPCost)} CP");
if (DurabilityCost != 0)
builder.AppendLine($"-{Simulation.CalculateDurabilityCost(DurabilityCost)} Durability");
if (Efficiency != 0)
{
if (IncreasesProgress)
builder.AppendLine($"+{Simulation.CalculateProgressGain(Efficiency)} Progress");
if (IncreasesQuality)
builder.AppendLine($"+{Simulation.CalculateQualityGain(Efficiency)} Quality");
}
if (!IncreasesStepCount)
builder.AppendLine($"Does Not Increase Step Count");
if (SuccessRate != 1f)
builder.AppendLine($"{Simulation.CalculateSuccessRate(SuccessRate) * 100}%% Success Rate");
return builder.ToString();
}
}
+26
View File
@@ -0,0 +1,26 @@
using System.Text;
namespace Craftimizer.Simulator.Actions;
internal abstract class BaseBuffAction : BaseAction
{
public abstract Effect Effect { get; }
public virtual EffectType[] ConflictingEffects => Array.Empty<EffectType>();
public override int DurabilityCost => 0;
public override void UseSuccess()
{
if (ConflictingEffects.Length != 0)
foreach(var effect in ConflictingEffects)
Simulation.RemoveEffect(effect);
Simulation.AddEffect(Effect.Type, Effect.Duration, Effect.Strength);
}
public override string GetTooltip(bool addUsability)
{
var builder = new StringBuilder(base.GetTooltip(addUsability));
builder.AppendLine($"{Effect.Duration} Steps");
return builder.ToString();
}
}
+13
View File
@@ -0,0 +1,13 @@
namespace Craftimizer.Simulator.Actions;
internal class BasicSynthesis : BaseAction
{
public override ActionCategory Category => ActionCategory.Synthesis;
public override int Level => 1;
public override uint ActionId => 100001;
public override int CPCost => 0;
// Basic Synthesis Mastery Trait
public override float Efficiency => Simulation.Input.Stats.Level >= 31 ? 1.20f : 1.00f;
public override bool IncreasesProgress => true;
}
+12
View File
@@ -0,0 +1,12 @@
namespace Craftimizer.Simulator.Actions;
internal class BasicTouch : BaseAction
{
public override ActionCategory Category => ActionCategory.Quality;
public override int Level => 5;
public override uint ActionId => 100002;
public override int CPCost => 18;
public override float Efficiency => 1.00f;
public override bool IncreasesQuality => true;
}
+20
View File
@@ -0,0 +1,20 @@
namespace Craftimizer.Simulator.Actions;
internal class ByregotsBlessing : BaseAction
{
public override ActionCategory Category => ActionCategory.Quality;
public override int Level => 50;
public override uint ActionId => 100339;
public override int CPCost => 24;
public override float Efficiency => 1.00f + (0.20f * (Simulation.GetEffect(EffectType.InnerQuiet)?.Strength ?? 0));
public override bool IncreasesQuality => true;
public override bool CanUse => Simulation.HasEffect(EffectType.InnerQuiet) && base.CanUse;
public override void UseSuccess()
{
base.UseSuccess();
Simulation.RemoveEffect(EffectType.InnerQuiet);
}
}
+17
View File
@@ -0,0 +1,17 @@
namespace Craftimizer.Simulator.Actions;
internal class CarefulObservation : BaseAction
{
public override ActionCategory Category => ActionCategory.Other;
public override int Level => 55;
public override uint ActionId => 100395;
public override int CPCost => 0;
public override int DurabilityCost => 0;
public override bool IncreasesStepCount => false;
public override bool CanUse => Simulation.Input.Stats.IsSpecialist && Simulation.CountPreviousAction(ActionType.CarefulObservation) < 3;
public override void UseSuccess() =>
Simulation.StepCondition();
}
+13
View File
@@ -0,0 +1,13 @@
namespace Craftimizer.Simulator.Actions;
internal class CarefulSynthesis : BaseAction
{
public override ActionCategory Category => ActionCategory.Synthesis;
public override int Level => 62;
public override uint ActionId => 100203;
public override int CPCost => 7;
// Careful Synthesis Mastery Trait
public override float Efficiency => Simulation.Input.Stats.Level >= 82 ? 1.80f : 1.50f;
public override bool IncreasesProgress => true;
}
+13
View File
@@ -0,0 +1,13 @@
namespace Craftimizer.Simulator.Actions;
internal class DelicateSynthesis : BaseAction
{
public override ActionCategory Category => ActionCategory.Synthesis;
public override int Level => 76;
public override uint ActionId => 100323;
public override int CPCost => 32;
public override float Efficiency => 1.00f;
public override bool IncreasesProgress => true;
public override bool IncreasesQuality => true;
}
+13
View File
@@ -0,0 +1,13 @@
namespace Craftimizer.Simulator.Actions;
internal class FinalAppraisal : BaseBuffAction
{
public override ActionCategory Category => ActionCategory.Synthesis;
public override int Level => 42;
public override uint ActionId => 19012;
public override int CPCost => 1;
public override bool IncreasesStepCount => false;
public override Effect Effect => new() { Type = EffectType.FinalAppraisal, Duration = 5 };
}
+13
View File
@@ -0,0 +1,13 @@
namespace Craftimizer.Simulator.Actions;
internal class FocusedSynthesis : BaseAction
{
public override ActionCategory Category => ActionCategory.Synthesis;
public override int Level => 67;
public override uint ActionId => 100235;
public override int CPCost => 5;
public override float Efficiency => 2.00f;
public override bool IncreasesProgress => true;
public override float SuccessRate => Simulation.IsPreviousAction(ActionType.Observe) ? 1.00f : 0.50f;
}
+13
View File
@@ -0,0 +1,13 @@
namespace Craftimizer.Simulator.Actions;
internal class FocusedTouch : BaseAction
{
public override ActionCategory Category => ActionCategory.Quality;
public override int Level => 68;
public override uint ActionId => 100243;
public override int CPCost => 18;
public override float Efficiency => 1.50f;
public override bool IncreasesQuality => true;
public override float SuccessRate => Simulation.IsPreviousAction(ActionType.Observe) ? 1.00f : 0.50f;
}
+12
View File
@@ -0,0 +1,12 @@
namespace Craftimizer.Simulator.Actions;
internal class GreatStrides : BaseBuffAction
{
public override ActionCategory Category => ActionCategory.Buffs;
public override int Level => 21;
public override uint ActionId => 260;
public override int CPCost => 32;
public override Effect Effect => new() { Type = EffectType.GreatStrides, Duration = 3 };
}
+22
View File
@@ -0,0 +1,22 @@
namespace Craftimizer.Simulator.Actions;
internal class Groundwork : BaseAction
{
public override ActionCategory Category => ActionCategory.Synthesis;
public override int Level => 72;
public override uint ActionId => 100403;
public override int CPCost => 18;
// Groundwork Mastery Trait
public override float Efficiency
{
get
{
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;
}
}
public override bool IncreasesProgress => true;
public override int DurabilityCost => 20;
}
+13
View File
@@ -0,0 +1,13 @@
namespace Craftimizer.Simulator.Actions;
internal class HastyTouch : BaseAction
{
public override ActionCategory Category => ActionCategory.Quality;
public override int Level => 9;
public override uint ActionId => 100355;
public override int CPCost => 0;
public override float Efficiency => 1.00f;
public override bool IncreasesQuality => true;
public override float SuccessRate => 0.60f;
}
+15
View File
@@ -0,0 +1,15 @@
namespace Craftimizer.Simulator.Actions;
internal class HeartAndSoul : BaseBuffAction
{
public override ActionCategory Category => ActionCategory.Other;
public override int Level => 86;
public override uint ActionId => 100419;
public override int CPCost => 0;
public override bool IncreasesStepCount => false;
public override Effect Effect => new() { Type = EffectType.HeartAndSoul };
public override bool CanUse => Simulation.Input.Stats.IsSpecialist && Simulation.CountPreviousAction(ActionType.HeartAndSoul) == 0;
}
+12
View File
@@ -0,0 +1,12 @@
namespace Craftimizer.Simulator.Actions;
internal class Innovation : BaseBuffAction
{
public override ActionCategory Category => ActionCategory.Buffs;
public override int Level => 26;
public override uint ActionId => 19004;
public override int CPCost => 18;
public override Effect Effect => new() { Type = EffectType.Innovation, Duration = 4 };
}
+24
View File
@@ -0,0 +1,24 @@
namespace Craftimizer.Simulator.Actions;
internal class IntensiveSynthesis : BaseAction
{
public override ActionCategory Category => ActionCategory.Synthesis;
public override int Level => 78;
public override uint ActionId => 100315;
public override int CPCost => 6;
public override float Efficiency => 4.00f;
public override bool IncreasesProgress => true;
public override bool IsGuaranteedAction => false;
public override bool CanUse =>
(Simulation.Condition == Condition.Good || Simulation.Condition == Condition.Excellent || Simulation.HasEffect(EffectType.HeartAndSoul))
&& base.CanUse;
public override void UseSuccess()
{
base.UseSuccess();
if (Simulation.Condition != Condition.Good && Simulation.Condition != Condition.Excellent)
Simulation.RemoveEffect(EffectType.HeartAndSoul);
}
}
+12
View File
@@ -0,0 +1,12 @@
namespace Craftimizer.Simulator.Actions;
internal class Manipulation : BaseBuffAction
{
public override ActionCategory Category => ActionCategory.Durability;
public override int Level => 65;
public override uint ActionId => 4574;
public override int CPCost => 96;
public override Effect Effect => new() { Type = EffectType.Manipulation, Duration = 8 };
}
+14
View File
@@ -0,0 +1,14 @@
namespace Craftimizer.Simulator.Actions;
internal class MastersMend : BaseAction
{
public override ActionCategory Category => ActionCategory.Durability;
public override int Level => 7;
public override uint ActionId => 100003;
public override int CPCost => 88;
public override int DurabilityCost => 0;
public override void UseSuccess() =>
Simulation.RestoreDurability(30);
}
+20
View File
@@ -0,0 +1,20 @@
namespace Craftimizer.Simulator.Actions;
internal class MuscleMemory : BaseAction
{
public override ActionCategory Category => ActionCategory.FirstTurn;
public override int Level => 54;
public override uint ActionId => 100379;
public override int CPCost => 6;
public override float Efficiency => 3.00f;
public override bool IncreasesProgress => true;
public override bool CanUse => Simulation.IsFirstStep && base.CanUse;
public override void UseSuccess()
{
base.UseSuccess();
Simulation.AddEffect(EffectType.MuscleMemory, 5);
}
}
+11
View File
@@ -0,0 +1,11 @@
namespace Craftimizer.Simulator.Actions;
internal class Observe : BaseAction
{
public override ActionCategory Category => ActionCategory.Other;
public override int Level => 13;
public override uint ActionId => 100010;
public override int CPCost => 7;
public override int DurabilityCost => 0;
}
+25
View File
@@ -0,0 +1,25 @@
namespace Craftimizer.Simulator.Actions;
internal class PreciseTouch : BaseAction
{
public override ActionCategory Category => ActionCategory.Quality;
public override int Level => 53;
public override uint ActionId => 100128;
public override int CPCost => 18;
public override float Efficiency => 1.50f;
public override bool IncreasesQuality => true;
public override bool IsGuaranteedAction => false;
public override bool CanUse =>
(Simulation.Condition == Condition.Good || Simulation.Condition == Condition.Excellent || Simulation.HasEffect(EffectType.HeartAndSoul))
&& base.CanUse;
public override void UseSuccess()
{
base.UseSuccess();
Simulation.StrengthenEffect(EffectType.InnerQuiet);
if (Simulation.Condition != Condition.Good && Simulation.Condition != Condition.Excellent)
Simulation.RemoveEffect(EffectType.HeartAndSoul);
}
}
+19
View File
@@ -0,0 +1,19 @@
namespace Craftimizer.Simulator.Actions;
internal class PreparatoryTouch : BaseAction
{
public override ActionCategory Category => ActionCategory.Quality;
public override int Level => 71;
public override uint ActionId => 100299;
public override int CPCost => 40;
public override float Efficiency => 2.00f;
public override bool IncreasesQuality => true;
public override int DurabilityCost => 20;
public override void UseSuccess()
{
base.UseSuccess();
Simulation.StrengthenEffect(EffectType.InnerQuiet);
}
}
+17
View File
@@ -0,0 +1,17 @@
namespace Craftimizer.Simulator.Actions;
internal class PrudentSynthesis : BaseAction
{
public override ActionCategory Category => ActionCategory.Synthesis;
public override int Level => 88;
public override uint ActionId => 100427;
public override int CPCost => 18;
public override float Efficiency => 1.80f;
public override bool IncreasesProgress => true;
public override int DurabilityCost => base.DurabilityCost / 2;
public override bool CanUse =>
!(Simulation.HasEffect(EffectType.WasteNot) || Simulation.HasEffect(EffectType.WasteNot2))
&& base.CanUse;
}
+17
View File
@@ -0,0 +1,17 @@
namespace Craftimizer.Simulator.Actions;
internal class PrudentTouch : BaseAction
{
public override ActionCategory Category => ActionCategory.Quality;
public override int Level => 66;
public override uint ActionId => 100227;
public override int CPCost => 25;
public override float Efficiency => 1.00f;
public override bool IncreasesQuality => true;
public override int DurabilityCost => base.DurabilityCost / 2;
public override bool CanUse =>
!(Simulation.HasEffect(EffectType.WasteNot) || Simulation.HasEffect(EffectType.WasteNot2))
&& base.CanUse;
}
+14
View File
@@ -0,0 +1,14 @@
namespace Craftimizer.Simulator.Actions;
internal class RapidSynthesis : BaseAction
{
public override ActionCategory Category => ActionCategory.Synthesis;
public override int Level => 9;
public override uint ActionId => 100363;
public override int CPCost => 0;
// Rapid Synthesis Mastery Trait
public override float Efficiency => Simulation.Input.Stats.Level >= 63 ? 5.00f : 2.50f;
public override bool IncreasesProgress => true;
public override float SuccessRate => 0.50f;
}
+20
View File
@@ -0,0 +1,20 @@
namespace Craftimizer.Simulator.Actions;
internal class Reflect : BaseAction
{
public override ActionCategory Category => ActionCategory.FirstTurn;
public override int Level => 69;
public override uint ActionId => 100387;
public override int CPCost => 6;
public override float Efficiency => 1.00f;
public override bool IncreasesQuality => true;
public override bool CanUse => Simulation.IsFirstStep && base.CanUse;
public override void UseSuccess()
{
base.UseSuccess();
Simulation.StrengthenEffect(EffectType.InnerQuiet);
}
}
+12
View File
@@ -0,0 +1,12 @@
namespace Craftimizer.Simulator.Actions;
internal class StandardTouch : BaseAction
{
public override ActionCategory Category => ActionCategory.Quality;
public override int Level => 18;
public override uint ActionId => 100004;
public override int CPCost => Simulation.IsPreviousAction(ActionType.BasicTouch) ? 18 : 32;
public override float Efficiency => 1.25f;
public override bool IncreasesQuality => true;
}
+20
View File
@@ -0,0 +1,20 @@
namespace Craftimizer.Simulator.Actions;
internal class TrainedEye : BaseAction
{
public override ActionCategory Category => ActionCategory.FirstTurn;
public override int Level => 80;
public override uint ActionId => 100283;
public override int CPCost => 250;
public override bool IncreasesQuality => true;
public override bool CanUse =>
Simulation.IsFirstStep &&
!Simulation.Input.Recipe.IsExpert &&
Simulation.Input.Stats.Level >= (Simulation.Input.Recipe.ClassJobLevel + 10) &&
base.CanUse;
public override void UseSuccess() =>
Simulation.IncreaseQualityRaw(Simulation.Input.Recipe.MaxQuality - Simulation.Quality);
}
+17
View File
@@ -0,0 +1,17 @@
namespace Craftimizer.Simulator.Actions;
internal class TrainedFinesse : BaseAction
{
public override ActionCategory Category => ActionCategory.Quality;
public override int Level => 90;
public override uint ActionId => 100435;
public override int CPCost => 32;
public override float Efficiency => 1.00f;
public override bool IncreasesQuality => true;
public override int DurabilityCost => 0;
public override bool CanUse =>
(Simulation.GetEffect(EffectType.InnerQuiet)?.Strength ?? 0) == 10
&& base.CanUse;
}
+23
View File
@@ -0,0 +1,23 @@
namespace Craftimizer.Simulator.Actions;
internal class TricksOfTheTrade : BaseAction
{
public override ActionCategory Category => ActionCategory.Other;
public override int Level => 13;
public override uint ActionId => 100371;
public override int CPCost => 0;
public override int DurabilityCost => 0;
public override bool IsGuaranteedAction => false;
public override bool CanUse =>
(Simulation.Condition == Condition.Good || Simulation.Condition == Condition.Excellent || Simulation.HasEffect(EffectType.HeartAndSoul))
&& base.CanUse;
public override void UseSuccess()
{
Simulation.RestoreCP(20);
if (Simulation.Condition != Condition.Good && Simulation.Condition != Condition.Excellent)
Simulation.RemoveEffect(EffectType.HeartAndSoul);
}
}
+13
View File
@@ -0,0 +1,13 @@
namespace Craftimizer.Simulator.Actions;
internal class Veneration : BaseBuffAction
{
public override ActionCategory Category => ActionCategory.Buffs;
public override int Level => 15;
public override uint ActionId => 19297;
public override int CPCost => 18;
public override int DurabilityCost => 0;
public override Effect Effect => new() { Type = EffectType.Veneration, Duration = 4 };
}
+13
View File
@@ -0,0 +1,13 @@
namespace Craftimizer.Simulator.Actions;
internal class WasteNot : BaseBuffAction
{
public override ActionCategory Category => ActionCategory.Durability;
public override int Level => 15;
public override uint ActionId => 4631;
public override int CPCost => 56;
public override Effect Effect => new() { Type = EffectType.WasteNot, Duration = 4 };
public override EffectType[] ConflictingEffects => new[] { EffectType.WasteNot2 };
}
+13
View File
@@ -0,0 +1,13 @@
namespace Craftimizer.Simulator.Actions;
internal class WasteNot2 : BaseBuffAction
{
public override ActionCategory Category => ActionCategory.Durability;
public override int Level => 47;
public override uint ActionId => 4639;
public override int CPCost => 98;
public override Effect Effect => new() { Type = EffectType.WasteNot2, Duration = 8 };
public override EffectType[] ConflictingEffects => new[] { EffectType.WasteNot };
}
+12
View File
@@ -0,0 +1,12 @@
namespace Craftimizer.Simulator;
public record CharacterStats
{
public int Craftsmanship { get; init; }
public int Control { get; init; }
public int CP { get; init; }
public int Level { get; init; }
public bool HasRelic { get; init; }
public bool IsSpecialist { get; init; }
public int CLvl { get; init; }
}
+13
View File
@@ -0,0 +1,13 @@
namespace Craftimizer.Simulator;
public enum ClassJob
{
Carpenter,
Blacksmith,
Armorer,
Goldsmith,
Leatherworker,
Weaver,
Alchemist,
Culinarian
}
+10
View File
@@ -0,0 +1,10 @@
namespace Craftimizer.Simulator;
public enum CompletionState
{
Incomplete,
ProgressComplete,
NoMoreDurability,
Other
}
+22
View File
@@ -0,0 +1,22 @@
namespace Craftimizer.Simulator;
public enum Condition : ushort
{
Poor = 0x0008,
Normal = 0x0001,
Good = 0x0002,
Excellent = 0x0004,
Centered = 0x0010,
Sturdy = 0x0020,
Pliant = 0x0040,
Malleable = 0x0080,
Primed = 0x0100,
GoodOmen = 0x0200,
}
public static class ConditionUtils
{
public static Condition[] GetPossibleConditions(ushort conditionsFlag) =>
Enum.GetValues<Condition>().Where(c => ((Condition)conditionsFlag).HasFlag(c)).ToArray();
}
+9
View File
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
+14
View File
@@ -0,0 +1,14 @@
namespace Craftimizer.Simulator;
public readonly record struct Effect
{
public EffectType Type { get; init; }
public int? Duration { get; init; }
public int? Strength { get; init; }
public bool HasDuration => Duration != null;
public bool HasStrength => Strength != null;
public Effect DecrementDuration() => this with { Duration = Duration - 1 };
public Effect IncrementStrength() => this with { Strength = Strength + 1 };
}
+15
View File
@@ -0,0 +1,15 @@
namespace Craftimizer.Simulator;
public enum EffectType
{
InnerQuiet,
WasteNot,
Veneration,
GreatStrides,
Innovation,
FinalAppraisal,
WasteNot2,
MuscleMemory,
Manipulation,
HeartAndSoul,
}
+17
View File
@@ -0,0 +1,17 @@
namespace Craftimizer.Simulator;
public record RecipeInfo
{
public bool IsExpert { get; init; }
public int ClassJobLevel { get; init; }
public int RLvl { get; init; }
public ushort ConditionsFlag { get; init; }
public int MaxDurability { get; init; }
public int MaxQuality { get; init; }
public int MaxProgress { get; init; }
public int QualityModifier { get; init; }
public int QualityDivider { get; init; }
public int ProgressModifier { get; init; }
public int ProgressDivider { get; init; }
}
+12
View File
@@ -0,0 +1,12 @@
using System;
namespace Craftimizer.Simulator;
public readonly record struct SimulationInput
{
public CharacterStats Stats { get; init; }
public RecipeInfo Recipe { get; init; }
public Random Random { get; init; }
public Condition[] AvailableConditions => ConditionUtils.GetPossibleConditions(Recipe.ConditionsFlag);
}
+46
View File
@@ -0,0 +1,46 @@
using Craftimizer.Simulator.Actions;
using System;
using System.Collections.Generic;
namespace Craftimizer.Simulator;
public readonly record struct SimulationState
{
public SimulationInput Input { get; init; }
public int ActionCount => ActionHistory.Count;
public int StepCount { get; init; }
public int Progress { get; init; }
public int Quality { get; init; }
public int Durability { get; init; }
public int CP { get; init; }
public Condition Condition { get; init; }
public List<Effect> ActiveEffects { get; init; }
public List<ActionType> ActionHistory { get; init; }
// https://github.com/ffxiv-teamcraft/simulator/blob/0682dfa76043ff4ccb38832c184d046ceaff0733/src/model/tables.ts#L2
private static readonly int[] HQPercentTable = {
1, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8,
9, 9, 9, 10, 10, 10, 11, 11, 11, 12, 12, 12, 13, 13, 13, 14, 14, 14, 15, 15, 15, 16, 16, 17, 17,
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 / Input.Recipe.MaxQuality * 100, 0, 100)];
public bool IsFirstStep => StepCount == 0;
public SimulationState(SimulationInput input)
{
Input = input;
StepCount = 0;
Progress = 0;
Quality = 0;
Durability = Input.Recipe.MaxDurability;
CP = Input.Stats.CP;
Condition = Condition.Normal;
ActiveEffects = new();
ActionHistory = new();
}
}
+345
View File
@@ -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));
}