diff --git a/documentation/DocumentationTest.ShellDocumentation.approved.md b/documentation/DocumentationTest.ShellDocumentation.approved.md
index 2bb0e6e..c26edd0 100644
--- a/documentation/DocumentationTest.ShellDocumentation.approved.md
+++ b/documentation/DocumentationTest.ShellDocumentation.approved.md
@@ -209,3 +209,30 @@
- Level 70: 8 9 10 11 12 13 14
- Level 80: 9 10 11 12 13 14 15
- Level 90: 10 11 12 13 14 15 16
+
+## Perfume cost (rarity 0 to 8, then gold)
+- Level 0: 0 0 0 0 0 0 0 0 0 - 0 gold
+- Level 1: 0 0 0 0 0 0 0 0 0 - 1000 gold
+- Level 10: 0 0 0 0 0 0 0 0 0 - 1000 gold
+- Level 11: 0 0 0 0 0 0 0 0 0 - 2000 gold
+- Level 20: 0 0 0 0 0 0 0 0 0 - 2000 gold
+- Level 21: 0 0 0 0 5 5 5 10 0 - 5000 gold
+- Level 39: 0 0 0 0 5 5 5 10 0 - 5000 gold
+- Level 40: 0 0 0 5 5 10 20 40 0 - 8000 gold
+- Level 49: 0 0 0 5 5 10 20 40 0 - 8000 gold
+- Level 50: 0 0 5 10 10 20 40 80 0 - 10000 gold
+- Level 59: 0 0 5 10 10 20 40 80 0 - 10000 gold
+- Level 60: 0 5 10 10 20 40 80 120 0 - 12500 gold
+- Level 69: 0 5 10 10 20 40 80 120 0 - 12500 gold
+- Level 70: 0 10 10 20 40 80 120 160 0 - 15000 gold
+- Level 79: 0 10 10 20 40 80 120 160 0 - 15000 gold
+- Level 80: 0 10 20 40 80 120 160 200 0 - 17500 gold
+- Level 89: 0 10 20 40 80 120 160 200 0 - 17500 gold
+- Level 90: 0 20 40 80 120 160 200 300 0 - 20000 gold
+- Level 99: 0 20 40 80 120 160 200 300 0 - 20000 gold
+- Level 100: 0 0 0 0 0 0 0 0 0 - 0 gold
+
+## Heroic perfume cost (rarity 0 to 8)
+- Level 1: 1 41 81 121 161 201 241 281 321
+- Level 50: 50 90 130 170 210 250 290 330 370
+- Level 99: 99 139 179 219 259 299 339 379 419
diff --git a/src/NosCore.Algorithm/ShellService/IShellService.cs b/src/NosCore.Algorithm/ShellService/IShellService.cs
index 95e61f7..557928c 100644
--- a/src/NosCore.Algorithm/ShellService/IShellService.cs
+++ b/src/NosCore.Algorithm/ShellService/IShellService.cs
@@ -51,5 +51,21 @@ public interface IShellService
/// The shell rarity
/// The pearl count
int GetIdentificationCost(byte shellLevel, byte rarity);
+
+ ///
+ /// Gets how many bottles of Perfume taking over an item bound to another player costs
+ ///
+ /// The item's level
+ /// The item's rarity, as the client holds it (a negative rarity wraps above 8)
+ /// Whether the item is a heroic item
+ /// The perfume count, 0 for a level or rarity the client gives no count
+ int GetPerfumeCost(byte itemLevel, byte rarity, bool heroic);
+
+ ///
+ /// Gets the gold that taking over an item bound to another player costs on top of the Perfume
+ ///
+ /// The item's level
+ /// The gold, 0 for level 0 and from level 100
+ int GetPerfumeGoldCost(byte itemLevel);
}
}
diff --git a/src/NosCore.Algorithm/ShellService/ShellService.cs b/src/NosCore.Algorithm/ShellService/ShellService.cs
index d5d36de..e26b7bc 100644
--- a/src/NosCore.Algorithm/ShellService/ShellService.cs
+++ b/src/NosCore.Algorithm/ShellService/ShellService.cs
@@ -144,5 +144,47 @@ public int GetIdentificationCost(byte shellLevel, byte rarity)
{
return shellLevel / 10 + rarity;
}
+
+ ///
+ public int GetPerfumeCost(byte itemLevel, byte rarity, bool heroic)
+ {
+ if (heroic)
+ {
+ return itemLevel + HeroicPerfumePerRarity * rarity;
+ }
+
+ var bracket = Array.FindLastIndex(PerfumeLevels, l => l <= itemLevel);
+ return bracket < 0 || rarity >= PerfumeByRarity[bracket].Length ? 0 : PerfumeByRarity[bracket][rarity];
+ }
+
+ ///
+ public int GetPerfumeGoldCost(byte itemLevel)
+ {
+ var bracket = Array.FindLastIndex(PerfumeLevels, l => l <= itemLevel);
+ return bracket < 0 ? 0 : PerfumeGold[bracket];
+ }
+
+ private const int HeroicPerfumePerRarity = 40;
+
+ // The client's perfume window computes both costs itself. Each entry is the first item level
+ // of a bracket; level 100 and above costs nothing, like level 0.
+ private static readonly byte[] PerfumeLevels = { 1, 11, 21, 40, 50, 60, 70, 80, 90, 100 };
+
+ private static readonly int[] PerfumeGold = { 1000, 2000, 5000, 8000, 10000, 12500, 15000, 17500, 20000, 0 };
+
+ // By rarity 0 to 7; rarity 8 and above costs no perfume.
+ private static readonly int[][] PerfumeByRarity =
+ {
+ new[] { 0, 0, 0, 0, 0, 0, 0, 0 },
+ new[] { 0, 0, 0, 0, 0, 0, 0, 0 },
+ new[] { 0, 0, 0, 0, 5, 5, 5, 10 },
+ new[] { 0, 0, 0, 5, 5, 10, 20, 40 },
+ new[] { 0, 0, 5, 10, 10, 20, 40, 80 },
+ new[] { 0, 5, 10, 10, 20, 40, 80, 120 },
+ new[] { 0, 10, 10, 20, 40, 80, 120, 160 },
+ new[] { 0, 10, 20, 40, 80, 120, 160, 200 },
+ new[] { 0, 20, 40, 80, 120, 160, 200, 300 },
+ Array.Empty(),
+ };
}
}
diff --git a/test/NosCore.Algorithm.Tests/DocumentationTest.cs b/test/NosCore.Algorithm.Tests/DocumentationTest.cs
index 2766a1d..bb14111 100644
--- a/test/NosCore.Algorithm.Tests/DocumentationTest.cs
+++ b/test/NosCore.Algorithm.Tests/DocumentationTest.cs
@@ -563,6 +563,20 @@ public void ShellDocumentation()
resultBuilder.AppendLine($"- Level {level,2}: {string.Join(" ", Enumerable.Range(1, 7).Select(r => shellService.GetIdentificationCost(level, (byte)r)))}");
}
+ resultBuilder.AppendLine();
+ resultBuilder.AppendLine("## Perfume cost (rarity 0 to 8, then gold)");
+ foreach (byte level in new byte[] { 0, 1, 10, 11, 20, 21, 39, 40, 49, 50, 59, 60, 69, 70, 79, 80, 89, 90, 99, 100 })
+ {
+ resultBuilder.AppendLine($"- Level {level,3}: {string.Join(" ", Enumerable.Range(0, 9).Select(r => shellService.GetPerfumeCost(level, (byte)r, false)))} - {shellService.GetPerfumeGoldCost(level)} gold");
+ }
+
+ resultBuilder.AppendLine();
+ resultBuilder.AppendLine("## Heroic perfume cost (rarity 0 to 8)");
+ foreach (byte level in new byte[] { 1, 50, 99 })
+ {
+ resultBuilder.AppendLine($"- Level {level,2}: {string.Join(" ", Enumerable.Range(0, 9).Select(r => shellService.GetPerfumeCost(level, (byte)r, true)))}");
+ }
+
Approvals.Verify(WriterFactory.CreateTextWriter(resultBuilder.ToString(), "md"));
}