From ca30a973901582aab4f1dd23fdcdb731212d339c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 14 Nov 2026 17:56:45 +0000 Subject: [PATCH] feat(items): add the gladiator's honor Adds the Gladiator's Honor (13/95) of issue #1339. Like the game client describes it, it can only be used in Vulcanus and increases the experience there by 20 %. Its effect adds the new Stats.VulcanusExperienceRate, which the new VulcanusExperiencePlugIn applies while the player is in Vulcanus. The map is configurable. The client doesn't define its duration, so it lasts like a seal (30 minutes). The higher drop rate of rare items is left out. Rebased onto master: this is the change of the earlier commits in one. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011xNc76CH3CGWUp5r4XmXWK --- .../docs/server-features/cash-shop.md | 7 + src/GameLogic/Attributes/Stats.cs | 6 + src/GameLogic/ItemConstants.cs | 5 + .../GladiatorsHonorConsumeHandlerPlugIn.cs | 45 +++++++ .../PlugIns/VulcanusExperiencePlugIn.cs | 38 ++++++ .../PlugIns/VulcanusMapConfiguration.cs | 22 ++++ .../Properties/PlayerMessage.Designer.cs | 9 ++ .../Properties/PlayerMessage.de.resx | 3 + .../Properties/PlayerMessage.pt.resx | 3 + src/GameLogic/Properties/PlayerMessage.resx | 3 + .../Properties/PlayerMessage.zh-CN.resx | 3 + .../Properties/PlugInResources.Designer.cs | 45 +++++++ .../Properties/PlugInResources.de.resx | 15 +++ src/GameLogic/Properties/PlugInResources.resx | 15 +++ .../Properties/PlugInResources.zh-CN.resx | 15 +++ .../Properties/ItemNames.Designer.cs | 7 + .../Properties/ItemNames.de.resx | 3 + .../Initialization/Properties/ItemNames.resx | 3 + .../Properties/ItemNames.zh-CN.resx | 3 + .../Properties/PlugInResources.Designer.cs | 6 + .../Properties/PlugInResources.de.resx | 6 + .../Properties/PlugInResources.resx | 6 + .../Properties/PlugInResources.zh-CN.resx | 6 + .../Skills/MagicEffectNumber.cs | 5 + .../Updates/AddGladiatorsHonorUpdatePlugIn.cs | 49 +++++++ .../VersionSeasonSix/Items/BuffItems.cs | 13 ++ .../VersionSeasonSix/Items/ItemRules.cs | 1 + .../BuffItemsDataTest.cs | 3 +- .../GladiatorsHonorDataTest.cs | 59 +++++++++ .../ItemStatProfilesDataTest.cs | 2 +- .../GladiatorsHonorTest.cs | 124 ++++++++++++++++++ 31 files changed, 528 insertions(+), 2 deletions(-) create mode 100644 src/GameLogic/PlayerActions/ItemConsumeActions/GladiatorsHonorConsumeHandlerPlugIn.cs create mode 100644 src/GameLogic/PlugIns/VulcanusExperiencePlugIn.cs create mode 100644 src/GameLogic/PlugIns/VulcanusMapConfiguration.cs create mode 100644 src/Persistence/Initialization/Updates/AddGladiatorsHonorUpdatePlugIn.cs create mode 100644 tests/MUnique.OpenMU.Persistence.Initialization.Tests/GladiatorsHonorDataTest.cs create mode 100644 tests/MUnique.OpenMU.Tests/GladiatorsHonorTest.cs diff --git a/docs-website/docs/server-features/cash-shop.md b/docs-website/docs/server-features/cash-shop.md index 3539911e0a..e8654081d5 100644 --- a/docs-website/docs/server-features/cash-shop.md +++ b/docs-website/docs/server-features/cash-shop.md @@ -284,6 +284,7 @@ buff icons are the ones which the game client shows for these items: | Master Seal of Ascension | master experience ×1.5 | 30 minutes | | Master Seal of Wealth | master experience ×2, item drop rate ×1.5 | 30 minutes | | Seal of Wealth (13/116) | item drop rate ×1.5 | 30 minutes | +| Gladiator's Honor | experience +20% in Vulcanus | 30 minutes | | Max AG Boost Aura | maximum AG + character level | 30 minutes | | Max SD Boost Aura | maximum SD + character level × 10 | 30 minutes | | Scroll of Quickness | attack speed +15 | 30 minutes | @@ -310,6 +311,12 @@ drops stays the same. Existing databases get the item drop rate with the configuration update *Add the item drop rate of seals*, which also adds the second Seal of Wealth (13/116). It isn't sold in the in-game shop. +The Gladiator's Honor can only be used in Vulcanus, and its experience bonus +only counts there. The game client doesn't tell its duration, so it lasts as +long as a seal. The map is set in the configuration of the plugins "Gladiator's +Honor consume handler" and "Vulcanus experience bonus". Its higher drop rate +of rare items isn't implemented yet. + :::note Not implemented yet: the Seal of Mobility and the Party EXP Scroll. ::: diff --git a/src/GameLogic/Attributes/Stats.cs b/src/GameLogic/Attributes/Stats.cs index 51863ffb76..27fc839925 100644 --- a/src/GameLogic/Attributes/Stats.cs +++ b/src/GameLogic/Attributes/Stats.cs @@ -127,6 +127,12 @@ public class Stats /// So far includes skeleton xfm ring, pet panda, pet skeleton bonus. public static AttributeDefinition BonusExperienceRate { get; } = new(new Guid("D48A9D05-533A-4556-9FC3-F71C8BEC4B8E"), "Bonus Experience Rate", "The bonus experience rate which is added to the base experience rate multiplier."); + /// + /// Gets the experience rate which is added while the player is in Vulcanus, e.g. by the Gladiator's Honor. + /// + /// The experience is multiplied by one plus this rate by the VulcanusExperiencePlugIn. + public static AttributeDefinition VulcanusExperienceRate { get; } = new(new Guid("199015AC-3139-4E6B-A7C0-5439DD4F7052"), "Vulcanus Experience Rate", "The experience rate which is added while the player is in Vulcanus."); + /// /// Gets the master level definition. /// diff --git a/src/GameLogic/ItemConstants.cs b/src/GameLogic/ItemConstants.cs index 3783b2339e..cd4ddd5a78 100644 --- a/src/GameLogic/ItemConstants.cs +++ b/src/GameLogic/ItemConstants.cs @@ -84,6 +84,11 @@ public class ItemConstants /// public static ItemIdentifier InvitationToSantaVillage => new(66, 13); + /// + /// Gets the identifier for the Gladiator's Honor, which increases the experience in Vulcanus. + /// + public static ItemIdentifier GladiatorsHonor => new(95, 13); + /// /// Gets the identifier for the demon. /// diff --git a/src/GameLogic/PlayerActions/ItemConsumeActions/GladiatorsHonorConsumeHandlerPlugIn.cs b/src/GameLogic/PlayerActions/ItemConsumeActions/GladiatorsHonorConsumeHandlerPlugIn.cs new file mode 100644 index 0000000000..4e7c4292b9 --- /dev/null +++ b/src/GameLogic/PlayerActions/ItemConsumeActions/GladiatorsHonorConsumeHandlerPlugIn.cs @@ -0,0 +1,45 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions; + +using System.Runtime.InteropServices; +using MUnique.OpenMU.GameLogic.PlugIns; +using MUnique.OpenMU.PlugIns; + +/// +/// The consume handler for the Gladiator's Honor, which applies its effect only in Vulcanus, +/// like the game client describes it. +/// +/// +/// The experience bonus of its effect is applied by the , +/// so it only counts while the player stays in Vulcanus. +/// +[PlugIn] +[Display(Name = nameof(PlugInResources.GladiatorsHonorConsumeHandlerPlugIn_Name), Description = nameof(PlugInResources.GladiatorsHonorConsumeHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))] +[Guid("FBB2F1AB-8097-41B0-A10E-8323C10D053A")] +public class GladiatorsHonorConsumeHandlerPlugIn : ApplyMagicEffectConsumeHandlerPlugIn, ISupportCustomConfiguration, ISupportDefaultCustomConfiguration +{ + /// + public VulcanusMapConfiguration? Configuration { get; set; } + + /// + public override ItemIdentifier Key => ItemConstants.GladiatorsHonor; + + /// + public override async ValueTask ConsumeItemAsync(Player player, Item item, Item? targetItem, FruitUsage fruitUsage) + { + var mapNumber = (this.Configuration ??= new VulcanusMapConfiguration()).MapNumber; + if (player.CurrentMap?.MapId != mapNumber) + { + await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.ItemCantBeUsedOnThisMap)).ConfigureAwait(false); + return false; + } + + return await base.ConsumeItemAsync(player, item, targetItem, fruitUsage).ConfigureAwait(false); + } + + /// + public object CreateDefaultConfig() => new VulcanusMapConfiguration(); +} diff --git a/src/GameLogic/PlugIns/VulcanusExperiencePlugIn.cs b/src/GameLogic/PlugIns/VulcanusExperiencePlugIn.cs new file mode 100644 index 0000000000..2e5e495193 --- /dev/null +++ b/src/GameLogic/PlugIns/VulcanusExperiencePlugIn.cs @@ -0,0 +1,38 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.PlugIns; + +using System.Runtime.InteropServices; +using MUnique.OpenMU.GameLogic.Attributes; +using MUnique.OpenMU.PlugIns; + +/// +/// Increases the experience of a player in Vulcanus by its , +/// which is given e.g. by the Gladiator's Honor. +/// +[PlugIn] +[Display(Name = nameof(PlugInResources.VulcanusExperiencePlugIn_Name), Description = nameof(PlugInResources.VulcanusExperiencePlugIn_Description), ResourceType = typeof(PlugInResources))] +[Guid("03E6A271-821E-49B8-B6E9-B24E95B6D16E")] +public class VulcanusExperiencePlugIn : IExperienceCalculationPlugIn, ISupportCustomConfiguration, ISupportDefaultCustomConfiguration +{ + /// + public VulcanusMapConfiguration? Configuration { get; set; } + + /// + public ValueTask CalculateExperienceAsync(Player player, ExperienceCalculationArgs args) + { + var mapNumber = (this.Configuration ??= new VulcanusMapConfiguration()).MapNumber; + if (player.CurrentMap?.MapId == mapNumber + && player.Attributes?[Stats.VulcanusExperienceRate] is > 0 and var rate) + { + args.Experience *= 1 + rate; + } + + return ValueTask.CompletedTask; + } + + /// + public object CreateDefaultConfig() => new VulcanusMapConfiguration(); +} diff --git a/src/GameLogic/PlugIns/VulcanusMapConfiguration.cs b/src/GameLogic/PlugIns/VulcanusMapConfiguration.cs new file mode 100644 index 0000000000..c56c2b329b --- /dev/null +++ b/src/GameLogic/PlugIns/VulcanusMapConfiguration.cs @@ -0,0 +1,22 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.PlugIns; + +/// +/// Configuration of the plugins of the Gladiator's Honor, which only apply in Vulcanus. +/// +public class VulcanusMapConfiguration +{ + /// + /// The default number of the map Vulcanus. + /// + internal const short DefaultMapNumber = 63; + + /// + /// Gets or sets the number of the map in which the Gladiator's Honor can be used and gives its bonus. + /// + [Display(ResourceType = typeof(PlugInResources), Name = nameof(PlugInResources.VulcanusMapConfiguration_MapNumber_Name))] + public short MapNumber { get; set; } = DefaultMapNumber; +} diff --git a/src/GameLogic/Properties/PlayerMessage.Designer.cs b/src/GameLogic/Properties/PlayerMessage.Designer.cs index 451d767b67..c750d28ae5 100644 --- a/src/GameLogic/Properties/PlayerMessage.Designer.cs +++ b/src/GameLogic/Properties/PlayerMessage.Designer.cs @@ -3198,5 +3198,14 @@ public static string ItemBoxRequiresLevel { return ResourceManager.GetString("ItemBoxRequiresLevel", resourceCulture); } } + + /// + /// Looks up a localized string similar to This item can't be used on this map.. + /// + public static string ItemCantBeUsedOnThisMap { + get { + return ResourceManager.GetString("ItemCantBeUsedOnThisMap", resourceCulture); + } + } } } diff --git a/src/GameLogic/Properties/PlayerMessage.de.resx b/src/GameLogic/Properties/PlayerMessage.de.resx index c6b035c784..e2d4d8850a 100644 --- a/src/GameLogic/Properties/PlayerMessage.de.resx +++ b/src/GameLogic/Properties/PlayerMessage.de.resx @@ -1021,4 +1021,7 @@ Inventarerweiterungen: {18} Dieser Warp ist nur für Game Master verfügbar. + + Dieser Gegenstand kann auf dieser Karte nicht benutzt werden. + diff --git a/src/GameLogic/Properties/PlayerMessage.pt.resx b/src/GameLogic/Properties/PlayerMessage.pt.resx index 3f21ddfa66..4a65e67edf 100644 --- a/src/GameLogic/Properties/PlayerMessage.pt.resx +++ b/src/GameLogic/Properties/PlayerMessage.pt.resx @@ -1174,4 +1174,7 @@ Extensões de inventário: {18} Este warp está disponível apenas para game masters. + + Este item não pode ser usado neste mapa. + diff --git a/src/GameLogic/Properties/PlayerMessage.resx b/src/GameLogic/Properties/PlayerMessage.resx index 95b8eb4d9c..0f177b57a8 100644 --- a/src/GameLogic/Properties/PlayerMessage.resx +++ b/src/GameLogic/Properties/PlayerMessage.resx @@ -1174,4 +1174,7 @@ Inventory Extensions: {18} This warp is only available to game masters. + + This item can't be used on this map. + diff --git a/src/GameLogic/Properties/PlayerMessage.zh-CN.resx b/src/GameLogic/Properties/PlayerMessage.zh-CN.resx index ae40bd8140..cab5e086c9 100644 --- a/src/GameLogic/Properties/PlayerMessage.zh-CN.resx +++ b/src/GameLogic/Properties/PlayerMessage.zh-CN.resx @@ -15,4 +15,7 @@ 红名玩家无法进入! 参加此活动需要入场券。 无法在卡利玛创建卡利玛之门。 + + 此物品无法在此地图使用。 + diff --git a/src/GameLogic/Properties/PlugInResources.Designer.cs b/src/GameLogic/Properties/PlugInResources.Designer.cs index afe520fe7a..c741763573 100644 --- a/src/GameLogic/Properties/PlugInResources.Designer.cs +++ b/src/GameLogic/Properties/PlugInResources.Designer.cs @@ -7710,5 +7710,50 @@ public static string WorldChatChatCommandPlugIn_Name { return ResourceManager.GetString("WorldChatChatCommandPlugIn_Name", resourceCulture); } } + + /// + /// Looks up a localized string similar to Gladiator's Honor consume handler. + /// + public static string GladiatorsHonorConsumeHandlerPlugIn_Name { + get { + return ResourceManager.GetString("GladiatorsHonorConsumeHandlerPlugIn_Name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Applies the effect of the Gladiator's Honor, which can only be used in Vulcanus.. + /// + public static string GladiatorsHonorConsumeHandlerPlugIn_Description { + get { + return ResourceManager.GetString("GladiatorsHonorConsumeHandlerPlugIn_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Vulcanus experience bonus. + /// + public static string VulcanusExperiencePlugIn_Name { + get { + return ResourceManager.GetString("VulcanusExperiencePlugIn_Name", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Increases the experience of players in Vulcanus by their Vulcanus experience rate, e.g. of the Gladiator's Honor.. + /// + public static string VulcanusExperiencePlugIn_Description { + get { + return ResourceManager.GetString("VulcanusExperiencePlugIn_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Map number. + /// + public static string VulcanusMapConfiguration_MapNumber_Name { + get { + return ResourceManager.GetString("VulcanusMapConfiguration_MapNumber_Name", resourceCulture); + } + } } } diff --git a/src/GameLogic/Properties/PlugInResources.de.resx b/src/GameLogic/Properties/PlugInResources.de.resx index c476911ee9..18671de125 100644 --- a/src/GameLogic/Properties/PlugInResources.de.resx +++ b/src/GameLogic/Properties/PlugInResources.de.resx @@ -2016,4 +2016,19 @@ Teilt dem Client beim Betreten des Spiels mit, welche Warps der Spieler im Warp-Fenster auswählen kann. + + Verbrauch der Ehre des Gladiators + + + Wendet den Effekt der Ehre des Gladiators an, die nur in Vulcanus benutzt werden kann. + + + Erfahrungsbonus in Vulcanus + + + Erhöht die Erfahrung von Spielern in Vulcanus um ihre Vulcanus-Erfahrungsrate, z. B. durch die Ehre des Gladiators. + + + Kartennummer + diff --git a/src/GameLogic/Properties/PlugInResources.resx b/src/GameLogic/Properties/PlugInResources.resx index e212c3adc8..ef1552cc76 100644 --- a/src/GameLogic/Properties/PlugInResources.resx +++ b/src/GameLogic/Properties/PlugInResources.resx @@ -2061,4 +2061,19 @@ Tells the client which warps the player can select in the warp window, when the player enters the game. + + Gladiator's Honor consume handler + + + Applies the effect of the Gladiator's Honor, which can only be used in Vulcanus. + + + Vulcanus experience bonus + + + Increases the experience of players in Vulcanus by their Vulcanus experience rate, e.g. of the Gladiator's Honor. + + + Map number + diff --git a/src/GameLogic/Properties/PlugInResources.zh-CN.resx b/src/GameLogic/Properties/PlugInResources.zh-CN.resx index c744611a71..1e7dd5306e 100644 --- a/src/GameLogic/Properties/PlugInResources.zh-CN.resx +++ b/src/GameLogic/Properties/PlugInResources.zh-CN.resx @@ -1804,4 +1804,19 @@ 处理玩家与幻影寺院盟军物品仓库的交互。盟军将圣物送至此处可获得一分。 幻影寺院幻影物品仓库交互 处理玩家与幻影寺院幻影物品仓库的交互。幻影军将圣物送至此处可获得一分。 + + 角斗士的荣耀使用 + + + 应用角斗士的荣耀的效果,该物品只能在囚禁之岛使用。 + + + 囚禁之岛经验加成 + + + 按玩家在囚禁之岛的经验倍率(例如角斗士的荣耀)增加其在囚禁之岛获得的经验。 + + + 地图编号 + diff --git a/src/Persistence/Initialization/Properties/ItemNames.Designer.cs b/src/Persistence/Initialization/Properties/ItemNames.Designer.cs index a7cfd7fa65..4585f0f10c 100644 --- a/src/Persistence/Initialization/Properties/ItemNames.Designer.cs +++ b/src/Persistence/Initialization/Properties/ItemNames.Designer.cs @@ -5487,5 +5487,12 @@ public static string TalismanOfItemProtection { return ResourceManager.GetString("TalismanOfItemProtection", resourceCulture); } } + + /// Gets GladiatorsHonor from the configuration name resources. + public static string GladiatorsHonor { + get { + return ResourceManager.GetString("GladiatorsHonor", resourceCulture); + } + } } } diff --git a/src/Persistence/Initialization/Properties/ItemNames.de.resx b/src/Persistence/Initialization/Properties/ItemNames.de.resx index f73c5ea46f..eb5cc25be7 100644 --- a/src/Persistence/Initialization/Properties/ItemNames.de.resx +++ b/src/Persistence/Initialization/Properties/ItemNames.de.resx @@ -2259,4 +2259,7 @@ Talisman des Gegenstandsschutzes + + Ehre des Gladiators + diff --git a/src/Persistence/Initialization/Properties/ItemNames.resx b/src/Persistence/Initialization/Properties/ItemNames.resx index 31ac86cfdf..3ab25f3868 100644 --- a/src/Persistence/Initialization/Properties/ItemNames.resx +++ b/src/Persistence/Initialization/Properties/ItemNames.resx @@ -2343,4 +2343,7 @@ Talisman of Item Protection + + Gladiator's Honor + \ No newline at end of file diff --git a/src/Persistence/Initialization/Properties/ItemNames.zh-CN.resx b/src/Persistence/Initialization/Properties/ItemNames.zh-CN.resx index 13e847f463..81dc265056 100644 --- a/src/Persistence/Initialization/Properties/ItemNames.zh-CN.resx +++ b/src/Persistence/Initialization/Properties/ItemNames.zh-CN.resx @@ -2343,4 +2343,7 @@ 物品保护护身符 + + 角斗士的荣耀 + \ No newline at end of file diff --git a/src/Persistence/Initialization/Properties/PlugInResources.Designer.cs b/src/Persistence/Initialization/Properties/PlugInResources.Designer.cs index c2d5a63c30..de813af443 100644 --- a/src/Persistence/Initialization/Properties/PlugInResources.Designer.cs +++ b/src/Persistence/Initialization/Properties/PlugInResources.Designer.cs @@ -1109,5 +1109,11 @@ public static string DataInitialization095d_Name { /// Gets the localized FixCrywolfGateTerrainUpdatePlugIn_Description text. public static string FixCrywolfGateTerrainUpdatePlugIn_Description => ResourceManager.GetString("FixCrywolfGateTerrainUpdatePlugIn_Description", resourceCulture)!; + /// Gets the localized AddGladiatorsHonorUpdatePlugIn_Name text. + public static string AddGladiatorsHonorUpdatePlugIn_Name => ResourceManager.GetString("AddGladiatorsHonorUpdatePlugIn_Name", resourceCulture)!; + + /// Gets the localized AddGladiatorsHonorUpdatePlugIn_Description text. + public static string AddGladiatorsHonorUpdatePlugIn_Description => ResourceManager.GetString("AddGladiatorsHonorUpdatePlugIn_Description", resourceCulture)!; + } } diff --git a/src/Persistence/Initialization/Properties/PlugInResources.de.resx b/src/Persistence/Initialization/Properties/PlugInResources.de.resx index 7530b3ef21..b50aed0394 100644 --- a/src/Persistence/Initialization/Properties/PlugInResources.de.resx +++ b/src/Persistence/Initialization/Properties/PlugInResources.de.resx @@ -972,4 +972,10 @@ Macht den südlichen Zugang zum Tor der Festung Crywolf im Tal von Loren begehbar, den Spieler außerhalb der Burgbelagerung benutzen. Der nördliche Zugang und alle anderen Felder bleiben unverändert. + + Ehre des Gladiators hinzufügen + + + Dieses Update fügt die Ehre des Gladiators hinzu, welche die Erfahrung in Vulcanus 30 Minuten lang um 20 % erhöht. + diff --git a/src/Persistence/Initialization/Properties/PlugInResources.resx b/src/Persistence/Initialization/Properties/PlugInResources.resx index ca6308cb0c..06d034594f 100644 --- a/src/Persistence/Initialization/Properties/PlugInResources.resx +++ b/src/Persistence/Initialization/Properties/PlugInResources.resx @@ -456,4 +456,10 @@ This update adds the Talisman of Guardian, which protects from the death penalty, and the Talisman of Item Protection, which keeps the durability of the equipped items. Fix the Crywolf entrance in Valley of Loren Makes the south entry of the gate to Crywolf Fortress in Valley of Loren walkable, which players use outside of Castle Siege. The north entry and all other tiles are kept. + + Add the Gladiator's Honor + + + This update adds the Gladiator's Honor, which increases the experience in Vulcanus by 20 % for 30 minutes. + diff --git a/src/Persistence/Initialization/Properties/PlugInResources.zh-CN.resx b/src/Persistence/Initialization/Properties/PlugInResources.zh-CN.resx index 07180accdc..e2f8b37c47 100644 --- a/src/Persistence/Initialization/Properties/PlugInResources.zh-CN.resx +++ b/src/Persistence/Initialization/Properties/PlugInResources.zh-CN.resx @@ -489,4 +489,10 @@ 修正罗兰峡谷通往狼魂要塞的入口 使罗兰峡谷中狼魂要塞传送门的南侧入口可以行走(攻城战以外的时间玩家使用该入口)。北侧入口及其他所有地块保持不变。 + + 添加角斗士的荣耀 + + + 此更新添加角斗士的荣耀,在 30 分钟内使囚禁之岛的经验增加 20%。 + diff --git a/src/Persistence/Initialization/Skills/MagicEffectNumber.cs b/src/Persistence/Initialization/Skills/MagicEffectNumber.cs index 3c62628ad4..ccbc967b8e 100644 --- a/src/Persistence/Initialization/Skills/MagicEffectNumber.cs +++ b/src/Persistence/Initialization/Skills/MagicEffectNumber.cs @@ -365,6 +365,11 @@ internal enum MagicEffectNumber : short /// MasterSealOfWealth = 0x66, + /// + /// The Gladiator's Honor effect, which increases the experience in Vulcanus. + /// + GladiatorsHonor = 0x67, + /// /// The max AG boost aura effect. /// diff --git a/src/Persistence/Initialization/Updates/AddGladiatorsHonorUpdatePlugIn.cs b/src/Persistence/Initialization/Updates/AddGladiatorsHonorUpdatePlugIn.cs new file mode 100644 index 0000000000..24251fd9cd --- /dev/null +++ b/src/Persistence/Initialization/Updates/AddGladiatorsHonorUpdatePlugIn.cs @@ -0,0 +1,49 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Persistence.Initialization.Updates; + +using System.Runtime.InteropServices; +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Items; +using MUnique.OpenMU.PlugIns; + +/// +/// This update adds the Gladiator's Honor with its effect, which increases the experience in Vulcanus. +/// The attribute of its effect is added by the , when it doesn't exist yet. +/// +[PlugIn] +[Display(Name = nameof(PlugInResources.AddGladiatorsHonorUpdatePlugIn_Name), Description = nameof(PlugInResources.AddGladiatorsHonorUpdatePlugIn_Description), ResourceType = typeof(PlugInResources))] +[Guid("86AE7CBA-FA9B-4D1B-B112-C7FA87BE4B37")] +public class AddGladiatorsHonorUpdatePlugIn : UpdatePlugInBase +{ + /// + public override string DataInitializationKey => VersionSeasonSix.DataInitialization.Id; + + /// + public override string Name => PlugInResources.AddGladiatorsHonorUpdatePlugIn_Name; + + /// + public override string Description => PlugInResources.AddGladiatorsHonorUpdatePlugIn_Description; + + /// + public override bool IsMandatory => true; + + /// + public override DateTime CreatedAt => new(2026, 11, 10, 6, 0, 0, DateTimeKind.Utc); + + /// + /// + /// The item gets the item rule flags, and it's created by the initializer of the buff items. + /// + public override IEnumerable DependsOn => [typeof(AddItemRuleFlagsPlugIn).GUID, typeof(AddBuffItemsUpdatePlugIn).GUID]; + + /// + protected override ValueTask ApplyAsync(IContext context, GameConfiguration gameConfiguration) + { + var addedItems = new BuffItems(context, gameConfiguration).CreateItems(); + ItemRules.Apply(addedItems); + return ValueTask.CompletedTask; + } +} diff --git a/src/Persistence/Initialization/VersionSeasonSix/Items/BuffItems.cs b/src/Persistence/Initialization/VersionSeasonSix/Items/BuffItems.cs index ffe08d23db..01b4f257da 100644 --- a/src/Persistence/Initialization/VersionSeasonSix/Items/BuffItems.cs +++ b/src/Persistence/Initialization/VersionSeasonSix/Items/BuffItems.cs @@ -68,6 +68,15 @@ internal class BuffItems : InitializerBase /// private const byte ItemProtectionTalismanSubType = 78; + /// The sub type of the effect of the Gladiator's Honor. + /// + private const byte GladiatorsHonorSubType = 79; + + /// + /// The experience rate which the Gladiator's Honor adds in Vulcanus, as the game client describes it. + /// + private const float GladiatorsHonorExperienceRate = 0.2f; + /// /// The defense which the game client shows for the scroll of defense. /// @@ -127,6 +136,10 @@ internal IReadOnlyList CreateItems() this.CreateItem(13, 81, 1, 1, () => ItemNames.TalismanOfGuardian, MagicEffectNumber.TalismanOfGuardian, DeathPenaltyTalismanSubType, TalismanDuration, false, Boost.Add(Stats.DeathPenaltyProtection, 1f)), this.CreateItem(13, 82, 1, 1, () => ItemNames.TalismanOfItemProtection, MagicEffectNumber.TalismanOfItemProtection, ItemProtectionTalismanSubType, TalismanDuration, false, Boost.Add(Stats.ItemDurabilityProtection, 1f)), + // The Gladiator's Honor, which can only be used in Vulcanus and only increases the experience there. + // The game client doesn't define its duration, so it lasts like a seal. + this.CreateItem(13, 95, 1, 1, () => ItemNames.GladiatorsHonor, MagicEffectNumber.GladiatorsHonor, GladiatorsHonorSubType, SealDuration, false, Boost.Add(Stats.VulcanusExperienceRate, GladiatorsHonorExperienceRate)), + // Auras, which increase the maximum by the character level. this.CreateItem(13, 104, 1, 1, () => ItemNames.MaxAgBoostAura, MagicEffectNumber.MaxAbilityBoostAura, 58, AuraDuration, false, Boost.PerLevel(Stats.MaximumAbility, 1f)), this.CreateItem(13, 105, 1, 1, () => ItemNames.MaxSdBoostAura, MagicEffectNumber.MaxShieldBoostAura, 59, AuraDuration, false, Boost.PerLevel(Stats.MaximumShield, 10f)), diff --git a/src/Persistence/Initialization/VersionSeasonSix/Items/ItemRules.cs b/src/Persistence/Initialization/VersionSeasonSix/Items/ItemRules.cs index d5dab99c0c..d1937f8967 100644 --- a/src/Persistence/Initialization/VersionSeasonSix/Items/ItemRules.cs +++ b/src/Persistence/Initialization/VersionSeasonSix/Items/ItemRules.cs @@ -206,6 +206,7 @@ public enum Blocked Rule(13, 82, Blocked.Trade, Blocked.Drop, Blocked.SellToNpc, Blocked.PersonalStore, Blocked.Repair), // Talisman of Item Protection Rule(13, 93, Blocked.Trade, Blocked.Drop, Blocked.Store, Blocked.SellToNpc, Blocked.PersonalStore, Blocked.Repair), // Master Seal of Ascension Rule(13, 94, Blocked.Trade, Blocked.Drop, Blocked.Store, Blocked.SellToNpc, Blocked.PersonalStore, Blocked.Repair), // Master Seal of Wealth + Rule(13, 95, Blocked.Trade, Blocked.Drop, Blocked.SellToNpc, Blocked.PersonalStore, Blocked.Repair), // Gladiator's Honor Rule(13, 104, Blocked.Trade, Blocked.Drop, Blocked.SellToNpc, Blocked.PersonalStore, Blocked.Repair), // Max AG Boost Aura Rule(13, 105, Blocked.Trade, Blocked.Drop, Blocked.SellToNpc, Blocked.PersonalStore, Blocked.Repair), // Max SD Boost Aura Rule(13, 106, Blocked.Trade, Blocked.Drop, Blocked.SellToNpc, Blocked.PersonalStore, Blocked.Repair), // Pet Unicorn diff --git a/tests/MUnique.OpenMU.Persistence.Initialization.Tests/BuffItemsDataTest.cs b/tests/MUnique.OpenMU.Persistence.Initialization.Tests/BuffItemsDataTest.cs index 9a63b76572..34c928438f 100644 --- a/tests/MUnique.OpenMU.Persistence.Initialization.Tests/BuffItemsDataTest.cs +++ b/tests/MUnique.OpenMU.Persistence.Initialization.Tests/BuffItemsDataTest.cs @@ -20,7 +20,7 @@ internal class BuffItemsDataTest { private static readonly (byte Group, short Number)[] BuffItems = [ - (13, 43), (13, 44), (13, 45), (13, 62), (13, 63), (13, 81), (13, 82), (13, 93), (13, 94), (13, 104), (13, 105), (13, 116), + (13, 43), (13, 44), (13, 45), (13, 62), (13, 63), (13, 81), (13, 82), (13, 93), (13, 94), (13, 95), (13, 104), (13, 105), (13, 116), (14, 72), (14, 73), (14, 74), (14, 75), (14, 76), (14, 77), (14, 78), (14, 79), (14, 80), (14, 81), (14, 82), (14, 97), (14, 98), (14, 140), ]; @@ -189,6 +189,7 @@ private static void AssertBuffItems(GameConfiguration gameConfiguration) AssertEffect(gameConfiguration, 13, 93, "Master Seal of Ascension", 1, 1, 101, 24, 1800, (Stats.MasterExperienceRate, 1.5f, AggregateType.Multiplicate)); AssertEffect(gameConfiguration, 13, 94, "Master Seal of Wealth", 1, 1, 102, 24, 1800, (Stats.MasterExperienceRate, 2f, AggregateType.Multiplicate), (Stats.BonusItemDropRate, 0.5f, AggregateType.AddRaw)); AssertEffect(gameConfiguration, 13, 116, "Seal of Wealth", 1, 1, 0x77, 76, 1800, (Stats.BonusItemDropRate, 0.5f, AggregateType.AddRaw)); + AssertEffect(gameConfiguration, 13, 95, "Gladiator's Honor", 1, 1, 103, 79, 1800, (Stats.VulcanusExperienceRate, 0.2f, AggregateType.AddRaw)); AssertEffect(gameConfiguration, 14, 72, "Scroll of Quickness", 2, 2, 0x2C, 67, 1800, (Stats.AttackSpeedAny, 15f, AggregateType.AddRaw)); AssertEffect(gameConfiguration, 14, 73, "Scroll of Defense", 2, 2, 0x2D, 68, 1800, (Stats.DefenseFinal, 30f, AggregateType.AddFinal)); AssertEffect(gameConfiguration, 14, 74, "Scroll of Wrath", 2, 2, 0x2E, 69, 1800, (Stats.PhysicalBaseDmg, 30f, AggregateType.AddFinal)); diff --git a/tests/MUnique.OpenMU.Persistence.Initialization.Tests/GladiatorsHonorDataTest.cs b/tests/MUnique.OpenMU.Persistence.Initialization.Tests/GladiatorsHonorDataTest.cs new file mode 100644 index 0000000000..0be91c574a --- /dev/null +++ b/tests/MUnique.OpenMU.Persistence.Initialization.Tests/GladiatorsHonorDataTest.cs @@ -0,0 +1,59 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Persistence.Initialization.Tests; + +using Microsoft.Extensions.Logging.Abstractions; +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.DataModel.Configuration.Items; +using MUnique.OpenMU.GameLogic.Attributes; +using MUnique.OpenMU.Persistence.Initialization.Updates; +using MUnique.OpenMU.Persistence.InMemory; + +/// +/// Tests the data of the Gladiator's Honor and the . +/// +[TestFixture] +internal class GladiatorsHonorDataTest +{ + /// + /// Tests that the update adds the item, its effect and its attribute to a database which was created before + /// they existed. Applying it twice doesn't add them twice. + /// + [Test] + public async Task UpdateAddsItemToExistingDatabaseAsync() + { + var (context, gameConfiguration) = await CreateSeason6ConfigurationAsync().ConfigureAwait(false); + using var _ = context; + var item = gameConfiguration.Items.Single(IsGladiatorsHonor); + gameConfiguration.Items.Remove(item); + gameConfiguration.MagicEffects.Remove(item.ConsumeEffect!); + gameConfiguration.Attributes.Remove(gameConfiguration.Attributes.Single(a => a.Id == Stats.VulcanusExperienceRate.Id)); + + var update = new AddGladiatorsHonorUpdatePlugIn(); + await update.ApplyUpdateAsync(context, gameConfiguration).ConfigureAwait(false); + await update.ApplyUpdateAsync(context, gameConfiguration).ConfigureAwait(false); + + var addedItem = gameConfiguration.Items.Single(IsGladiatorsHonor); + var powerUp = addedItem.ConsumeEffect!.PowerUpDefinitions.Single(); + Assert.That(gameConfiguration.MagicEffects.Count(e => e.Number == 0x67), Is.EqualTo(1)); + Assert.That(gameConfiguration.Attributes.Count(a => a.Id == Stats.VulcanusExperienceRate.Id), Is.EqualTo(1)); + Assert.That(powerUp.TargetAttribute, Is.SameAs(gameConfiguration.Attributes.Single(a => a.Id == Stats.VulcanusExperienceRate.Id))); + Assert.That(powerUp.Boost!.ConstantValue.Value, Is.EqualTo(0.2f).Within(0.0001f)); + Assert.That(addedItem.IsTradable, Is.False); + } + + private static bool IsGladiatorsHonor(ItemDefinition item) => item is { Group: 13, Number: 95 }; + + private static async Task<(IContext Context, GameConfiguration GameConfiguration)> CreateSeason6ConfigurationAsync() + { + var contextProvider = new InMemoryPersistenceContextProvider(); + var dataInitialization = new VersionSeasonSix.DataInitialization(contextProvider, new NullLoggerFactory()); + await dataInitialization.CreateInitialDataAsync(1, true).ConfigureAwait(false); + + var context = contextProvider.CreateNewContext(); + var gameConfiguration = (await context.GetAsync().ConfigureAwait(false)).First(); + return (context, gameConfiguration); + } +} diff --git a/tests/MUnique.OpenMU.Persistence.Initialization.Tests/ItemStatProfilesDataTest.cs b/tests/MUnique.OpenMU.Persistence.Initialization.Tests/ItemStatProfilesDataTest.cs index 70bb95f006..a9f2842b1e 100644 --- a/tests/MUnique.OpenMU.Persistence.Initialization.Tests/ItemStatProfilesDataTest.cs +++ b/tests/MUnique.OpenMU.Persistence.Initialization.Tests/ItemStatProfilesDataTest.cs @@ -204,7 +204,7 @@ public void Season6ItemCountsPerProfile() { "armor: 327", "book: 3", - "default: 243", + "default: 244", "firstTierWing: 4", "helper: 27", "orb: 12", diff --git a/tests/MUnique.OpenMU.Tests/GladiatorsHonorTest.cs b/tests/MUnique.OpenMU.Tests/GladiatorsHonorTest.cs new file mode 100644 index 0000000000..027b707f63 --- /dev/null +++ b/tests/MUnique.OpenMU.Tests/GladiatorsHonorTest.cs @@ -0,0 +1,124 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Tests; + +using Moq; +using MUnique.OpenMU.DataModel.Entities; +using MUnique.OpenMU.GameLogic; +using MUnique.OpenMU.GameLogic.Attributes; +using MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions; +using MUnique.OpenMU.GameLogic.PlugIns; +using BasicModel = MUnique.OpenMU.Persistence.BasicModel; + +/// +/// Tests the plugins of the Gladiator's Honor: the +/// and the . +/// +[TestFixture] +public class GladiatorsHonorTest +{ + private const byte ItemSlot = 12; + + private const float ExperienceRate = 0.2f; + + /// + /// Tests that the item can't be used outside of Vulcanus, so it isn't used up. + /// + [Test] + public async Task CanNotBeUsedOutsideOfVulcanusAsync() + { + var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false); + var item = await AddItemAsync(player).ConfigureAwait(false); + + var consumed = await new GladiatorsHonorConsumeHandlerPlugIn().ConsumeItemAsync(player, item, null, FruitUsage.Undefined).ConfigureAwait(false); + + Assert.That(consumed, Is.False); + Assert.That(player.Inventory!.GetItem(ItemSlot), Is.SameAs(item)); + Assert.That(player.Attributes![Stats.VulcanusExperienceRate], Is.Zero); + } + + /// + /// Tests that the item applies its effect in the configured map and is used up. + /// + [Test] + public async Task AppliesEffectInVulcanusAsync() + { + var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false); + var item = await AddItemAsync(player).ConfigureAwait(false); + var plugIn = new GladiatorsHonorConsumeHandlerPlugIn + { + Configuration = new VulcanusMapConfiguration { MapNumber = (short)player.CurrentMap!.MapId }, + }; + + var consumed = await plugIn.ConsumeItemAsync(player, item, null, FruitUsage.Undefined).ConfigureAwait(false); + + Assert.That(consumed, Is.True); + Assert.That(item.Durability, Is.Zero); + Assert.That(player.Attributes![Stats.VulcanusExperienceRate], Is.EqualTo(ExperienceRate).Within(0.001f)); + } + + /// + /// Tests that the experience is increased by the rate in Vulcanus. + /// + [Test] + public async Task IncreasesExperienceInVulcanusAsync() + { + var player = await CreatePlayerWithEffectAsync().ConfigureAwait(false); + var plugIn = new VulcanusExperiencePlugIn + { + Configuration = new VulcanusMapConfiguration { MapNumber = (short)player.CurrentMap!.MapId }, + }; + var args = new ExperienceCalculationArgs(Mock.Of(), false, 1000); + + await plugIn.CalculateExperienceAsync(player, args).ConfigureAwait(false); + + Assert.That(args.Experience, Is.EqualTo(1200).Within(0.01)); + } + + /// + /// Tests that the experience isn't increased outside of Vulcanus. + /// + [Test] + public async Task DoesNotIncreaseExperienceOutsideOfVulcanusAsync() + { + var player = await CreatePlayerWithEffectAsync().ConfigureAwait(false); + var args = new ExperienceCalculationArgs(Mock.Of(), false, 1000); + + await new VulcanusExperiencePlugIn().CalculateExperienceAsync(player, args).ConfigureAwait(false); + + Assert.That(args.Experience, Is.EqualTo(1000)); + } + + private static async ValueTask CreatePlayerWithEffectAsync() + { + var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false); + var item = await AddItemAsync(player).ConfigureAwait(false); + var plugIn = new GladiatorsHonorConsumeHandlerPlugIn + { + Configuration = new VulcanusMapConfiguration { MapNumber = (short)player.CurrentMap!.MapId }, + }; + Assert.That(await plugIn.ConsumeItemAsync(player, item, null, FruitUsage.Undefined).ConfigureAwait(false), Is.True); + return player; + } + + private static async ValueTask AddItemAsync(Player player) + { + var effect = new BasicModel.MagicEffectDefinition + { + Number = 0x67, + SubType = 79, + Duration = new BasicModel.PowerUpDefinitionValue { ConstantValue = { Value = 1800 } }, + }; + effect.PowerUpDefinitions.Add(new BasicModel.PowerUpDefinition + { + TargetAttribute = Stats.VulcanusExperienceRate, + Boost = new BasicModel.PowerUpDefinitionValue { ConstantValue = { Value = ExperienceRate } }, + }); + var definition = new BasicModel.ItemDefinition { Group = 13, Number = 95, Width = 1, Height = 1, Durability = 1, ConsumeEffect = effect }; + var item = new BasicModel.Item { Definition = definition, Durability = 1 }; + await player.Inventory!.AddItemAsync(ItemSlot, item).ConfigureAwait(false); + return item; + } +}