diff --git a/Anvil/RecipeData/Mechanics/ActionKindByName.cs b/Anvil/RecipeData/Mechanics/ActionKindByName.cs new file mode 100644 index 0000000..3f7d302 --- /dev/null +++ b/Anvil/RecipeData/Mechanics/ActionKindByName.cs @@ -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 Map { get; } = + new Dictionary + { + // 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 ActionSheetWhitelist { get; } = + new HashSet + { + // Step-counter buff actions + "Veneration", + "Innovation", + "Manipulation", + "Waste Not", + "Waste Not II", + "Great Strides", + "Final Appraisal", + // Cosmic singletons + "Material Miracle", + "Stellar Steady Hand", + }; +} diff --git a/Anvil/RecipeData/Mechanics/ActionMechanicsEntry.cs b/Anvil/RecipeData/Mechanics/ActionMechanicsEntry.cs new file mode 100644 index 0000000..74f960f --- /dev/null +++ b/Anvil/RecipeData/Mechanics/ActionMechanicsEntry.cs @@ -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; } +} diff --git a/Anvil/RecipeData/Mechanics/ActionMechanicsTable.cs b/Anvil/RecipeData/Mechanics/ActionMechanicsTable.cs new file mode 100644 index 0000000..b75d862 --- /dev/null +++ b/Anvil/RecipeData/Mechanics/ActionMechanicsTable.cs @@ -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 Entries { get; } = + new Dictionary + { + // ---- 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, + }, + }; +} diff --git a/Anvil/RecipeData/Mechanics/BuffMechanicsEntry.cs b/Anvil/RecipeData/Mechanics/BuffMechanicsEntry.cs new file mode 100644 index 0000000..1476a8e --- /dev/null +++ b/Anvil/RecipeData/Mechanics/BuffMechanicsEntry.cs @@ -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; } +} diff --git a/Anvil/RecipeData/Mechanics/BuffMechanicsTable.cs b/Anvil/RecipeData/Mechanics/BuffMechanicsTable.cs new file mode 100644 index 0000000..197dd00 --- /dev/null +++ b/Anvil/RecipeData/Mechanics/BuffMechanicsTable.cs @@ -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 Entries { get; } = + new Dictionary + { + [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, + }, + }; +} diff --git a/Anvil/RecipeData/Mechanics/ConditionMechanicsEntry.cs b/Anvil/RecipeData/Mechanics/ConditionMechanicsEntry.cs new file mode 100644 index 0000000..51f6fc5 --- /dev/null +++ b/Anvil/RecipeData/Mechanics/ConditionMechanicsEntry.cs @@ -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; } +} diff --git a/Anvil/RecipeData/Mechanics/ConditionMechanicsTable.cs b/Anvil/RecipeData/Mechanics/ConditionMechanicsTable.cs new file mode 100644 index 0000000..c466b10 --- /dev/null +++ b/Anvil/RecipeData/Mechanics/ConditionMechanicsTable.cs @@ -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 Entries { get; } = + new Dictionary + { + [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, + }, + }; +}