feat(recipedata): add plain-data records and enums

Module 01 public-API surface: six sealed records with init-only
properties plus their nine enums. All BCL-only - no Lumina or Dalamud
types in any public property, which is what keeps the simulator and
catalog xUnit-testable per the Critical Boundary in 00-Anvil-Scope §3.3.

- AnvilRecipe + AnvilRecipeIngredient: flat representation of the
  Recipe + RecipeLevelTable sheet pair with the five Cosmic-Exploration
  flags. v0.1.0 ships with MissionHas* always false (SH-15 option B);
  the schema stays in place so v0.2.0 can wire the WKS mission sheet
  without touching the type.
- AnvilItem: slim per-recipe / per-food item view (full ~50k-row item
  mirror is left to the UI layer).
- AnvilAction + AnvilActionKind + AnvilActionCategory + AnvilActionFlags:
  one logical action per Kind value with RowIdByClassJob mapping; Cosmic
  actions use ClassJobId 0 as the sentinel for "every crafter".
  AnvilActionFlags bit 1 << 2 is intentionally vacant (ConsumesGreatStrides
  was removed in spec rev 5; consumption logic lives in 02-CraftingSimulator).
- AnvilBuff + AnvilBuffKind + AnvilBuffBehavior + AnvilBuffCategory:
  static buff catalog entry with the duration field that matches Behavior
  (Steps / Seconds / Actions). Two Cosmic buffs (MaterialMiracleBuff,
  StellarSteadyHandBuff).
- AnvilCondition + AnvilConditionKind: eleven conditions including the
  Cosmic Robust variant. DisplayName comes from AnvilStrings.resx, not
  Lumina (the Status sheet does not expose the crafting condition labels
  cleanly).
- AnvilFood + AnvilFoodBonus + AnvilFoodKind + AnvilFoodStat: ItemFood
  mirror with IsRelative flag (percentage vs flat bonus).
This commit is contained in:
2026-05-27 19:53:54 +02:00
parent 96553a849a
commit 47790a3f68
6 changed files with 336 additions and 0 deletions
+114
View File
@@ -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<uint, uint> 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,
}
+67
View File
@@ -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,
}
+35
View File
@@ -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,
}
+46
View File
@@ -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<AnvilFoodBonus> 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,
}
+17
View File
@@ -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; }
}
+57
View File
@@ -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<AnvilRecipeIngredient> Ingredients { get; init; }
public required string DisplayName { get; init; }
}
public sealed record AnvilRecipeIngredient(uint ItemId, byte Amount);