feat(recipedata): add hardcoded mechanics tables

Three game-mechanics tables for the simulator-relevant constants that the
Lumina sheets do not expose, plus the reverse name map the adapter uses
to walk the CraftAction + Action sheets.

- ActionMechanicsTable: 38 entries with Category, CP cost, durability,
  efficiency, IQ-stack bonus, charges, granted buff, and flags.
  Cross-checked against the spec mechanics table in 01-RecipeData.md
  §2.3.2 (verified there against Artisan's RawInformation/Character/
  Skills.cs). Combo-state CP costs (Standard/Advanced Touch) carry the
  base value; the simulator applies the discount. TrainedEye uses
  short.MaxValue as the sentinel for "fill Recipe.QualityMax in one
  step"; ByregotsBlessing carries base EfficiencyQuality=100 with the
  IQ multiplier added by the simulator.
- BuffMechanicsTable: 14 entries with StatusId, Icon, StackMax, Behavior,
  the three duration fields (Steps / Seconds / Actions populated per
  Behavior), Category, and LegacyStatusId for the older Innovation 259
  and Manipulation 258 ids. Cross-checked against ffxiv-datamining
  Status.csv (StatusId / Icon / StackMax) and Artisan's tooltip-derived
  step durations. Expedience uses the explicit StatusId 3812 - the other
  two "Expedience" rows (2712 / 3092) are non-crafting status effects
  that would otherwise produce an ambiguous name match.
- ConditionMechanicsTable: 11 entries with Quality / Progress / CP /
  Durability multipliers plus a BaseProbability slot. Robust mirrors
  Sturdy's durability discount and keeps Quality neutral (per the
  spec mechanics table - the enum doc comment in AnvilCondition.cs
  predates rev 5 and is noted as superseded in the table header).
  SplendorCosmic's Good=1.75 override lives in the simulator.
  BaseProbability stays 0.0; the static catalog has no useful per-recipe
  spawn distribution because FFXIV derives that from Recipe.IsExpert +
  RecipeLevelTable.Stars at runtime.
- ActionKindByName: reverse map from English action names to
  AnvilActionKind, plus the Action-sheet whitelist (seven step-counter
  buff actions + two Cosmic singletons) used to filter the Action sheet
  walk down to the crafting subset.
