diff --git a/Anvil/RecipeData/AnvilAction.cs b/Anvil/RecipeData/AnvilAction.cs new file mode 100644 index 0000000..99a8c74 --- /dev/null +++ b/Anvil/RecipeData/AnvilAction.cs @@ -0,0 +1,114 @@ +// One AnvilAction represents the logical action ("Basic Synthesis"), not the +// eight per-job sheet rows that back it. The simulator and solver operate on +// the logical identifier; the RowIdByClassJob map keeps the per-job RowIds +// reachable for UseAction / IsActionHighlighted hooks. Cosmic-Exploration +// actions break the eight-row rule and are stored under the sentinel +// ClassJobId 0 (covers every crafter job at once). + +using System.Collections.Generic; + +namespace Anvil.RecipeData; + +public sealed record AnvilAction +{ + public required AnvilActionKind Kind { get; init; } + public required string DisplayName { get; init; } + public required uint IconId { get; init; } + public required byte LevelRequired { get; init; } + public required short CpCost { get; init; } + public required short DurabilityCost { get; init; } + public required short EfficiencyProgress { get; init; } + public required short EfficiencyQuality { get; init; } + public required AnvilActionCategory Category { get; init; } + public required AnvilBuffKind? GrantsBuff { get; init; } + public required byte IqStackBonus { get; init; } + public required AnvilActionFlags Flags { get; init; } + public required IReadOnlyDictionary RowIdByClassJob { get; init; } + public required bool SpecialistOnly { get; init; } + public required byte ChargesPerCraft { get; init; } +} + +public enum AnvilActionKind +{ + // Progress actions (8) + BasicSynthesis, + CarefulSynthesis, + RapidSynthesis, + IntensiveSynthesis, + PrudentSynthesis, + Groundwork, + MuscleMemory, + DelicateSynthesis, + + // Quality actions (14) + BasicTouch, + StandardTouch, + AdvancedTouch, + PreciseTouch, + PreparatoryTouch, + PrudentTouch, + RefinedTouch, + ByregotsBlessing, + HastyTouch, + DaringTouch, + TrainedEye, + TrainedFinesse, + Reflect, + TricksOfTheTrade, + + // Buff actions (7) + Veneration, + Innovation, + GreatStrides, + FinalAppraisal, + Manipulation, + WasteNot, + WasteNot2, + + // Repair actions (2) + MastersMend, + ImmaculateMend, + + // Observe / Specialist (5) + Observe, + CarefulObservation, + HeartAndSoul, + QuickInnovation, + TrainedPerfection, + + // Cosmic Exploration (2) + MaterialMiracle, + StellarSteadyHand, +} + +public enum AnvilActionCategory +{ + HybridProgressQuality, + Cosmic, + ProgressAction, + QualityAction, + BuffAction, + RepairAction, + ObserveAction, +} + +// Bit 1 << 2 is intentionally vacant. An earlier draft used it for +// ConsumesGreatStrides; the rule lives in the simulator (every quality +// action with EfficiencyQuality > 0 consumes an active GreatStrides) and +// does not need a per-action data-layer flag. Keeping the slot empty avoids +// renumbering downstream bits if/when persistence consumes this enum. +[System.Flags] +public enum AnvilActionFlags : ushort +{ + None = 0, + RequiresGoodOrExcellent = 1 << 0, + RequiresFirstStep = 1 << 1, + // 1 << 2 reserved (see comment above) + SpecialistOnly = 1 << 3, + NoBuffTick = 1 << 4, + NoConditionChange = 1 << 5, + RequiresExpedience = 1 << 6, + RequiresFullIQ = 1 << 7, + RequiresLowLevelRecipe = 1 << 8, + CosmicOnly = 1 << 9, +} diff --git a/Anvil/RecipeData/AnvilBuff.cs b/Anvil/RecipeData/AnvilBuff.cs new file mode 100644 index 0000000..d6c0524 --- /dev/null +++ b/Anvil/RecipeData/AnvilBuff.cs @@ -0,0 +1,67 @@ +// Static buff definition. The runtime "how many stacks, how many steps left" +// state lives in the simulator (02-CraftingSimulator). RecipeData only owns +// the immutable catalog entry: StatusId + Icon + StackMax + Behavior plus +// the three duration fields that are populated depending on Behavior. +// +// MaxStacks normalisation: the Status sheet encodes "single-active buff +// without a stack counter" as MaxStacks=0. Anvil normalises that to 1 so +// consumers do not have to distinguish two zero-states. + +namespace Anvil.RecipeData; + +public sealed record AnvilBuff +{ + public required AnvilBuffKind Kind { get; init; } + public required uint StatusId { get; init; } + public required string DisplayName { get; init; } + public required uint IconId { get; init; } + public required byte StackMax { get; init; } + public required AnvilBuffBehavior Behavior { get; init; } + public required byte? DefaultDurationSteps { get; init; } + public required float? DefaultDurationSeconds { get; init; } + public required byte? DefaultDurationActions { get; init; } + public required AnvilBuffCategory Category { get; init; } +} + +public enum AnvilBuffKind +{ + InnerQuiet, + WasteNot, + WasteNot2, + Veneration, + GreatStrides, + Innovation, + FinalAppraisal, + MuscleMemory, + Manipulation, + HeartAndSoul, + Expedience, + TrainedPerfection, + + // Cosmic Exploration (Patch 7.x) + MaterialMiracleBuff, + StellarSteadyHandBuff, +} + +public enum AnvilBuffBehavior +{ + StepCounter, + HybridStepOrUse, + UseConsumed, + StackBased, + InstantTrigger, + TimedSeconds, + ActionCounter, +} + +public enum AnvilBuffCategory +{ + QualityBooster, + ProgressBooster, + DurabilitySaver, + DurabilityRepair, + ConditionBypass, + Utility, + CosmicConditionShaper, + CosmicSuccessGuarantee, +} diff --git a/Anvil/RecipeData/AnvilCondition.cs b/Anvil/RecipeData/AnvilCondition.cs new file mode 100644 index 0000000..699594e --- /dev/null +++ b/Anvil/RecipeData/AnvilCondition.cs @@ -0,0 +1,35 @@ +// Static condition catalog. DisplayName comes from AnvilStrings.resx rather +// than Lumina because the in-game Status sheet does not expose the crafting +// condition labels (Centered / Sturdy / ...) in a directly consumable form. + +namespace Anvil.RecipeData; + +public sealed record AnvilCondition +{ + public required AnvilConditionKind Kind { get; init; } + public required string DisplayName { get; init; } + public required float QualityMultiplier { get; init; } + public required float ProgressMultiplier { get; init; } + public required float CpDiscountMultiplier { get; init; } + public required float DurabilityDiscountMultiplier { get; init; } + public required float BaseProbability { get; init; } +} + +// Robust is the Cosmic-Exploration condition variant. Mechanics follow the +// ConditionMechanicsTable: Sturdy-equivalent durability discount, no +// quality multiplier. The simulator maps Robust to Sturdy as the default +// follow-up condition (Artisan Simulator.cs:522). +public enum AnvilConditionKind +{ + Normal, + Good, + Excellent, + Poor, + Centered, + Sturdy, + Pliant, + Malleable, + Primed, + GoodOmen, + Robust, +} diff --git a/Anvil/RecipeData/AnvilFood.cs b/Anvil/RecipeData/AnvilFood.cs new file mode 100644 index 0000000..b41edca --- /dev/null +++ b/Anvil/RecipeData/AnvilFood.cs @@ -0,0 +1,46 @@ +// Food / medicine with crafting-relevant stat bonuses. The adapter filters +// ItemFood entries to the three crafting BaseParams (CP=11, Craftsmanship=70, +// Control=71) and drops everything else (Spiritbond, Reduced-Durability-Loss +// etc. - not solver-relevant). +// +// AnvilFoodBonus mirrors the Lumina ItemFood.Params shape 1:1 including the +// IsRelative flag so the simulator can tell percentage bonuses (typical for +// crafting consumables) from flat bonuses without re-walking the sheet. + +using System.Collections.Generic; + +namespace Anvil.RecipeData; + +public sealed record AnvilFood +{ + public required uint ItemId { get; init; } + public required string DisplayName { get; init; } + public required uint IconId { get; init; } + public required short DurationSeconds { get; init; } + public required AnvilFoodKind Kind { get; init; } + public required IReadOnlyList Bonuses { get; init; } + public required bool HasHqVariant { get; init; } +} + +public sealed record AnvilFoodBonus +{ + public required AnvilFoodStat Stat { get; init; } + public required short Value { get; init; } + public required short ValueHq { get; init; } + public required short Max { get; init; } + public required short MaxHq { get; init; } + public required bool IsRelative { get; init; } +} + +public enum AnvilFoodKind +{ + Food, + Medicine, +} + +public enum AnvilFoodStat +{ + Craftsmanship, + Control, + Cp, +} diff --git a/Anvil/RecipeData/AnvilItem.cs b/Anvil/RecipeData/AnvilItem.cs new file mode 100644 index 0000000..decf60e --- /dev/null +++ b/Anvil/RecipeData/AnvilItem.cs @@ -0,0 +1,17 @@ +// Slim item view. The catalog only holds items that appear as recipe output, +// recipe ingredients, or food/medicine entries. A full Item-sheet mirror +// would be ~50k rows and is the UI layer's problem when (later) a global +// item search lands. + +namespace Anvil.RecipeData; + +public sealed record AnvilItem +{ + public required uint ItemId { get; init; } + public required string DisplayName { get; init; } + public required uint IconId { get; init; } + public required byte ItemLevel { get; init; } + public required bool CanBeHq { get; init; } + public required bool IsCollectable { get; init; } + public required bool IsCraftable { get; init; } +} diff --git a/Anvil/RecipeData/AnvilRecipe.cs b/Anvil/RecipeData/AnvilRecipe.cs new file mode 100644 index 0000000..e6b6f16 --- /dev/null +++ b/Anvil/RecipeData/AnvilRecipe.cs @@ -0,0 +1,57 @@ +// Plain-data record describing a single FFXIV crafting recipe. Public surface +// is intentionally BCL-only (no Lumina or Dalamud types) so the simulator and +// solver can be tested in xUnit without loading Dalamud.dll into the test +// AppDomain. The adapter (Internal/LuminaRecipeAdapter) builds these from the +// Recipe + RecipeLevelTable sheet pair and flattens both sheets into one +// record so consumers never have to do a two-sheet walk. + +using System.Collections.Generic; + +namespace Anvil.RecipeData; + +public sealed record AnvilRecipe +{ + public required uint RecipeId { get; init; } + public required uint OutputItemId { get; init; } + public required byte OutputAmount { get; init; } + public required uint ClassJobId { get; init; } + public required ushort RecipeLevel { get; init; } + public required int Difficulty { get; init; } + public required int QualityMax { get; init; } + public required byte Durability { get; init; } + public required int RequiredCraftsmanship { get; init; } + public required int RequiredControl { get; init; } + public required int QualityForHQ { get; init; } + public required bool IsExpertRecipe { get; init; } + public required bool CanHQ { get; init; } + + // Cosmic Exploration (Patch 7.x). v0.1.0 ships with MissionHas* always + // false (SH-15 option B) - the data schema stays in place so v0.2.0+ can + // light up the surface by resolving the WKS mission sheet without + // touching the type. IsCosmic / IsSplendorCosmic are still set by the + // recipe-detection path so the catalog knows which recipes belong to the + // Cosmic surface even while the action flags stay dormant. + // + // Adapter invariant (validated when the catalog is built): + // IsSplendorCosmic == true implies IsCosmic == true + // MissionHasMaterialMiracle == true implies IsCosmic == true + // MissionHasSteadyHand == true implies IsCosmic == true + // + // Inconsistent recipe states (sub-flag without master flag) throw at + // catalog-build time instead of silently corrupting the simulator. + public required bool IsCosmic { get; init; } + public required bool IsSplendorCosmic { get; init; } + public required bool MissionHasMaterialMiracle { get; init; } + public required bool MissionHasSteadyHand { get; init; } + public required bool IsIshgardExpert { get; init; } + + public required byte Stars { get; init; } + public required byte ProgressDivider { get; init; } + public required byte ProgressModifier { get; init; } + public required byte QualityDivider { get; init; } + public required byte QualityModifier { get; init; } + public required IReadOnlyList Ingredients { get; init; } + public required string DisplayName { get; init; } +} + +public sealed record AnvilRecipeIngredient(uint ItemId, byte Amount);