This commit is contained in:
2026-05-27 20:25:52 +02:00
parent 47790a3f68
commit d7e8c42cc7
7 changed files with 1008 additions and 0 deletions
@@ -0,0 +1,94 @@
// Reverse map from FFXIV English action names to AnvilActionKind. Used by
// the adapter when walking CraftAction + Action sheets - the adapter looks
// each Name up here and skips (with a warning) anything that does not
// resolve. The map is keyed by the exact English string the game ships
// (Lumina ExtractText with the en-US client). Localised game strings are
// resolved through the catalog's DisplayName field, not here.
//
// Patch maintenance: if a future FFXIV patch renames an action or adds a
// new crafting action, this map and the AnvilActionKind enum need a
// parallel update. The adapter's "unknown action name" warning is the
// detection signal.
using System.Collections.Generic;
namespace Anvil.RecipeData.Mechanics;
internal static class ActionKindByName
{
public static IReadOnlyDictionary<string, AnvilActionKind> Map { get; } =
new Dictionary<string, AnvilActionKind>
{
// Progress
["Basic Synthesis"] = AnvilActionKind.BasicSynthesis,
["Careful Synthesis"] = AnvilActionKind.CarefulSynthesis,
["Rapid Synthesis"] = AnvilActionKind.RapidSynthesis,
["Intensive Synthesis"] = AnvilActionKind.IntensiveSynthesis,
["Prudent Synthesis"] = AnvilActionKind.PrudentSynthesis,
["Groundwork"] = AnvilActionKind.Groundwork,
["Muscle Memory"] = AnvilActionKind.MuscleMemory,
["Delicate Synthesis"] = AnvilActionKind.DelicateSynthesis,
// Quality
["Basic Touch"] = AnvilActionKind.BasicTouch,
["Standard Touch"] = AnvilActionKind.StandardTouch,
["Advanced Touch"] = AnvilActionKind.AdvancedTouch,
["Precise Touch"] = AnvilActionKind.PreciseTouch,
["Preparatory Touch"] = AnvilActionKind.PreparatoryTouch,
["Prudent Touch"] = AnvilActionKind.PrudentTouch,
["Refined Touch"] = AnvilActionKind.RefinedTouch,
["Byregot's Blessing"] = AnvilActionKind.ByregotsBlessing,
["Hasty Touch"] = AnvilActionKind.HastyTouch,
["Daring Touch"] = AnvilActionKind.DaringTouch,
["Trained Eye"] = AnvilActionKind.TrainedEye,
["Trained Finesse"] = AnvilActionKind.TrainedFinesse,
["Reflect"] = AnvilActionKind.Reflect,
["Tricks of the Trade"] = AnvilActionKind.TricksOfTheTrade,
// Buff
["Veneration"] = AnvilActionKind.Veneration,
["Innovation"] = AnvilActionKind.Innovation,
["Great Strides"] = AnvilActionKind.GreatStrides,
["Final Appraisal"] = AnvilActionKind.FinalAppraisal,
["Manipulation"] = AnvilActionKind.Manipulation,
["Waste Not"] = AnvilActionKind.WasteNot,
["Waste Not II"] = AnvilActionKind.WasteNot2,
// Repair
["Master's Mend"] = AnvilActionKind.MastersMend,
["Immaculate Mend"] = AnvilActionKind.ImmaculateMend,
// Observe / Specialist
["Observe"] = AnvilActionKind.Observe,
["Careful Observation"] = AnvilActionKind.CarefulObservation,
["Heart and Soul"] = AnvilActionKind.HeartAndSoul,
["Quick Innovation"] = AnvilActionKind.QuickInnovation,
["Trained Perfection"] = AnvilActionKind.TrainedPerfection,
// Cosmic Exploration
["Material Miracle"] = AnvilActionKind.MaterialMiracle,
["Stellar Steady Hand"] = AnvilActionKind.StellarSteadyHand,
};
// The whitelist of Action-sheet (vs CraftAction-sheet) names. Used by the
// adapter's Action-sheet walk: only rows whose Name matches one of these
// are considered for inclusion (the Action sheet otherwise carries
// thousands of combat / fishing / gathering rows that would pollute the
// catalog). Note that Action-sheet entries with PrimaryCostValue == 0
// (legacy ARR singletons) are filtered out separately.
public static IReadOnlySet<string> ActionSheetWhitelist { get; } =
new HashSet<string>
{
// Step-counter buff actions
"Veneration",
"Innovation",
"Manipulation",
"Waste Not",
"Waste Not II",
"Great Strides",
"Final Appraisal",
// Cosmic singletons
"Material Miracle",
"Stellar Steady Hand",
};
}
@@ -0,0 +1,30 @@
// Per-action mechanics row. Source of truth for everything the CraftAction
// and Action Lumina sheets do not carry: category, CP cost, durability,
// efficiency, IQ-stack bonus, charges, granted buff, and flags. The adapter
// merges this with the per-job sheet rows (DisplayName, IconId,
// LevelRequired, RowIdByClassJob, SpecialistOnly) to build an AnvilAction.
//
// CP cost lives in the table because the spec mechanics table is the
// curated, version-verified source. The sheet's Cost column matches in
// practice; if a patch drifts the two apart, the adapter logs the diff
// while keeping the table value (so the simulator stays deterministic
// against the spec-pinned numbers).
//
// Combo-state CP costs (StandardTouch is 18 CP after BasicTouch etc.) are
// not modelled here - the table holds the base value and the simulator
// applies the combo discount in its own scope.
namespace Anvil.RecipeData.Mechanics;
internal sealed record ActionMechanicsEntry
{
public required AnvilActionCategory Category { 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 byte IqStackBonus { get; init; }
public required byte ChargesPerCraft { get; init; }
public required AnvilBuffKind? GrantsBuff { get; init; }
public required AnvilActionFlags Flags { get; init; }
}
@@ -0,0 +1,526 @@
// Hardcoded action mechanics table. Values are FFXIV game data (Square
// Enix' domain) and are cross-checked against the spec mechanics table in
// 01-RecipeData.md §2.3.2 - which was itself verified against Artisan
// (RawInformation/Character/Skills.cs). The table is the source of truth
// for everything the simulator needs that the Lumina sheets do not carry:
// category, CP cost, durability, efficiency, IQ-stack bonus, charges,
// granted buff, and flags.
//
// Note on combo-state CP costs: StandardTouch and AdvancedTouch are listed
// at their base CP (32 / 46). The combo discount (StandardTouch costs 18
// after BasicTouch, AdvancedTouch costs 18 after StandardTouch) lives in
// the simulator, not in the data layer.
//
// Note on TrainedEye efficiency: short.MaxValue is the sentinel for
// "max-quality override" - the simulator recognises the Kind and applies
// Recipe.QualityMax in one step instead of using the efficiency arithmetic.
// Likewise ByregotsBlessing has base EfficiencyQuality=100; the simulator
// adds the 20% per IQ stack itself.
using System.Collections.Generic;
namespace Anvil.RecipeData.Mechanics;
internal static class ActionMechanicsTable
{
public static IReadOnlyDictionary<AnvilActionKind, ActionMechanicsEntry> Entries { get; } =
new Dictionary<AnvilActionKind, ActionMechanicsEntry>
{
// ---- Progress actions ----
[AnvilActionKind.BasicSynthesis] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.ProgressAction,
CpCost = 0,
DurabilityCost = 10,
EfficiencyProgress = 120,
EfficiencyQuality = 0,
IqStackBonus = 0,
ChargesPerCraft = 0,
GrantsBuff = null,
Flags = AnvilActionFlags.None,
},
[AnvilActionKind.CarefulSynthesis] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.ProgressAction,
CpCost = 7,
DurabilityCost = 10,
EfficiencyProgress = 180,
EfficiencyQuality = 0,
IqStackBonus = 0,
ChargesPerCraft = 0,
GrantsBuff = null,
Flags = AnvilActionFlags.None,
},
[AnvilActionKind.RapidSynthesis] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.ProgressAction,
CpCost = 0,
DurabilityCost = 10,
EfficiencyProgress = 500,
EfficiencyQuality = 0,
IqStackBonus = 0,
ChargesPerCraft = 0,
GrantsBuff = null,
// 50% base success rate lives in the simulator, not in flags.
Flags = AnvilActionFlags.None,
},
[AnvilActionKind.IntensiveSynthesis] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.ProgressAction,
CpCost = 6,
DurabilityCost = 10,
EfficiencyProgress = 400,
EfficiencyQuality = 0,
IqStackBonus = 0,
ChargesPerCraft = 0,
GrantsBuff = null,
Flags = AnvilActionFlags.RequiresGoodOrExcellent,
},
[AnvilActionKind.PrudentSynthesis] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.ProgressAction,
CpCost = 18,
DurabilityCost = 5,
EfficiencyProgress = 180,
EfficiencyQuality = 0,
IqStackBonus = 0,
ChargesPerCraft = 0,
GrantsBuff = null,
// WasteNot-lockout is a simulator rule (cannot use under WasteNot/2).
Flags = AnvilActionFlags.None,
},
[AnvilActionKind.Groundwork] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.ProgressAction,
CpCost = 18,
DurabilityCost = 20,
EfficiencyProgress = 360,
EfficiencyQuality = 0,
IqStackBonus = 0,
ChargesPerCraft = 0,
GrantsBuff = null,
// Halved efficiency when remaining durability < cost - simulator rule.
Flags = AnvilActionFlags.None,
},
[AnvilActionKind.MuscleMemory] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.ProgressAction,
CpCost = 6,
DurabilityCost = 10,
EfficiencyProgress = 300,
EfficiencyQuality = 0,
IqStackBonus = 0,
ChargesPerCraft = 0,
GrantsBuff = AnvilBuffKind.MuscleMemory,
Flags = AnvilActionFlags.RequiresFirstStep,
},
[AnvilActionKind.DelicateSynthesis] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.HybridProgressQuality,
CpCost = 32,
DurabilityCost = 10,
EfficiencyProgress = 100,
EfficiencyQuality = 100,
IqStackBonus = 0,
ChargesPerCraft = 0,
GrantsBuff = null,
Flags = AnvilActionFlags.None,
},
// ---- Quality actions ----
[AnvilActionKind.BasicTouch] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.QualityAction,
CpCost = 18,
DurabilityCost = 10,
EfficiencyProgress = 0,
EfficiencyQuality = 100,
IqStackBonus = 0,
ChargesPerCraft = 0,
GrantsBuff = null,
Flags = AnvilActionFlags.None,
},
[AnvilActionKind.StandardTouch] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.QualityAction,
// Base 32 CP; simulator drops to 18 after BasicTouch combo.
CpCost = 32,
DurabilityCost = 10,
EfficiencyProgress = 0,
EfficiencyQuality = 125,
IqStackBonus = 0,
ChargesPerCraft = 0,
GrantsBuff = null,
Flags = AnvilActionFlags.None,
},
[AnvilActionKind.AdvancedTouch] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.QualityAction,
// Base 46 CP; simulator drops to 18 after StandardTouch combo.
CpCost = 46,
DurabilityCost = 10,
EfficiencyProgress = 0,
EfficiencyQuality = 150,
IqStackBonus = 0,
ChargesPerCraft = 0,
GrantsBuff = null,
Flags = AnvilActionFlags.None,
},
[AnvilActionKind.PreciseTouch] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.QualityAction,
CpCost = 18,
DurabilityCost = 10,
EfficiencyProgress = 0,
EfficiencyQuality = 150,
IqStackBonus = 1,
ChargesPerCraft = 0,
GrantsBuff = null,
Flags = AnvilActionFlags.RequiresGoodOrExcellent,
},
[AnvilActionKind.PreparatoryTouch] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.QualityAction,
CpCost = 40,
DurabilityCost = 20,
EfficiencyProgress = 0,
EfficiencyQuality = 200,
IqStackBonus = 1,
ChargesPerCraft = 0,
GrantsBuff = null,
Flags = AnvilActionFlags.None,
},
[AnvilActionKind.PrudentTouch] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.QualityAction,
CpCost = 25,
DurabilityCost = 5,
EfficiencyProgress = 0,
EfficiencyQuality = 100,
IqStackBonus = 0,
ChargesPerCraft = 0,
GrantsBuff = null,
Flags = AnvilActionFlags.None,
},
[AnvilActionKind.RefinedTouch] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.QualityAction,
CpCost = 24,
DurabilityCost = 10,
EfficiencyProgress = 0,
EfficiencyQuality = 100,
// +1 only after BasicTouch combo; simulator gates the bonus.
IqStackBonus = 1,
ChargesPerCraft = 0,
GrantsBuff = null,
Flags = AnvilActionFlags.None,
},
[AnvilActionKind.ByregotsBlessing] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.QualityAction,
CpCost = 24,
DurabilityCost = 10,
EfficiencyProgress = 0,
// Base 100; simulator adds 20 per IQ stack and consumes them.
EfficiencyQuality = 100,
IqStackBonus = 0,
ChargesPerCraft = 0,
GrantsBuff = null,
Flags = AnvilActionFlags.None,
},
[AnvilActionKind.HastyTouch] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.QualityAction,
CpCost = 0,
DurabilityCost = 10,
EfficiencyProgress = 0,
EfficiencyQuality = 100,
IqStackBonus = 0,
ChargesPerCraft = 0,
GrantsBuff = null,
// 60% base success rate is a simulator rule, not a flag.
Flags = AnvilActionFlags.None,
},
[AnvilActionKind.DaringTouch] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.QualityAction,
CpCost = 0,
DurabilityCost = 10,
EfficiencyProgress = 0,
EfficiencyQuality = 150,
IqStackBonus = 0,
ChargesPerCraft = 0,
GrantsBuff = null,
Flags = AnvilActionFlags.RequiresExpedience,
},
[AnvilActionKind.TrainedEye] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.QualityAction,
CpCost = 250,
DurabilityCost = 10,
EfficiencyProgress = 0,
// Sentinel: simulator treats short.MaxValue as
// "fill Recipe.QualityMax in one step".
EfficiencyQuality = short.MaxValue,
IqStackBonus = 0,
ChargesPerCraft = 1,
GrantsBuff = null,
Flags = AnvilActionFlags.RequiresFirstStep | AnvilActionFlags.RequiresLowLevelRecipe,
},
[AnvilActionKind.TrainedFinesse] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.QualityAction,
CpCost = 32,
DurabilityCost = 0,
EfficiencyProgress = 0,
EfficiencyQuality = 100,
IqStackBonus = 0,
ChargesPerCraft = 0,
GrantsBuff = null,
Flags = AnvilActionFlags.RequiresFullIQ,
},
[AnvilActionKind.Reflect] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.QualityAction,
CpCost = 6,
DurabilityCost = 10,
EfficiencyProgress = 0,
EfficiencyQuality = 100,
IqStackBonus = 1,
ChargesPerCraft = 0,
GrantsBuff = null,
Flags = AnvilActionFlags.RequiresFirstStep,
},
[AnvilActionKind.TricksOfTheTrade] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.QualityAction,
CpCost = 0,
DurabilityCost = 0,
EfficiencyProgress = 0,
// 0 EffQuality; simulator returns 20 CP via the Kind-specific path.
EfficiencyQuality = 0,
IqStackBonus = 0,
ChargesPerCraft = 0,
GrantsBuff = null,
Flags = AnvilActionFlags.RequiresGoodOrExcellent,
},
// ---- Buff actions ----
[AnvilActionKind.Veneration] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.BuffAction,
CpCost = 18,
DurabilityCost = 0,
EfficiencyProgress = 0,
EfficiencyQuality = 0,
IqStackBonus = 0,
ChargesPerCraft = 0,
GrantsBuff = AnvilBuffKind.Veneration,
Flags = AnvilActionFlags.None,
},
[AnvilActionKind.Innovation] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.BuffAction,
CpCost = 18,
DurabilityCost = 0,
EfficiencyProgress = 0,
EfficiencyQuality = 0,
IqStackBonus = 0,
ChargesPerCraft = 0,
GrantsBuff = AnvilBuffKind.Innovation,
Flags = AnvilActionFlags.None,
},
[AnvilActionKind.GreatStrides] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.BuffAction,
CpCost = 32,
DurabilityCost = 0,
EfficiencyProgress = 0,
EfficiencyQuality = 0,
IqStackBonus = 0,
ChargesPerCraft = 0,
GrantsBuff = AnvilBuffKind.GreatStrides,
Flags = AnvilActionFlags.None,
},
[AnvilActionKind.FinalAppraisal] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.BuffAction,
CpCost = 1,
DurabilityCost = 0,
EfficiencyProgress = 0,
EfficiencyQuality = 0,
IqStackBonus = 0,
ChargesPerCraft = 0,
GrantsBuff = AnvilBuffKind.FinalAppraisal,
Flags = AnvilActionFlags.NoBuffTick | AnvilActionFlags.NoConditionChange,
},
[AnvilActionKind.Manipulation] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.BuffAction,
CpCost = 96,
DurabilityCost = 0,
EfficiencyProgress = 0,
EfficiencyQuality = 0,
IqStackBonus = 0,
ChargesPerCraft = 0,
GrantsBuff = AnvilBuffKind.Manipulation,
Flags = AnvilActionFlags.None,
},
[AnvilActionKind.WasteNot] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.BuffAction,
CpCost = 56,
DurabilityCost = 0,
EfficiencyProgress = 0,
EfficiencyQuality = 0,
IqStackBonus = 0,
ChargesPerCraft = 0,
GrantsBuff = AnvilBuffKind.WasteNot,
Flags = AnvilActionFlags.None,
},
[AnvilActionKind.WasteNot2] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.BuffAction,
CpCost = 98,
DurabilityCost = 0,
EfficiencyProgress = 0,
EfficiencyQuality = 0,
IqStackBonus = 0,
ChargesPerCraft = 0,
GrantsBuff = AnvilBuffKind.WasteNot2,
Flags = AnvilActionFlags.None,
},
// ---- Repair actions ----
[AnvilActionKind.MastersMend] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.RepairAction,
CpCost = 88,
DurabilityCost = 0,
EfficiencyProgress = 0,
EfficiencyQuality = 0,
IqStackBonus = 0,
ChargesPerCraft = 0,
GrantsBuff = null,
// +30 durability restoration lives in the simulator.
Flags = AnvilActionFlags.None,
},
[AnvilActionKind.ImmaculateMend] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.RepairAction,
CpCost = 112,
DurabilityCost = 0,
EfficiencyProgress = 0,
EfficiencyQuality = 0,
IqStackBonus = 0,
ChargesPerCraft = 0,
GrantsBuff = null,
// Full durability restoration lives in the simulator.
Flags = AnvilActionFlags.None,
},
// ---- Observe / Specialist ----
[AnvilActionKind.Observe] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.ObserveAction,
CpCost = 7,
DurabilityCost = 0,
EfficiencyProgress = 0,
EfficiencyQuality = 0,
IqStackBonus = 0,
ChargesPerCraft = 0,
GrantsBuff = null,
Flags = AnvilActionFlags.None,
},
[AnvilActionKind.CarefulObservation] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.ObserveAction,
CpCost = 0,
DurabilityCost = 0,
EfficiencyProgress = 0,
EfficiencyQuality = 0,
IqStackBonus = 0,
ChargesPerCraft = 3,
GrantsBuff = null,
Flags =
AnvilActionFlags.SpecialistOnly
| AnvilActionFlags.NoBuffTick
| AnvilActionFlags.NoConditionChange,
},
[AnvilActionKind.HeartAndSoul] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.ObserveAction,
CpCost = 0,
DurabilityCost = 0,
EfficiencyProgress = 0,
EfficiencyQuality = 0,
IqStackBonus = 0,
ChargesPerCraft = 1,
GrantsBuff = AnvilBuffKind.HeartAndSoul,
Flags =
AnvilActionFlags.SpecialistOnly
| AnvilActionFlags.NoBuffTick
| AnvilActionFlags.NoConditionChange,
},
[AnvilActionKind.QuickInnovation] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.BuffAction,
CpCost = 0,
DurabilityCost = 0,
EfficiencyProgress = 0,
EfficiencyQuality = 0,
IqStackBonus = 0,
ChargesPerCraft = 1,
// Grants the regular Innovation buff; simulator hardcodes the
// 1-step duration override on this trigger.
GrantsBuff = AnvilBuffKind.Innovation,
Flags =
AnvilActionFlags.SpecialistOnly
| AnvilActionFlags.NoBuffTick
| AnvilActionFlags.NoConditionChange,
},
[AnvilActionKind.TrainedPerfection] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.BuffAction,
CpCost = 0,
DurabilityCost = 0,
EfficiencyProgress = 0,
EfficiencyQuality = 0,
IqStackBonus = 0,
ChargesPerCraft = 1,
GrantsBuff = AnvilBuffKind.TrainedPerfection,
Flags = AnvilActionFlags.None,
},
// ---- Cosmic Exploration (v0.1.0 catalog-present, recipe-gated off) ----
[AnvilActionKind.MaterialMiracle] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.Cosmic,
CpCost = 1,
DurabilityCost = 0,
EfficiencyProgress = 0,
EfficiencyQuality = 0,
IqStackBonus = 0,
ChargesPerCraft = 1,
GrantsBuff = AnvilBuffKind.MaterialMiracleBuff,
Flags =
AnvilActionFlags.CosmicOnly
| AnvilActionFlags.NoBuffTick
| AnvilActionFlags.NoConditionChange,
},
[AnvilActionKind.StellarSteadyHand] = new ActionMechanicsEntry
{
Category = AnvilActionCategory.Cosmic,
CpCost = 1,
DurabilityCost = 0,
EfficiencyProgress = 0,
EfficiencyQuality = 0,
IqStackBonus = 0,
ChargesPerCraft = 2,
// 100% success-rate effect lives in the StellarSteadyHand buff
// via ActionCounter behavior - no Action-flag duplication.
GrantsBuff = AnvilBuffKind.StellarSteadyHandBuff,
Flags = AnvilActionFlags.CosmicOnly,
},
};
}
@@ -0,0 +1,30 @@
// Per-buff mechanics row. Carries the constants that the FFXIV Status sheet
// does not expose directly: the AnvilBuffBehavior class and the
// duration triple (steps / seconds / actions, populated according to
// Behavior). The Status sheet does deliver StatusId / Icon / StackMax;
// the table mirrors them so consumers and the adapter cross-checks share
// a single source.
//
// Step durations live in the table because FFXIV exposes them only in the
// Status tooltip text ("for the next four steps"). String-parsing the
// tooltip would be fragile across locales and patches.
namespace Anvil.RecipeData.Mechanics;
internal sealed record BuffMechanicsEntry
{
public required uint StatusId { 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; }
// Older Status-sheet RowIds that still resolve to the same Anvil buff
// (e.g. Innovation 259 -> 2189, Manipulation 258 -> 1164). Used by the
// adapter's reverse mapping so save-state restore from earlier patches
// still resolves correctly. Null when no legacy id exists.
public required uint? LegacyStatusId { get; init; }
}
@@ -0,0 +1,193 @@
// Hardcoded buff mechanics table. Values cross-checked against the spec
// mechanics table in 01-RecipeData.md §2.4 - which was itself verified
// against ffxiv-datamining Status.csv (StatusId, Icon, StackMax) and
// Artisan's tooltip-derived duration constants (Steps / Seconds / Actions).
//
// All step-counter durations come from the in-game tooltip text rather
// than a structured Status sheet column - that is why the table is the
// source of truth instead of a sheet read.
using System.Collections.Generic;
namespace Anvil.RecipeData.Mechanics;
internal static class BuffMechanicsTable
{
public static IReadOnlyDictionary<AnvilBuffKind, BuffMechanicsEntry> Entries { get; } =
new Dictionary<AnvilBuffKind, BuffMechanicsEntry>
{
[AnvilBuffKind.InnerQuiet] = new BuffMechanicsEntry
{
StatusId = 251,
IconId = 217321,
StackMax = 10,
Behavior = AnvilBuffBehavior.StackBased,
DefaultDurationSteps = null,
DefaultDurationSeconds = null,
DefaultDurationActions = null,
Category = AnvilBuffCategory.QualityBooster,
LegacyStatusId = null,
},
[AnvilBuffKind.WasteNot] = new BuffMechanicsEntry
{
StatusId = 252,
IconId = 211701,
StackMax = 1,
Behavior = AnvilBuffBehavior.StepCounter,
DefaultDurationSteps = 4,
DefaultDurationSeconds = null,
DefaultDurationActions = null,
Category = AnvilBuffCategory.DurabilitySaver,
LegacyStatusId = null,
},
[AnvilBuffKind.WasteNot2] = new BuffMechanicsEntry
{
StatusId = 257,
IconId = 211702,
StackMax = 1,
Behavior = AnvilBuffBehavior.StepCounter,
DefaultDurationSteps = 8,
DefaultDurationSeconds = null,
DefaultDurationActions = null,
Category = AnvilBuffCategory.DurabilitySaver,
LegacyStatusId = null,
},
[AnvilBuffKind.Veneration] = new BuffMechanicsEntry
{
StatusId = 2226,
IconId = 216126,
StackMax = 1,
Behavior = AnvilBuffBehavior.StepCounter,
DefaultDurationSteps = 4,
DefaultDurationSeconds = null,
DefaultDurationActions = null,
Category = AnvilBuffCategory.ProgressBooster,
LegacyStatusId = null,
},
[AnvilBuffKind.GreatStrides] = new BuffMechanicsEntry
{
StatusId = 254,
IconId = 216105,
StackMax = 1,
Behavior = AnvilBuffBehavior.HybridStepOrUse,
DefaultDurationSteps = 3,
DefaultDurationSeconds = null,
DefaultDurationActions = null,
Category = AnvilBuffCategory.QualityBooster,
LegacyStatusId = null,
},
[AnvilBuffKind.Innovation] = new BuffMechanicsEntry
{
StatusId = 2189,
IconId = 211652,
StackMax = 1,
Behavior = AnvilBuffBehavior.StepCounter,
DefaultDurationSteps = 4,
DefaultDurationSeconds = null,
DefaultDurationActions = null,
Category = AnvilBuffCategory.QualityBooster,
LegacyStatusId = 259,
},
[AnvilBuffKind.FinalAppraisal] = new BuffMechanicsEntry
{
StatusId = 2190,
IconId = 216124,
StackMax = 1,
Behavior = AnvilBuffBehavior.HybridStepOrUse,
DefaultDurationSteps = 5,
DefaultDurationSeconds = null,
DefaultDurationActions = null,
Category = AnvilBuffCategory.Utility,
LegacyStatusId = null,
},
[AnvilBuffKind.MuscleMemory] = new BuffMechanicsEntry
{
StatusId = 2191,
IconId = 216125,
StackMax = 1,
Behavior = AnvilBuffBehavior.HybridStepOrUse,
DefaultDurationSteps = 5,
DefaultDurationSeconds = null,
DefaultDurationActions = null,
Category = AnvilBuffCategory.ProgressBooster,
LegacyStatusId = null,
},
[AnvilBuffKind.Manipulation] = new BuffMechanicsEntry
{
StatusId = 1164,
IconId = 211651,
StackMax = 1,
Behavior = AnvilBuffBehavior.StepCounter,
DefaultDurationSteps = 8,
DefaultDurationSeconds = null,
DefaultDurationActions = null,
Category = AnvilBuffCategory.DurabilityRepair,
LegacyStatusId = 258,
},
[AnvilBuffKind.HeartAndSoul] = new BuffMechanicsEntry
{
StatusId = 2665,
IconId = 216127,
StackMax = 1,
Behavior = AnvilBuffBehavior.UseConsumed,
DefaultDurationSteps = null,
DefaultDurationSeconds = null,
DefaultDurationActions = null,
Category = AnvilBuffCategory.ConditionBypass,
LegacyStatusId = null,
},
[AnvilBuffKind.Expedience] = new BuffMechanicsEntry
{
// Two non-crafting Status rows also carry the name "Expedience"
// (RowIds 2712 / 3092). Reverse mapping must use the explicit
// StatusId, not a name lookup, to avoid ambiguity.
StatusId = 3812,
IconId = 216128,
StackMax = 1,
Behavior = AnvilBuffBehavior.InstantTrigger,
DefaultDurationSteps = null,
DefaultDurationSeconds = null,
DefaultDurationActions = null,
Category = AnvilBuffCategory.Utility,
LegacyStatusId = null,
},
[AnvilBuffKind.TrainedPerfection] = new BuffMechanicsEntry
{
StatusId = 3813,
IconId = 216129,
StackMax = 1,
Behavior = AnvilBuffBehavior.UseConsumed,
DefaultDurationSteps = null,
DefaultDurationSeconds = null,
DefaultDurationActions = null,
Category = AnvilBuffCategory.Utility,
LegacyStatusId = null,
},
// ---- Cosmic Exploration (Patch 7.x; catalog-present in v0.1.0) ----
[AnvilBuffKind.MaterialMiracleBuff] = new BuffMechanicsEntry
{
StatusId = 4220,
IconId = 216254,
StackMax = 1,
Behavior = AnvilBuffBehavior.TimedSeconds,
DefaultDurationSteps = null,
DefaultDurationSeconds = 45.0f,
DefaultDurationActions = null,
Category = AnvilBuffCategory.CosmicConditionShaper,
LegacyStatusId = null,
},
[AnvilBuffKind.StellarSteadyHandBuff] = new BuffMechanicsEntry
{
StatusId = 4839,
IconId = 211551,
StackMax = 1,
Behavior = AnvilBuffBehavior.ActionCounter,
DefaultDurationSteps = null,
DefaultDurationSeconds = null,
DefaultDurationActions = 3,
Category = AnvilBuffCategory.CosmicSuccessGuarantee,
LegacyStatusId = null,
},
};
}
@@ -0,0 +1,16 @@
// Per-condition mechanics row. Mirrors AnvilCondition's stat multipliers and
// the per-step base probability. BaseProbability defaults to 0.0 in the
// table because FFXIV computes the spawn distribution from Recipe.IsExpert
// and RecipeLevelTable.Stars at simulator runtime - the static catalog has
// no useful per-recipe value to expose.
namespace Anvil.RecipeData.Mechanics;
internal sealed record ConditionMechanicsEntry
{
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; }
}
@@ -0,0 +1,119 @@
// Hardcoded condition mechanics table. Values follow the spec mechanics
// table in 01-RecipeData.md §2.5, which was itself verified against
// Artisan's Simulator.cs.
//
// Robust is the Cosmic-Exploration condition variant. Its mechanics follow
// the spec mechanics table (Quality-Mult = 1.0, Durability-Discount = 0.5,
// no other stat changes) and Artisan Simulator.cs:430. The Sturdy follow-up
// mapping that Artisan applies belongs to the simulator, not the catalog.
// (Note: the enum doc comment in AnvilCondition.cs mentions a "+50% quality"
// behaviour for Robust - that wording predates the verified mechanics table
// and the values below are the source of truth.)
//
// SplendorCosmic raises Good's quality multiplier from 1.5 to 1.75 on
// recipes flagged IsSplendorCosmic. The override lives in the simulator;
// the table holds the non-Splendor default.
using System.Collections.Generic;
namespace Anvil.RecipeData.Mechanics;
internal static class ConditionMechanicsTable
{
public static IReadOnlyDictionary<AnvilConditionKind, ConditionMechanicsEntry> Entries { get; } =
new Dictionary<AnvilConditionKind, ConditionMechanicsEntry>
{
[AnvilConditionKind.Normal] = new ConditionMechanicsEntry
{
QualityMultiplier = 1.0f,
ProgressMultiplier = 1.0f,
CpDiscountMultiplier = 1.0f,
DurabilityDiscountMultiplier = 1.0f,
BaseProbability = 0.0f,
},
[AnvilConditionKind.Good] = new ConditionMechanicsEntry
{
QualityMultiplier = 1.5f,
ProgressMultiplier = 1.0f,
CpDiscountMultiplier = 1.0f,
DurabilityDiscountMultiplier = 1.0f,
BaseProbability = 0.0f,
},
[AnvilConditionKind.Excellent] = new ConditionMechanicsEntry
{
QualityMultiplier = 4.0f,
ProgressMultiplier = 1.0f,
CpDiscountMultiplier = 1.0f,
DurabilityDiscountMultiplier = 1.0f,
BaseProbability = 0.0f,
},
[AnvilConditionKind.Poor] = new ConditionMechanicsEntry
{
QualityMultiplier = 0.5f,
ProgressMultiplier = 1.0f,
CpDiscountMultiplier = 1.0f,
DurabilityDiscountMultiplier = 1.0f,
BaseProbability = 0.0f,
},
[AnvilConditionKind.Centered] = new ConditionMechanicsEntry
{
QualityMultiplier = 1.0f,
ProgressMultiplier = 1.0f,
CpDiscountMultiplier = 1.0f,
DurabilityDiscountMultiplier = 1.0f,
// +25% success rate is a simulator rule, not a stat multiplier.
BaseProbability = 0.0f,
},
[AnvilConditionKind.Sturdy] = new ConditionMechanicsEntry
{
QualityMultiplier = 1.0f,
ProgressMultiplier = 1.0f,
CpDiscountMultiplier = 1.0f,
DurabilityDiscountMultiplier = 0.5f,
BaseProbability = 0.0f,
},
[AnvilConditionKind.Pliant] = new ConditionMechanicsEntry
{
QualityMultiplier = 1.0f,
ProgressMultiplier = 1.0f,
CpDiscountMultiplier = 0.5f,
DurabilityDiscountMultiplier = 1.0f,
BaseProbability = 0.0f,
},
[AnvilConditionKind.Malleable] = new ConditionMechanicsEntry
{
QualityMultiplier = 1.0f,
ProgressMultiplier = 1.5f,
CpDiscountMultiplier = 1.0f,
DurabilityDiscountMultiplier = 1.0f,
BaseProbability = 0.0f,
},
[AnvilConditionKind.Primed] = new ConditionMechanicsEntry
{
QualityMultiplier = 1.0f,
ProgressMultiplier = 1.0f,
CpDiscountMultiplier = 1.0f,
DurabilityDiscountMultiplier = 1.0f,
// +2 step duration on newly-applied buffs is a simulator rule.
BaseProbability = 0.0f,
},
[AnvilConditionKind.GoodOmen] = new ConditionMechanicsEntry
{
QualityMultiplier = 1.0f,
ProgressMultiplier = 1.0f,
CpDiscountMultiplier = 1.0f,
DurabilityDiscountMultiplier = 1.0f,
// Forces Good as the next condition - simulator rule.
BaseProbability = 0.0f,
},
[AnvilConditionKind.Robust] = new ConditionMechanicsEntry
{
// Quality stays neutral; only durability is discounted.
QualityMultiplier = 1.0f,
ProgressMultiplier = 1.0f,
CpDiscountMultiplier = 1.0f,
DurabilityDiscountMultiplier = 0.5f,
BaseProbability = 0.0f,
},
};
}