baseline: OpenMU upstream b5a0961 (fresh source)

This commit is contained in:
Acentech Dev
2026-07-14 19:00:35 +03:00
parent 450402e47d
commit 36fc125d5c
11968 changed files with 748705 additions and 0 deletions

View File

@@ -0,0 +1,158 @@
// <copyright file="AddMasterPointAction.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Character;
using MUnique.OpenMU.GameLogic.Views.Character;
/// <summary>
/// Action to add a master skill point to learn or increase the level of a master skill.
/// </summary>
public class AddMasterPointAction
{
private const int MinimumSkillLevelOfRequiredSkill = 10;
/// <summary>
/// Adds the master point.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="skillId">The skill identifier.</param>
public async ValueTask AddMasterPointAsync(Player player, ushort skillId)
{
using var loggerScope = player.Logger.BeginScope(this.GetType());
if (player.SelectedCharacter is null)
{
player.Logger.LogWarning("No character selected, player {0}", player);
return;
}
if (player.SelectedCharacter.MasterLevelUpPoints < 1)
{
player.Logger.LogWarning("No free master level up point, player {0}", player);
return;
}
var skill = player.GameContext.Configuration.Skills.FirstOrDefault(s => s.Number == skillId);
if (skill is null)
{
player.Logger.LogWarning("Skill {0} does not exist, player {1}", skillId, player);
return;
}
if (skill.MasterDefinition is null)
{
player.Logger.LogWarning("Not a master skill, skillId: {0}, player {1}", skill.Number, player);
return;
}
var learnedSkill = player.SelectedCharacter.LearnedSkills.FirstOrDefault(ls => ls.Skill?.Number == skillId);
if (learnedSkill is null)
{
player.Logger.LogDebug("Trying to add master skill, skillId: {0}, player {1}", skill.Number, player);
if (this.CheckRequisitions(player, skill))
{
player.Logger.LogDebug("Adding master skill, skillId: {0}, player {1}", skill.Number, player);
await player.SkillList!.AddLearnedSkillAsync(skill).ConfigureAwait(false);
learnedSkill = player.SkillList?.GetSkill(skillId);
if (learnedSkill is { })
{
await this.AddMasterPointToLearnedSkillAsync(player, learnedSkill).ConfigureAwait(false);
}
else
{
player.Logger.LogDebug($"Learned Skill {skillId} not found.");
}
}
}
else
{
await this.AddMasterPointToLearnedSkillAsync(player, learnedSkill).ConfigureAwait(false);
}
}
private async ValueTask AddMasterPointToLearnedSkillAsync(Player player, SkillEntry learnedSkill)
{
learnedSkill.ThrowNotInitializedProperty(learnedSkill.Skill is null, nameof(learnedSkill.Skill));
var requiredPoints = learnedSkill.Level == 0 ? learnedSkill.Skill.MasterDefinition!.MinimumLevel : 1;
if (player.SelectedCharacter!.MasterLevelUpPoints >= requiredPoints && learnedSkill.Level < learnedSkill.Skill.MasterDefinition!.MaximumLevel)
{
player.Logger.LogDebug("Adding {0} points to skill, skillId: {1}, player {2}", requiredPoints, learnedSkill.Skill.Number, player);
learnedSkill.Level += requiredPoints;
learnedSkill.PowerUpDuration = null;
learnedSkill.PowerUps = null;
var currentSkill = learnedSkill;
while (player.SkillList!.Skills.FirstOrDefault(s => s.Skill?.MasterDefinition?.ReplacedSkill == currentSkill.Skill) is { } childSkill)
{
// Because the learned skill might have been replaced by a child skill (active), we also need to nullify the child's powerups to force an update
childSkill.PowerUpDuration = null;
childSkill.PowerUps = null;
currentSkill = childSkill;
}
player.SelectedCharacter.MasterLevelUpPoints -= requiredPoints;
await player.InvokeViewPlugInAsync<IMasterSkillLevelChangedPlugIn>(p => p.MasterSkillLevelChangedAsync(learnedSkill)).ConfigureAwait(false);
}
else
{
player.Logger.LogDebug("Not enough master level up points to add master points, player {0}, available {1}, required {2}", player, player.SelectedCharacter.MasterLevelUpPoints, requiredPoints);
}
}
private bool CheckRequisitions(Player player, Skill skill)
{
if (player.SelectedCharacter!.MasterLevelUpPoints < skill.MasterDefinition!.MinimumLevel)
{
player.Logger.LogWarning("Not enough master level up points, player {0}, available {1}, required {2}", player, player.SelectedCharacter.MasterLevelUpPoints, skill.MasterDefinition.MinimumLevel);
return false;
}
if (!skill.QualifiedCharacters.Contains(player.SelectedCharacter.CharacterClass!))
{
player.Logger.LogWarning("Character not in a qualified class to learn the skill, account {0}, character {1}", player.Account!.LoginName, player.SelectedCharacter.Name);
return false;
}
if (!this.CheckRank(skill.MasterDefinition, player.SelectedCharacter))
{
player.Logger.LogWarning("No skill of the previous rank at the required minimum level of {0}, player {1}, skill {2} {3}", MinimumSkillLevelOfRequiredSkill, player, skill.Number, skill.Name);
return false;
}
if (!this.CheckRequiredSkill(skill.MasterDefinition, player))
{
player.Logger.LogWarning("Required skill not available of not at the required minimum level of {0}, player {1}, skill {2} {3}", MinimumSkillLevelOfRequiredSkill, player, skill.Number, skill.Name);
return false;
}
return true;
}
private bool CheckRank(MasterSkillDefinition definition, DataModel.Entities.Character character)
{
if (definition.Rank <= 1)
{
return true;
}
var learnedRequiredSkills = character.LearnedSkills
.Where(l => l.Skill?.MasterDefinition?.Root != null
&& l.Skill.MasterDefinition.Root.Id == definition.Root?.Id
&& l.Skill.MasterDefinition.Rank == definition.Rank - 1);
return learnedRequiredSkills?.Any(lrs => lrs.Level >= MinimumSkillLevelOfRequiredSkill) ?? false;
}
private bool CheckRequiredSkill(MasterSkillDefinition definition, Player player)
{
var result = true;
if (definition.RequiredMasterSkills is not null && definition.RequiredMasterSkills.Any())
{
result = definition.RequiredMasterSkills.All(s =>
player.SelectedCharacter!.LearnedSkills.Any(learned => learned.Skill == s && learned.Level >= MinimumSkillLevelOfRequiredSkill)
|| (s.MasterDefinition is null && player.SkillList!.ContainsSkill((ushort)s.Number)));
}
return result;
}
}

View File

@@ -0,0 +1,152 @@
// <copyright file="CreateCharacterAction.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Character;
using System.Text.RegularExpressions;
using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.GameLogic.PlugIns;
using MUnique.OpenMU.GameLogic.Views.Character;
/// <summary>
/// Action to create a new character in the character selection screen.
/// </summary>
public class CreateCharacterAction
{
/// <summary>
/// Tries to create a new character.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="characterName">Name of the character.</param>
/// <param name="characterClassId">The character class identifier.</param>
public async ValueTask CreateCharacterAsync(Player player, string characterName, int characterClassId)
{
using var loggerScope = player.Logger.BeginScope(this.GetType());
if (player.PlayerState.CurrentState != PlayerState.CharacterSelection)
{
player.Logger.LogError($"Account {player.Account!.LoginName} not in the right state, but {player.PlayerState.CurrentState}.");
return;
}
var characterClass = player.GameContext.Configuration.CharacterClasses.FirstOrDefault(c => c.Number == characterClassId);
if (characterClass is not null)
{
var character = await this.CreateCharacterAsync(player, characterName, characterClass).ConfigureAwait(false);
if (character != null)
{
await player.InvokeViewPlugInAsync<IShowCreatedCharacterPlugIn>(p => p.ShowCreatedCharacterAsync(character)).ConfigureAwait(false);
return;
}
}
await player.InvokeViewPlugInAsync<IShowCharacterCreationFailedPlugIn>(p => p.ShowCharacterCreationFailedAsync()).ConfigureAwait(false);
}
/// <summary>
/// Creates the default key configuration for a newly created character.
/// </summary>
/// <returns>The default key configuration.</returns>
/// <remarks>
/// The key configuration is an opaque blob which is interpreted by the game client. Within it,
/// the potion quick-slots Q, W, E and R are stored as offsets into the potion item group, at
/// byte indices 21 (Q), 22 (W), 23 (E) and 25 (R). We bind Q to the healing potion and W to the
/// mana potion; E and R stay unbound. An all-zero configuration would otherwise make the client
/// bind offset 0 (the apple, which it treats as a healing item) to all four slots, so each one
/// would act as a health potion.
/// </remarks>
private static byte[] CreateDefaultKeyConfiguration()
{
const byte healingPotion = 1;
const byte manaPotion = 4;
const byte unbound = 0xFF;
var keyConfiguration = new byte[30];
keyConfiguration[21] = healingPotion; // Q
keyConfiguration[22] = manaPotion; // W
keyConfiguration[23] = unbound; // E
keyConfiguration[25] = unbound; // R
return keyConfiguration;
}
private async ValueTask<DataModel.Entities.Character?> CreateCharacterAsync(Player player, string name, CharacterClass characterClass)
{
var account = player.Account;
if (account is null)
{
player.Logger.LogWarning("Account Object is null.");
throw new ArgumentNullException(nameof(player));
}
player.Logger.LogDebug("Enter CreateCharacter: {0} {1} {2}", account.LoginName, name, characterClass);
var isValidName = string.IsNullOrWhiteSpace(player.GameContext.Configuration.CharacterNameRegex) || Regex.IsMatch(name, player.GameContext.Configuration.CharacterNameRegex);
player.Logger.LogDebug("CreateCharacter: Character Name matches = {0}", isValidName);
if (!isValidName)
{
return null;
}
var freeSlot = this.GetFreeSlot(player);
if (freeSlot is null)
{
return null;
}
if (!characterClass.CanGetCreated || characterClass.HomeMap is null)
{
return null;
}
var character = player.PersistenceContext.CreateNew<DataModel.Entities.Character>();
character.CharacterClass = characterClass;
character.Name = name;
character.CharacterSlot = freeSlot.Value;
character.CreateDate = DateTime.UtcNow;
character.KeyConfiguration = CreateDefaultKeyConfiguration();
var attributes = character.CharacterClass.StatAttributes.Select(a => player.PersistenceContext.CreateNew<StatAttribute>(a.Attribute, a.BaseValue)).ToList();
attributes.ForEach(character.Attributes.Add);
character.CurrentMap = characterClass.HomeMap;
var randomSpawnGate = character.CurrentMap!.ExitGates.Where(g => g.IsSpawnGate).SelectRandom();
if (randomSpawnGate is not null)
{
character.PositionX = (byte)Rand.NextInt(randomSpawnGate.X1, randomSpawnGate.X2);
character.PositionY = (byte)Rand.NextInt(randomSpawnGate.Y1, randomSpawnGate.Y2);
}
character.Inventory = player.PersistenceContext.CreateNew<ItemStorage>();
account.Characters.Add(character);
player.GameContext.PlugInManager.GetPlugInPoint<ICharacterCreatedPlugIn>()?.CharacterCreated(player, character);
try
{
await player.SaveProgressAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
account.Characters.Remove(character);
player.PersistenceContext.Detach(character);
player.Logger.LogError(ex, "Error when trying to create character '{0}'", name);
var message = ex.InnerException?.Message ?? ex.Message;
if (message.Contains("IX_Character_Name") || message.Contains("23505"))
{
await player.ShowLocalizedBlueMessageAsync(PlayerMessage.CharacterWithNameAlreadyExists).ConfigureAwait(false);
}
return null;
}
player.Logger.LogDebug("Creating Character Complete.");
return character;
}
private byte? GetFreeSlot(Player player)
{
var usedSlots = player.Account!.Characters.Select(c => (int)c.CharacterSlot);
var freeSlots = Enumerable.Range(0, player.GameContext.Configuration.MaximumCharactersPerAccount).Except(usedSlots).ToList();
if (freeSlots.Any())
{
return (byte)freeSlots.First();
}
return null;
}
}

View File

@@ -0,0 +1,70 @@
// <copyright file="DeleteCharacterAction.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Character;
using MUnique.OpenMU.GameLogic.PlugIns;
using MUnique.OpenMU.GameLogic.Views.Character;
using MUnique.OpenMU.Interfaces;
/// <summary>
/// Action to delete a character in the character selection screen.
/// </summary>
public class DeleteCharacterAction
{
/// <summary>
/// Tries to delete the character.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="characterName">Name of the character.</param>
/// <param name="securityCode">The security code.</param>
public async ValueTask DeleteCharacterAsync(Player player, string characterName, string securityCode)
{
using var loggerScope = player.Logger.BeginScope(this.GetType());
var result = await this.DeleteCharacterRequestAsync(player, characterName, securityCode).ConfigureAwait(false);
await player.InvokeViewPlugInAsync<IShowCharacterDeleteResponsePlugIn>(p => p.ShowCharacterDeleteResponseAsync(result)).ConfigureAwait(false);
}
private async ValueTask<CharacterDeleteResult> DeleteCharacterRequestAsync(Player player, string characterName, string securityCode)
{
if (player.PlayerState.CurrentState != PlayerState.CharacterSelection)
{
player.Logger.LogError($"Account {player.Account?.LoginName} not in the right state, but {player.PlayerState.CurrentState}.");
return CharacterDeleteResult.Unsuccessful;
}
var character = player.Account!.Characters.FirstOrDefault(c => c.Name == characterName);
if (character is null)
{
player.Logger.LogError("Character not found. Hacker maybe tried to delete other players character!" +
Environment.NewLine + "\tAccName: " + player.Account.LoginName +
Environment.NewLine + "\tTried to delete Character: " + characterName);
return CharacterDeleteResult.Unsuccessful;
}
var checkAsPassword = string.IsNullOrEmpty(player.Account.SecurityCode);
if (checkAsPassword && !BCrypt.Net.BCrypt.Verify(securityCode, player.Account.PasswordHash))
{
return CharacterDeleteResult.WrongSecurityCode;
}
if (!checkAsPassword && player.Account.SecurityCode != securityCode)
{
return CharacterDeleteResult.WrongSecurityCode;
}
if (player.GameContext is IGameServerContext gameServerContext && await gameServerContext.GuildServer.GetGuildPositionAsync(character.Id).ConfigureAwait(false) != GuildPosition.Undefined)
{
await player.ShowLocalizedBlueMessageAsync(PlayerMessage.CantDeleteGuildMember).ConfigureAwait(false);
return CharacterDeleteResult.Unsuccessful;
}
player.Account.Characters.Remove(character);
player.GameContext.PlugInManager.GetPlugInPoint<ICharacterDeletedPlugIn>()?.CharacterDeleted(player, character);
await player.PersistenceContext.DeleteAsync(character).ConfigureAwait(false);
return CharacterDeleteResult.Successful;
}
}

View File

@@ -0,0 +1,32 @@
// <copyright file="FocusCharacterAction.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Character;
using MUnique.OpenMU.GameLogic.Views.Character;
/// <summary>
/// Action to focus a character in the character selection screen.
/// </summary>
public class FocusCharacterAction
{
/// <summary>
/// Focuses the character.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="characterName">Name of the character.</param>
public async ValueTask FocusCharacterAsync(Player player, string characterName)
{
if (player.PlayerState.CurrentState != PlayerState.CharacterSelection)
{
return;
}
var character = player.Account?.Characters.FirstOrDefault(c => c.Name == characterName);
if (character is not null)
{
await player.InvokeViewPlugInAsync<ICharacterFocusedPlugIn>(p => p.CharacterFocusedAsync(character)).ConfigureAwait(false);
}
}
}

View File

@@ -0,0 +1,66 @@
// <copyright file="IncreaseStatsAction.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Character;
using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.Character;
using MUnique.OpenMU.Interfaces;
/// <summary>
/// Action to increase stat attributes.
/// </summary>
public class IncreaseStatsAction
{
/// <summary>
/// Increases the specified stat attribute by one point, if enough points are available.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="targetAttribute">The stat attribute definition.</param>
/// <param name="amount">The amount of points.</param>
public async ValueTask IncreaseStatsAsync(Player player, AttributeDefinition targetAttribute, ushort amount = 1)
{
if (player.SelectedCharacter is not { } selectedCharacter)
{
throw new InvalidOperationException("No character selected");
}
if (amount < 1)
{
throw new ArgumentOutOfRangeException(nameof(amount), "The amount must be greater than 0.");
}
if (!selectedCharacter.CanIncreaseStats(amount))
{
await player.ShowLocalizedBlueMessageAsync(PlayerMessage.NotEnoughLevelUpPointsAvailable).ConfigureAwait(false);
return;
}
var attributeDef = selectedCharacter.CharacterClass?.GetStatAttribute(targetAttribute);
if (attributeDef is { IncreasableByPlayer: true })
{
if (attributeDef.Attribute?.MaximumValue is { } maximumValue
&& player.Attributes![attributeDef.Attribute] is { } current
&& current + amount > maximumValue)
{
amount = (ushort)(maximumValue - current);
if (amount == 0)
{
await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.MaximumAttributeValueReachedFormat), attributeDef.Attribute?.MaximumValue, new LocalizedString(attributeDef.Attribute?.Designation).GetTranslation(player.Culture)).ConfigureAwait(false);
return;
}
}
player.Attributes![attributeDef.Attribute] += amount;
selectedCharacter.LevelUpPoints -= Math.Min(selectedCharacter.LevelUpPoints, amount);
await player.InvokeViewPlugInAsync<IStatIncreaseResultPlugIn>(p => p.StatIncreaseResultAsync(targetAttribute, amount)).ConfigureAwait(false);
}
else
{
await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.AttributeNotAvailable)).ConfigureAwait(false);
}
}
}

View File

@@ -0,0 +1,25 @@
// <copyright file="RequestCharacterListAction.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Character;
using MUnique.OpenMU.GameLogic.Views.Character;
/// <summary>
/// Action to request the character list.
/// </summary>
public class RequestCharacterListAction
{
/// <summary>
/// Requests the character list and advances the player state to <see cref="PlayerState.CharacterSelection"/>.
/// </summary>
/// <param name="player">The player who requests the character list.</param>
public async ValueTask RequestCharacterListAsync(Player player)
{
if (await player.PlayerState.TryAdvanceToAsync(PlayerState.CharacterSelection).ConfigureAwait(false))
{
await player.InvokeViewPlugInAsync<IShowCharacterListPlugIn>(p => p.ShowCharacterListAsync()).ConfigureAwait(false);
}
}
}

View File

@@ -0,0 +1,24 @@
// <copyright file="SaveKeyConfigurationAction.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Character;
/// <summary>
/// Action to save the players key configuration (hotkeys for skills, potions etc.).
/// </summary>
public class SaveKeyConfigurationAction
{
/// <summary>
/// Saves the key configuration.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="keyConfiguration">The key configuration.</param>
public void SaveKeyConfiguration(Player player, byte[] keyConfiguration)
{
if (player.SelectedCharacter is { } character)
{
character.KeyConfiguration = keyConfiguration;
}
}
}

View File

@@ -0,0 +1,34 @@
// <copyright file="SelectCharacterAction.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Character;
/// <summary>
/// Action to select a character and enter the world with it.
/// </summary>
public class SelectCharacterAction
{
/// <summary>
/// Selects the character and enters the world.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="characterName">Name of the character.</param>
public async ValueTask SelectCharacterAsync(Player player, string characterName)
{
using var loggerScope = player.Logger.BeginScope(this.GetType());
if (player.PlayerState.CurrentState != PlayerState.CharacterSelection)
{
player.Logger.LogError("Could not select character because of wrong current player state: {0}", player.PlayerState.CurrentState);
await player.DisconnectAsync().ConfigureAwait(false);
return;
}
await player.SetSelectedCharacterAsync(player.Account?.Characters.FirstOrDefault(c => c.Name.Equals(characterName))).ConfigureAwait(false);
if (player.SelectedCharacter is null)
{
player.Logger.LogError("Could not select character because character not found: [{0}]", characterName);
await player.DisconnectAsync().ConfigureAwait(false);
}
}
}

View File

@@ -0,0 +1,52 @@
// <copyright file="BannableChatMessageBaseProcessor.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Chat;
/// <summary>
/// A chat message processor for normal chat.
/// </summary>
public abstract class BannableChatMessageBaseProcessor : IChatMessageProcessor
{
/// <inheritdoc />
public async ValueTask ProcessMessageAsync(Player sender, (string Message, string PlayerName) content)
{
TimeSpan remainingChatBan = this.RemainingChatBanTimeSpan(sender);
if (this.IsSenderBanned(remainingChatBan))
{
if (remainingChatBan.TotalMinutes >= 1)
{
await sender.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.ChatBanMinutesRemaining), (int)Math.Ceiling(remainingChatBan.TotalMinutes)).ConfigureAwait(false);
}
else
{
await sender.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.ChatBanSecondsRemaining), (int)Math.Ceiling(remainingChatBan.TotalSeconds)).ConfigureAwait(false);
}
return;
}
await this.SubclassProcessMessageAsync(sender, content).ConfigureAwait(false);
}
/// <summary>
/// A method to be overriden for processing a specific chat message.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="content">The content.</param>
/// <returns>A value task with the result.</returns>
public abstract ValueTask SubclassProcessMessageAsync(Player sender, (string Message, string PlayerName) content);
private TimeSpan RemainingChatBanTimeSpan(Player sender)
{
DateTime chatBanUntil = sender.Account?.ChatBanUntil ?? default;
DateTime currentDateTime = DateTime.UtcNow;
return chatBanUntil - currentDateTime;
}
private bool IsSenderBanned(TimeSpan remainingChatBan)
{
return remainingChatBan > TimeSpan.Zero;
}
}

View File

@@ -0,0 +1,105 @@
// <copyright file="ChatMessageAction.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Chat;
using MUnique.OpenMU.GameLogic.Views;
/// <summary>
/// Action to send chat messages.
/// </summary>
public class ChatMessageAction
{
private readonly IDictionary<string, ChatMessageType> _messagePrefixes;
private readonly IDictionary<ChatMessageType, IChatMessageProcessor> _chatProcessMessages;
/// <summary>
/// Initializes a new instance of the <see cref="ChatMessageAction"/> class.
/// </summary>
public ChatMessageAction()
{
this._messagePrefixes = new SortedDictionary<string, ChatMessageType>(new ReverseComparer())
{
{ "~", ChatMessageType.Party },
{ "@", ChatMessageType.Guild },
{ "@@", ChatMessageType.Alliance },
{ "$", ChatMessageType.Gens },
{ "!", ChatMessageType.GlobalNotification },
{ "/", ChatMessageType.Command },
};
this._chatProcessMessages = new Dictionary<ChatMessageType, IChatMessageProcessor>
{
{ ChatMessageType.Command, new ChatMessageCommandProcessor() },
{ ChatMessageType.Whisper, new ChatMessageWhisperProcessor() },
{ ChatMessageType.Party, new ChatMessagePartyProcessor() },
{ ChatMessageType.Alliance, new ChatMessageAllianceProcessor() },
{ ChatMessageType.Guild, new ChatMessageGuildProcessor() },
{ ChatMessageType.GlobalNotification, new ChatMessageGlobalNotificationProcessor() },
{ ChatMessageType.Normal, new ChatMessageNormalProcessor() },
};
}
/// <summary>
/// Sends a chat message from the player to other players.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="playerName">Name of the <paramref name="sender"/>, except for <see cref="ChatMessageType.Whisper"/>, then its the receiver name.</param>
/// <param name="message">The message which should be sent.</param>
/// <param name="whisper">If set to <c>true</c> the message is whispered to the player with the <paramref name="playerName"/>; Otherwise, it's not a whisper.</param>
public async ValueTask ChatMessageAsync(Player sender, string playerName, string message, bool whisper)
{
using var loggerScope = sender.Logger.BeginScope(this.GetType());
ChatMessageType messageType = this.GetMessageType(message, whisper);
if (sender.SelectedCharacter is null)
{
// Is possible to receive null?
return;
}
if (messageType != ChatMessageType.Whisper && playerName != sender.SelectedCharacter?.Name)
{
sender.Logger.LogWarning("Maybe Hacker, Charname in chat packet != charname\t [{0}] <> [{1}]", sender.SelectedCharacter?.Name, playerName);
}
if (!this._chatProcessMessages.ContainsKey(messageType))
{
sender.Logger.LogDebug("Not implemented chat message type: {0}", messageType);
return;
}
await this._chatProcessMessages[messageType].ProcessMessageAsync(sender, (message, playerName)).ConfigureAwait(true);
}
private ChatMessageType GetMessageType(string message, bool whisper)
{
if (whisper)
{
return ChatMessageType.Whisper;
}
// byte 13: begin message
foreach (var keyValuePair in this._messagePrefixes)
{
if (message.StartsWith(keyValuePair.Key, StringComparison.InvariantCulture))
{
return keyValuePair.Value;
}
}
return ChatMessageType.Normal;
}
/// <summary>
/// We have to implement a reverse comparer, so that the strings which are longer, come first.
/// </summary>
private class ReverseComparer : IComparer<string>
{
public int Compare(string? x, string? y)
{
return string.Compare(y, x, StringComparison.InvariantCultureIgnoreCase);
}
}
}

View File

@@ -0,0 +1,33 @@
// <copyright file="ChatMessageAllianceProcessor.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Chat;
using System.ComponentModel;
using MUnique.OpenMU.GameLogic.PlugIns;
/// <summary>
/// A chat message processor for alliance chat.
/// </summary>
public class ChatMessageAllianceProcessor : BannableChatMessageBaseProcessor
{
/// <inheritdoc />
public override async ValueTask SubclassProcessMessageAsync(Player sender, (string Message, string PlayerName) content)
{
var eventArgs = new CancelEventArgs();
sender.GameContext.PlugInManager.GetPlugInPoint<IChatMessageReceivedPlugIn>()?.ChatMessageReceived(sender, content.Message, eventArgs);
if (eventArgs.Cancel)
{
return;
}
if (!(sender.GuildStatus != null && (sender.GameContext as IGameServerContext)?.EventPublisher is { } publisher))
{
return;
}
// TODO: Use DI to get the IEventPublisher
await publisher.AllianceMessageAsync(sender.GuildStatus.GuildId, sender.SelectedCharacter!.Name, content.Message).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,32 @@
// <copyright file="ChatMessageCommandProcessor.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Chat;
using MUnique.OpenMU.GameLogic.PlugIns.ChatCommands;
/// <summary>
/// A chat message processor which handles chat commands.
/// </summary>
public class ChatMessageCommandProcessor : IChatMessageProcessor
{
/// <inheritdoc />
public async ValueTask ProcessMessageAsync(Player sender, (string Message, string PlayerName) content)
{
var commandKey = content.Message.Split(' ').First();
var commandHandler = sender.GameContext.PlugInManager.GetStrategy<IChatCommandPlugIn>(commandKey);
if (commandHandler is null)
{
return;
}
if (sender.SelectedCharacter!.CharacterStatus < commandHandler.MinCharacterStatusRequirement)
{
sender.Logger.LogWarning($"{sender.Name} is trying to execute {commandKey} command without meeting the requirements");
return;
}
await commandHandler.HandleCommandAsync(sender, content.Message).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,33 @@
// <copyright file="ChatMessageGlobalNotificationProcessor.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Chat;
using System.ComponentModel;
using MUnique.OpenMU.GameLogic.PlugIns;
/// <summary>
/// A chat message processor which sends a global notification.
/// </summary>
public class ChatMessageGlobalNotificationProcessor : IChatMessageProcessor
{
/// <inheritdoc />
public async ValueTask ProcessMessageAsync(Player sender, (string Message, string PlayerName) content)
{
var eventArgs = new CancelEventArgs();
sender.GameContext.PlugInManager.GetPlugInPoint<IChatMessageReceivedPlugIn>()
?.ChatMessageReceived(sender, content.Message, eventArgs);
if (eventArgs.Cancel)
{
return;
}
if (sender.SelectedCharacter!.CharacterStatus < CharacterStatus.GameMaster)
{
return;
}
await sender.GameContext.SendGlobalNotificationAsync(content.Message).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,32 @@
// <copyright file="ChatMessageGuildProcessor.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Chat;
using System.ComponentModel;
using MUnique.OpenMU.GameLogic.PlugIns;
/// <summary>
/// A chat message processor which sends the message to the guild.
/// </summary>
public class ChatMessageGuildProcessor : BannableChatMessageBaseProcessor
{
/// <inheritdoc />
public override async ValueTask SubclassProcessMessageAsync(Player sender, (string Message, string PlayerName) content)
{
var eventArgs = new CancelEventArgs();
sender.GameContext.PlugInManager.GetPlugInPoint<IChatMessageReceivedPlugIn>()?.ChatMessageReceived(sender, content.Message, eventArgs);
if (eventArgs.Cancel)
{
return;
}
if (!(sender.GuildStatus != null && (sender.GameContext as IGameServerContext)?.EventPublisher is { } publisher))
{
return;
}
await publisher.GuildMessageAsync(sender.GuildStatus.GuildId, sender.SelectedCharacter!.Name, content.Message).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,20 @@
// <copyright file="ChatMessageNormalProcessor.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Chat;
using MUnique.OpenMU.GameLogic.Views;
/// <summary>
/// A chat message processor for normal chat.
/// </summary>
public class ChatMessageNormalProcessor : BannableChatMessageBaseProcessor
{
/// <inheritdoc/>
public override async ValueTask SubclassProcessMessageAsync(Player sender, (string Message, string PlayerName) content)
{
sender.Logger.LogDebug("Sending Chat Message to Observers, Count: {0}", sender.Observers.Count);
await sender.ForEachWorldObserverAsync<IChatViewPlugIn>(p => p.ChatMessageAsync(content.Message, sender.SelectedCharacter!.Name, ChatMessageType.Normal), true).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,27 @@
// <copyright file="ChatMessagePartyProcessor.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Chat;
using System.ComponentModel;
using MUnique.OpenMU.GameLogic.PlugIns;
/// <summary>
/// A chat message processor which sends the message to the party.
/// </summary>
public class ChatMessagePartyProcessor : BannableChatMessageBaseProcessor
{
/// <inheritdoc />
public override async ValueTask SubclassProcessMessageAsync(Player sender, (string Message, string PlayerName) content)
{
var eventArgs = new CancelEventArgs();
sender.GameContext.PlugInManager.GetPlugInPoint<IChatMessageReceivedPlugIn>()?.ChatMessageReceived(sender, content.Message, eventArgs);
if (eventArgs.Cancel)
{
return;
}
sender.Party?.SendChatMessageAsync(content.Message, sender.SelectedCharacter!.Name);
}
}

View File

@@ -0,0 +1,30 @@
// <copyright file="ChatMessageWhisperProcessor.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Chat;
using System.ComponentModel;
using MUnique.OpenMU.GameLogic.PlugIns;
using MUnique.OpenMU.GameLogic.Views;
/// <summary>
/// A chat message processor which sends the message to the whisper receiver.
/// </summary>
public class ChatMessageWhisperProcessor : BannableChatMessageBaseProcessor
{
/// <inheritdoc />
public override async ValueTask SubclassProcessMessageAsync(Player sender, (string Message, string PlayerName) content)
{
var whisperReceiver = sender.GameContext.GetPlayerByCharacterName(content.PlayerName);
if (whisperReceiver != null)
{
var eventArgs = new CancelEventArgs();
sender.GameContext.PlugInManager.GetPlugInPoint<IWhisperMessageReceivedPlugIn>()?.WhisperMessageReceived(sender, whisperReceiver, content.Message, eventArgs);
if (!eventArgs.Cancel)
{
await whisperReceiver.InvokeViewPlugInAsync<IChatViewPlugIn>(p => p.ChatMessageAsync(content.Message, sender.SelectedCharacter!.Name, ChatMessageType.Whisper)).ConfigureAwait(false);
}
}
}
}

View File

@@ -0,0 +1,19 @@
// <copyright file="IChatMessageProcessor.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Chat;
/// <summary>
/// Interface for a chat message processor.
/// </summary>
public interface IChatMessageProcessor
{
/// <summary>
/// Sends a chat message from the player to other players.
/// </summary>
/// <param name="sender" cref="Player">The sending Player.</param>
/// <param name="content">The chat message's content.</param>
/// <returns>The value task with the result.</returns>
ValueTask ProcessMessageAsync(Player sender, (string Message, string PlayerName) content);
}

View File

@@ -0,0 +1,50 @@
// <copyright file="CloseNpcDialogAction.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions;
using MUnique.OpenMU.GameLogic.Views.NPC;
/// <summary>
/// Action to close a npc dialog.
/// </summary>
public class CloseNpcDialogAction
{
private const ushort ChaosGoblinId = 238;
/// <summary>
/// Closes the currently opened npc dialog.
/// </summary>
/// <param name="player">The player who wants to close the dialog.</param>
public async ValueTask CloseNpcDialogAsync(Player player)
{
using var loggerScope = player.Logger.BeginScope(this.GetType());
var npc = player.OpenedNpc;
if (npc != null && await player.PlayerState.TryAdvanceToAsync(PlayerState.EnteredWorld).ConfigureAwait(false))
{
player.Logger.LogDebug($"Player {player.SelectedCharacter?.Name} closes NPC {player.OpenedNpc}");
player.OpenedNpc = null;
player.Vault = null;
await player.InvokeViewPlugInAsync<INpcDialogClosedPlugIn>(p => p.DialogClosedAsync(npc.Definition)).ConfigureAwait(false);
if (npc.Id == ChaosGoblinId)
{
try
{
await Task.Delay(1000).ConfigureAwait(false);
player.Logger.LogInformation("Saving changes after closing the chaos goblin ...");
await player.SaveProgressAsync().ConfigureAwait(false);
player.Logger.LogInformation("Saved changes after closing the chaos goblin ...");
}
catch (Exception ex)
{
player.Logger.LogError(ex, "Couldn't save changes after closing the chaos goblin for player {player}", player);
}
}
}
else
{
player.Logger.LogDebug($"Dialog of NPC {player.OpenedNpc} could not be closed by player {player.SelectedCharacter?.Name} because the player has the wrong state {player.PlayerState}");
}
}
}

View File

@@ -0,0 +1,122 @@
// <copyright file="BaseEventTicketCrafting.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Craftings;
using MUnique.OpenMU.GameLogic.PlayerActions.Items;
using MUnique.OpenMU.GameLogic.Views.NPC;
/// <summary>
/// Base class for a crafting of Event Tickets.
/// </summary>
public abstract class BaseEventTicketCrafting : BaseItemCraftingHandler
{
private readonly string _requiredEventItemName1;
private readonly string _requiredEventItemName2;
private readonly string _resultItemName;
/// <summary>
/// Initializes a new instance of the <see cref="BaseEventTicketCrafting" /> class.
/// </summary>
/// <param name="resultItemName">Name of the result item.</param>
/// <param name="requiredEventItemName1">The name of the first required item.</param>
/// <param name="requiredEventItemName2">The name of the second required item.</param>
protected BaseEventTicketCrafting(string resultItemName, string requiredEventItemName1, string requiredEventItemName2)
{
this._resultItemName = resultItemName;
this._requiredEventItemName1 = requiredEventItemName1;
this._requiredEventItemName2 = requiredEventItemName2;
}
/// <summary>
/// Gets the <see cref="CraftingResult"/> for a incorrect mix items result.
/// </summary>
protected virtual CraftingResult IncorrectMixItemsResult => CraftingResult.IncorrectMixItems;
/// <inheritdoc />
public override CraftingResult? TryGetRequiredItems(Player player, out IList<CraftingRequiredItemLink> itemLinks, out byte successRate)
{
successRate = 0;
itemLinks = new List<CraftingRequiredItemLink>(3);
var item1 = player.TemporaryStorage!.Items.FirstOrDefault(item => item.Definition?.Name.ValueInNeutralLanguage == this._requiredEventItemName1);
var item2 = player.TemporaryStorage.Items.FirstOrDefault(item => item.Definition?.Name.ValueInNeutralLanguage == this._requiredEventItemName2);
var chaos = player.TemporaryStorage.Items.FirstOrDefault(item => item.Definition?.Name.ValueInNeutralLanguage == "Jewel of Chaos");
if (item1 is null || item2 is null || item1.Level != item2.Level || chaos is null)
{
return this.IncorrectMixItemsResult;
}
itemLinks.Add(new CraftingRequiredItemLink(
item1.GetAsEnumerable(),
new TransientItemCraftingRequiredItem
{
PossibleItems = { item1.Definition! },
MaximumAmount = 1,
MinimumAmount = 1,
}));
itemLinks.Add(new CraftingRequiredItemLink(
item2.GetAsEnumerable(),
new TransientItemCraftingRequiredItem
{
PossibleItems = { item2.Definition! },
MaximumAmount = 1,
MinimumAmount = 1,
}));
itemLinks.Add(new CraftingRequiredItemLink(
chaos.GetAsEnumerable(),
new TransientItemCraftingRequiredItem
{
PossibleItems = { chaos.Definition! },
MaximumAmount = 1,
MinimumAmount = 1,
}));
successRate = this.GetSuccessRate(item1.Level);
return default;
}
/// <inheritdoc />
protected sealed override int GetPrice(byte successRate, IList<CraftingRequiredItemLink> requiredItems)
{
return this.GetPrice(this.GetEventLevel(requiredItems));
}
/// <summary>
/// Gets the price of the crafting for the specified event level.
/// </summary>
/// <param name="eventLevel">The event level.</param>
/// <returns>The price of the crafting for the specified event level.</returns>
protected abstract int GetPrice(int eventLevel);
/// <summary>
/// Gets the success rate of the crafting for the specified event level.
/// </summary>
/// <param name="eventLevel">The event level.</param>
/// <returns>The success rate of the crafting for the specified event level.</returns>
protected abstract byte GetSuccessRate(int eventLevel);
/// <inheritdoc />
protected override async ValueTask<List<Item>> CreateOrModifyResultItemsAsync(IList<CraftingRequiredItemLink> requiredItems, Player player, byte socketIndex, byte successRate)
{
var item = player.PersistenceContext.CreateNew<Item>();
item.Definition = player.GameContext.Configuration.Items.First(i => i.Name.ValueInNeutralLanguage == this._resultItemName);
item.Level = this.GetEventLevel(requiredItems);
item.Durability = 1;
if (player.TemporaryStorage is { } temporaryStorage)
{
await temporaryStorage!.AddItemAsync(item).ConfigureAwait(false);
}
return new List<Item> { item };
}
private byte GetEventLevel(IList<CraftingRequiredItemLink> requiredItems)
{
var item = requiredItems.First(ri => ri.Items.Any(i => i.Definition?.Name.ValueInNeutralLanguage == this._requiredEventItemName1));
return item.Items.First().Level;
}
}

View File

@@ -0,0 +1,46 @@
// <copyright file="BloodCastleTicketCrafting.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Craftings;
using MUnique.OpenMU.GameLogic.Views.NPC;
/// <summary>
/// Crafting for Blood Castle Tickets.
/// </summary>
public class BloodCastleTicketCrafting : BaseEventTicketCrafting
{
/// <summary>
/// Initializes a new instance of the <see cref="BloodCastleTicketCrafting"/> class.
/// </summary>
public BloodCastleTicketCrafting()
: base("Invisibility Cloak", "Scroll of Archangel", "Blood Bone")
{
}
/// <inheritdoc />
protected override CraftingResult IncorrectMixItemsResult => CraftingResult.IncorrectBloodCastleItems;
/// <inheritdoc />
protected override int GetPrice(int eventLevel)
{
return eventLevel switch
{
2 => 80_000,
3 => 150_000,
4 => 250_000,
5 => 400_000,
6 => 600_000,
7 => 850_000,
8 => 1_050_000,
_ => 50_000,
};
}
/// <inheritdoc />
protected override byte GetSuccessRate(int eventLevel)
{
return 80;
}
}

View File

@@ -0,0 +1,67 @@
// <copyright file="ChaosWeaponAndFirstWingsCrafting.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Craftings;
using MUnique.OpenMU.DataModel.Configuration.ItemCrafting;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.GameLogic.PlayerActions.Items;
/// <summary>
/// Crafting for Chaos Weapon and First Wings.
/// </summary>
public class ChaosWeaponAndFirstWingsCrafting : SimpleItemCraftingHandler
{
/// <summary>
/// Initializes a new instance of the <see cref="ChaosWeaponAndFirstWingsCrafting"/> class.
/// </summary>
/// <param name="settings">The settings.</param>
public ChaosWeaponAndFirstWingsCrafting(SimpleCraftingSettings settings)
: base(settings)
{
}
/// <inheritdoc/>
protected override void AddRandomItemOption(Item resultItem, Player player, byte successRate)
{
if (resultItem.Definition!.PossibleItemOptions.FirstOrDefault(o =>
o.PossibleOptions.Any(p => p.OptionType == ItemOptionTypes.Option))
is { } option)
{
int i = Rand.NextInt(0, 3);
if (Rand.NextRandomBool((successRate / 5) + (4 * (i + 1))))
{
var link = player.PersistenceContext.CreateNew<ItemOptionLink>();
link.ItemOption = option.PossibleOptions.First();
link.Level = 3 - i;
resultItem.ItemOptions.Add(link);
}
}
}
/// <inheritdoc/>
protected override void AddRandomLuckOption(Item resultItem, Player player, byte successRate)
{
if (Rand.NextRandomBool((successRate / 5) + 4)
&& resultItem.Definition!.PossibleItemOptions.FirstOrDefault(o =>
o.PossibleOptions.Any(po => po.OptionType == ItemOptionTypes.Luck))
is { } luck)
{
var luckOption = player.PersistenceContext.CreateNew<ItemOptionLink>();
luckOption.ItemOption = luck.PossibleOptions.First();
resultItem.ItemOptions.Add(luckOption);
}
}
/// <inheritdoc/>
protected override void AddRandomSkill(Item resultItem, byte successRate)
{
if (Rand.NextRandomBool((successRate / 5) + 6)
&& !resultItem.HasSkill
&& resultItem.Definition!.Skill is { })
{
resultItem.HasSkill = true;
}
}
}

View File

@@ -0,0 +1,40 @@
// <copyright file="DarkHorseCrafting.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Craftings;
using MUnique.OpenMU.DataModel.Configuration.ItemCrafting;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.GameLogic.PlayerActions.Items;
/// <summary>
/// Crafting for Dark Horse.
/// </summary>
public class DarkHorseCrafting : SimpleItemCraftingHandler
{
/// <summary>
/// Initializes a new instance of the <see cref="DarkHorseCrafting"/> class.
/// </summary>
/// <param name="settings">The settings.</param>
public DarkHorseCrafting(SimpleCraftingSettings settings)
: base(settings)
{
}
/// <inheritdoc/>
protected override void AddRandomItemOption(Item resultItem, Player player, byte successRate)
{
if (resultItem.Definition!.PossibleItemOptions.FirstOrDefault(o =>
o.PossibleOptions.Any(p => p.OptionType == ItemOptionTypes.DarkHorse))
is { } horseOptions)
{
foreach (var option in horseOptions.PossibleOptions)
{
var link = player.PersistenceContext.CreateNew<ItemOptionLink>();
link.ItemOption = option;
resultItem.ItemOptions.Add(link);
}
}
}
}

View File

@@ -0,0 +1,40 @@
// <copyright file="DevilSquareTicketCrafting.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Craftings;
/// <summary>
/// Crafting for Devil Square Event Tickets.
/// </summary>
public class DevilSquareTicketCrafting : BaseEventTicketCrafting
{
/// <summary>
/// Initializes a new instance of the <see cref="DevilSquareTicketCrafting"/> class.
/// </summary>
public DevilSquareTicketCrafting()
: base("Devil's Invitation", "Devil's Eye", "Devil's Key")
{
}
/// <inheritdoc />
protected override int GetPrice(int eventLevel)
{
return eventLevel switch
{
2 => 200000,
3 => 400000,
4 => 700000,
5 => 1100000,
6 => 1600000,
7 => 2000000,
_ => 100000,
};
}
/// <inheritdoc />
protected override byte GetSuccessRate(int eventLevel)
{
return (byte)(eventLevel < 5 ? 80 : 70); // Future to-do: There is a +10% increase if Crywolf event is beaten
}
}

View File

@@ -0,0 +1,88 @@
// <copyright file="DinorantCrafting.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Craftings;
using MUnique.OpenMU.DataModel.Configuration.ItemCrafting;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.PlayerActions.Items;
using MUnique.OpenMU.GameLogic.Views.NPC;
/// <summary>
/// Crafting for Dinorant.
/// </summary>
public class DinorantCrafting : SimpleItemCraftingHandler
{
/// <summary>
/// Initializes a new instance of the <see cref="DinorantCrafting"/> class.
/// </summary>
/// <param name="settings">The settings.</param>
public DinorantCrafting(SimpleCraftingSettings settings)
: base(settings)
{
}
/// <inheritdoc />
public override CraftingResult? TryGetRequiredItems(Player player, out IList<CraftingRequiredItemLink> items, out byte successRate)
{
var craftingResult = base.TryGetRequiredItems(player, out items, out successRate);
if (craftingResult is null)
{
var uniriaLink = items.Where(i => i.ItemRequirement.PossibleItems.Any(i => i.Name.ValueInNeutralLanguage == "Horn of Uniria"));
foreach (var item in uniriaLink.First().Items)
{
if (item.Durability < 255)
{
return CraftingResult.IncorrectMixItems;
}
}
}
return craftingResult;
}
/// <inheritdoc/>
protected override void AddRandomItemOption(Item resultItem, Player player, byte successRate)
{
if (Rand.NextRandomBool(30)
&& resultItem.Definition!.PossibleItemOptions.FirstOrDefault(o =>
o.PossibleOptions.Any(p => p.OptionType == ItemOptionTypes.Option))
is { } option)
{
var link = player.PersistenceContext.CreateNew<ItemOptionLink>();
link.ItemOption = option.PossibleOptions.SelectRandom();
resultItem.ItemOptions.Add(link);
// There is a second rollout for an additional bonus option to the first (but only if it doesn't coincide).
if (Rand.NextRandomBool(20))
{
var bonusOpt = option.PossibleOptions.SelectRandom();
if (bonusOpt != link.ItemOption)
{
var bonusLink = player.PersistenceContext.CreateNew<ItemOptionLink>();
bonusLink.ItemOption = bonusOpt;
resultItem.ItemOptions.Add(bonusLink);
}
}
}
// Dinorant options were originally coded within the normal item option; each has a different level.
foreach (var dinoOption in resultItem.ItemOptions)
{
if (dinoOption.ItemOption!.PowerUpDefinition!.TargetAttribute == Stats.DamageReceiveDecrement)
{
dinoOption.Level = 1;
}
else if (dinoOption.ItemOption!.PowerUpDefinition!.TargetAttribute == Stats.MaximumAbility)
{
dinoOption.Level = 2;
}
else
{
dinoOption.Level = 4;
}
}
}
}

View File

@@ -0,0 +1,122 @@
// <copyright file="FenrirUpgradeCrafting.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Craftings;
using MUnique.OpenMU.DataModel.Configuration.ItemCrafting;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.PlayerActions.Items;
using MUnique.OpenMU.GameLogic.Views.NPC;
/// <summary>
/// A crafting to upgrade a Red Fenrir to a Blue or Black one.
/// </summary>
public class FenrirUpgradeCrafting : BaseItemCraftingHandler
{
private readonly ItemPriceCalculator _priceCalculator = new();
/// <inheritdoc/>
public override CraftingResult? TryGetRequiredItems(Player player, out IList<CraftingRequiredItemLink> items, out byte successRateByItems)
{
successRateByItems = 0;
items = new List<CraftingRequiredItemLink>(4);
var inputItems = player.TemporaryStorage!.Items.ToList();
var itemsLevelAndOption4 = inputItems
.Where(item => item.Level >= 4
&& item.ItemOptions.Any(o => o.ItemOption?.OptionType == ItemOptionTypes.Option))
.ToList();
var randomWeapons = itemsLevelAndOption4
.Where(item => item.IsWearable()
&& item.Definition!.BasePowerUpAttributes.Any(a => a.TargetAttribute == Stats.AttackSpeedByWeapon))
.ToList();
var randomArmors = itemsLevelAndOption4
.Where(item => item.IsWearable()
&& item.Definition!.BasePowerUpAttributes.Any(a => a.TargetAttribute == Stats.DefenseBase))
.ToList();
if (randomArmors.Any() && randomWeapons.Any())
{
// Either Weapons or Armors, not both
return CraftingResult.IncorrectMixItems;
}
if (!randomArmors.Any() && !randomWeapons.Any())
{
return CraftingResult.LackingMixItems;
}
var hornOfFenrir = inputItems.FirstOrDefault(item => item.Definition?.Name.ValueInNeutralLanguage == "Horn of Fenrir");
var chaos = inputItems.FirstOrDefault(item => item.Definition?.Name.ValueInNeutralLanguage == "Jewel of Chaos");
var jewelsOfLife = inputItems.Where(item => item.Definition?.Name.ValueInNeutralLanguage == "Jewel of Life").Take(5).ToList();
if (hornOfFenrir is null
|| chaos is null
|| jewelsOfLife.Count < 5)
{
return CraftingResult.LackingMixItems;
}
inputItems.Remove(hornOfFenrir);
inputItems.Remove(chaos);
jewelsOfLife.ForEach(item => inputItems.Remove(item));
randomWeapons.ForEach(item => inputItems.Remove(item));
randomArmors.ForEach(item => inputItems.Remove(item));
if (inputItems.Any())
{
return CraftingResult.TooManyItems;
}
items.Add(new CraftingRequiredItemLink(hornOfFenrir.GetAsEnumerable(), new TransientItemCraftingRequiredItem { PossibleItems = { hornOfFenrir.Definition! }, MinimumAmount = 1, MaximumAmount = 1, Reference = 1, SuccessResult = MixResult.StaysAsIs }));
items.Add(new CraftingRequiredItemLink(chaos.GetAsEnumerable(), new TransientItemCraftingRequiredItem { PossibleItems = { chaos.Definition! }, MinimumAmount = 1, MaximumAmount = 1 }));
items.Add(new CraftingRequiredItemLink(jewelsOfLife, new TransientItemCraftingRequiredItem { PossibleItems = { jewelsOfLife.First().Definition! }, MinimumAmount = 5, MaximumAmount = 5 }));
if (randomWeapons.Any())
{
items.Add(new CraftingRequiredItemLink(randomWeapons, new TransientItemCraftingRequiredItem { MinimumAmount = 1, MaximumAmount = 1, Reference = 2 }));
successRateByItems = (byte)Math.Min(79, randomWeapons.Sum(this._priceCalculator.CalculateSellingPrice) * 100 / 1_000_000);
}
else
{
items.Add(new CraftingRequiredItemLink(randomArmors, new TransientItemCraftingRequiredItem { MinimumAmount = 1, MaximumAmount = 1, Reference = 3 }));
successRateByItems = (byte)Math.Min(79, randomArmors.Sum(this._priceCalculator.CalculateSellingPrice) * 100 / 1_000_000);
}
return null;
}
/// <inheritdoc />
protected override int GetPrice(byte successRate, IList<CraftingRequiredItemLink> requiredItems)
{
return 10_000_000;
}
/// <inheritdoc/>
protected override async ValueTask<List<Item>> CreateOrModifyResultItemsAsync(IList<CraftingRequiredItemLink> requiredItems, Player player, byte socketIndex, byte successRate)
{
var fenrir = requiredItems.First(i => i.ItemRequirement.Reference == 1).Items.First();
fenrir.Durability = 255;
IEnumerable<IncreasableItemOption> fenrirOptions;
if (requiredItems.Any(i => i.ItemRequirement.Reference == 2))
{
fenrirOptions = fenrir.Definition!.PossibleItemOptions.SelectMany(opt =>
opt.PossibleOptions.Where(o => o.OptionType == ItemOptionTypes.BlackFenrir));
}
else
{
fenrirOptions = fenrir.Definition!.PossibleItemOptions.SelectMany(opt =>
opt.PossibleOptions.Where(o => o.OptionType == ItemOptionTypes.BlueFenrir));
}
foreach (var option in fenrirOptions)
{
var optionLink = player.PersistenceContext.CreateNew<ItemOptionLink>();
optionLink.ItemOption = option;
fenrir.ItemOptions.Add(optionLink);
}
return new List<Item> { fenrir };
}
}

View File

@@ -0,0 +1,193 @@
// <copyright file="FenrirUpgradeCraftingGold.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Craftings;
using MUnique.OpenMU.DataModel.Configuration.ItemCrafting;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.PlayerActions.Items;
using MUnique.OpenMU.GameLogic.Views.NPC;
/// <summary>
/// A crafting to upgrade a Red Fenrir to a Blue, Black or Golden one.
/// A golden can be crafted by adding only excellent items to the mix.
/// </summary>
public class FenrirUpgradeCraftingGold : BaseItemCraftingHandler
{
private readonly ItemPriceCalculator _priceCalculator = new();
/// <inheritdoc/>
public override CraftingResult? TryGetRequiredItems(Player player, out IList<CraftingRequiredItemLink> items, out byte successRateByItems)
{
successRateByItems = 0;
items = new List<CraftingRequiredItemLink>(4);
var inputItems = player.TemporaryStorage!.Items.ToList();
var itemsLevelAndOption4gold = inputItems
.Where(item => item.Level >= 11
&& item.ItemOptions.Any(o => o.ItemOption?.OptionType == ItemOptionTypes.Option)
&& item.IsExcellent())
.ToList();
var itemsLevelAndOption4 = inputItems
.Where(item => (item.Level >= 11
&& item.ItemOptions.Any(o => o.ItemOption?.OptionType == ItemOptionTypes.Option)
&& !item.IsExcellent())
|| (item.Level >= 4
&& item.Level <= 10
&& item.ItemOptions.Any(o => o.ItemOption?.OptionType == ItemOptionTypes.Option)))
.ToList();
var randomWeapons = itemsLevelAndOption4
.Where(item => item.IsWearable()
&& item.Definition!.BasePowerUpAttributes.Any(a => a.TargetAttribute == Stats.AttackSpeedByWeapon))
.ToList();
var randomArmors = itemsLevelAndOption4
.Where(item => item.IsWearable()
&& item.Definition!.BasePowerUpAttributes.Any(a => a.TargetAttribute == Stats.DefenseBase))
.ToList();
var randomWeaponsGold = itemsLevelAndOption4gold
.Where(item => item.IsWearable()
&& item.Definition!.BasePowerUpAttributes.Any(a => a.TargetAttribute == Stats.AttackSpeedByWeapon))
.ToList();
var randomArmorsGold = itemsLevelAndOption4gold
.Where(item => item.IsWearable()
&& item.Definition!.BasePowerUpAttributes.Any(a => a.TargetAttribute == Stats.DefenseBase))
.ToList();
if (randomArmors.Any() && randomWeapons.Any())
{
// Either Weapons or Armors, not both
return CraftingResult.IncorrectMixItems;
}
if (randomArmors.Any() && randomWeaponsGold.Any())
{
// Either Weapons or Armors, not both
return CraftingResult.IncorrectMixItems;
}
if (randomArmors.Any() && randomArmorsGold.Any())
{
// Either Weapons or Armors, not both
return CraftingResult.IncorrectMixItems;
}
if (randomWeapons.Any() && randomWeaponsGold.Any())
{
// Either Weapons or Armors, not both
return CraftingResult.IncorrectMixItems;
}
if (randomWeapons.Any() && randomArmorsGold.Any())
{
// Either Weapons or Armors, not both
return CraftingResult.IncorrectMixItems;
}
if (randomArmorsGold.Any() && randomWeaponsGold.Any())
{
// Either Weapons or Armors, not both
return CraftingResult.IncorrectMixItems;
}
if (!(randomArmors.Any() || randomWeapons.Any() || randomArmorsGold.Any() || randomWeaponsGold.Any()))
{
return CraftingResult.LackingMixItems;
}
var hornOfFenrir = inputItems.FirstOrDefault(item => item.Definition?.Name.ValueInNeutralLanguage == "Horn of Fenrir");
var chaos = inputItems.FirstOrDefault(item => item.Definition?.Name.ValueInNeutralLanguage == "Jewel of Chaos");
var jewelsOfLife = inputItems.Where(item => item.Definition?.Name.ValueInNeutralLanguage == "Jewel of Life").Take(5).ToList();
if (hornOfFenrir is null
|| chaos is null
|| jewelsOfLife.Count < 5)
{
return CraftingResult.LackingMixItems;
}
inputItems.Remove(hornOfFenrir);
inputItems.Remove(chaos);
jewelsOfLife.ForEach(item => inputItems.Remove(item));
randomWeapons.ForEach(item => inputItems.Remove(item));
randomArmors.ForEach(item => inputItems.Remove(item));
randomWeaponsGold.ForEach(item => inputItems.Remove(item));
randomArmorsGold.ForEach(item => inputItems.Remove(item));
if (inputItems.Any())
{
return CraftingResult.TooManyItems;
}
items.Add(new CraftingRequiredItemLink(hornOfFenrir.GetAsEnumerable(), new TransientItemCraftingRequiredItem { PossibleItems = { hornOfFenrir.Definition! }, MinimumAmount = 1, MaximumAmount = 1, Reference = 1, SuccessResult = MixResult.StaysAsIs }));
items.Add(new CraftingRequiredItemLink(chaos.GetAsEnumerable(), new TransientItemCraftingRequiredItem { PossibleItems = { chaos.Definition! }, MinimumAmount = 1, MaximumAmount = 1 }));
items.Add(new CraftingRequiredItemLink(jewelsOfLife, new TransientItemCraftingRequiredItem { PossibleItems = { jewelsOfLife.First().Definition! }, MinimumAmount = 5, MaximumAmount = 5 }));
if (randomWeapons.Any())
{
items.Add(new CraftingRequiredItemLink(randomWeapons, new TransientItemCraftingRequiredItem { MinimumAmount = 1, MaximumAmount = 1, Reference = 2 }));
successRateByItems = (byte)Math.Min(79, randomWeapons.Sum(this._priceCalculator.CalculateSellingPrice) * 100 / 1_000_000);
}
if (randomArmors.Any())
{
items.Add(new CraftingRequiredItemLink(randomArmors, new TransientItemCraftingRequiredItem { MinimumAmount = 1, MaximumAmount = 1, Reference = 3 }));
successRateByItems = (byte)Math.Min(79, randomArmors.Sum(this._priceCalculator.CalculateSellingPrice) * 100 / 1_000_000);
}
if (randomWeaponsGold.Any())
{
items.Add(new CraftingRequiredItemLink(randomWeaponsGold, new TransientItemCraftingRequiredItem { MinimumAmount = 1, MaximumAmount = 1, Reference = 4 }));
successRateByItems = (byte)Math.Min(79, randomWeaponsGold.Sum(this._priceCalculator.CalculateSellingPrice) * 100 / 1_000_000);
}
if (randomArmorsGold.Any())
{
items.Add(new CraftingRequiredItemLink(randomArmorsGold, new TransientItemCraftingRequiredItem { MinimumAmount = 1, MaximumAmount = 1, Reference = 4 }));
successRateByItems = (byte)Math.Min(79, randomArmorsGold.Sum(this._priceCalculator.CalculateSellingPrice) * 100 / 1_000_000);
}
return null;
}
/// <inheritdoc />
protected override int GetPrice(byte successRate, IList<CraftingRequiredItemLink> requiredItems)
{
return 10_000_000;
}
/// <inheritdoc/>
protected override async ValueTask<List<Item>> CreateOrModifyResultItemsAsync(IList<CraftingRequiredItemLink> requiredItems, Player player, byte socketIndex, byte successRate)
{
var fenrir = requiredItems.First(i => i.ItemRequirement.Reference == 1).Items.First();
fenrir.Durability = 255;
IEnumerable<IncreasableItemOption> fenrirOptions;
if (requiredItems.Any(i => i.ItemRequirement.Reference == 2))
{
fenrirOptions = fenrir.Definition!.PossibleItemOptions.SelectMany(opt =>
opt.PossibleOptions.Where(o => o.OptionType == ItemOptionTypes.BlackFenrir));
}
else if (requiredItems.Any(i => i.ItemRequirement.Reference == 3))
{
fenrirOptions = fenrir.Definition!.PossibleItemOptions.SelectMany(opt =>
opt.PossibleOptions.Where(o => o.OptionType == ItemOptionTypes.BlueFenrir));
}
else
{
fenrirOptions = fenrir.Definition!.PossibleItemOptions.SelectMany(opt =>
opt.PossibleOptions.Where(o => o.OptionType == ItemOptionTypes.GoldFenrir));
}
foreach (var option in fenrirOptions)
{
var optionLink = player.PersistenceContext.CreateNew<ItemOptionLink>();
optionLink.ItemOption = option;
fenrir.ItemOptions.Add(optionLink);
}
return new List<Item> { fenrir };
}
}

View File

@@ -0,0 +1,74 @@
// <copyright file="GuardianOptionCrafting.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Craftings;
using MUnique.OpenMU.DataModel.Configuration.ItemCrafting;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.GameLogic.PlayerActions.Items;
using MUnique.OpenMU.GameLogic.Views.NPC;
/// <summary>
/// Crafting to add the Guardian Options (Level 380) to corresponding items.
/// </summary>
public class GuardianOptionCrafting : SimpleItemCraftingHandler
{
/// <summary>
/// Initializes a new instance of the <see cref="GuardianOptionCrafting"/> class.
/// </summary>
/// <param name="settings">The settings.</param>
public GuardianOptionCrafting(SimpleCraftingSettings settings)
: base(settings)
{
}
/// <summary>
/// Gets the reference to the affected item which must be specified in the <see cref="ItemCraftingRequiredItem.Reference"/>.
/// </summary>
public static byte ItemReference { get; } = 0x88;
/// <inheritdoc />
public override CraftingResult? TryGetRequiredItems(Player player, out IList<CraftingRequiredItemLink> items, out byte successRate)
{
if (base.TryGetRequiredItems(player, out items, out successRate) is { } error)
{
return error;
}
if (items.Where(i => i.ItemRequirement.Reference == ItemReference).Sum(i => i.Items.Count()) > 1)
{
return CraftingResult.TooManyItems;
}
return default;
}
/// <inheritdoc />
protected override async ValueTask<List<Item>> CreateOrModifyResultItemsAsync(IList<CraftingRequiredItemLink> requiredItems, Player player, byte socketSlot, byte successRate)
{
var item = requiredItems.First(i => i.ItemRequirement.Reference == ItemReference && i.Items.Any()).Items.First();
foreach (var optionDefinition in item.Definition!.PossibleItemOptions.First(o => o.PossibleOptions.Any(p => p.OptionType == ItemOptionTypes.GuardianOption)).PossibleOptions)
{
var optionLink = player.PersistenceContext.CreateNew<ItemOptionLink>();
optionLink.ItemOption = optionDefinition;
item.ItemOptions.Add(optionLink);
}
return new List<Item> { item };
}
/// <inheritdoc />
protected override bool RequiredItemMatches(Item item, ItemCraftingRequiredItem requiredItem)
{
if (requiredItem.Reference == 0)
{
return base.RequiredItemMatches(item, requiredItem);
}
return base.RequiredItemMatches(item, requiredItem)
&& item.Definition!.PossibleItemOptions.Any(o =>
o.PossibleOptions.Any(p => p.OptionType == ItemOptionTypes.GuardianOption))
&& item.ItemOptions.All(o => o.ItemOption!.OptionType != ItemOptionTypes.GuardianOption);
}
}

View File

@@ -0,0 +1,41 @@
// <copyright file="IllusionTempleTicketCrafting.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Craftings;
using MUnique.OpenMU.GameLogic.Views.NPC;
/// <summary>
/// Crafting for Illusion Temple Event Tickets.
/// </summary>
public class IllusionTempleTicketCrafting : BaseEventTicketCrafting
{
/// <summary>
/// Initializes a new instance of the <see cref="IllusionTempleTicketCrafting"/> class.
/// </summary>
public IllusionTempleTicketCrafting()
: base("Scroll of Blood", "Old Scroll", "Illusion Sorcerer Covenant")
{
}
/// <inheritdoc />
protected override int GetPrice(int eventLevel)
{
return eventLevel switch
{
2 => 5000000,
3 => 7000000,
4 => 9000000,
5 => 11000000,
6 => 13000000,
_ => 3000000,
};
}
/// <inheritdoc />
protected override byte GetSuccessRate(int eventLevel)
{
return 70;
}
}

View File

@@ -0,0 +1,164 @@
// <copyright file="MountSeedSphereCrafting.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Craftings;
using MUnique.OpenMU.DataModel.Configuration.ItemCrafting;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.GameLogic.PlayerActions.Items;
using MUnique.OpenMU.GameLogic.Views.NPC;
/// <summary>
/// Crafting to mount a seed sphere on a socket item.
/// </summary>
public class MountSeedSphereCrafting : SimpleItemCraftingHandler
{
/// <summary>
/// Initializes a new instance of the <see cref="MountSeedSphereCrafting"/> class.
/// </summary>
/// <param name="settings">The settings.</param>
public MountSeedSphereCrafting(SimpleCraftingSettings settings)
: base(settings)
{
}
/// <summary>
/// Gets the reference to the mounting seed sphere which must be specified in the <see cref="ItemCraftingRequiredItem.Reference"/>.
/// </summary>
public static byte SeedSphereReference { get; } = 0x77;
/// <summary>
/// Gets the reference to the socket item which must be specified in the <see cref="ItemCraftingRequiredItem.Reference"/>.
/// </summary>
public static byte SocketItemReference { get; } = 0x88;
/// <inheritdoc />
public override CraftingResult? TryGetRequiredItems(Player player, out IList<CraftingRequiredItemLink> items, out byte successRate)
{
var result = base.TryGetRequiredItems(player, out items, out successRate);
if (result != default)
{
return result;
}
// We need to check, if the seed sphere can be mounted on the item.
// Weapons: Fire, Lightning, Ice
// Armors: Water, Earth, Wind
var seedSphere = items.Single(i => i.ItemRequirement.Reference == SeedSphereReference).Items.Single();
var socketItem = items.Single(i => i.ItemRequirement.Reference == SocketItemReference).Items.Single();
seedSphere.ThrowNotInitializedProperty(seedSphere.Definition is null, nameof(seedSphere.Definition));
socketItem.ThrowNotInitializedProperty(socketItem.Definition is null, nameof(socketItem.Definition));
var seedOption = seedSphere.Definition.PossibleItemOptions.Single();
if (socketItem.Definition.PossibleItemOptions.All(iod => iod != seedOption))
{
return CraftingResult.IncorrectMixItems;
}
return null;
}
/// <inheritdoc />
protected override async ValueTask<List<Item>> CreateOrModifyResultItemsAsync(IList<CraftingRequiredItemLink> requiredItems, Player player, byte socketSlot, byte successRate)
{
var seedSphere = requiredItems.Single(i => i.ItemRequirement.Reference == SeedSphereReference).Items.Single();
var socketItem = requiredItems.Single(i => i.ItemRequirement.Reference == SocketItemReference).Items.Single();
if (socketItem.SocketCount <= socketSlot)
{
throw new ArgumentException($"The item has no socket at slot {socketSlot}.");
}
if (socketItem.ItemOptions.Any(link => link.Index == socketSlot && link.ItemOption?.OptionType == ItemOptionTypes.SocketOption))
{
throw new ArgumentException("The socket of the item is not free.");
}
seedSphere.ThrowNotInitializedProperty(seedSphere.Definition is null, nameof(seedSphere.Definition));
var sphereOption = player.PersistenceContext.CreateNew<ItemOptionLink>();
sphereOption.ItemOption = seedSphere.Definition.PossibleItemOptions
.SelectMany(o => o.PossibleOptions)
.Single(o => o.OptionType == ItemOptionTypes.SocketOption
&& o.Number == seedSphere.Level);
sphereOption.Level = seedSphere.Level;
sphereOption.Index = socketSlot;
socketItem.ItemOptions.Add(sphereOption);
var currentSocketOptionCount = socketItem.ItemOptions.Count(optionLink => optionLink.ItemOption?.OptionType == ItemOptionTypes.SocketOption);
if (currentSocketOptionCount == 3
&& Rand.NextRandomBool(30)
&& this.GetPossibleBonusOption(socketItem) is { } bonusOption)
{
var bonusOptionLink = player.PersistenceContext.CreateNew<ItemOptionLink>();
bonusOptionLink.ItemOption = bonusOption;
socketItem.ItemOptions.Add(bonusOptionLink);
}
return new List<Item> { socketItem };
}
private IncreasableItemOption? GetPossibleBonusOption(Item socketItem)
{
socketItem.ThrowNotInitializedProperty(socketItem.Definition is null, nameof(socketItem.Definition));
var possibleBonusOptions = socketItem.Definition.PossibleItemOptions
.FirstOrDefault(p => p.PossibleOptions.Any(o => o.OptionType == ItemOptionTypes.SocketBonusOption));
if (possibleBonusOptions is null)
{
return null;
}
var options = socketItem.ItemOptions
.Where(link => link.ItemOption?.OptionType == ItemOptionTypes.SocketOption && link.Index < 3)
.OrderBy(link => link.Index)
.Select(link => link.ItemOption!)
.Distinct()
.ToList();
if (options.Count < 3)
{
return null;
}
/* About the numbers:
0 Attack +11
1 Skill Attack Increase +11
2 Attack/Wiz +5
3 Skill Attack Increase +11
4 Defense Increase +24
5 Max Life +29
*/
if (options[0].SubOptionType == (int)SocketSubOptionType.Fire
&& options[1].SubOptionType == (int)SocketSubOptionType.Lightning
&& options[2].SubOptionType == (int)SocketSubOptionType.Ice)
{
return possibleBonusOptions.PossibleOptions.OrderBy(p => p.Number).FirstOrDefault();
}
if (options[0].SubOptionType == (int)SocketSubOptionType.Lightning
&& options[1].SubOptionType == (int)SocketSubOptionType.Ice
&& options[2].SubOptionType == (int)SocketSubOptionType.Fire)
{
return possibleBonusOptions.PossibleOptions.OrderBy(p => p.Number).LastOrDefault();
}
if (options[0].SubOptionType == (int)SocketSubOptionType.Water
&& options[1].SubOptionType == (int)SocketSubOptionType.Earth
&& options[2].SubOptionType == (int)SocketSubOptionType.Wind)
{
return possibleBonusOptions.PossibleOptions.OrderBy(p => p.Number).FirstOrDefault();
}
if (options[0].SubOptionType == (int)SocketSubOptionType.Earth
&& options[1].SubOptionType == (int)SocketSubOptionType.Wind
&& options[2].SubOptionType == (int)SocketSubOptionType.Water)
{
return possibleBonusOptions.PossibleOptions.OrderBy(p => p.Number).LastOrDefault();
}
return null;
}
}

View File

@@ -0,0 +1,177 @@
// <copyright file="RefineStoneCrafting.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Craftings;
using MUnique.OpenMU.DataModel.Configuration.ItemCrafting;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.GameLogic.PlayerActions.Items;
/// <summary>
/// Crafting which creates refine stones.
/// </summary>
public class RefineStoneCrafting : SimpleItemCraftingHandler
{
#region List of excluded Items (if item level < 4)
private readonly ISet<(byte Group, short Number)> _excludedItems = new HashSet<(byte, short)>
{
(0, 0), // Kris
(0, 1), // Short Sword
(0, 2), // Rapier
(0, 4), // Sword of Assassin
(1, 0), // Small Axe
(1, 1), // Hand Axe
(1, 2), // Double Axe
(2, 0), // Mace
(2, 1), // Morning Star
(2, 2), // Flail
(3, 1), // Spear
(3, 2), // Dragon Lance
(3, 3), // Giant Trident
(3, 5), // Double Poleaxe
(4, 0), // Short Bow
(4, 1), // Bow
(4, 2), // Elven Bow
(4, 3), // Battle Bow
(4, 8), // Crossbow
(4, 9), // Golden Crossbow
(4, 10), // Arquebus
(4, 11), // Light Crossbow
(5, 0), // Skull Staff
(5, 1), // Angelic Staff
(5, 2), // Serpent Staff
(6, 0), // Small Shield
(6, 1), // Horn Shield
(6, 2), // Kite Shield
(6, 3), // Elven Shield
(6, 4), // Buckler
(6, 6), // Skull Shield
(6, 7), // Spiked Shield
(6, 9), // Plate Shield
(6, 10), // Big Round Shield
(7, 0), // Bronze Helm
(7, 2), // Pad Helm
(7, 4), // Bone Helm
(7, 5), // Leather Helm
(7, 6), // Scale Helm
(7, 7), // Sphinx Mask
(7, 8), // Brass Helm
(7, 10), // Vine Helm
(7, 11), // Silk Helm
(7, 12), // Wind Helm
(8, 0), // Bronze Armor
(8, 2), // Pad Armor
(8, 4), // Bone Armor
(8, 5), // Leather Armor
(8, 6), // Scale Armor
(8, 7), // Sphinx Armor
(8, 8), // Brass Armor
(8, 10), // Vine Armor
(8, 11), // Silk Armor
(8, 12), // Wind Armor
(9, 0), // Bronze Pants
(9, 2), // Pad Pants
(9, 4), // Bone Pants
(9, 5), // Leather Pants
(9, 6), // Scale Pants
(9, 7), // Sphinx Pants
(9, 8), // Brass Pants
(9, 10), // Vine Pants
(9, 11), // Silk Pants
(9, 12), // Wind Pants
(10, 0), // Bronze Gloves
(10, 2), // Pad Gloves
(10, 4), // Bone Gloves
(10, 5), // Leather Gloves
(10, 6), // Scale Gloves
(10, 7), // Sphinx Gloves
(10, 8), // Brass Gloves
(10, 10), // Vine Gloves
(10, 11), // Silk Gloves
(10, 12), // Wind Gloves
(11, 0), // Bronze Boots
(11, 2), // Pad Boots
(11, 4), // Bone Boots
(11, 5), // Leather Boots
(11, 6), // Scale Boots
(11, 7), // Sphinx Boots
(11, 8), // Brass Boots
(11, 10), // Vine Boots
(11, 11), // Silk Boots
(11, 12), // Wind Boots
};
#endregion
/// <summary>
/// Initializes a new instance of the <see cref="RefineStoneCrafting"/> class.
/// </summary>
/// <param name="settings">The settings.</param>
public RefineStoneCrafting(SimpleCraftingSettings settings)
: base(settings)
{
}
/// <summary>
/// Gets the reference of the lower refine stone which must be specified in the <see cref="ItemCraftingRequiredItem.Reference"/>.
/// </summary>
public static byte LowerRefineStoneReference { get; } = 0x11;
/// <summary>
/// Gets the reference of the higher refine stone which must be specified in the <see cref="ItemCraftingRequiredItem.Reference"/>.
/// </summary>
public static byte HigherRefineStoneReference { get; } = 0x22;
/// <inheritdoc />
protected override bool RequiredItemMatches(Item item, ItemCraftingRequiredItem requiredItem)
{
return base.RequiredItemMatches(item, requiredItem)
&& item.IsWearable()
&& !item.IsAncient()
&& !item.ItemOptions.Any(o => o.ItemOption?.OptionType == ItemOptionTypes.HarmonyOption)
&& (!this._excludedItems.Contains((item.Definition!.Group, item.Definition.Number)) || item.Level > 3);
}
/// <inheritdoc />
protected override async ValueTask<List<Item>> CreateOrModifyResultItemsAsync(IList<CraftingRequiredItemLink> referencedItems, Player player, byte socketSlot, byte successRate)
{
var higherRefineStoneItems = referencedItems
.FirstOrDefault(r => r.ItemRequirement.Reference == HigherRefineStoneReference)?.Items.Count() ?? 0;
var lowerRefineStoneItems = referencedItems
.FirstOrDefault(r => r.ItemRequirement.Reference == LowerRefineStoneReference)?.Items.Count() ?? 0;
var result = new List<Item>();
if (higherRefineStoneItems > 0)
{
result.AddRange(await this.CreateRefineStonesAsync(higherRefineStoneItems, 50, 44, player).ConfigureAwait(false));
}
if (lowerRefineStoneItems > 0)
{
result.AddRange(await this.CreateRefineStonesAsync(lowerRefineStoneItems, 20, 43, player).ConfigureAwait(false));
}
return result;
}
private async Task<List<Item>> CreateRefineStonesAsync(int count, int chance, byte refineStoneNumber, Player player)
{
var createdItems = new List<Item>();
for (int i = 0; i < count; i++)
{
if (Rand.NextRandomBool(chance))
{
var refineStone = player.PersistenceContext.CreateNew<Item>();
refineStone.Definition = player.GameContext.Configuration.Items.First(item => item.Group == 14 && item.Number == refineStoneNumber);
refineStone.Durability = 1;
await player.TemporaryStorage!.AddItemAsync(refineStone).ConfigureAwait(false);
createdItems.Add(refineStone);
}
}
return createdItems;
}
}

View File

@@ -0,0 +1,54 @@
// <copyright file="RemoveSeedSphereCrafting.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Craftings;
using MUnique.OpenMU.DataModel.Configuration.ItemCrafting;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.GameLogic.PlayerActions.Items;
/// <summary>
/// Crafting to remove a mounted seed sphere from a socket item.
/// </summary>
public class RemoveSeedSphereCrafting : SimpleItemCraftingHandler
{
/// <summary>
/// Initializes a new instance of the <see cref="RemoveSeedSphereCrafting"/> class.
/// </summary>
/// <param name="settings">The settings.</param>
public RemoveSeedSphereCrafting(SimpleCraftingSettings settings)
: base(settings)
{
}
/// <summary>
/// Gets the reference to the socket item which must be specified in the <see cref="ItemCraftingRequiredItem.Reference"/>.
/// </summary>
public static byte SocketItemReference { get; } = 0x88;
/// <inheritdoc />
protected override async ValueTask<List<Item>> CreateOrModifyResultItemsAsync(IList<CraftingRequiredItemLink> requiredItems, Player player, byte socketSlot, byte successRate)
{
var socketItem = requiredItems.Single(i => i.ItemRequirement.Reference == SocketItemReference).Items.Single();
var socketOption = socketItem.ItemOptions
.FirstOrDefault(optionLink => optionLink.ItemOption?.OptionType == ItemOptionTypes.SocketOption && optionLink.Index == socketSlot);
if (socketOption is null)
{
throw new ArgumentException($"No seed sphere is mounted on the socket slot {socketSlot}.");
}
socketItem.ItemOptions.Remove(socketOption);
await player.PersistenceContext.DeleteAsync(socketOption).ConfigureAwait(false);
if (socketOption.Index < 3
&& socketItem.ItemOptions.FirstOrDefault(o => o.ItemOption?.OptionType == ItemOptionTypes.SocketBonusOption) is { } bonusOption)
{
socketItem.ItemOptions.Remove(bonusOption);
await player.PersistenceContext.DeleteAsync(bonusOption).ConfigureAwait(false);
}
return new List<Item> { socketItem };
}
}

View File

@@ -0,0 +1,45 @@
// <copyright file="RestoreItemCrafting.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Craftings;
using MUnique.OpenMU.DataModel.Configuration.ItemCrafting;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.GameLogic.PlayerActions.Items;
/// <summary>
/// Crafting which removes the <see cref="ItemOptionTypes.HarmonyOption"/> from an item.
/// </summary>
public class RestoreItemCrafting : SimpleItemCraftingHandler
{
/// <summary>
/// Initializes a new instance of the <see cref="RestoreItemCrafting"/> class.
/// </summary>
/// <param name="settings">The settings.</param>
public RestoreItemCrafting(SimpleCraftingSettings settings)
: base(settings)
{
}
private static int[] PricePerOptLvl => [100_000, 110_000, 120_000, 130_000, 140_000, 150_000, 200_000, 220_000, 240_000, 280_000, 320_000, 360_000, 400_000, 500_000];
/// <inheritdoc />
protected override int GetPrice(byte successRate, IList<CraftingRequiredItemLink> requiredItems)
{
return PricePerOptLvl[requiredItems.FirstOrDefault()?
.Items.FirstOrDefault()?
.ItemOptions.FirstOrDefault(io => io.ItemOption?.OptionType == ItemOptionTypes.HarmonyOption)?
.Level ?? 0];
}
/// <inheritdoc />
protected override async ValueTask<List<Item>> CreateOrModifyResultItemsAsync(IList<CraftingRequiredItemLink> requiredItems, Player player, byte socketSlot, byte successRate)
{
var item = requiredItems.First().Items.First();
var johOptionLink = item.ItemOptions.First(link => link.ItemOption?.OptionType == ItemOptionTypes.HarmonyOption);
item.ItemOptions.Remove(johOptionLink);
await player.PersistenceContext.DeleteAsync(johOptionLink).ConfigureAwait(false);
return new List<Item> { item };
}
}

View File

@@ -0,0 +1,55 @@
// <copyright file="SecondWingsCrafting.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Craftings;
using MUnique.OpenMU.DataModel.Configuration.ItemCrafting;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.GameLogic.PlayerActions.Items;
/// <summary>
/// Crafting for Second Wings (including first capes).
/// </summary>
public class SecondWingsCrafting : SimpleItemCraftingHandler
{
/// <summary>
/// Initializes a new instance of the <see cref="SecondWingsCrafting"/> class.
/// </summary>
/// <param name="settings">The settings.</param>
public SecondWingsCrafting(SimpleCraftingSettings settings)
: base(settings)
{
}
/// <inheritdoc/>
protected override void AddRandomItemOption(Item resultItem, Player player, byte successRate)
{
if (resultItem.Definition!.PossibleItemOptions.Where(o => o.PossibleOptions.Any(p => p.OptionType == ItemOptionTypes.Option))
is { } options && options.Any())
{
(int chance, int level) = Rand.NextInt(0, 3) switch
{
0 => (20, 1),
1 => (10, 2),
_ => (4, 3),
}; // From 300 created wings about 20+10+4=34 (~11%) will have item option
if (Rand.NextRandomBool(chance))
{
var link = player.PersistenceContext.CreateNew<ItemOptionLink>();
link.Level = level;
if (options.Count() > 1)
{
link.ItemOption = options.ElementAt(Rand.NextInt(0, 2)).PossibleOptions.First();
}
else
{
link.ItemOption = options.ElementAt(0).PossibleOptions.First(); // Cape of Lord
}
resultItem.ItemOptions.Add(link);
}
}
}
}

View File

@@ -0,0 +1,63 @@
// <copyright file="SeedSphereCrafting.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Craftings;
using MUnique.OpenMU.DataModel.Configuration.ItemCrafting;
using MUnique.OpenMU.GameLogic.PlayerActions.Items;
/// <summary>
/// Crafting for seed spheres.
/// Creates the corresponding seed sphere for the given sphere and seed,
/// sets the option of the seed and the level of the sphere to the resulting seed sphere.
/// </summary>
public class SeedSphereCrafting : SimpleItemCraftingHandler
{
/// <summary>
/// Initializes a new instance of the <see cref="SeedSphereCrafting"/> class.
/// </summary>
/// <param name="settings">The settings.</param>
public SeedSphereCrafting(SimpleCraftingSettings settings)
: base(settings)
{
}
/// <summary>
/// Gets the reference to the affected sphere which must be specified in the <see cref="ItemCraftingRequiredItem.Reference"/>.
/// </summary>
public static byte SphereReference { get; } = 0x66;
/// <summary>
/// Gets the reference to the affected seed which must be specified in the <see cref="ItemCraftingRequiredItem.Reference"/>.
/// </summary>
public static byte SeedReference { get; } = 0x77;
/// <inheritdoc />
protected override async ValueTask<List<Item>> CreateOrModifyResultItemsAsync(IList<CraftingRequiredItemLink> requiredItems, Player player, byte socketSlot, byte successRate)
{
var seed = requiredItems.Single(i => i.ItemRequirement.Reference == SeedReference).Items.Single();
var sphere = requiredItems.Single(i => i.ItemRequirement.Reference == SphereReference).Items.Single();
seed.ThrowNotInitializedProperty(seed.Definition is null, nameof(seed.Definition));
sphere.ThrowNotInitializedProperty(sphere.Definition is null, nameof(sphere.Definition));
// The following is a bit "magic", because it implicitly relies on the item data and how its built up.
// Because it has some structure, we can calculate which number the resulting seed sphere will have.
const int seedTypes = 6; // There are 6 different kind of seeds
const int seedNumberStart = 60;
const int sphereNumberStart = 70;
const int seedSphereNumberStart = 100;
var sphereLevel = sphere.Definition.Number - sphereNumberStart;
var resultSphereNumber = seedSphereNumberStart
+ (seed.Definition.Number - seedNumberStart)
+ (sphereLevel * seedTypes);
var result = player.PersistenceContext.CreateNew<Item>();
result.Definition = player.GameContext.Configuration.Items.Single(i => i.Number == resultSphereNumber && i.Group == 12);
result.Level = seed.Level; // The level defines the kind of option
await player.TemporaryStorage!.AddItemAsync(result).ConfigureAwait(false);
return new List<Item> { result };
}
}

View File

@@ -0,0 +1,83 @@
// <copyright file="ThirdWingsCrafting.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Craftings;
using MUnique.OpenMU.DataModel.Configuration.ItemCrafting;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.GameLogic.PlayerActions.Items;
/// <summary>
/// Crafting for Third Wings.
/// </summary>
public class ThirdWingsCrafting : SimpleItemCraftingHandler
{
/// <summary>
/// Initializes a new instance of the <see cref="ThirdWingsCrafting"/> class.
/// </summary>
/// <param name="settings">The settings.</param>
public ThirdWingsCrafting(SimpleCraftingSettings settings)
: base(settings)
{
}
/// <inheritdoc/>
protected override void AddRandomItemOption(Item resultItem, Player player, byte successRate)
{
if (resultItem.Definition!.PossibleItemOptions.Where(o => o.PossibleOptions.Any(p => p.OptionType == ItemOptionTypes.Option))
is { } options && options.Any())
{
(int chance1, int level) = Rand.NextInt(0, 4) switch
{
0 => (0, 0),
1 => (12, 1),
2 => (6, 2),
_ => (3, 3),
}; // From 400 created wings about 0+12+6+3=21 (~5%) will have item option
if (Rand.NextRandomBool(chance1))
{
var link = player.PersistenceContext.CreateNew<ItemOptionLink>();
link.Level = level;
(int chance2, int type) = Rand.NextRandomBool()
? (40, 1)
: (30, 2);
if (Rand.NextRandomBool(chance2))
{
link.ItemOption = options.ElementAt(type).PossibleOptions.First(); // Additional dmg (phys, wiz, curse) or defense
}
else
{
link.ItemOption = options.ElementAt(0).PossibleOptions.First(); // HP recovery %
}
resultItem.ItemOptions.Add(link);
}
}
}
/// <inheritdoc/>
protected override void AddRandomExcellentOptions(Item resultItem, Player player)
{
if (resultItem.Definition!.PossibleItemOptions.FirstOrDefault(o => o.PossibleOptions.Any(p => p.OptionType == ItemOptionTypes.Wing))
is { } wingOption)
{
(int chance, int type) = Rand.NextInt(0, 4) switch
{
0 => (4, 0), // Ignore def
1 => (2, 1), // 5% full reflect
2 => (7, 2), // 5% HP restore
_ => (7, 3), // 5% mana restore
}; // From 400 created wings about 4+2+7+7=20 (5%) will have wing "exc" option
if (Rand.NextRandomBool(chance))
{
var link = player.PersistenceContext.CreateNew<ItemOptionLink>();
link.ItemOption = wingOption.PossibleOptions.ElementAt(type);
resultItem.ItemOptions.Add(link);
}
}
}
}

View File

@@ -0,0 +1,24 @@
// <copyright file="TransientItemCraftingRequiredItem.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Craftings;
using MUnique.OpenMU.DataModel.Configuration.ItemCrafting;
using MUnique.OpenMU.DataModel.Configuration.Items;
/// <summary>
/// Transient implementation of <see cref="ItemCraftingRequiredItem"/>.
/// </summary>
/// <seealso cref="MUnique.OpenMU.DataModel.Configuration.ItemCrafting.ItemCraftingRequiredItem" />
internal sealed class TransientItemCraftingRequiredItem : ItemCraftingRequiredItem
{
/// <summary>
/// Initializes a new instance of the <see cref="TransientItemCraftingRequiredItem"/> class.
/// </summary>
public TransientItemCraftingRequiredItem()
{
this.PossibleItems = new List<ItemDefinition>();
this.RequiredItemOptions = new List<ItemOptionType>();
}
}

View File

@@ -0,0 +1,323 @@
// <copyright file="DuelActions.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Duel;
using Microsoft.Extensions.Logging;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.GameLogic.GuildWar;
using MUnique.OpenMU.GameLogic.PlugIns.ChatCommands;
using MUnique.OpenMU.GameLogic.Views.Duel;
/// <summary>
/// Actions regarding duels.
/// </summary>
public class DuelActions
{
/// <summary>
/// Handles a duel request.
/// </summary>
/// <param name="player">The player which sends the request.</param>
/// <param name="target">The target player which receives the request.</param>
public async ValueTask HandleDuelRequestAsync(Player player, Player target)
{
ArgumentNullException.ThrowIfNull(player);
ArgumentNullException.ThrowIfNull(target);
if (player.DuelRoom is not null)
{
await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.AlreadyInDuel)).ConfigureAwait(false);
return;
}
if (target.DuelRoom is not null)
{
await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.OtherPlayerAlreadyInDuel)).ConfigureAwait(false);
return;
}
if (!await CheckIfDuelCanBeStartedAsync(player, target).ConfigureAwait(false))
{
return;
}
if (await player.GameContext.DuelRoomManager.GetFreeDuelRoomAsync(player, target).ConfigureAwait(false) is not { } duelRoom)
{
await player.InvokeViewPlugInAsync<IShowDuelRequestResultPlugIn>(p => p.ShowDuelRequestResultAsync(DuelStartResult.FailedByNoFreeRoom, target)).ConfigureAwait(false);
return;
}
duelRoom.State = DuelState.DuelRequested;
player.DuelRoom = duelRoom;
target.DuelRoom = duelRoom;
await target.InvokeViewPlugInAsync<IShowDuelRequestPlugIn>(p => p.ShowDuelRequestAsync(player)).ConfigureAwait(false);
}
/// <summary>
/// Handles the duel response.
/// </summary>
/// <param name="player">The player which responds to the request.</param>
/// <param name="target">The target, which is the requester of the duel.</param>
/// <param name="accepted">If set to <c>true</c> the duel request was accepted by the player.</param>
public async ValueTask HandleDuelResponseAsync(Player player, Player target, bool accepted)
{
if (player.DuelRoom is not { } duelRoom)
{
player.Logger.LogWarning($"Player {player.Name} sent duel response, but has no context.");
return;
}
if (duelRoom != target.DuelRoom || duelRoom.Requester != target)
{
player.Logger.LogWarning($"Player {player.Name} sent duel response, but the duel contexts didn't match.");
return;
}
if (!await CheckIfDuelCanBeStartedAsync(player, target).ConfigureAwait(false))
{
await duelRoom.ResetAndDisposeAsync(DuelStartResult.FailedByError).ConfigureAwait(false);
return;
}
if (!accepted)
{
await duelRoom.ResetAndDisposeAsync(DuelStartResult.Refused).ConfigureAwait(false);
return;
}
var duelConfig = player.GameContext.Configuration.DuelConfiguration;
if (duelConfig is null)
{
player.Logger.LogError("Duel configuration is not set.");
await duelRoom.ResetAndDisposeAsync(DuelStartResult.FailedByError).ConfigureAwait(false);
return;
}
if (duelConfig.DuelAreas.FirstOrDefault(area => area.Index == duelRoom.Index) is not { } duelArea)
{
player.Logger.LogError("Duel area with index {index} was not found.", duelRoom.Index);
await duelRoom.ResetAndDisposeAsync(DuelStartResult.FailedByError).ConfigureAwait(false);
return;
}
if (duelArea.FirstPlayerGate is null || duelArea.SecondPlayerGate is null)
{
player.Logger.LogError("Duel area with index {index} has missing exit gates.", duelRoom.Index);
await duelRoom.ResetAndDisposeAsync(DuelStartResult.FailedByError).ConfigureAwait(false);
return;
}
// we risk that the money is not sufficient anymore,
// but we don't care anymore if it fails.
player.TryRemoveMoney(duelConfig.EntranceFee);
target.TryRemoveMoney(duelConfig.EntranceFee);
duelRoom.State = DuelState.DuelAccepted;
await duelRoom.Requester.WarpToAsync(duelArea.FirstPlayerGate).ConfigureAwait(false);
await duelRoom.Opponent.WarpToAsync(duelArea.SecondPlayerGate).ConfigureAwait(false);
_ = Task.Run(duelRoom.RunDuelAsync);
}
/// <summary>
/// Handles the duel stop request of a player.
/// </summary>
/// <param name="player">The player which requests to stop the duel.</param>
public async ValueTask HandleStopDuelRequestAsync(Player player)
{
var duelRoom = player.DuelRoom;
if (duelRoom is null)
{
player.Logger.LogWarning($"Player {player.Name} sent request to stop the duel, but it has no duel active.");
return;
}
if (duelRoom.State is DuelState.DuelFinished)
{
player.Logger.LogWarning($"Player {player.Name} sent request to stop the duel, but it is already finished.");
return;
}
await duelRoom.CancelDuelAsync().ConfigureAwait(false);
}
/// <summary>
/// Handles the duel channel join request asynchronous.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="requestedDuelIndex">Index of the requested duel.</param>
/// <returns>The value task with the result.</returns>
public async ValueTask HandleDuelChannelJoinRequestAsync(Player player, byte requestedDuelIndex)
{
var config = player.GameContext.Configuration.DuelConfiguration;
if (config is null)
{
player.Logger.LogError("Duel configuration is not set.");
return;
}
if (player.GameContext.DuelRoomManager.GetRoomByIndex(requestedDuelIndex) is not { } duelRoom)
{
player.Logger.LogWarning($"Player {player.Name} tried to join duel channel with index {requestedDuelIndex}, but it doesn't exist.");
return;
}
if (duelRoom.Spectators.Count >= config.MaximumSpectatorsPerDuelRoom)
{
player.Logger.LogWarning($"Player {player.Name} tried to join duel channel with index {requestedDuelIndex}, but it is full.");
await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.DuelChannelIsFull)).ConfigureAwait(false);
return;
}
// move to duel map
var spectatorsGate = config.DuelAreas.FirstOrDefault(area => area.Index == requestedDuelIndex)?.SpectatorsGate;
if (spectatorsGate is null)
{
player.Logger.LogError("Duel area with index {index} was not found or has missing spectators gate.", requestedDuelIndex);
return;
}
if (!await duelRoom.TryAddSpectatorAsync(player).ConfigureAwait(false))
{
await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.DuelChannelIsFull)).ConfigureAwait(false);
}
}
/// <summary>
/// Handles the duel channel quit request asynchronous.
/// </summary>
/// <param name="player">The player.</param>
/// <returns>The value task with the result.</returns>
public async ValueTask HandleDuelChannelQuitRequestAsync(Player player)
{
if (player.DuelRoom is not { } duelRoom)
{
return;
}
if (duelRoom.IsDuelist(player))
{
// todo: log this attempt?
return;
}
if (player.GameContext.Configuration.DuelConfiguration is not { } config)
{
return;
}
if (config.DuelAreas.FirstOrDefault(area => area.Index == duelRoom.Index) is not { } area)
{
return;
}
var isInDuelMap = area.SpectatorsGate?.Map == player.CurrentMap?.Definition;
if (!isInDuelMap)
{
return;
}
if (config.Exit is { Map: not null } exitGate)
{
await player.WarpToAsync(exitGate).ConfigureAwait(false);
}
else
{
await player.WarpToSafezoneAsync().ConfigureAwait(false);
}
await player.RemoveInvisibleEffectAsync().ConfigureAwait(false);
await duelRoom.RemoveSpectatorAsync(player).ConfigureAwait(false);
}
private static async ValueTask<bool> CheckIfDuelCanBeStartedAsync(Player player, Player target)
{
if (player == target)
{
player.Logger.LogError("Player requested a duel with himself.");
return false;
}
if (player.SelectedCharacter is not { } selectedCharacter)
{
player.Logger.LogError("Player requested a duel while not selecting a character.");
return false;
}
if (target.SelectedCharacter is not { } targetCharacter)
{
// target logged out in the mean time.
return false;
}
if (player.CurrentMap != target.CurrentMap)
{
await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.DuelRequestMustBeOnSameMap)).ConfigureAwait(false);
return false;
}
if (player.CurrentMiniGame is not null)
{
await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.NoDuelDuringMiniGame)).ConfigureAwait(false);
return false;
}
if (selectedCharacter.State >= HeroState.PlayerKiller2ndStage)
{
await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.NoDuelWhileBeingPlayerKiller)).ConfigureAwait(false);
return false;
}
if (targetCharacter.State >= HeroState.PlayerKiller2ndStage)
{
await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.NoDuelWithPlayerKiller)).ConfigureAwait(false);
return false;
}
if (player.GuildWarContext?.State is GuildWarState.Requested or GuildWarState.Started
|| target.GuildWarContext?.State is GuildWarState.Requested or GuildWarState.Started)
{
await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.NoDuelDuringGuildWar)).ConfigureAwait(false);
return false;
}
if (player.IsAnySelfDefenseActive()
|| target.IsAnySelfDefenseActive())
{
await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.NoDuelDuringSelfDefense)).ConfigureAwait(false);
return false;
}
if (player.OpenedNpc is not null
|| target.OpenedNpc is not null)
{
await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.NoDuelWithOpenedNpcDialog)).ConfigureAwait(false);
return false;
}
if (player.PlayerState.CurrentState != PlayerState.EnteredWorld
|| target.PlayerState.CurrentState != PlayerState.EnteredWorld)
{
await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.NoDuelWithWrongState)).ConfigureAwait(false);
return false;
}
var duelConfig = player.GameContext.Configuration.DuelConfiguration;
if (player.Level < duelConfig?.MinimumCharacterLevel || target.Level < duelConfig?.MinimumCharacterLevel)
{
await player.InvokeViewPlugInAsync<IShowDuelRequestResultPlugIn>(p => p.ShowDuelRequestResultAsync(DuelStartResult.FailedByTooLowLevel, target)).ConfigureAwait(false);
return false;
}
if (player.Money < duelConfig?.EntranceFee || target.Money < duelConfig?.EntranceFee)
{
await player.InvokeViewPlugInAsync<IShowDuelRequestResultPlugIn>(p => p.ShowDuelRequestResultAsync(DuelStartResult.FailedByNotEnoughMoney, target)).ConfigureAwait(false);
return false;
}
return true;
}
}

View File

@@ -0,0 +1,65 @@
// <copyright file="EndDuelWhenLeavingDuelMapPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Duel;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Ends the duel when a player leaves the duel map.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.EndDuelWhenLeavingDuelMapPlugIn_Name), Description = nameof(PlugInResources.EndDuelWhenLeavingDuelMapPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("3DF85180-4C51-437A-8072-8F42EEFED983")]
public class EndDuelWhenLeavingDuelMapPlugIn : IObjectRemovedFromMapPlugIn
{
private readonly DuelActions _duelActions = new();
/// <summary>
/// Handles the logic for when a player leaves the duel map.
/// </summary>
/// <param name="map">The game map.</param>
/// <param name="removedObject">The player who left.</param>
/// <returns>The value task with the result.</returns>
public async ValueTask ObjectRemovedFromMapAsync(GameMap map, ILocateable removedObject)
{
if (removedObject is not Player player)
{
return;
}
if (player.DuelRoom is not { } duelRoom)
{
return;
}
// When respawning during the duel, don't end the duel
var removedFromDuelMap = duelRoom.Area.FirstPlayerGate?.Map == map.Definition;
if (removedFromDuelMap
&& duelRoom.IsDuelist(player)
&& duelRoom.State is (DuelState.DuelStarted or DuelState.DuelAccepted)
&& player.IsAlive)
{
await duelRoom.CancelDuelAsync().ConfigureAwait(false);
return;
}
if (duelRoom.IsDuelist(player))
{
return;
}
var config = player.GameContext.Configuration.DuelConfiguration;
var isInDuelMap = config?.DuelAreas.Any(area => area.SpectatorsGate?.Map == player.CurrentMap?.Definition) ?? false;
if (!isInDuelMap)
{
return;
}
await duelRoom.RemoveSpectatorAsync(player).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,47 @@
// <copyright file="UpdateDuelScorePlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Duel;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// This plugin increases the score of the duel result, when a kill occurred.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.UpdateDuelScorePlugIn_Name), Description = nameof(PlugInResources.UpdateDuelScorePlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("C5CAC184-F8A9-41A0-A34E-04A07DB81F5E")]
public class UpdateDuelScorePlugIn : IAttackableGotKilledPlugIn
{
/// <summary>
/// Is called when an <see cref="IAttackable" /> object got killed by another.
/// </summary>
/// <param name="killed">The killed <see cref="IAttackable" />.</param>
/// <param name="killer">The killer.</param>
public async ValueTask AttackableGotKilledAsync(IAttackable killed, IAttacker? killer)
{
if (killer is Player { DuelRoom: not null } killerPlayer
&& killed is Player { DuelRoom: not null } killedPlayer
&& killerPlayer.DuelRoom == killedPlayer.DuelRoom)
{
var duelRoom = killerPlayer.DuelRoom;
using var l = await duelRoom.Lock.LockAsync();
if (duelRoom.State is not DuelState.DuelStarted)
{
return;
}
if (duelRoom.Requester == killerPlayer)
{
duelRoom.ScoreRequester++;
}
else
{
duelRoom.ScoreOpponent++;
}
}
}
}

View File

@@ -0,0 +1,29 @@
// <copyright file="EnterBalgassRefugeeAction.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions;
/// <summary>
/// Action to enter the barracks of balgass through an npc.
/// </summary>
public class EnterBalgassRefugeeAction : EnterQuestMapAction
{
private const byte IntoTheDarknessZoneQuestNumber = 6;
private const byte LegacyQuestGroup = 0;
private const short GatekeepterNpcNumber = 408;
private const short BalgassRefugeeMapNumber = 42;
/// <summary>
/// Initializes a new instance of the <see cref="EnterBalgassRefugeeAction"/> class.
/// </summary>
public EnterBalgassRefugeeAction()
: base(
GatekeepterNpcNumber,
0,
BalgassRefugeeMapNumber,
LegacyQuestGroup,
IntoTheDarknessZoneQuestNumber)
{
}
}

View File

@@ -0,0 +1,31 @@
// <copyright file="EnterBarracksOfBalgassAction.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions;
/// <summary>
/// Action to enter the barracks of balgass through an npc.
/// </summary>
public class EnterBarracksOfBalgassAction : EnterQuestMapAction
{
private const byte InfiltrationOfBarracksOfBalgassQuestNumber = 5;
private const byte IntoTheDarknessZoneQuestNumber = 6;
private const byte LegacyQuestGroup = 0;
private const short WerewolfNpcNumber = 407;
private const short BarracksOfBalgassMapNumber = 41;
/// <summary>
/// Initializes a new instance of the <see cref="EnterBarracksOfBalgassAction"/> class.
/// </summary>
public EnterBarracksOfBalgassAction()
: base(
WerewolfNpcNumber,
3000000,
BarracksOfBalgassMapNumber,
LegacyQuestGroup,
InfiltrationOfBarracksOfBalgassQuestNumber,
IntoTheDarknessZoneQuestNumber)
{
}
}

View File

@@ -0,0 +1,54 @@
// <copyright file="EnterMarketPlaceAction.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions;
using MUnique.OpenMU.DataModel.Configuration;
/// <summary>
/// Action which warps a player between Lorencia and the Loren Market, triggered by the 'Warp'
/// button of the Market Union Member Julia window. It warps both ways, depending on which side
/// the player is currently on.
/// </summary>
public class EnterMarketPlaceAction
{
/// <summary>
/// The map number of the Loren Market.
/// </summary>
private const short LorenMarketMapNumber = 79;
/// <summary>
/// The map number of Lorencia, where the player is warped back to.
/// </summary>
private const short LorenciaMapNumber = 0;
/// <summary>
/// Warps the player between Lorencia and the Loren Market through Market Union Member Julia.
/// </summary>
/// <param name="player">The player who requested the warp.</param>
public async ValueTask WarpToMarketPlaceAsync(Player player)
{
// Only allow the warp through the actual Julia window, so a crafted packet can't be used
// as a free teleport from anywhere.
if (player.OpenedNpc?.Definition.NpcWindow != NpcWindow.JuliaWarpMarketServer)
{
return;
}
// Julia warps both ways: from the Loren Market back to Lorencia, and from anywhere else
// (her counterpart in Lorencia) into the Loren Market.
var targetMapNumber = player.CurrentMap?.Definition.Number == LorenMarketMapNumber
? LorenciaMapNumber
: LorenMarketMapNumber;
var targetMap = await player.GameContext.GetMapAsync((ushort)targetMapNumber).ConfigureAwait(false);
if (targetMap?.SafeZoneSpawnGate is not { } targetGate)
{
return;
}
player.OpenedNpc = null;
await player.WarpToAsync(targetGate).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,98 @@
// <copyright file="EnterQuestMapAction.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions;
using MUnique.OpenMU.GameLogic.PlayerActions.Quests;
/// <summary>
/// Abstract base class for actions to enter maps which are only available when
/// in a specific quest.
/// </summary>
public abstract class EnterQuestMapAction
{
private readonly byte _questGroup;
private readonly short[] _questNumbers;
private readonly short _npcNumber;
private readonly int _price;
private readonly short _targetMapNumber;
/// <summary>
/// Initializes a new instance of the <see cref="EnterQuestMapAction"/> class.
/// </summary>
/// <param name="npcNumber">The NPC number.</param>
/// <param name="price">The price.</param>
/// <param name="targetMapNumber">The target map number.</param>
/// <param name="questGroup">The quest group.</param>
/// <param name="questNumbers">The quest numbers.</param>
protected EnterQuestMapAction(short npcNumber, int price, short targetMapNumber, byte questGroup, params short[] questNumbers)
{
this._questGroup = questGroup;
this._questNumbers = questNumbers;
this._npcNumber = npcNumber;
this._price = price;
this._targetMapNumber = targetMapNumber;
}
/// <summary>
/// Tries the enter quest map.
/// </summary>
/// <param name="player">The player.</param>
public async ValueTask TryEnterQuestMapAsync(Player player)
{
var openedNpc = player.OpenedNpc;
if (openedNpc?.Definition?.Number != this._npcNumber)
{
player.Logger.LogWarning($"NPC {this._npcNumber} not opened.");
return;
}
var activeQuestNumber = player.GetQuestState(this._questGroup)?.ActiveQuest?.Number;
if (activeQuestNumber is null)
{
player.Logger.LogWarning("No quest is active.");
return;
}
if (!this._questNumbers.Contains(activeQuestNumber.Value))
{
player.Logger.LogWarning($"Quest {activeQuestNumber} does not qualify to enter the map.");
return;
}
var targetMap = player.GameContext.Configuration.Maps.FirstOrDefault(m => m.Number == this._targetMapNumber);
if (targetMap is null)
{
player.Logger.LogError($"Map {this._targetMapNumber} wasn't found in the game configuration.");
return;
}
var targetGate = targetMap.ExitGates.FirstOrDefault();
if (targetGate is null)
{
player.Logger.LogError("Map {targetMap} has no exit gate", targetMap.Name.ValueInNeutralLanguage);
return;
}
if (this._price > 0 && !player.TryRemoveMoney(this._price))
{
player.Logger.LogError($"Not enough money to enter the map.");
await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.NotEnoughMoneyToEnter), targetMap.Name.GetTranslation(player.Culture)).ConfigureAwait(false);
return;
}
var partyPlayers = player.Party?.PartyList.Where(p => p.IsInRange(player.Position, 10)).OfType<Player>().ToList();
if (partyPlayers is null)
{
await player.WarpToAsync(targetGate).ConfigureAwait(false);
return;
}
foreach (var partyPlayer in partyPlayers)
{
await partyPlayer.WarpToAsync(targetGate).ConfigureAwait(false);
}
}
}

View File

@@ -0,0 +1,54 @@
// <copyright file="GameMapDefinitionExtensions.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions;
using System.Diagnostics.CodeAnalysis;
/// <summary>
/// Extension methods for <see cref="GameMapDefinition"/>.
/// </summary>
public static class GameMapDefinitionExtensions
{
/// <summary>
/// Checks the <see cref="GameMapDefinition.MapRequirements"/> for the specified player.
/// </summary>
/// <param name="gameMapDefinition">The game map definition.</param>
/// <param name="player">The player.</param>
/// <param name="errorMessage">The error message, which is available when this method returns <c>true</c>.</param>
/// <returns><c>False</c>, if the requirements are fulfilled; Otherwise, <c>true</c>.</returns>
public static bool TryGetRequirementError(this GameMapDefinition gameMapDefinition, Player player, [MaybeNullWhen(false)] out string errorMessage)
{
errorMessage = null;
if (gameMapDefinition.MapRequirements is null || !gameMapDefinition.MapRequirements.Any())
{
return false;
}
foreach (var requirement in gameMapDefinition.MapRequirements)
{
if (player.Attributes is null || player.Attributes[requirement.Attribute] < requirement.MinimumValue)
{
errorMessage = player.GetLocalizedMessage(PlayerMessage.MissingMapRequirement, requirement.Attribute?.Description);
return true;
}
}
return false;
}
/// <summary>
/// Gets the safezone gate of a map.
/// </summary>
/// <param name="gameMapDefinition">The game map definition.</param>
/// <param name="terrain">The terrain, if available.</param>
/// <returns>The safezone gate of a map.</returns>
public static ExitGate? GetSafezoneGate(this GameMapDefinition gameMapDefinition, GameMapTerrain? terrain = null)
{
terrain ??= new GameMapTerrain(gameMapDefinition);
return gameMapDefinition.ExitGates?.FirstOrDefault(g => g.IsSpawnGate && terrain.SafezoneMap[g.X1, g.Y1])
?? gameMapDefinition.ExitGates?.FirstOrDefault(g => g.IsSpawnGate);
}
}

View File

@@ -0,0 +1,52 @@
// <copyright file="GuildCreateAction.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Guild;
using MUnique.OpenMU.GameLogic.Views.Guild;
/// <summary>
/// Action to create a guild.
/// </summary>
public class GuildCreateAction
{
/// <summary>
/// Creates the guild.
/// </summary>
/// <param name="creator">The creator.</param>
/// <param name="guildName">Name of the guild.</param>
/// <param name="guildEmblem">The guild emblem.</param>
public async ValueTask CreateGuildAsync(Player creator, string guildName, byte[] guildEmblem)
{
using var loggerScope = creator.Logger.BeginScope(this.GetType());
if (creator.PlayerState.CurrentState != PlayerState.EnteredWorld)
{
creator.Logger.LogError($"Account {creator.Account?.LoginName} not in the right state, but {creator.PlayerState.CurrentState}.");
return;
}
var guildServer = (creator.GameContext as IGameServerContext)?.GuildServer;
if (guildServer is null)
{
creator.Logger.LogError($"No guild server available");
return;
}
if (await guildServer.GuildExistsAsync(guildName).ConfigureAwait(false))
{
await creator.InvokeViewPlugInAsync<IShowGuildCreateResultPlugIn>(p => p.ShowGuildCreateResultAsync(GuildCreateErrorDetail.GuildAlreadyExist)).ConfigureAwait(false);
return;
}
if (await guildServer.CreateGuildAsync(guildName, creator.SelectedCharacter!.Name, creator.SelectedCharacter.Id, guildEmblem, ((IGameServerContext)creator.GameContext).Id).ConfigureAwait(false))
{
await creator.InvokeViewPlugInAsync<IShowGuildCreateResultPlugIn>(p => p.ShowGuildCreateResultAsync(GuildCreateErrorDetail.None)).ConfigureAwait(false);
creator.Logger.LogInformation("Guild created: [{0}], Master: [{1}]", guildName, creator.SelectedCharacter.Name);
}
else
{
await creator.InvokeViewPlugInAsync<IShowGuildCreateResultPlugIn>(p => p.ShowGuildCreateResultAsync(GuildCreateErrorDetail.GuildAlreadyExist)).ConfigureAwait(false);
}
}
}

View File

@@ -0,0 +1,23 @@
// <copyright file="GuildInfoRequestAction.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Guild;
using MUnique.OpenMU.GameLogic.Views.Guild;
/// <summary>
/// Action to request the information (name, symbol) of a guild.
/// </summary>
public class GuildInfoRequestAction
{
/// <summary>
/// Requests the guild information.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="guildId">The guild identifier.</param>
public async ValueTask RequestGuildInfoAsync(Player player, uint guildId)
{
await player.InvokeViewPlugInAsync<IShowGuildInfoPlugIn>(p => p.ShowGuildInfoAsync(guildId)).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,71 @@
// <copyright file="GuildKickPlayerAction.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Guild;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.Guild;
using MUnique.OpenMU.Interfaces;
/// <summary>
/// Action to kick a player out of a guild.
/// </summary>
public class GuildKickPlayerAction
{
/// <summary>
/// Kicks the player out of the guild.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="nickname">The nickname.</param>
/// <param name="securityCode">The security code.</param>
public async ValueTask KickPlayerAsync(Player player, string nickname, string securityCode)
{
using var loggerScope = player.Logger.BeginScope(this.GetType());
if (player.PlayerState.CurrentState != PlayerState.EnteredWorld)
{
player.Logger.LogError($"Account {player.Account?.LoginName} not in the right state, but {player.PlayerState.CurrentState}.");
return;
}
if (player.GuildStatus is null)
{
player.Logger.LogError($"Player {player} not in a guild.");
return;
}
var guildServer = (player.GameContext as IGameServerContext)?.GuildServer;
if (guildServer is null)
{
player.Logger.LogWarning("No guild server available");
return;
}
if (player.Account!.SecurityCode != null && player.Account.SecurityCode != securityCode)
{
await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.WrongSecurityCode)).ConfigureAwait(false);
player.Logger.LogDebug("Wrong Security Code: [{0}] <> [{1}], Player: {2}", securityCode, player.Account.SecurityCode, player.SelectedCharacter?.Name);
await player.InvokeViewPlugInAsync<IGuildKickResultPlugIn>(p => p.GuildKickResultAsync(GuildKickSuccess.Failed)).ConfigureAwait(false);
return;
}
var isKickingHimself = player.SelectedCharacter!.Name == nickname;
if (!isKickingHimself && player.GuildStatus?.Position != GuildPosition.GuildMaster)
{
player.Logger.LogWarning("Suspicious kick request for player with name: {0} (player is not a guild master) to kick {1}, could be hack attempt.", player.Name, nickname);
await player.InvokeViewPlugInAsync<IGuildKickResultPlugIn>(p => p.GuildKickResultAsync(GuildKickSuccess.FailedBecausePlayerIsNotGuildMaster)).ConfigureAwait(false);
return;
}
if (isKickingHimself && player.GuildStatus?.Position == GuildPosition.GuildMaster)
{
var guildId = player.GuildStatus.GuildId;
await player.InvokeViewPlugInAsync<IGuildKickResultPlugIn>(p => p.GuildKickResultAsync(GuildKickSuccess.GuildDisband)).ConfigureAwait(false);
await guildServer.KickMemberAsync(guildId, nickname).ConfigureAwait(false);
return;
}
await guildServer.KickMemberAsync(player.GuildStatus!.GuildId, nickname).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,33 @@
// <copyright file="GuildListRequestAction.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Guild;
using MUnique.OpenMU.GameLogic.Views.Guild;
/// <summary>
/// Action to request the guild list.
/// </summary>
public class GuildListRequestAction
{
/// <summary>
/// Requests the guild list of the guild the player is currently part of.
/// </summary>
/// <param name="player">The player.</param>
public async ValueTask RequestGuildListAsync(Player player)
{
if (player.GuildStatus is null)
{
return;
}
// TODO: We may want to retrieve guild and guild members in one call, to avoid multiple calls. But for now, we can live with that.
if ((player.GameContext as IGameServerContext)?.GuildServer is { } guildServer
&& await guildServer.GetGuildAsync(player.GuildStatus.GuildId).ConfigureAwait(false) is { } guild)
{
var players = await guildServer.GetGuildListAsync(player.GuildStatus.GuildId).ConfigureAwait(false);
await player.InvokeViewPlugInAsync<IShowGuildListPlugIn>(p => p.ShowGuildListAsync(players, guild)).ConfigureAwait(false);
}
}
}

View File

@@ -0,0 +1,50 @@
// <copyright file="GuildMasterAnswerAction.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Guild;
using MUnique.OpenMU.GameLogic.Views.Guild;
/// <summary>
/// Action to answer the dialog of the guild master npc.
/// </summary>
public class GuildMasterAnswerAction
{
/// <summary>
/// Type of the answer.
/// </summary>
public enum Answer
{
/// <summary>
/// Cancels the guild master npc dialog.
/// </summary>
Cancel = 0,
/// <summary>
/// The guild master npc dialog should be shown.
/// </summary>
ShowDialog = 1,
}
/// <summary>
/// Processes the answer.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="answer">The answer.</param>
public async ValueTask ProcessAnswerAsync(Player player, Answer answer)
{
if (player.PlayerState.CurrentState == PlayerState.EnteredWorld && answer == Answer.ShowDialog)
{
await player.InvokeViewPlugInAsync<IShowGuildCreationDialogPlugIn>(p => p.ShowGuildCreationDialogAsync()).ConfigureAwait(false);
}
else if (player.OpenedNpc?.Definition.NpcWindow == NpcWindow.GuildMaster && await player.PlayerState.TryAdvanceToAsync(PlayerState.EnteredWorld).ConfigureAwait(false))
{
player.OpenedNpc = null;
}
else
{
// nothing to do.
}
}
}

View File

@@ -0,0 +1,228 @@
// <copyright file="GuildRelationshipChangeAction.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Guild;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.Guild;
using MUnique.OpenMU.Interfaces;
/// <summary>
/// Action which handles guild relationship changes (alliance creation/removal and hostility).
/// </summary>
public class GuildRelationshipChangeAction
{
/// <summary>
/// Handles an incoming relationship change request from a guild master.
/// Validates and forwards the request to the target guild master.
/// </summary>
/// <param name="player">The player requesting the relationship change.</param>
/// <param name="targetPlayerId">The player id of the target guild master.</param>
/// <param name="relationshipType">The type of relationship change (Alliance or Hostility).</param>
/// <param name="requestType">The type of request (Join or Leave).</param>
public async ValueTask RequestAsync(Player player, ushort targetPlayerId, GuildRelationshipType relationshipType, GuildRelationshipRequestType requestType)
{
var (success, (sourceGuildId, serverContext, sourceGuild)) = await this.CommonChecksAsync(player, targetPlayerId, relationshipType, requestType).ConfigureAwait(false);
if (!success)
{
return;
}
// Find the target player
var targetPlayer = await player.GetObservingPlayerWithIdAsync(targetPlayerId).ConfigureAwait(false);
if (targetPlayer?.GuildStatus is not { } targetGuildStatus
|| await serverContext.GuildServer.GetGuildAsync(targetGuildStatus.GuildId).ConfigureAwait(false) is not { Name: not null } targetGuild)
{
await player.InvokeViewPlugInAsync<IGuildRelationshipChangeResultPlugIn>(p => p.ShowResultAsync(relationshipType, requestType, GuildRelationshipChangeResultType.Failed, targetPlayerId)).ConfigureAwait(false);
return;
}
if (targetGuildStatus.Position != GuildPosition.GuildMaster)
{
await player.InvokeViewPlugInAsync<IGuildRelationshipChangeResultPlugIn>(p => p.ShowResultAsync(relationshipType, requestType, GuildRelationshipChangeResultType.Failed, targetPlayerId)).ConfigureAwait(false);
return;
}
if (relationshipType == GuildRelationshipType.Hostility)
{
if (requestType == GuildRelationshipRequestType.Join
&& (targetGuild.Hostility is not null || sourceGuild.Hostility is not null))
{
await player.InvokeViewPlugInAsync<IGuildRelationshipChangeResultPlugIn>(p => p.ShowResultAsync(relationshipType, requestType, GuildRelationshipChangeResultType.AlreadyInHostility, targetPlayerId)).ConfigureAwait(false);
return;
}
if (requestType == GuildRelationshipRequestType.Leave
&& targetGuild.Hostility?.Name != sourceGuild.Name
&& sourceGuild.Hostility?.Name != targetGuild.Name)
{
await player.InvokeViewPlugInAsync<IGuildRelationshipChangeResultPlugIn>(p => p.ShowResultAsync(relationshipType, requestType, GuildRelationshipChangeResultType.HostileGuildDoesNotExist, targetPlayerId)).ConfigureAwait(false);
return;
}
}
if (relationshipType == GuildRelationshipType.Alliance
&& requestType == GuildRelationshipRequestType.Join)
{
if (targetGuild.AllianceGuild is not null)
{
await player.InvokeViewPlugInAsync<IGuildRelationshipChangeResultPlugIn>(p => p.ShowResultAsync(relationshipType, requestType, GuildRelationshipChangeResultType.AlreadyInAlliance, targetPlayerId)).ConfigureAwait(false);
return;
}
if (sourceGuild.AllianceGuild is not null && sourceGuild.AllianceGuild != sourceGuild)
{
// Request is not done by the master of the alliance, but by the master of a sub-guild
await player.InvokeViewPlugInAsync<IGuildRelationshipChangeResultPlugIn>(p => p.ShowResultAsync(relationshipType, requestType, GuildRelationshipChangeResultType.NoAuthorization, targetPlayerId)).ConfigureAwait(false);
return;
}
// Check the maximum alliance size configured for this game version.
// A value of 0 means no limit is configured (e.g. for game versions that pre-date alliances).
var maxAllianceSize = (int)(player.Attributes?[Stats.MaximumAllianceSize] ?? 0);
if (maxAllianceSize > 0)
{
var allianceGuilds = await serverContext.GuildServer.GetAllianceGuildsAsync(sourceGuildId).ConfigureAwait(false);
// Compute the size of the alliance after the target guild would be added.
// When there is no alliance yet (count == 0), the source guild itself is the first
// member, so the resulting alliance would have 2 guilds (source + target).
var sizeAfterAdding = allianceGuilds.Count == 0 ? 2 : allianceGuilds.Count + 1;
if (sizeAfterAdding > maxAllianceSize)
{
await player.InvokeViewPlugInAsync<IGuildRelationshipChangeResultPlugIn>(p => p.ShowResultAsync(relationshipType, requestType, GuildRelationshipChangeResultType.MaximumNumberOfGuildsInAllianceReached, targetPlayerId)).ConfigureAwait(false);
return;
}
}
}
// Store the pending request on the target player and ask for consent
targetPlayer.PendingAllianceRequest = (player, relationshipType, requestType);
await targetPlayer.InvokeViewPlugInAsync<IShowGuildRelationshipRequestPlugIn>(p => p.ShowRequestAsync(
player,
relationshipType,
requestType)).ConfigureAwait(false);
}
/// <summary>
/// Handles an incoming relationship leave request from a guild master.
/// Validates the request and processes the leave action.
/// </summary>
/// <param name="player">The player requesting the relationship change.</param>
/// <param name="targetGuildName">The name of the guild which should be removed. If <see langword="null"/>, then the own guild should be removed.</param>
public async ValueTask RequestLeaveAllianceAsync(Player player, string? targetGuildName = null)
{
var (success, (sourceGuildId, serverContext, sourceGuild)) = await this.CommonChecksAsync(player, 0, GuildRelationshipType.Alliance, GuildRelationshipRequestType.Leave).ConfigureAwait(false);
if (!success)
{
return;
}
var targetGuildId = sourceGuildId;
var leaveWithOwnGuild = string.IsNullOrEmpty(targetGuildName) || sourceGuild.Name == targetGuildName;
if (!leaveWithOwnGuild)
{
if (!await serverContext.GuildServer.IsAllianceMasterAsync(sourceGuildId).ConfigureAwait(false))
{
await player.InvokeViewPlugInAsync<IGuildRelationshipChangeResultPlugIn>(p => p.ShowResultAsync(GuildRelationshipType.Alliance, GuildRelationshipRequestType.Leave, GuildRelationshipChangeResultType.NoAuthorization, null)).ConfigureAwait(false);
return;
}
targetGuildId = await serverContext.GuildServer.GetGuildIdByNameAsync(targetGuildName!).ConfigureAwait(false);
}
var removeSuccess = await serverContext.GuildServer.RemoveAllianceAsync(targetGuildId).ConfigureAwait(false);
await player.InvokeViewPlugInAsync<IGuildRelationshipChangeResultPlugIn>(p => p.ShowRemoveResultAsync(removeSuccess)).ConfigureAwait(false);
}
/// <summary>
/// Processes the response from the target guild master to an alliance request.
/// </summary>
/// <param name="player">The target guild master who is responding.</param>
/// <param name="relationshipType">The type of relationship change (Alliance or Hostility).</param>
/// <param name="requestType">The type of request (Join or Leave).</param>
/// <param name="accepted">Whether the relationship change was accepted.</param>
public async ValueTask ProcessResponseAsync(Player player, GuildRelationshipType relationshipType, GuildRelationshipRequestType requestType, bool accepted)
{
var pending = player.PendingAllianceRequest;
var (requester, pendingRelationshipType, pendingRequestType) = pending;
player.PendingAllianceRequest = default;
if (pendingRelationshipType != relationshipType || pendingRequestType != requestType)
{
// No pending request or mismatch in the request details, ignore the response, leave with default state
return;
}
if (requester is null
|| requester.GuildStatus is not { } requesterGuildStatus
|| player.GuildStatus is not { } responderGuildStatus
|| player.GameContext is not IGameServerContext serverContext)
{
return;
}
var guildMasterId = player.GetId(requester);
if (!accepted)
{
await requester.InvokeViewPlugInAsync<IGuildRelationshipChangeResultPlugIn>(p => p.ShowResultAsync(relationshipType, requestType, GuildRelationshipChangeResultType.RequestCancelled, guildMasterId)).ConfigureAwait(false);
return;
}
var res = pending switch
{
(_, GuildRelationshipType.Alliance, GuildRelationshipRequestType.Join) =>
await serverContext.GuildServer.CreateAllianceAsync(requesterGuildStatus.GuildId, responderGuildStatus.GuildId).ConfigureAwait(false)
switch
{
AllianceCreationResult.Success => GuildRelationshipChangeResultType.Success,
AllianceCreationResult.MasterGuildNotFound or AllianceCreationResult.TargetGuildNotFound => GuildRelationshipChangeResultType.GuildNotFound,
AllianceCreationResult.TargetGuildAlreadyInAlliance => GuildRelationshipChangeResultType.AlreadyInAlliance,
AllianceCreationResult.MaximumAllianceSizeReached => GuildRelationshipChangeResultType.MaximumNumberOfGuildsInAllianceReached,
_ => GuildRelationshipChangeResultType.Failed,
},
(_, GuildRelationshipType.Hostility, _) => await serverContext.GuildServer.SetHostilityAsync(requesterGuildStatus.GuildId, responderGuildStatus.GuildId, pendingRequestType == GuildRelationshipRequestType.Join).ConfigureAwait(false)
? GuildRelationshipChangeResultType.Success
: GuildRelationshipChangeResultType.Failed,
_ => GuildRelationshipChangeResultType.Failed,
};
await requester.InvokeViewPlugInAsync<IGuildRelationshipChangeResultPlugIn>(p => p.ShowResultAsync(relationshipType, requestType, res, guildMasterId)).ConfigureAwait(false);
await player.InvokeViewPlugInAsync<IGuildRelationshipChangeResultPlugIn>(p => p.ShowResultAsync(relationshipType, requestType, res, guildMasterId)).ConfigureAwait(false);
}
private async ValueTask<(bool Success, GuildData GuildData)> CommonChecksAsync(Player player, ushort? targetPlayerId, GuildRelationshipType relationshipType, GuildRelationshipRequestType requestType)
{
if (player.PendingAllianceRequest != default)
{
// There is already a pending request, so we cannot process another one at the moment. This can happen with multiple requests from different players.
await player.InvokeViewPlugInAsync<IGuildRelationshipChangeResultPlugIn>(p => p.ShowResultAsync(relationshipType, requestType, GuildRelationshipChangeResultType.RequestCancelled, targetPlayerId)).ConfigureAwait(false);
return (false, null!);
}
if (player.GuildStatus is not { } guildStatus
|| player.GameContext is not IGameServerContext serverContext)
{
await player.InvokeViewPlugInAsync<IGuildRelationshipChangeResultPlugIn>(p => p.ShowResultAsync(relationshipType, requestType, GuildRelationshipChangeResultType.Failed, targetPlayerId)).ConfigureAwait(false);
return (false, null!);
}
if (guildStatus.Position != GuildPosition.GuildMaster)
{
await player.InvokeViewPlugInAsync<IGuildRelationshipChangeResultPlugIn>(p => p.ShowResultAsync(relationshipType, requestType, GuildRelationshipChangeResultType.NoAuthorization, targetPlayerId)).ConfigureAwait(false);
return (false, null!);
}
var sourceGuild = await serverContext.GuildServer.GetGuildAsync(guildStatus.GuildId).ConfigureAwait(false);
if (sourceGuild is null)
{
await player.InvokeViewPlugInAsync<IGuildRelationshipChangeResultPlugIn>(p => p.ShowResultAsync(relationshipType, requestType, GuildRelationshipChangeResultType.GuildNotFound, targetPlayerId)).ConfigureAwait(false);
return (false, null!);
}
return (true, new(guildStatus.GuildId, serverContext, sourceGuild));
}
private record GuildData(uint GuildId, IGameServerContext Context, Guild Guild);
}

View File

@@ -0,0 +1,51 @@
// <copyright file="GuildRequestAction.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Guild;
using MUnique.OpenMU.GameLogic.Views.Guild;
using MUnique.OpenMU.Interfaces;
/// <summary>
/// Action to request guild membership from a guild master player.
/// </summary>
public class GuildRequestAction
{
/// <summary>
/// Requests the guild from the guild master player with the specified id.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="guildMasterId">The guild master identifier.</param>
public async ValueTask RequestGuildAsync(Player player, ushort guildMasterId)
{
if (player.Level < 6)
{
await player.InvokeViewPlugInAsync<IGuildJoinResponsePlugIn>(p => p.ShowGuildJoinResponseAsync(GuildRequestAnswerResult.MinimumLevel6)).ConfigureAwait(false);
return;
}
if (player.GuildStatus is not null)
{
await player.InvokeViewPlugInAsync<IGuildJoinResponsePlugIn>(p => p.ShowGuildJoinResponseAsync(GuildRequestAnswerResult.AlreadyHaveGuild)).ConfigureAwait(false);
return;
}
var guildMaster = player.CurrentMap?.GetObject(guildMasterId) as Player;
if (guildMaster?.GuildStatus?.Position != GuildPosition.GuildMaster)
{
await player.InvokeViewPlugInAsync<IGuildJoinResponsePlugIn>(p => p.ShowGuildJoinResponseAsync(GuildRequestAnswerResult.NotTheGuildMaster)).ConfigureAwait(false);
return; // targeted player not in a guild or not the guild master
}
if (guildMaster.LastGuildRequester != null || player.PlayerState.CurrentState != PlayerState.EnteredWorld)
{
await player.InvokeViewPlugInAsync<IGuildJoinResponsePlugIn>(p => p.ShowGuildJoinResponseAsync(GuildRequestAnswerResult.GuildMasterOrRequesterIsBusy)).ConfigureAwait(false);
return;
}
guildMaster.LastGuildRequester = player;
await guildMaster.InvokeViewPlugInAsync<IShowGuildJoinRequestPlugIn>(p => p.ShowGuildJoinRequestAsync(player)).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,62 @@
// <copyright file="GuildRequestAnswerAction.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Guild;
using MUnique.OpenMU.GameLogic.Views.Guild;
using MUnique.OpenMU.Interfaces;
/// <summary>
/// Action for a guild master player to answer the guild membership request.
/// </summary>
public class GuildRequestAnswerAction
{
/// <summary>
/// Answers the request.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="accept">If set to <c>true</c>, the membership has been accepted. Otherwise, not.</param>
public async ValueTask AnswerRequestAsync(Player player, bool accept)
{
using var loggerScope = player.Logger.BeginScope(this.GetType());
var guildServer = (player.GameContext as IGameServerContext)?.GuildServer;
if (guildServer is null)
{
return;
}
var lastGuildRequester = player.LastGuildRequester;
if (lastGuildRequester?.SelectedCharacter is null)
{
return;
}
if (lastGuildRequester.GuildStatus is not null)
{
await lastGuildRequester.InvokeViewPlugInAsync<IGuildJoinResponsePlugIn>(p => p.ShowGuildJoinResponseAsync(GuildRequestAnswerResult.AlreadyHaveGuild)).ConfigureAwait(false);
return;
}
if (player.GuildStatus?.Position != GuildPosition.GuildMaster)
{
player.Logger.LogWarning("Suspicious request for player with name: {0} (player is not a guild master), could be hack attempt.", player.Name);
await lastGuildRequester.InvokeViewPlugInAsync<IGuildJoinResponsePlugIn>(p => p.ShowGuildJoinResponseAsync(GuildRequestAnswerResult.NotTheGuildMaster)).ConfigureAwait(false);
return;
}
if (player.PlayerState.CurrentState != PlayerState.EnteredWorld
|| lastGuildRequester.PlayerState.CurrentState != PlayerState.EnteredWorld)
{
await lastGuildRequester.InvokeViewPlugInAsync<IGuildJoinResponsePlugIn>(p => p.ShowGuildJoinResponseAsync(GuildRequestAnswerResult.GuildMasterOrRequesterIsBusy)).ConfigureAwait(false);
}
if (accept)
{
await guildServer.CreateGuildMemberAsync(player.GuildStatus.GuildId, lastGuildRequester.SelectedCharacter.Id, lastGuildRequester.SelectedCharacter.Name, GuildPosition.NormalMember, ((IGameServerContext)player.GameContext).Id).ConfigureAwait(false);
}
await lastGuildRequester.InvokeViewPlugInAsync<IGuildJoinResponsePlugIn>(p => p.ShowGuildJoinResponseAsync(accept ? GuildRequestAnswerResult.Accepted : GuildRequestAnswerResult.Refused)).ConfigureAwait(false);
player.LastGuildRequester = null;
}
}

View File

@@ -0,0 +1,208 @@
// <copyright file="GuildWarAnswerAction.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Guild;
using System.ComponentModel;
using MUnique.OpenMU.GameLogic.GuildWar;
using MUnique.OpenMU.GameLogic.Views.Guild;
/// <summary>
/// Action to handle the response of the requested guild master about the guild war.
/// </summary>
public class GuildWarAnswerAction
{
/// <summary>
/// Processes the answer.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="isWarAccepted">The answer.</param>
public async ValueTask ProcessAnswerAsync(Player player, bool isWarAccepted)
{
if (player.GuildWarContext is not { } guildWarContext
|| guildWarContext.Requester is not { } requester)
{
await player.InvokeViewPlugInAsync<IShowShowGuildWarRequestResultPlugIn>(p => p.ShowResultAsync(GuildWarRequestResult.GuildNotFound)).ConfigureAwait(false);
return;
}
SoccerGameMap? soccerMap = null;
if (guildWarContext.WarType == GuildWarType.Soccer
&& player.GameContext.Configuration.Maps.FirstOrDefault(m => m.BattleZone?.Type == BattleType.Soccer) is { } definition)
{
soccerMap = (SoccerGameMap?)await player.GameContext.GetMapAsync(definition.Number.ToUnsigned()).ConfigureAwait(false);
}
var soccerInitFailed = false;
if (guildWarContext.WarType == GuildWarType.Soccer && (soccerMap is null || soccerMap.IsBattleOngoing))
{
soccerInitFailed = true;
await player.InvokeViewPlugInAsync<IShowShowGuildWarRequestResultPlugIn>(p => p.ShowResultAsync(GuildWarRequestResult.Failed)).ConfigureAwait(false);
}
if (!isWarAccepted
|| requester.GuildWarContext is not { } requesterGuildWarContext
|| soccerInitFailed)
{
player.GuildWarContext = null;
requester.GuildWarContext = null;
return;
}
soccerMap?.InitializeBattle();
guildWarContext.State = GuildWarState.Started;
requesterGuildWarContext.State = GuildWarState.Started;
var playerTeam = this.GetTeamPlayers(player);
var requesterTeam = this.GetTeamPlayers(requester);
var score = guildWarContext.Score;
#pragma warning disable VSTHRD101 // Avoid unsupported async delegates
score.PropertyChanged += async (_, args) =>
{
try
{
if (args.PropertyName != nameof(score.HasEnded))
{
return;
}
guildWarContext.State = GuildWarState.Ended;
requesterGuildWarContext.State = GuildWarState.Ended;
if (player.GameContext is IGameServerContext gameContext && score.Winners.HasValue)
{
var winner = score.Winners == player.GuildWarContext.Team ? player.GuildStatus!.GuildId : requester.GuildStatus!.GuildId;
await gameContext.GuildServer.IncreaseGuildScoreAsync(winner).ConfigureAwait(false);
}
}
catch
{
// must be catched because it's async void.
}
};
#pragma warning restore VSTHRD101 // Avoid unsupported async delegates
foreach (var guildPlayer in playerTeam)
{
guildPlayer.GuildWarContext = guildWarContext;
await guildPlayer.InvokeViewPlugInAsync<IShowGuildWarDeclaredPlugIn>(p => p.ShowDeclaredAsync()).ConfigureAwait(false);
await guildPlayer.InvokeViewPlugInAsync<IGuildWarScoreUpdatePlugIn>(p => p.UpdateScoreAsync()).ConfigureAwait(false);
RegisterScoreChangedEventWeakly(guildPlayer, score, soccerMap);
}
foreach (var guildPlayer in requesterTeam)
{
guildPlayer.GuildWarContext = requesterGuildWarContext;
await guildPlayer.InvokeViewPlugInAsync<IShowGuildWarDeclaredPlugIn>(p => p.ShowDeclaredAsync()).ConfigureAwait(false);
await guildPlayer.InvokeViewPlugInAsync<IGuildWarScoreUpdatePlugIn>(p => p.UpdateScoreAsync()).ConfigureAwait(false);
RegisterScoreChangedEventWeakly(guildPlayer, score, soccerMap);
}
if (guildWarContext.WarType == GuildWarType.Soccer && soccerMap is { })
{
await this.MovePartyToArenaAsync(player.GuildWarContext.Team, playerTeam, soccerMap).ConfigureAwait(false);
await this.MovePartyToArenaAsync(requester.GuildWarContext.Team, requesterTeam, soccerMap).ConfigureAwait(false);
await soccerMap.StartBattleAsync(score).ConfigureAwait(false);
}
}
private static void RegisterScoreChangedEventWeakly(Player guildPlayer, GuildWarScore score, SoccerGameMap? soccerMap)
{
var playerReference = new WeakReference<Player>(guildPlayer);
#pragma warning disable VSTHRD100 // Avoid async void methods
async void OnScorePropertyChanged(object? sender, PropertyChangedEventArgs args)
#pragma warning restore VSTHRD100 // Avoid async void methods
{
try
{
if (playerReference.TryGetTarget(out var p))
{
await OnScoreChangedAsync(score, p, soccerMap, args).ConfigureAwait(false);
}
else
{
score.PropertyChanged -= OnScorePropertyChanged;
}
}
catch (Exception ex)
{
guildPlayer.Logger.LogError(ex, "Error handling a changed guild war score.");
}
}
score.PropertyChanged += OnScorePropertyChanged;
}
private static async ValueTask OnScoreChangedAsync(GuildWarScore score, Player player, SoccerGameMap? soccerMap, PropertyChangedEventArgs args)
{
try
{
if (args.PropertyName == nameof(score.HasEnded))
{
if (player.GuildWarContext is { } context)
{
var isWinner = context.Team == score.Winners;
await player.InvokeViewPlugInAsync<IShowGuildWarResultPlugIn>(p => p.ShowResultAsync(context.EnemyTeamName, isWinner ? GuildWarResult.Won : GuildWarResult.Lost)).ConfigureAwait(false);
if (soccerMap is not null)
{
var spawnGates = soccerMap.Definition.ExitGates.Where(g => g.IsSpawnGate);
if (spawnGates.Any())
{
await player.WarpToAsync(spawnGates.SelectRandom()!).ConfigureAwait(false);
}
}
player.GuildWarContext = null;
}
}
else
{
await player.InvokeViewPlugInAsync<IGuildWarScoreUpdatePlugIn>(p => p.UpdateScoreAsync()).ConfigureAwait(false);
}
}
catch (Exception ex)
{
player.Logger.LogError(ex, "Unexpected error when notifying the player about a guild war score update");
}
}
private ICollection<Player> GetTeamPlayers(Player guildMaster)
{
if (guildMaster.Party is { } party)
{
return party.PartyList.OfType<Player>().Where(p => p.GuildStatus?.GuildId == guildMaster.GuildStatus?.GuildId).ToList();
}
return new List<Player>(1) { guildMaster };
}
private async ValueTask MovePartyToArenaAsync(GuildWarTeam team, ICollection<Player> members, SoccerGameMap soccerMap)
{
var ground = soccerMap.Definition.BattleZone?.Ground;
if (ground is null)
{
return;
}
var increaseX = soccerMap.Definition.BattleZone?.LeftTeamSpawnPointX is not null;
var exitGate = new ExitGate
{
X1 = (team == GuildWarTeam.First ? soccerMap.Definition.BattleZone?.LeftTeamSpawnPointX : soccerMap.Definition.BattleZone?.RightTeamSpawnPointX) ?? ground.X1,
X2 = (team == GuildWarTeam.First ? soccerMap.Definition.BattleZone?.LeftTeamSpawnPointX : soccerMap.Definition.BattleZone?.RightTeamSpawnPointX) ?? ground.X2,
Y1 = (team == GuildWarTeam.First ? soccerMap.Definition.BattleZone?.LeftTeamSpawnPointY : soccerMap.Definition.BattleZone?.RightTeamSpawnPointY) ?? ground.Y1,
Y2 = (team == GuildWarTeam.First ? soccerMap.Definition.BattleZone?.LeftTeamSpawnPointY : soccerMap.Definition.BattleZone?.RightTeamSpawnPointY) ?? ground.Y2,
Map = soccerMap.Definition,
};
foreach (var member in members)
{
await member.WarpToAsync(exitGate).ConfigureAwait(false);
if (increaseX)
{
exitGate.X1++;
exitGate.X2++;
}
}
}
}

View File

@@ -0,0 +1,91 @@
// <copyright file="GuildWarRequestAction.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.Guild;
using MUnique.OpenMU.GameLogic.GuildWar;
using MUnique.OpenMU.GameLogic.Views.Guild;
using MUnique.OpenMU.Interfaces;
/// <summary>
/// Action to request a guild war.
/// </summary>
public class GuildWarRequestAction
{
/// <summary>
/// Requests the a guild war at the guild master of the target guild.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="targetGuildName">Name of the target guild.</param>
public ValueTask RequestWarAsync(Player player, string targetGuildName)
{
return this.TryRequestWarAsync(player, targetGuildName, GuildWarType.Normal);
}
/// <summary>
/// Requests the a battle soccer at the guild master of the target guild.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="targetGuildName">Name of the target guild.</param>
public ValueTask RequestBattleSoccerAsync(Player player, string targetGuildName)
{
return this.TryRequestWarAsync(player, targetGuildName, GuildWarType.Soccer);
}
private async ValueTask TryRequestWarAsync(Player player, string targetGuildName, GuildWarType guildWarType)
{
if (player.GuildStatus is not { } guildStatus
|| player.GameContext is not IGameServerContext serverContext
|| await serverContext.GuildServer.GetGuildAsync(player.GuildStatus.GuildId).ConfigureAwait(false) is not { Name: not null } guild)
{
await player.InvokeViewPlugInAsync<IShowShowGuildWarRequestResultPlugIn>(p => p.ShowResultAsync(GuildWarRequestResult.NotInGuild)).ConfigureAwait(false);
return;
}
if (guildStatus.Position != GuildPosition.GuildMaster)
{
await player.InvokeViewPlugInAsync<IShowShowGuildWarRequestResultPlugIn>(p => p.ShowResultAsync(GuildWarRequestResult.NotTheGuildMaster)).ConfigureAwait(false);
return;
}
if (!await serverContext.GuildServer.GuildExistsAsync(targetGuildName).ConfigureAwait(false))
{
await player.InvokeViewPlugInAsync<IShowShowGuildWarRequestResultPlugIn>(p => p.ShowResultAsync(GuildWarRequestResult.GuildNotFound)).ConfigureAwait(false);
return;
}
var targetGuildId = await serverContext.GuildServer.GetGuildIdByNameAsync(targetGuildName).ConfigureAwait(false);
Player? targetGuildMaster = null;
await serverContext.ForEachGuildPlayerAsync(targetGuildId, p =>
{
targetGuildMaster = p.GuildStatus?.Position == GuildPosition.GuildMaster ? p : targetGuildMaster;
return Task.CompletedTask;
}).ConfigureAwait(false);
if (targetGuildMaster is null)
{
await player.InvokeViewPlugInAsync<IShowShowGuildWarRequestResultPlugIn>(p => p.ShowResultAsync(GuildWarRequestResult.GuildMasterOffline)).ConfigureAwait(false);
return;
}
if (targetGuildMaster.GuildWarContext is not null || player.GuildWarContext is not null)
{
await player.InvokeViewPlugInAsync<IShowShowGuildWarRequestResultPlugIn>(p => p.ShowResultAsync(GuildWarRequestResult.AlreadyInWar)).ConfigureAwait(false);
return;
}
var score = new GuildWarScore
{
FirstGuildName = targetGuildName,
SecondGuildName = guild.Name!,
MaximumScore = (byte)(guildWarType == GuildWarType.Soccer ? 100 : 20),
};
targetGuildMaster.GuildWarContext = new GuildWarContext(guildWarType, score, GuildWarTeam.First, player);
player.GuildWarContext = new GuildWarContext(guildWarType, score, GuildWarTeam.Second, null);
await targetGuildMaster.InvokeViewPlugInAsync<IShowGuildWarRequestPlugIn>(p => p.ShowRequestAsync(guild.Name!, guildWarType)).ConfigureAwait(false);
await player.InvokeViewPlugInAsync<IShowShowGuildWarRequestResultPlugIn>(p => p.ShowResultAsync(GuildWarRequestResult.RequestSentToGuildMaster)).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,123 @@
// <copyright file="HitAction.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.PlugIns;
using MUnique.OpenMU.GameLogic.Views.World;
/// <summary>
/// Action to hit targets without a skill with pure melee damage.
/// </summary>
public class HitAction
{
/// <summary>
/// Hits the specified target by the specified player.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="target">The target.</param>
/// <param name="attackAnimation">The attack animation.</param>
/// <param name="lookingDirection">The looking direction.</param>
public async ValueTask HitAsync(Player player, IAttackable target, byte attackAnimation, Direction lookingDirection)
{
if (player.Attributes is not { } attributes)
{
return;
}
if (attributes[Stats.IsStunned] > 0)
{
player.Logger.LogWarning("Probably Hacker - player {Player} is attacking in stunned state", player);
return;
}
if (attributes[Stats.IsAsleep] > 0)
{
player.Logger.LogWarning("Probably Hacker - player {Player} is attacking in asleep state", player);
return;
}
if (player.IsAtSafezone())
{
player.Logger.LogWarning("Probably Hacker - player {Player} is attacking from safezone", player);
return;
}
if (player.GameContext.PlugInManager.GetPlugInPoint<ISpeedHackCheatCheckPlugIn>() is { } speedCheck)
{
var eventArgs = new SpeedHackCheckEventArgs();
await speedCheck.AttackCheatCheckAsync(player, eventArgs).ConfigureAwait(false);
if (eventArgs.IsCheatDetected)
{
return;
}
}
if (target.IsAtSafezone())
{
return;
}
if (target is IObservable targetAsObservable)
{
using var readerLock = await targetAsObservable.ObserverLock.ReaderLockAsync();
if (!targetAsObservable.Observers.Contains(player))
{
// Target out of range
return;
}
}
player.Rotation = lookingDirection;
await target.AttackByAsync(player, null, false).ConfigureAwait(false);
if (player.Attributes?[Stats.TransformationSkin] is { } skin and not 0
&& await this.ApplySkinnedMonstersSkillAsync(player, target, (short)skin).ConfigureAwait(false) is var (skill, effectApplied))
{
await player.ForEachWorldObserverAsync<IShowSkillAnimationPlugIn>(p => p.ShowSkillAnimationAsync(player, target, skill, effectApplied), true).ConfigureAwait(false);
return;
}
await player.ForEachWorldObserverAsync<IShowAnimationPlugIn>(p => p.ShowAnimationAsync(player, attackAnimation, target, lookingDirection), false).ConfigureAwait(false);
}
private async ValueTask<(Skill Skill, bool EffectApplied)?> ApplySkinnedMonstersSkillAsync(Player player, IAttackable target, short skin)
{
var effectApplied = false;
if (player.GameContext.Configuration.Monsters.FirstOrDefault(m => m.Number == skin)?.AttackSkill
is not { ElementalModifierTarget: not null } skill)
{
return null;
}
var modifier = skill.ElementalModifierTarget!;
var resistance = target.Attributes[modifier];
if (resistance >= 255 || !Rand.NextRandomBool(1 / (resistance + 1)))
{
return (skill, effectApplied);
}
// Currently, we just support one effect for monsters.
// E.g. Poison for Poison BullFighters.
if (skill.MagicEffectDef is { Duration: not null } effectDefinition
&& !target.MagicEffectList.ActiveEffects.ContainsKey(effectDefinition.Number)
&& effectDefinition.PowerUpDefinitions.FirstOrDefault() is { Boost: not null } powerUpDef)
{
var powerUp = target.Attributes.CreateElement(powerUpDef);
var powerUpDuration = target.Attributes.CreateDurationElement(effectDefinition.Duration);
var magicEffect = powerUpDef.TargetAttribute == Stats.IsPoisoned
? new PoisonMagicEffect(powerUp, effectDefinition, TimeSpan.FromSeconds(powerUpDuration.Value), player, target)
: new MagicEffect(powerUp, effectDefinition, TimeSpan.FromSeconds(powerUpDuration.Value));
await target.MagicEffectList.AddEffectAsync(magicEffect).ConfigureAwait(false);
effectApplied = true;
}
if (modifier == Stats.LightningResistance)
{
await target.MoveRandomlyAsync().ConfigureAwait(false);
}
return (skill, effectApplied);
}
}

View File

@@ -0,0 +1,36 @@
// -----------------------------------------------------------------------
// <copyright file="AlcoholConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
// -----------------------------------------------------------------------
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The alcohol consume handler.
/// </summary>
[Guid("7FC2FE02-9215-4AD3-958F-D2279CD84266")]
[PlugIn]
[Display(Name = nameof(PlugInResources.AlcoholConsumeHandlerPlugIn_Name), Description = nameof(PlugInResources.AlcoholConsumeHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))]
public class AlcoholConsumeHandlerPlugIn : ApplyMagicEffectConsumeHandlerPlugIn
{
/// <inheritdoc />
public override ItemIdentifier Key => ItemConstants.Alcohol;
/// <inheritdoc/>
public override async ValueTask<bool> ConsumeItemAsync(Player player, Item item, Item? targetItem, FruitUsage fruitUsage)
{
if (await base.ConsumeItemAsync(player, item, targetItem, fruitUsage).ConfigureAwait(false))
{
var effectDefinition = item.Definition?.ConsumeEffect;
await player.InvokeViewPlugInAsync<IConsumeSpecialItemPlugIn>(p => p.ConsumeSpecialItemAsync(item, (ushort)(effectDefinition?.Duration?.ConstantValue.Value ?? 0))).ConfigureAwait(false);
return true;
}
return false;
}
}

View File

@@ -0,0 +1,40 @@
// -----------------------------------------------------------------------
// <copyright file="AntidoteConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
// -----------------------------------------------------------------------
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using System.Runtime.InteropServices;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Consume handler for the antidote potion. It removes the poison effect from the player.
/// </summary>
[Guid("F838B348-DAA5-475B-BCED-41A076D08948")]
[PlugIn]
[Display(Name = nameof(PlugInResources.AntidoteConsumeHandlerPlugIn_Name), Description = nameof(PlugInResources.AntidoteConsumeHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))]
public class AntidoteConsumeHandlerPlugIn : BaseConsumeHandlerPlugIn
{
private const short PoisonEffectNumber = 0x37;
/// <inheritdoc />
public override ItemIdentifier Key => ItemConstants.Antidote;
/// <inheritdoc />
public override async ValueTask<bool> ConsumeItemAsync(Player player, Item item, Item? targetItem, FruitUsage fruitUsage)
{
if (await base.ConsumeItemAsync(player, item, targetItem, fruitUsage).ConfigureAwait(false))
{
if (player.MagicEffectList.ActiveEffects.TryGetValue(PoisonEffectNumber, out var effect))
{
effect.Dispose();
}
return true;
}
return false;
}
}

View File

@@ -0,0 +1,25 @@
// -----------------------------------------------------------------------
// <copyright file="AppleConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
// -----------------------------------------------------------------------
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using System.Runtime.InteropServices;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Consume handler for apples.
/// </summary>
[Guid("58518298-42BC-48D1-AB07-17A9D83A2103")]
[PlugIn]
[Display(Name = nameof(PlugInResources.AppleConsumeHandlerPlugIn_Name), Description = nameof(PlugInResources.AppleConsumeHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))]
public class AppleConsumeHandlerPlugIn : HealthPotionConsumeHandlerPlugIn
{
/// <inheritdoc />
public override ItemIdentifier Key => ItemConstants.Apple;
/// <inheritdoc/>
protected override int Multiplier => 0;
}

View File

@@ -0,0 +1,73 @@
// <copyright file="ApplyMagicEffectConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.GameLogic.Attributes;
/// <summary>
/// <see cref="IItemConsumeHandlerPlugIn"/> for <see cref="Item"/>s which have a defined <see cref="ItemDefinition.ConsumeEffect"/>.
/// </summary>
public class ApplyMagicEffectConsumeHandlerPlugIn : BaseConsumeHandlerPlugIn
{
/// <inheritdoc />
public override ItemIdentifier Key => new(0xFF, 0xFF);
/// <inheritdoc />
public override async ValueTask<bool> ConsumeItemAsync(Player player, Item item, Item? targetItem, FruitUsage fruitUsage)
{
if (item.Definition?.ConsumeEffect is not { } effectDefinition)
{
return false;
}
return await this.ConsumeItemAsyncCore(player, item, targetItem, fruitUsage, effectDefinition).ConfigureAwait(false);
}
/// <summary>
/// Consumes the item at the specified slot with the specified effect and reduces its durability by one.
/// If the durability has reached 0, the item is getting destroyed.
/// If a target slot is specified, the consumption targets the item on this slot (e.g. upgrade of an item by a jewel).
/// </summary>
/// <param name="player">The player which is consuming.</param>
/// <param name="item">The item which gets consumed.</param>
/// <param name="targetItem">The item which is the target of the consumption (e.g. upgrade target of a jewel).</param>
/// <param name="fruitUsage">In case the item is a fruit, this parameter defines how the fruit should be used.</param>
/// <param name="effectDefinition">The effect definition.</param>
/// <returns>
/// The success of the consumption.
/// </returns>
protected async ValueTask<bool> ConsumeItemAsyncCore(Player player, Item item, Item? targetItem, FruitUsage fruitUsage, MagicEffectDefinition effectDefinition)
{
if (!effectDefinition.PowerUpDefinitions.Any()
|| effectDefinition.Duration?.ConstantValue.Value is not { } durationInSeconds)
{
return false;
}
if (await player.MagicEffectList.TryGetActiveEffectOfSubTypeAsync(effectDefinition.SubType).ConfigureAwait(false) is { } existingEffect)
{
await existingEffect.DisposeAsync().ConfigureAwait(false);
}
var boosts = effectDefinition.PowerUpDefinitions
.Where(def => def.Boost is not null && def.TargetAttribute is not null)
.Select(def => new MagicEffect.ElementWithTarget(player.Attributes!.CreateElement(def), def.TargetAttribute!))
.ToArray();
if (boosts.Length == 0)
{
return false;
}
if (!await base.ConsumeItemAsync(player, item, targetItem, fruitUsage).ConfigureAwait(false))
{
return false;
}
var effect = new MagicEffect(TimeSpan.FromSeconds(durationInSeconds), effectDefinition, boosts!);
await player.MagicEffectList.AddEffectAsync(effect).ConfigureAwait(false);
return true;
}
}

View File

@@ -0,0 +1,59 @@
// -----------------------------------------------------------------------
// <copyright file="BaseConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
// -----------------------------------------------------------------------
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
/// <summary>
/// Base class of an item consumption handler.
/// </summary>
public abstract class BaseConsumeHandlerPlugIn : IItemConsumeHandlerPlugIn
{
/// <inheritdoc />
public abstract ItemIdentifier Key { get; }
/// <inheritdoc/>
public virtual async ValueTask<bool> ConsumeItemAsync(Player player, Item item, Item? targetItem, FruitUsage fruitUsage)
{
if (!this.CheckPreconditions(player, item))
{
return false;
}
await this.ConsumeSourceItemAsync(player, item).ConfigureAwait(false);
return true;
}
/// <summary>
/// Consumes the source item.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="item">The item.</param>
protected async ValueTask ConsumeSourceItemAsync(Player player, Item item)
{
if (item.Durability > 0)
{
item.Durability -= 1;
}
}
/// <summary>
/// Checks the preconditions.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="item">The item.</param>
/// <returns><c>True</c>, if preconditions are met.</returns>
protected virtual bool CheckPreconditions(Player player, Item item)
{
if (player.PlayerState.CurrentState != PlayerState.EnteredWorld
|| item.Durability == 0)
{
return false;
}
return true;
}
}

View File

@@ -0,0 +1,49 @@
// -----------------------------------------------------------------------
// <copyright file="BlessJewelConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
// -----------------------------------------------------------------------
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using System.Runtime.InteropServices;
using MUnique.OpenMU.Persistence;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Consume handler for upgrading items up to level 6 using the Jewel of Bless.
/// </summary>
[Guid("E95A0292-B3B4-4E8C-AC5A-7F3DB4F01A37")]
[PlugIn]
[Display(Name = nameof(PlugInResources.BlessJewelConsumeHandlerPlugIn_Name), Description = nameof(PlugInResources.BlessJewelConsumeHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))]
public class BlessJewelConsumeHandlerPlugIn : UpgradeItemLevelJewelConsumeHandlerPlugIn<BlessJewelConsumeHandlerPlugInConfiguration>
{
/// <inheritdoc />
public override ItemIdentifier Key => ItemConstants.JewelOfBless;
/// <inheritdoc />
public override object CreateDefaultConfig()
{
return new BlessJewelConsumeHandlerPlugInConfiguration
{
MaximumLevel = 5,
MinimumLevel = 0,
SuccessRatePercentage = 100,
SuccessRateBonusWithLuckPercentage = 0,
ResetToLevel0WhenFailMinLevel = 0,
};
}
/// <inheritdoc/>
protected override bool ModifyItem(Item item, IContext persistenceContext)
{
if (this.Configuration?.RepairTargetItems.Contains(item.Definition!) is true
&& item.Durability < item.GetMaximumDurabilityOfOnePiece())
{
item.Durability = item.GetMaximumDurabilityOfOnePiece();
return true;
}
return base.ModifyItem(item, persistenceContext);
}
}

View File

@@ -0,0 +1,18 @@
// <copyright file="BlessJewelConsumeHandlerPlugInConfiguration.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using MUnique.OpenMU.DataModel.Configuration.Items;
/// <summary>
/// The configuration for the <see cref="BlessJewelConsumeHandlerPlugIn"/>.
/// </summary>
public class BlessJewelConsumeHandlerPlugInConfiguration : UpgradeItemLevelConfiguration
{
/// <summary>
/// Gets or sets the items which can be repaired by consuming a bless on them.
/// </summary>
public ICollection<ItemDefinition> RepairTargetItems { get; set; } = new List<ItemDefinition>();
}

View File

@@ -0,0 +1,45 @@
// <copyright file="ComplexPotionConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
/// <summary>
/// Abstract consume handler for complex potions which combines a <see cref="HealthPotionConsumeHandlerPlugIn"/> and a <see cref="ShieldPotionConsumeHandlerPlugIn"/>.
/// </summary>
public abstract class ComplexPotionConsumeHandlerPlugIn : BaseConsumeHandlerPlugIn
{
private readonly HealthPotionConsumeHandlerPlugIn _healthPotionConsumeHandlerPlugIn;
private readonly ShieldPotionConsumeHandlerPlugIn _shieldPotionConsumeHandlerPlugIn;
/// <summary>
/// Initializes a new instance of the <see cref="ComplexPotionConsumeHandlerPlugIn"/> class.
/// </summary>
/// <param name="healthPotionConsumeHandlerPlugIn">The health potion consume handler.</param>
/// <param name="shieldPotionConsumeHandlerPlugIn">The shield potion consume handler.</param>
protected ComplexPotionConsumeHandlerPlugIn(HealthPotionConsumeHandlerPlugIn healthPotionConsumeHandlerPlugIn, ShieldPotionConsumeHandlerPlugIn shieldPotionConsumeHandlerPlugIn)
{
this._healthPotionConsumeHandlerPlugIn = healthPotionConsumeHandlerPlugIn ?? throw new ArgumentNullException(nameof(healthPotionConsumeHandlerPlugIn));
this._shieldPotionConsumeHandlerPlugIn = shieldPotionConsumeHandlerPlugIn ?? throw new ArgumentNullException(nameof(shieldPotionConsumeHandlerPlugIn));
}
/// <inheritdoc />
public override async ValueTask<bool> ConsumeItemAsync(Player player, Item item, Item? targetItem, FruitUsage fruitUsage)
{
if (await base.ConsumeItemAsync(player, item, targetItem, fruitUsage).ConfigureAwait(false))
{
await this._healthPotionConsumeHandlerPlugIn.RecoverAsync(player, item).ConfigureAwait(false);
await this._shieldPotionConsumeHandlerPlugIn.RecoverAsync(player, item).ConfigureAwait(false);
return true;
}
return false;
}
/// <inheritdoc />
protected override bool CheckPreconditions(Player player, Item item)
{
return base.CheckPreconditions(player, item)
&& player.PotionCooldownUntil <= DateTime.UtcNow;
}
}

View File

@@ -0,0 +1,187 @@
// <copyright file="FruitConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using System.Runtime.InteropServices;
using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.Views.Character;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Implementation of a consume handler for fruits.
/// </summary>
[Guid("151E4292-96FE-4FF5-A51B-060B510D3DF8")]
[PlugIn]
[Display(Name = nameof(PlugInResources.FruitConsumeHandlerPlugIn_Name), Description = nameof(PlugInResources.FruitConsumeHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))]
public class FruitConsumeHandlerPlugIn : BaseConsumeHandlerPlugIn
{
/// <inheritdoc />
public override ItemIdentifier Key => ItemConstants.Fruits;
/// <inheritdoc />
public override async ValueTask<bool> ConsumeItemAsync(Player player, Item item, Item? targetItem, FruitUsage fruitUsage)
{
if (!this.CheckPreconditions(player, item))
{
return false;
}
var isAdding = fruitUsage != FruitUsage.RemovePoints;
var statAttribute = this.GetStatAttribute(item);
if (player.Level < 10 || item.Level > 4)
{
await player.InvokeViewPlugInAsync<IFruitConsumptionResponsePlugIn>(
p => p.ShowResponseAsync(isAdding ? FruitConsumptionResult.PlusPrevented : FruitConsumptionResult.MinusPrevented, 0, statAttribute)).ConfigureAwait(false);
return false;
}
var statAttributeDefinition = player.SelectedCharacter!.CharacterClass?.StatAttributes.FirstOrDefault(a =>
a.IncreasableByPlayer && a.Attribute == statAttribute);
if (statAttributeDefinition is null)
{
await player.InvokeViewPlugInAsync<IFruitConsumptionResponsePlugIn>(
p => p.ShowResponseAsync(isAdding ? FruitConsumptionResult.PlusPrevented : FruitConsumptionResult.MinusPrevented, 0, statAttribute)).ConfigureAwait(false);
return false;
}
if (player.Inventory!.EquippedItems.Any())
{
await player.InvokeViewPlugInAsync<IFruitConsumptionResponsePlugIn>(p => p.ShowResponseAsync(FruitConsumptionResult.PreventedByEquippedItems, 0, statAttribute)).ConfigureAwait(false);
return false;
}
var maximumRemainingPoints = player.SelectedCharacter.GetMaximumFruitPoints()
- (isAdding
? player.SelectedCharacter.UsedFruitPoints
: player.SelectedCharacter.UsedNegFruitPoints);
if (maximumRemainingPoints <= 0)
{
await player.InvokeViewPlugInAsync<IFruitConsumptionResponsePlugIn>(p => p.ShowResponseAsync(isAdding ? FruitConsumptionResult.PlusPreventedByMaximum : FruitConsumptionResult.MinusPreventedByMaximum, 0, statAttribute)).ConfigureAwait(false);
return false;
}
if (!isAdding && player.Attributes![statAttribute] <= statAttributeDefinition.BaseValue)
{
await player.InvokeViewPlugInAsync<IFruitConsumptionResponsePlugIn>(p => p.ShowResponseAsync(FruitConsumptionResult.MinusPreventedByDefault, 0, statAttribute)).ConfigureAwait(false);
return false;
}
var successPercentage = this.GetSuccessPercentage(player, isAdding);
if (Rand.NextRandomBool(successPercentage))
{
var randomPoints = (byte)Math.Min(maximumRemainingPoints, this.GetRandomPoints(isAdding));
if (isAdding)
{
player.Attributes![statAttribute] += randomPoints;
player.SelectedCharacter.UsedFruitPoints += randomPoints;
await player.InvokeViewPlugInAsync<IFruitConsumptionResponsePlugIn>(p => p.ShowResponseAsync(FruitConsumptionResult.PlusSuccess, randomPoints, statAttribute)).ConfigureAwait(false);
}
else
{
player.Attributes![statAttribute] -= randomPoints;
player.SelectedCharacter.UsedNegFruitPoints += randomPoints;
await player.InvokeViewPlugInAsync<IFruitConsumptionResponsePlugIn>(p => p.ShowResponseAsync(FruitConsumptionResult.MinusSuccess, randomPoints, statAttribute)).ConfigureAwait(false);
}
}
else
{
await player.InvokeViewPlugInAsync<IFruitConsumptionResponsePlugIn>(p => p.ShowResponseAsync(isAdding ? FruitConsumptionResult.PlusFailed : FruitConsumptionResult.MinusFailed, 0, statAttribute)).ConfigureAwait(false);
}
await this.ConsumeSourceItemAsync(player, item).ConfigureAwait(false);
return true;
}
private int GetRandomPoints(bool isAdding)
{
var random = Rand.NextInt(0, 101);
if (isAdding)
{
if (random < 70)
{
return 1;
}
if (random < 95)
{
return 2;
}
return 3;
}
if (random < 50)
{
return 1;
}
if (random < 75)
{
return 3;
}
if (random < 91)
{
return 5;
}
if (random < 98)
{
return 7;
}
return 9;
}
private int GetSuccessPercentage(Player player, bool isAdding)
{
var currentUseCount = isAdding
? player.SelectedCharacter!.UsedFruitPoints
: player.SelectedCharacter!.UsedNegFruitPoints;
var maximumUseCount = player.SelectedCharacter.GetMaximumFruitPoints();
if (currentUseCount <= 10)
{
return 100;
}
if ((currentUseCount - 10) < (maximumUseCount * 0.1))
{
return 90;
}
if ((currentUseCount - 10) < (maximumUseCount * 0.3))
{
return 80;
}
if ((currentUseCount - 10) < (maximumUseCount * 0.5))
{
return 70;
}
if ((currentUseCount - 10) < (maximumUseCount * 0.8))
{
return 60;
}
return 50;
}
private AttributeDefinition GetStatAttribute(Item item)
{
return item.Level switch
{
0 => Stats.BaseEnergy,
1 => Stats.BaseVitality,
2 => Stats.BaseAgility,
3 => Stats.BaseStrength,
4 => Stats.BaseLeadership,
_ => throw new ArgumentException($"Invalid item level {item.Level}"),
};
}
}

View File

@@ -0,0 +1,26 @@
// <copyright file="FruitUsage.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
/// <summary>
/// Defines how the fruit is used. Only applies, if the the item is a fruit.
/// </summary>
public enum FruitUsage
{
/// <summary>
/// The undefined usage. Used when it doesn't apply.
/// </summary>
Undefined,
/// <summary>
/// Adds 1~3 stat points to the character.
/// </summary>
AddPoints,
/// <summary>
/// Removes 1~9 stat points from the character.
/// </summary>
RemovePoints,
}

View File

@@ -0,0 +1,41 @@
// <copyright file="HarmonyJewelConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using System.Runtime.InteropServices;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The consume handler for the harmony jewel.
/// </summary>
[Guid("DAC3E5C2-FF0F-4773-AFBF-EBDC0C35336D")]
[PlugIn]
[Display(Name = nameof(PlugInResources.HarmonyJewelConsumeHandlerPlugIn_Name), Description = nameof(PlugInResources.HarmonyJewelConsumeHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))]
public class HarmonyJewelConsumeHandlerPlugIn : ItemUpgradeConsumeHandlerPlugIn
{
/// <summary>
/// Initializes a new instance of the <see cref="HarmonyJewelConsumeHandlerPlugIn" /> class.
/// </summary>
public HarmonyJewelConsumeHandlerPlugIn()
: base(new ItemUpgradeConfiguration(ItemOptionTypes.HarmonyOption, true, false, 0.6, ItemFailResult.None))
{
}
/// <inheritdoc />
public override ItemIdentifier Key => ItemConstants.JewelOfHarmony;
/// <inheritdoc />
protected override bool ItemCanHaveOption(Item item)
{
if (item.IsAncient())
{
// Until S16E2 ancient and socket items couldn't have harmony options: https://muonline.webzen.com/en/gameinfo/guide/detail/117
return false;
}
return base.ItemCanHaveOption(item);
}
}

View File

@@ -0,0 +1,22 @@
// -----------------------------------------------------------------------
// <copyright file="HealthPotionConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
// -----------------------------------------------------------------------
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.GameLogic.Attributes;
/// <summary>
/// The consume handler for a potion that recovers health.
/// </summary>
public abstract class HealthPotionConsumeHandlerPlugIn : RecoverConsumeHandlerPlugIn.ManaHealthConsumeHandlerPlugIn, IItemConsumeHandlerPlugIn
{
/// <inheritdoc/>
protected override AttributeDefinition MaximumAttribute => Stats.MaximumHealth;
/// <inheritdoc/>
protected override AttributeDefinition CurrentAttribute => Stats.CurrentHealth;
}

View File

@@ -0,0 +1,29 @@
// <copyright file="HigherRefineStoneConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using System.Runtime.InteropServices;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Consume handler for the Higher Refine Stone which increases the item option of <see cref="ItemOptionTypes.HarmonyOption"/>.
/// </summary>
[Guid("A9F58DF6-06DB-4187-B386-9F00382333EE")]
[PlugIn]
[Display(Name = nameof(PlugInResources.HigherRefineStoneConsumeHandlerPlugIn_Name), Description = nameof(PlugInResources.HigherRefineStoneConsumeHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))]
public class HigherRefineStoneConsumeHandlerPlugIn : RefineStoneUpgradeConsumeHandlerPlugIn
{
/// <summary>
/// Initializes a new instance of the <see cref="HigherRefineStoneConsumeHandlerPlugIn" /> class.
/// </summary>
public HigherRefineStoneConsumeHandlerPlugIn()
: base(new ItemUpgradeConfiguration(ItemOptionTypes.HarmonyOption, false, true, 0.8, ItemFailResult.SetOptionToBaseLevel))
{
}
/// <inheritdoc />
public override ItemIdentifier Key => ItemConstants.HigherRefineStone;
}

View File

@@ -0,0 +1,30 @@
// -----------------------------------------------------------------------
// <copyright file="IItemConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
// -----------------------------------------------------------------------
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using System.Runtime.InteropServices;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Consume handler to modify items which are specified by the target slot.
/// </summary>
[Guid("420FB26E-CE78-4942-8589-3A416EF4E31F")]
[PlugInPoint("Item consume handlers", "Plugins which will be executed to consume an item.")]
internal interface IItemConsumeHandlerPlugIn : IStrategyPlugIn<ItemIdentifier>
{
/// <summary>
/// Consumes the item at the specified slot, and reduces its durability by one.
/// If the durability has reached 0, the item is getting destroyed.
/// If a target slot is specified, the consumption targets the item on this slot (e.g. upgrade of an item by a jewel).
/// </summary>
/// <param name="player">The player which is consuming.</param>
/// <param name="item">The item which gets consumed.</param>
/// <param name="targetItem">The item which is the target of the consumption (e.g. upgrade target of a jewel).</param>
/// <param name="fruitUsage">In case the item is a fruit, this parameter defines how the fruit should be used.</param>
/// <returns>The success of the consumption.</returns>
ValueTask<bool> ConsumeItemAsync(Player player, Item item, Item? targetItem, FruitUsage fruitUsage);
}

View File

@@ -0,0 +1,83 @@
// <copyright file="ItemConsumeAction.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using System.ComponentModel;
using MUnique.OpenMU.GameLogic.PlugIns;
using MUnique.OpenMU.GameLogic.Views.Inventory;
/// <summary>
/// Action to consume an item.
/// </summary>
public class ItemConsumeAction
{
private readonly IItemConsumeHandlerPlugIn _magicEffectHandler = new ApplyMagicEffectConsumeHandlerPlugIn();
/// <summary>
/// Handles the consume request.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="inventorySlot">The inventory slot.</param>
/// <param name="inventoryTargetSlot">The inventory target slot.</param>
/// <param name="fruitUsage">The fruit usage.</param>
public async ValueTask HandleConsumeRequestAsync(Player player, byte inventorySlot, byte inventoryTargetSlot, FruitUsage fruitUsage)
{
var item = player.Inventory?.GetItem(inventorySlot);
if (item?.Definition is null)
{
await player.InvokeViewPlugInAsync<IRequestedItemConsumptionFailedPlugIn>(p => p.RequestedItemConsumptionFailedAsync()).ConfigureAwait(false);
return;
}
var consumeHandler = player.GameContext.PlugInManager.GetStrategy<ItemIdentifier, IItemConsumeHandlerPlugIn>(new ItemIdentifier(item.Definition.Number, item.Definition.Group))
?? player.GameContext.PlugInManager.GetStrategy<ItemIdentifier, IItemConsumeHandlerPlugIn>(new ItemIdentifier(null, item.Definition.Group));
if (consumeHandler is null && item.Definition.Skill is { } && !item.IsWearable())
{
consumeHandler = player.GameContext.PlugInManager.GetStrategy<ItemIdentifier, IItemConsumeHandlerPlugIn>(ItemConstants.AllScrolls);
}
if (consumeHandler is null && item.Definition.ConsumeEffect is { })
{
consumeHandler = this._magicEffectHandler;
}
if (consumeHandler is null)
{
await player.InvokeViewPlugInAsync<IRequestedItemConsumptionFailedPlugIn>(p => p.RequestedItemConsumptionFailedAsync()).ConfigureAwait(false);
await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.UsingThisItemNotImplemented)).ConfigureAwait(false);
return;
}
var targetItem = player.Inventory!.GetItem(inventoryTargetSlot);
if (player.GameContext.PlugInManager.GetPlugInPoint<IItemConsumingPlugIn>() is { } plugInPoint)
{
var eventArgs = new CancelEventArgs();
plugInPoint.ItemConsuming(player, item, targetItem, eventArgs);
if (eventArgs.Cancel)
{
return;
}
}
if (!await consumeHandler.ConsumeItemAsync(player, item, targetItem, fruitUsage).ConfigureAwait(false))
{
await player.InvokeViewPlugInAsync<IRequestedItemConsumptionFailedPlugIn>(p => p.RequestedItemConsumptionFailedAsync()).ConfigureAwait(false);
return;
}
if (item.Durability == 0)
{
await player.DestroyInventoryItemAsync(item).ConfigureAwait(false);
}
else
{
await player.InvokeViewPlugInAsync<IItemDurabilityChangedPlugIn>(p => p.ItemDurabilityChangedAsync(item, true)).ConfigureAwait(false);
}
player.GameContext.PlugInManager.GetPlugInPoint<IItemConsumedPlugIn>()?.ItemConsumed(player, item, targetItem);
}
}

View File

@@ -0,0 +1,62 @@
// -----------------------------------------------------------------------
// <copyright file="ItemModifyConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
// -----------------------------------------------------------------------
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using MUnique.OpenMU.GameLogic.Views.Inventory;
using MUnique.OpenMU.Persistence;
/// <summary>
/// Consume handler to modify items which are specified by the target slot.
/// </summary>
public abstract class ItemModifyConsumeHandlerPlugIn : BaseConsumeHandlerPlugIn
{
/// <inheritdoc/>
public override async ValueTask<bool> ConsumeItemAsync(Player player, Item item, Item? targetItem, FruitUsage fruitUsage)
{
if (player.PlayerState.CurrentState != PlayerState.EnteredWorld)
{
return false;
}
if (targetItem is null)
{
return false;
}
if (targetItem.ItemSlot <= InventoryConstants.LastEquippableItemSlotIndex)
{
// It shouldn't be possible to upgrade an equipped item.
// The original server allowed this, however people managed to downgrade their maxed out weapons to +6 when some
// visual bugs on the client occured :D Example: On the server side there is a jewel of bless on a certain slot,
// but client shows a health potion. When the client then consumes the potion it would apply the bless to item slot 0.
return false;
}
if (!this.CheckPreconditions(player, item))
{
return false;
}
if (!this.ModifyItem(targetItem, player.PersistenceContext))
{
return false;
}
await this.ConsumeSourceItemAsync(player, item).ConfigureAwait(false);
await player.InvokeViewPlugInAsync<IItemUpgradedPlugIn>(p => p.ItemUpgradedAsync(targetItem)).ConfigureAwait(false);
return true;
}
/// <summary>
/// Modifies the item.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="persistenceContext">The persistence context.</param>
/// <returns>Flag indicating whether the modification of the item occured.</returns>
protected abstract bool ModifyItem(Item item, IContext persistenceContext);
}

View File

@@ -0,0 +1,244 @@
// -----------------------------------------------------------------------
// <copyright file="ItemUpgradeConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
// -----------------------------------------------------------------------
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.Persistence;
/// <summary>
/// An item consume handler which upgrades the target item.
/// </summary>
public abstract class ItemUpgradeConsumeHandlerPlugIn : ItemModifyConsumeHandlerPlugIn
{
/// <summary>
/// Initializes a new instance of the <see cref="ItemUpgradeConsumeHandlerPlugIn"/> class.
/// </summary>
/// <param name="configuration">The configuration.</param>
internal ItemUpgradeConsumeHandlerPlugIn(ItemUpgradeConfiguration configuration)
{
this.Configuration = configuration;
}
/// <summary>
/// Specifies what should happen with the item, if the upgrading failed randomly.
/// </summary>
public enum ItemFailResult
{
/// <summary>
/// Nothing happens.
/// </summary>
None,
/// <summary>
/// Sets the option to the base level.
/// </summary>
SetOptionToBaseLevel,
/// <summary>
/// Removes the option.
/// </summary>
RemoveOption,
}
/// <summary>
/// Gets the upgrade configuration.
/// </summary>
internal ItemUpgradeConfiguration Configuration { get; }
/// <inheritdoc/>
protected override bool ModifyItem(Item item, IContext persistenceContext)
{
if (!this.ItemCanHaveOption(item))
{
return false;
}
if (this.ItemHasOptionAlready(item))
{
return this.TryUpgradeItemOption(item);
}
return this.TryAddItemOption(item, persistenceContext);
}
/// <summary>
/// Checks if an item can have the configured option.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>Flag indicating whether the item can have the option.</returns>
protected virtual bool ItemCanHaveOption(Item item)
{
return item.Definition?.PossibleItemOptions.Any(o => o.PossibleOptions.Any(p => p.OptionType == this.Configuration.OptionType)) ?? false;
}
/// <summary>
/// Tries to upgrade the item option.
/// </summary>
/// <param name="item">The item to upgrade.</param>
/// <returns>Flag indicating whether the item option was upgraded.</returns>
protected virtual bool TryUpgradeItemOption(Item item)
{
if (!this.Configuration.IncreasesOption)
{
return false;
}
var itemOption = item.ItemOptions.First(o => o.ItemOption?.OptionType == this.Configuration.OptionType);
var increasableOption = itemOption.ItemOption;
var higherOptionPossible = increasableOption?.LevelDependentOptions.Any(o => o.Level > itemOption.Level && o.RequiredItemLevel <= item.Level) ?? false;
if (!higherOptionPossible)
{
return false;
}
if (Rand.NextRandomBool(this.Configuration.SuccessChance))
{
itemOption.Level++;
}
else
{
this.HandleFailedUpgrade(item, itemOption);
}
return true;
}
private void HandleFailedUpgrade(Item item, ItemOptionLink itemOption)
{
switch (this.Configuration.FailResult)
{
case ItemFailResult.RemoveOption:
item.ItemOptions.Remove(itemOption);
break;
case ItemFailResult.SetOptionToBaseLevel:
itemOption.Level = itemOption.ItemOption?.LevelDependentOptions.Min(ldo => ldo.Level) ?? itemOption.Level;
break;
default:
// do nothing
break;
}
}
private bool TryAddItemOption(Item item, IContext persistenceContext)
{
if (!this.Configuration.AddsOption || item.Definition is null)
{
return false;
}
if (Rand.NextRandomBool(this.Configuration.SuccessChance))
{
var possibleOptions = item.Definition.PossibleItemOptions.
SelectMany(o => o.PossibleOptions).
Where(o => o.OptionType == this.Configuration.OptionType
&& (!o.LevelDependentOptions.Any() || o.LevelDependentOptions.Any(ldo => ldo.RequiredItemLevel <= item.Level))).ToList();
if (!possibleOptions.Any())
{
return false;
}
var optionLink = persistenceContext.CreateNew<ItemOptionLink>();
if (this.Configuration.OptionType == ItemOptionTypes.HarmonyOption)
{
// Str and agi reduction options are not always applicable, and so should be removed from the pool
if (!item.Definition.Requirements.Any(r => r.Attribute == Stats.TotalStrengthRequirementValue)
&& possibleOptions.FirstOrDefault(po => po.LevelDependentOptions
.Any(ldo => ldo.PowerUpDefinition?.TargetAttribute == Stats.RequiredStrengthReduction)) is { } strReductOpt)
{
possibleOptions.Remove(strReductOpt);
}
if (!item.Definition.Requirements.Any(r => r.Attribute == Stats.TotalAgilityRequirementValue)
&& possibleOptions.FirstOrDefault(po => po.LevelDependentOptions
.Any(ldo => ldo.PowerUpDefinition?.TargetAttribute == Stats.RequiredAgilityReduction)) is { } agiReductOpt)
{
possibleOptions.Remove(agiReductOpt);
}
optionLink.ItemOption = possibleOptions.SelectWeightedRandom(possibleOptions.Select(po => (int)po.Weight));
optionLink.Level = optionLink.ItemOption?.LevelDependentOptions.Select(ldo => ldo.Level).Min() ?? 0;
}
else
{
// ItemOptionTypes.Option
optionLink.ItemOption = possibleOptions.SelectRandom();
optionLink.Level = 1;
}
item.ItemOptions.Add(optionLink);
}
return true;
}
private bool ItemHasOptionAlready(Item item)
{
return item.ItemOptions.Any(o => o.ItemOption?.OptionType == this.Configuration.OptionType);
}
/// <summary>
/// The upgrade configuration.
/// </summary>
internal class ItemUpgradeConfiguration
{
/// <summary>
/// Initializes a new instance of the <see cref="ItemUpgradeConfiguration"/> class.
/// </summary>
/// <param name="optionType">Type of the option.</param>
/// <param name="addsOption">if set to <c>true</c> [adds option].</param>
/// <param name="increasesOption">if set to <c>true</c> [increases option].</param>
/// <param name="successChance">The success chance.</param>
/// <param name="failResult">The fail result.</param>
public ItemUpgradeConfiguration(ItemOptionType optionType, bool addsOption, bool increasesOption, double successChance, ItemFailResult failResult)
{
this.OptionType = optionType;
this.AddsOption = addsOption;
this.IncreasesOption = increasesOption;
this.SuccessChance = successChance;
this.FailResult = failResult;
}
/// <summary>
/// Gets the type of the option.
/// </summary>
public ItemOptionType OptionType { get; }
/// <summary>
/// Gets or sets a value indicating whether the handler adds option, if the item does not already have it.
/// </summary>
public bool AddsOption { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the handler increases the option.
/// </summary>
public bool IncreasesOption { get; set; }
/// <summary>
/// Gets or sets the success chance between 0 and 1.
/// </summary>
public double SuccessChance { get; set; }
/// <summary>
/// Gets or sets what should happen with the item if the upgrade fails because of no success.
/// </summary>
public ItemFailResult FailResult { get; set; }
/// <summary>
/// Gets or sets the option type which can boost the success rate.
/// </summary>
/// <remarks>
/// e.g. luck option which adds 25 per cent.
/// </remarks>
public ItemOptionType? BoostOptionType { get; set; }
/// <summary>
/// Gets or sets the success chance boost if the target item has the option of type specified in <see cref="BoostOptionType"/>.
/// </summary>
public double SuccessChanceBoost { get; set; }
}
}

View File

@@ -0,0 +1,28 @@
// <copyright file="LargeComplexPotionConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using System.Runtime.InteropServices;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Consume handler for large complex potions.
/// </summary>
[Guid("F5A8C0C4-7960-4815-83C2-F57339CD6FE2")]
[PlugIn]
[Display(Name = nameof(PlugInResources.LargeComplexPotionConsumeHandlerPlugIn_Name), Description = nameof(PlugInResources.LargeComplexPotionConsumeHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))]
public class LargeComplexPotionConsumeHandlerPlugIn : ComplexPotionConsumeHandlerPlugIn
{
/// <summary>
/// Initializes a new instance of the <see cref="LargeComplexPotionConsumeHandlerPlugIn"/> class.
/// </summary>
public LargeComplexPotionConsumeHandlerPlugIn()
: base(new LargeHealthPotionConsumeHandlerPlugIn(), new LargeShieldPotionConsumeHandlerPlugIn())
{
}
/// <inheritdoc />
public override ItemIdentifier Key => ItemConstants.LargeComplexPotion;
}

View File

@@ -0,0 +1,25 @@
// -----------------------------------------------------------------------
// <copyright file="LargeHealthPotionConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
// -----------------------------------------------------------------------
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using System.Runtime.InteropServices;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Consume handler for big health potions.
/// </summary>
[Guid("5035BBF5-A45D-454F-9D15-4DB6F725DCFB")]
[PlugIn]
[Display(Name = nameof(PlugInResources.LargeHealthPotionConsumeHandlerPlugIn_Name), Description = nameof(PlugInResources.LargeHealthPotionConsumeHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))]
public class LargeHealthPotionConsumeHandlerPlugIn : HealthPotionConsumeHandlerPlugIn
{
/// <inheritdoc />
public override ItemIdentifier Key => ItemConstants.LargeHealingPotion;
/// <inheritdoc/>
protected override int Multiplier => 3;
}

View File

@@ -0,0 +1,25 @@
// -----------------------------------------------------------------------
// <copyright file="LargeManaPotionConsumeHandler.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
// -----------------------------------------------------------------------
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using System.Runtime.InteropServices;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Consume handler for gib mana potions.
/// </summary>
[Guid("21CB28A4-BE9A-421C-9C7C-6F2E0FC9D614")]
[PlugIn]
[Display(Name = nameof(PlugInResources.LargeManaPotionConsumeHandler_Name), Description = nameof(PlugInResources.LargeManaPotionConsumeHandler_Description), ResourceType = typeof(PlugInResources))]
public class LargeManaPotionConsumeHandler : ManaPotionConsumeHandler
{
/// <inheritdoc />
public override ItemIdentifier Key => ItemConstants.LargeManaPotion;
/// <inheritdoc/>
protected override int Multiplier => 3;
}

View File

@@ -0,0 +1,25 @@
// -----------------------------------------------------------------------
// <copyright file="LargeShieldPotionConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
// -----------------------------------------------------------------------
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using System.Runtime.InteropServices;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Consume handler for large shield potions.
/// </summary>
[Guid("683C4BF1-8794-41B0-9742-B17B73A12BFE")]
[PlugIn]
[Display(Name = nameof(PlugInResources.LargeShieldPotionConsumeHandlerPlugIn_Name), Description = nameof(PlugInResources.LargeShieldPotionConsumeHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))]
public class LargeShieldPotionConsumeHandlerPlugIn : ShieldPotionConsumeHandlerPlugIn
{
/// <inheritdoc />
public override ItemIdentifier Key => ItemConstants.LargeShieldPotion;
/// <inheritdoc />
protected override double RecoverPercentage => 100;
}

View File

@@ -0,0 +1,57 @@
// <copyright file="LearnablesConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using System.Runtime.InteropServices;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Consume handler for items (e.g. scrolls, orbs) which add a skill when being consumed.
/// </summary>
[Guid("FD86947E-0B94-4490-8158-63B11A61565F")]
[PlugIn]
[Display(Name = nameof(PlugInResources.LearnablesConsumeHandlerPlugIn_Name), Description = nameof(PlugInResources.LearnablesConsumeHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))]
public class LearnablesConsumeHandlerPlugIn : BaseConsumeHandlerPlugIn
{
/// <inheritdoc />
public override ItemIdentifier Key => ItemConstants.AllScrolls;
/// <inheritdoc/>
public override async ValueTask<bool> ConsumeItemAsync(Player player, Item item, Item? targetItem, FruitUsage fruitUsage)
{
var skill = this.GetLearnableSkill(item, player.GameContext.Configuration);
if (skill is null || player.SkillList!.ContainsSkill(skill.Number.ToUnsigned()))
{
return false;
}
if (!await base.ConsumeItemAsync(player, item, targetItem, fruitUsage).ConfigureAwait(false))
{
return false;
}
await player.SkillList.AddLearnedSkillAsync(skill).ConfigureAwait(false);
return true;
}
/// <inheritdoc />
protected override bool CheckPreconditions(Player player, Item item)
{
return base.CheckPreconditions(player, item)
&& player.CompliesRequirements(item);
}
/// <summary>
/// Gets the learnable skill.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="gameConfiguration">The game configuration.</param>
/// <returns>The skill to learn.</returns>
protected virtual Skill? GetLearnableSkill(Item item, GameConfiguration gameConfiguration)
{
return item.Definition?.Skill;
}
}

View File

@@ -0,0 +1,31 @@
// -----------------------------------------------------------------------
// <copyright file="LifeJewelConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
// -----------------------------------------------------------------------
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using System.Runtime.InteropServices;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Consume handler for the Jewel of Life which adds and increases the item option of <see cref="ItemOptionTypes.Option"/>.
/// </summary>
[Guid("8AC6592D-D51C-47C9-B491-4778C615691D")]
[PlugIn]
[Display(Name = nameof(PlugInResources.LifeJewelConsumeHandlerPlugIn_Name), Description = nameof(PlugInResources.LifeJewelConsumeHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))]
public class LifeJewelConsumeHandlerPlugIn : ItemUpgradeConsumeHandlerPlugIn
{
/// <summary>
/// Initializes a new instance of the <see cref="LifeJewelConsumeHandlerPlugIn" /> class.
/// </summary>
public LifeJewelConsumeHandlerPlugIn()
: base(new ItemUpgradeConfiguration(ItemOptionTypes.Option, true, true, 0.5, ItemFailResult.RemoveOption))
{
}
/// <inheritdoc />
public override ItemIdentifier Key => ItemConstants.JewelOfLife;
}

View File

@@ -0,0 +1,29 @@
// <copyright file="LowerRefineStoneConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using System.Runtime.InteropServices;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Consume handler for the Lower Refine Stone which increases the item option of <see cref="ItemOptionTypes.HarmonyOption"/>.
/// </summary>
[Guid("71380E37-7AA9-447A-8A83-D08B676E55E1")]
[PlugIn]
[Display(Name = nameof(PlugInResources.LowerRefineStoneConsumeHandlerPlugIn_Name), Description = nameof(PlugInResources.LowerRefineStoneConsumeHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))]
public class LowerRefineStoneConsumeHandlerPlugIn : RefineStoneUpgradeConsumeHandlerPlugIn
{
/// <summary>
/// Initializes a new instance of the <see cref="LowerRefineStoneConsumeHandlerPlugIn" /> class.
/// </summary>
public LowerRefineStoneConsumeHandlerPlugIn()
: base(new ItemUpgradeConfiguration(ItemOptionTypes.HarmonyOption, false, true, 0.2, ItemFailResult.SetOptionToBaseLevel))
{
}
/// <inheritdoc />
public override ItemIdentifier Key => ItemConstants.LowerRefineStone;
}

View File

@@ -0,0 +1,23 @@
// -----------------------------------------------------------------------
// <copyright file="ManaPotionConsumeHandler.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
// -----------------------------------------------------------------------
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.Views.Character;
/// <summary>
/// Consume handler for potions which refills the players attribute <see cref="Stats.CurrentMana"/>.
/// </summary>
public abstract class ManaPotionConsumeHandler : RecoverConsumeHandlerPlugIn.ManaHealthConsumeHandlerPlugIn, IItemConsumeHandlerPlugIn
{
/// <inheritdoc/>
protected override AttributeDefinition MaximumAttribute => Stats.MaximumMana;
/// <inheritdoc/>
protected override AttributeDefinition CurrentAttribute => Stats.CurrentMana;
}

View File

@@ -0,0 +1,28 @@
// <copyright file="MediumComplexPotionConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using System.Runtime.InteropServices;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Consume handler for medium complex potions.
/// </summary>
[Guid("D4ED0E2E-3CAA-4B35-BA17-230E29EC324B")]
[PlugIn]
[Display(Name = nameof(PlugInResources.MediumComplexPotionConsumeHandlerPlugIn_Name), Description = nameof(PlugInResources.MediumComplexPotionConsumeHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))]
public class MediumComplexPotionConsumeHandlerPlugIn : ComplexPotionConsumeHandlerPlugIn
{
/// <summary>
/// Initializes a new instance of the <see cref="MediumComplexPotionConsumeHandlerPlugIn"/> class.
/// </summary>
public MediumComplexPotionConsumeHandlerPlugIn()
: base(new MediumHealthPotionConsumeHandlerPlugIn(), new MediumShieldPotionConsumeHandlerPlugIn())
{
}
/// <inheritdoc />
public override ItemIdentifier Key => ItemConstants.MediumComplexPotion;
}

View File

@@ -0,0 +1,25 @@
// -----------------------------------------------------------------------
// <copyright file="MediumHealthPotionConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
// -----------------------------------------------------------------------
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using System.Runtime.InteropServices;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Consume handler for middle health potions.
/// </summary>
[Guid("2ED0A431-B562-4097-AAE4-C972074BDCBA")]
[PlugIn]
[Display(Name = nameof(PlugInResources.MediumHealthPotionConsumeHandlerPlugIn_Name), Description = nameof(PlugInResources.MediumHealthPotionConsumeHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))]
public class MediumHealthPotionConsumeHandlerPlugIn : HealthPotionConsumeHandlerPlugIn
{
/// <inheritdoc />
public override ItemIdentifier Key => ItemConstants.MediumHealingPotion;
/// <inheritdoc/>
protected override int Multiplier => 2;
}

View File

@@ -0,0 +1,25 @@
// -----------------------------------------------------------------------
// <copyright file="MediumManaPotionConsumeHandler.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
// -----------------------------------------------------------------------
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using System.Runtime.InteropServices;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Consume handler for middle health potions.
/// </summary>
[Guid("EC1F3DDF-5AF1-455C-AE0C-11A8018AE7D4")]
[PlugIn]
[Display(Name = nameof(PlugInResources.MediumManaPotionConsumeHandler_Name), Description = nameof(PlugInResources.MediumManaPotionConsumeHandler_Description), ResourceType = typeof(PlugInResources))]
public class MediumManaPotionConsumeHandler : ManaPotionConsumeHandler
{
/// <inheritdoc />
public override ItemIdentifier Key => ItemConstants.MediumManaPotion;
/// <inheritdoc/>
protected override int Multiplier => 2;
}

View File

@@ -0,0 +1,25 @@
// -----------------------------------------------------------------------
// <copyright file="MediumShieldPotionConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
// -----------------------------------------------------------------------
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using System.Runtime.InteropServices;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Consume handler for middle shield potions.
/// </summary>
[Guid("5205B818-68DE-4639-BA50-85CD285CDC95")]
[PlugIn]
[Display(Name = nameof(PlugInResources.MediumShieldPotionConsumeHandlerPlugIn_Name), Description = nameof(PlugInResources.MediumShieldPotionConsumeHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))]
public class MediumShieldPotionConsumeHandlerPlugIn : ShieldPotionConsumeHandlerPlugIn
{
/// <inheritdoc />
public override ItemIdentifier Key => ItemConstants.MediumShieldPotion;
/// <inheritdoc />
protected override double RecoverPercentage => 40;
}

View File

@@ -0,0 +1,56 @@
// <copyright file="RecoverConsumeHandlerConfiguration.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using System.ComponentModel.DataAnnotations;
using MUnique.OpenMU.DataModel.Composition;
/// <summary>
/// The configuration of a <see cref="RecoverConsumeHandlerPlugIn"/>.
/// </summary>
public class RecoverConsumeHandlerConfiguration
{
/// <summary>
/// Gets or sets the total recover percentage.
/// </summary>
[Display(ResourceType = typeof(PlugInResources), Name = nameof(PlugInResources.RecoverConsumeHandlerConfiguration_TotalRecoverPercentage_Name))]
public double TotalRecoverPercentage { get; set; }
/// <summary>
/// Gets or sets the recover percentage increase by potion level.
/// </summary>
[Display(ResourceType = typeof(PlugInResources), Name = nameof(PlugInResources.RecoverConsumeHandlerConfiguration_RecoverPercentageIncreaseByPotionLevel_Name))]
public double RecoverPercentageIncreaseByPotionLevel { get; set; }
/// <summary>
/// Gets or sets the recover delay reduction by potion level.
/// A value between 0 and 1 (exclusive).
/// 1 would mean that the recover works instantly since level 1.
/// </summary>
[Display(ResourceType = typeof(PlugInResources), Name = nameof(PlugInResources.RecoverConsumeHandlerConfiguration_RecoverDelayReductionByPotionLevel_Name), Description = nameof(PlugInResources.RecoverConsumeHandlerConfiguration_RecoverDelayReductionByPotionLevel_Description))]
public double RecoverDelayReductionByPotionLevel { get; set; }
/// <summary>
/// Gets or sets the value which is additionally recovered.
/// From this value, the character level is subtracted.
/// </summary>
[Display(ResourceType = typeof(PlugInResources), Name = nameof(PlugInResources.RecoverConsumeHandlerConfiguration_AdditionalRecoverMinusCharacterLevel_Name), Description = nameof(PlugInResources.RecoverConsumeHandlerConfiguration_AdditionalRecoverMinusCharacterLevel_Description))]
public int AdditionalRecoverMinusCharacterLevel { get; set; }
/// <summary>
/// Gets or sets the cooldown time for the next consumption.
/// </summary>
[Display(ResourceType = typeof(PlugInResources), Name = nameof(PlugInResources.RecoverConsumeHandlerConfiguration_CooldownTime_Name), Description = nameof(PlugInResources.RecoverConsumeHandlerConfiguration_CooldownTime_Description))]
public TimeSpan CooldownTime { get; set; }
/// <summary>
/// Gets or sets the recover steps. If none are defined, the recover happens
/// instantly.
/// </summary>
[Display(ResourceType = typeof(PlugInResources), Name = nameof(PlugInResources.RecoverConsumeHandlerConfiguration_RecoverSteps_Name), Description = nameof(PlugInResources.RecoverConsumeHandlerConfiguration_RecoverSteps_Description))]
[ScaffoldColumn(true)]
[MemberOfAggregate]
public ICollection<RecoverStep> RecoverSteps { get; set; } = new List<RecoverStep>();
}

View File

@@ -0,0 +1,156 @@
// <copyright file="RecoverConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Consume handler which can recover attributes.
/// </summary>
public abstract class RecoverConsumeHandlerPlugIn : BaseConsumeHandlerPlugIn, ISupportCustomConfiguration<RecoverConsumeHandlerConfiguration>, ISupportDefaultCustomConfiguration
{
/// <summary>
/// Gets or sets the configuration.
/// </summary>
public RecoverConsumeHandlerConfiguration? Configuration { get; set; }
/// <summary>
/// Gets the attribute which contains the value which should get recovered.
/// </summary>
protected abstract AttributeDefinition CurrentAttribute { get; }
/// <summary>
/// Gets the attribute which contains the maximum value of the <see cref="CurrentAttribute"/>.
/// </summary>
protected abstract AttributeDefinition MaximumAttribute { get; }
/// <inheritdoc/>
public override async ValueTask<bool> ConsumeItemAsync(Player player, Item item, Item? targetItem, FruitUsage fruitUsage)
{
if (await base.ConsumeItemAsync(player, item, targetItem, fruitUsage).ConfigureAwait(false))
{
await this.RecoverAsync(player, item).ConfigureAwait(false);
return true;
}
return false;
}
/// <inheritdoc />
public abstract object CreateDefaultConfig();
/// <summary>
/// Recovers the attributes of the specified player.
/// </summary>
/// <param name="player">The player.</param>
/// <param name="item">The item.</param>
internal async ValueTask RecoverAsync(Player player, Item item)
{
if (player.Attributes is null)
{
return;
}
var configuration = this.Configuration ??= (RecoverConsumeHandlerConfiguration)this.CreateDefaultConfig();
var recoverPercentage = configuration.TotalRecoverPercentage + (item.Level * configuration.RecoverPercentageIncreaseByPotionLevel);
var additionalRecover = Math.Max(0, configuration.AdditionalRecoverMinusCharacterLevel - player.Attributes[Stats.Level]);
var totalRecoverAmount = (player.Attributes[this.MaximumAttribute] * recoverPercentage / 100.0) + additionalRecover;
var delayReduction = configuration.RecoverDelayReductionByPotionLevel * item.Level;
if (configuration.RecoverSteps.Count == 0 || delayReduction >= 1)
{
player.Attributes[this.CurrentAttribute] = (uint)Math.Min(player.Attributes[this.MaximumAttribute], player.Attributes[this.CurrentAttribute] + totalRecoverAmount);
await this.OnAfterRecoverAsync(player).ConfigureAwait(false);
}
else
{
_ = this.RecoverByStepsAsync(player, delayReduction, configuration, totalRecoverAmount);
}
player.PotionCooldownUntil = DateTime.UtcNow.Add(this.Configuration.CooldownTime);
}
/// <inheritdoc />
protected override bool CheckPreconditions(Player player, Item item)
{
return base.CheckPreconditions(player, item)
&& player.PotionCooldownUntil <= DateTime.UtcNow;
}
/// <summary>
/// Called after the attribute was recovered. Can be handled to update
/// the view.
/// </summary>
/// <param name="player">The player.</param>
protected virtual ValueTask OnAfterRecoverAsync(Player player)
{
return default;
}
private async Task RecoverByStepsAsync(Player player, double delayReduction, RecoverConsumeHandlerConfiguration configuration, double totalRecoverAmount)
{
foreach (var step in configuration.RecoverSteps)
{
if (!player.IsAlive || player.Attributes is not { } playerAttributes)
{
break;
}
var delay = TimeSpan.FromMilliseconds(step.Delay.TotalMilliseconds * (1.0 - delayReduction));
if (delay.TotalMilliseconds > 0)
{
await Task.Delay(delay).ConfigureAwait(false);
}
var recoverAmount = totalRecoverAmount * (step.RecoverPercentage / 100.0);
playerAttributes[this.CurrentAttribute] = (uint)Math.Min(playerAttributes[this.MaximumAttribute], playerAttributes[this.CurrentAttribute] + recoverAmount);
await this.OnAfterRecoverAsync(player).ConfigureAwait(false);
}
}
/// <summary>
/// The base class for health and mana /.
/// </summary>
public abstract class ManaHealthConsumeHandlerPlugIn : RecoverConsumeHandlerPlugIn
{
/// <summary>
/// Gets the multiplier of 50 for the additional recover.
/// </summary>
protected abstract int Multiplier { get; }
/// <inheritdoc />
public override object CreateDefaultConfig()
{
return new RecoverConsumeHandlerConfiguration
{
TotalRecoverPercentage = this.Multiplier * 10,
AdditionalRecoverMinusCharacterLevel = (this.Multiplier + 1) * 50,
RecoverDelayReductionByPotionLevel = 1.0 / 16.0,
RecoverPercentageIncreaseByPotionLevel = 1,
CooldownTime = TimeSpan.FromSeconds(0.5),
RecoverSteps =
{
new RecoverStep
{
Delay = TimeSpan.FromMilliseconds(200),
RecoverPercentage = 20,
},
new RecoverStep
{
Delay = TimeSpan.FromMilliseconds(600),
RecoverPercentage = 60,
},
new RecoverStep
{
Delay = TimeSpan.FromMilliseconds(200),
RecoverPercentage = 20,
},
},
};
}
}
}

View File

@@ -0,0 +1,24 @@
// <copyright file="RecoverStep.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
/// <summary>
/// Defines one step of a recovery.
/// </summary>
public class RecoverStep
{
/// <summary>
/// Gets or sets the delay after which the recovery of the defined <see cref="RecoverPercentage"/>
/// occurs.
/// </summary>
public TimeSpan Delay { get; set; }
/// <summary>
/// Gets or sets the recover percentage of the <see cref="RecoverConsumeHandlerConfiguration.TotalRecoverPercentage"/>
/// after waiting for the <see cref="Delay"/>.
/// The total of all steps of a <see cref="RecoverConsumeHandlerConfiguration"/> should be 100.
/// </summary>
public int RecoverPercentage { get; set; }
}

View File

@@ -0,0 +1,45 @@
// -----------------------------------------------------------------------
// <copyright file="RefineStoneUpgradeConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
// -----------------------------------------------------------------------
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.GameLogic.Attributes;
/// <summary>
/// Consume handler for a Refine Stone which increases the item option of <see cref="ItemOptionTypes.HarmonyOption"/>.
/// </summary>
public abstract class RefineStoneUpgradeConsumeHandlerPlugIn : ItemUpgradeConsumeHandlerPlugIn
{
/// <summary>
/// Initializes a new instance of the <see cref="RefineStoneUpgradeConsumeHandlerPlugIn"/> class.
/// </summary>
/// <param name="configuration">The configuration.</param>
private protected RefineStoneUpgradeConsumeHandlerPlugIn(ItemUpgradeConfiguration configuration)
: base(configuration)
{
}
/// <inheritdoc/>
protected override bool TryUpgradeItemOption(Item item)
{
var harmonyOption = item.ItemOptions.First(o => o.ItemOption?.OptionType == this.Configuration.OptionType);
var levelOptions = harmonyOption.ItemOption?.LevelDependentOptions;
if (levelOptions?.FirstOrDefault()?.PowerUpDefinition?.TargetAttribute == Stats.MinimumPhysBaseDmg)
{ // The difference betwen the max and min dmg of a weapon must be at least 1
var weaponMinDmg = item.Definition!.BasePowerUpAttributes.First(bpu => bpu.TargetAttribute == Stats.MinimumPhysBaseDmgByWeapon).BaseValue;
var weaponMaxDmg = item.Definition!.BasePowerUpAttributes.First(bpu => bpu.TargetAttribute == Stats.MaximumPhysBaseDmgByWeapon).BaseValue;
var nextMinDmgBoost = levelOptions.FirstOrDefault(o => o.Level == harmonyOption.Level + 1)?.PowerUpDefinition?.Boost?.ConstantValue.Value;
if (nextMinDmgBoost is float boost && weaponMaxDmg - (weaponMinDmg + boost) < 1)
{
return false;
}
}
return base.TryUpgradeItemOption(item);
}
}

View File

@@ -0,0 +1,57 @@
// -----------------------------------------------------------------------
// <copyright file="ShieldPotionConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
// -----------------------------------------------------------------------
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.GameLogic.Attributes;
/// <summary>
/// Consume handler for shield potions, which recover the <see cref="Stats.CurrentShield"/>.
/// </summary>
public abstract class ShieldPotionConsumeHandlerPlugIn : RecoverConsumeHandlerPlugIn, IItemConsumeHandlerPlugIn
{
/// <inheritdoc/>
protected override AttributeDefinition MaximumAttribute => Stats.MaximumShield;
/// <inheritdoc/>
protected override AttributeDefinition CurrentAttribute => Stats.CurrentShield;
/// <summary>
/// Gets the recover percentage.
/// </summary>
protected abstract double RecoverPercentage { get; }
/// <inheritdoc />
public sealed override object CreateDefaultConfig()
{
return new RecoverConsumeHandlerConfiguration
{
TotalRecoverPercentage = this.RecoverPercentage,
RecoverDelayReductionByPotionLevel = 1.0 / 16.0,
RecoverPercentageIncreaseByPotionLevel = 1,
CooldownTime = TimeSpan.FromSeconds(0.5),
RecoverSteps =
{
new RecoverStep
{
Delay = TimeSpan.FromMilliseconds(200),
RecoverPercentage = 20,
},
new RecoverStep
{
Delay = TimeSpan.FromMilliseconds(600),
RecoverPercentage = 60,
},
new RecoverStep
{
Delay = TimeSpan.FromMilliseconds(200),
RecoverPercentage = 20,
},
},
};
}
}

View File

@@ -0,0 +1,50 @@
// -----------------------------------------------------------------------
// <copyright file="SiegePotionConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
// -----------------------------------------------------------------------
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.PlugIns.ChatCommands;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The alcohol consume handler.
/// </summary>
[Guid("9D50CE95-5354-43A7-8DD5-9D6953700DFA")]
[PlugIn]
[Display(Name = nameof(PlugInResources.SiegePotionConsumeHandlerPlugIn_Name), Description = nameof(PlugInResources.SiegePotionConsumeHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))]
public class SiegePotionConsumeHandlerPlugIn : ApplyMagicEffectConsumeHandlerPlugIn
{
/// <inheritdoc />
public override ItemIdentifier Key => ItemConstants.SiegePotion;
/// <inheritdoc/>
public override async ValueTask<bool> ConsumeItemAsync(Player player, Item item, Item? targetItem, FruitUsage fruitUsage)
{
if (item.Level == 0
&& player.GameContext.Configuration.MagicEffects.FirstOrDefault(e => e.Number == 10) is { } blessEffectDefinition)
{
return await base.ConsumeItemAsyncCore(player, item, targetItem, fruitUsage, blessEffectDefinition).ConfigureAwait(false);
}
if (item.Level == 1
&& player.GameContext.Configuration.MagicEffects.FirstOrDefault(e => e.Number == 11) is { } effectDefinition)
{
if (await base.ConsumeItemAsyncCore(player, item, targetItem, fruitUsage, effectDefinition).ConfigureAwait(false))
{
await player.InvokeViewPlugInAsync<IConsumeSpecialItemPlugIn>(p => p.ConsumeSpecialItemAsync(item, (ushort)(effectDefinition.Duration?.ConstantValue.Value ?? 0))).ConfigureAwait(false);
return true;
}
}
else
{
await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.ItemEffectNotFound)).ConfigureAwait(false);
}
return false;
}
}

View File

@@ -0,0 +1,28 @@
// <copyright file="SmallComplexPotionConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using System.Runtime.InteropServices;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Consume handler for small complex potions.
/// </summary>
[Guid("9424D511-0AF7-4FC2-BD11-24799368D651")]
[PlugIn]
[Display(Name = nameof(PlugInResources.SmallComplexPotionConsumeHandlerPlugIn_Name), Description = nameof(PlugInResources.SmallComplexPotionConsumeHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))]
public class SmallComplexPotionConsumeHandlerPlugIn : ComplexPotionConsumeHandlerPlugIn
{
/// <summary>
/// Initializes a new instance of the <see cref="SmallComplexPotionConsumeHandlerPlugIn"/> class.
/// </summary>
public SmallComplexPotionConsumeHandlerPlugIn()
: base(new SmallHealthPotionConsumeHandlerPlugIn(), new SmallShieldPotionConsumeHandlerPlugIn())
{
}
/// <inheritdoc />
public override ItemIdentifier Key => ItemConstants.SmallComplexPotion;
}

View File

@@ -0,0 +1,25 @@
// -----------------------------------------------------------------------
// <copyright file="SmallHealthPotionConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
// -----------------------------------------------------------------------
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using System.Runtime.InteropServices;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Consume handler for small health potions.
/// </summary>
[Guid("BF28D5A4-D97E-44AA-88CB-D448B1BF7A75")]
[PlugIn]
[Display(Name = nameof(PlugInResources.SmallHealthPotionConsumeHandlerPlugIn_Name), Description = nameof(PlugInResources.SmallHealthPotionConsumeHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))]
public class SmallHealthPotionConsumeHandlerPlugIn : HealthPotionConsumeHandlerPlugIn
{
/// <inheritdoc />
public override ItemIdentifier Key => ItemConstants.SmallHealingPotion;
/// <inheritdoc/>
protected override int Multiplier => 1;
}

View File

@@ -0,0 +1,25 @@
// -----------------------------------------------------------------------
// <copyright file="SmallManaPotionConsumeHandler.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
// -----------------------------------------------------------------------
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using System.Runtime.InteropServices;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Consume handler for small health potions.
/// </summary>
[Guid("A55849BC-7BD7-4444-A35A-F1AC1D48F179")]
[PlugIn]
[Display(Name = nameof(PlugInResources.SmallManaPotionConsumeHandler_Name), Description = nameof(PlugInResources.SmallManaPotionConsumeHandler_Description), ResourceType = typeof(PlugInResources))]
public class SmallManaPotionConsumeHandler : ManaPotionConsumeHandler
{
/// <inheritdoc />
public override ItemIdentifier Key => ItemConstants.SmallManaPotion;
/// <inheritdoc/>
protected override int Multiplier => 1;
}

View File

@@ -0,0 +1,25 @@
// -----------------------------------------------------------------------
// <copyright file="SmallShieldPotionConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
// -----------------------------------------------------------------------
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using System.Runtime.InteropServices;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Consume handler for small shield potions.
/// </summary>
[Guid("C403C8D7-9143-42BC-9894-CA285303E17A")]
[PlugIn]
[Display(Name = nameof(PlugInResources.SmallShieldPotionConsumeHandlerPlugIn_Name), Description = nameof(PlugInResources.SmallShieldPotionConsumeHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))]
public class SmallShieldPotionConsumeHandlerPlugIn : ShieldPotionConsumeHandlerPlugIn
{
/// <inheritdoc />
public override ItemIdentifier Key => ItemConstants.SmallShieldPotion;
/// <inheritdoc />
protected override double RecoverPercentage => 20;
}

View File

@@ -0,0 +1,48 @@
// -----------------------------------------------------------------------
// <copyright file="SoulJewelConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
// -----------------------------------------------------------------------
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using System.Runtime.InteropServices;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Consume handler for the Jewel of Soul which increases the item level by one until the level of 9 with a chance of 50%.
/// </summary>
[Guid("A76CDA49-1C56-401A-96D1-294D9A68A7B9")]
[PlugIn]
[Display(Name = nameof(PlugInResources.SoulJewelConsumeHandlerPlugIn_Name), Description = nameof(PlugInResources.SoulJewelConsumeHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))]
public class SoulJewelConsumeHandlerPlugIn : UpgradeItemLevelJewelConsumeHandlerPlugIn<UpgradeItemLevelConfiguration>
{
/// <summary>
/// Initializes a new instance of the <see cref="SoulJewelConsumeHandlerPlugIn"/> class.
/// </summary>
public SoulJewelConsumeHandlerPlugIn()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SoulJewelConsumeHandlerPlugIn"/> class.
/// </summary>
/// <param name="randomizer">The randomizer.</param>
internal SoulJewelConsumeHandlerPlugIn(IRandomizer randomizer)
: base(randomizer)
{
}
/// <inheritdoc />
public override ItemIdentifier Key => ItemConstants.JewelOfSoul;
/// <inheritdoc />
public override object CreateDefaultConfig() => new UpgradeItemLevelConfiguration
{
MaximumLevel = 8,
MinimumLevel = 0,
SuccessRatePercentage = 50,
SuccessRateBonusWithLuckPercentage = 25,
ResetToLevel0WhenFailMinLevel = 7,
};
}

View File

@@ -0,0 +1,41 @@
// <copyright file="SummoningOrbConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using System.Runtime.InteropServices;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The summoning orb consume handler.
/// There is only one "Orb" item definition which allows to learn different skills, depending on the item level.
/// This consume handler determines the skill by adding the item level to the skill number
/// of the <see cref="ItemDefinition.Skill"/>.
/// </summary>
[Guid("71C8E542-4868-487E-BC92-0B7CC7CAEC8B")]
[PlugIn]
[Display(Name = nameof(PlugInResources.SummoningOrbConsumeHandlerPlugIn_Name), Description = nameof(PlugInResources.SummoningOrbConsumeHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))]
public class SummoningOrbConsumeHandlerPlugIn : LearnablesConsumeHandlerPlugIn
{
/// <inheritdoc />
public override ItemIdentifier Key => ItemConstants.SummonOrb;
/// <summary>
/// Gets the learnable skill.
/// </summary>
/// <param name="item">The item.</param>
/// <param name="gameConfiguration">The game configuration.</param>
/// <returns>
/// The skill to learn.
/// </returns>
protected override Skill GetLearnableSkill(Item item, GameConfiguration gameConfiguration)
{
item.ThrowNotInitializedProperty(item.Definition?.Skill is null, "Definition.Skill");
var baseSkillNumber = item.Definition.Skill.Number;
var targetSkillNumber = baseSkillNumber + item.Level;
return gameConfiguration.Skills.First(s => s.Number == targetSkillNumber);
}
}

View File

@@ -0,0 +1,44 @@
// -----------------------------------------------------------------------
// <copyright file="TownPortalScrollConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
// -----------------------------------------------------------------------
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using System.Runtime.InteropServices;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Consume handler for the town portal scroll.
/// It warps the player to the nearest town.
/// </summary>
/// <remarks>
/// We might need a field for the "nearest" town in the <see cref="GameMapDefinition"/>.
/// <see cref="GameMapDefinition.SafezoneMap"/> might not be suitable.
/// </remarks>
[Guid("825C3110-75F1-4157-A189-15B365B4791E")]
[PlugIn]
[Display(Name = nameof(PlugInResources.TownPortalScrollConsumeHandlerPlugIn_Name), Description = nameof(PlugInResources.TownPortalScrollConsumeHandlerPlugIn_Description), ResourceType = typeof(PlugInResources))]
public class TownPortalScrollConsumeHandlerPlugIn : BaseConsumeHandlerPlugIn
{
/// <inheritdoc />
public override ItemIdentifier Key => ItemConstants.TownPortalScroll;
/// <inheritdoc />
public override async ValueTask<bool> ConsumeItemAsync(Player player, Item item, Item? targetItem, FruitUsage fruitUsage)
{
if (await base.ConsumeItemAsync(player, item, targetItem, fruitUsage).ConfigureAwait(false))
{
var targetMapDef = player.CurrentMap!.Definition.SafezoneMap ?? player.SelectedCharacter!.CharacterClass!.HomeMap;
if (targetMapDef is { }
&& await player.GameContext.GetMapAsync((ushort)targetMapDef.Number).ConfigureAwait(false) is { SafeZoneSpawnGate: { } spawnGate })
{
await player.WarpToAsync(spawnGate).ConfigureAwait(false);
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,64 @@
// -----------------------------------------------------------------------
// <copyright file="UpgradeItemLevelConfiguration.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
// -----------------------------------------------------------------------
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using System.ComponentModel.DataAnnotations;
using MUnique.OpenMU.DataModel.Configuration.Items;
/// <summary>
/// Configuration for a <see cref="UpgradeItemLevelJewelConsumeHandlerPlugIn{TConfig}"/>.
/// </summary>
public class UpgradeItemLevelConfiguration
{
/// <summary>
/// Gets or sets the success rate percentage.
/// </summary>
[Display(ResourceType = typeof(PlugInResources), Name = nameof(PlugInResources.UpgradeItemLevelConfiguration_SuccessRatePercentage_Name), Description = nameof(PlugInResources.UpgradeItemLevelConfiguration_SuccessRatePercentage_Description))]
public byte SuccessRatePercentage { get; set; }
/// <summary>
/// Gets or sets the success rate bonus with luck percentage.
/// </summary>
[Display(ResourceType = typeof(PlugInResources), Name = nameof(PlugInResources.UpgradeItemLevelConfiguration_SuccessRateBonusWithLuckPercentage_Name), Description = nameof(PlugInResources.UpgradeItemLevelConfiguration_SuccessRateBonusWithLuckPercentage_Description))]
public byte SuccessRateBonusWithLuckPercentage { get; set; }
/// <summary>
/// Gets or sets the minimum item level which the item has to have before applying the jewel.
/// </summary>
[Display(ResourceType = typeof(PlugInResources), Name = nameof(PlugInResources.UpgradeItemLevelConfiguration_MinimumLevel_Name), Description = nameof(PlugInResources.UpgradeItemLevelConfiguration_MinimumLevel_Description))]
public byte MinimumLevel { get; set; }
/// <summary>
/// Gets or sets the maximum item level which the item has to have before applying the jewel.
/// </summary>
[Display(ResourceType = typeof(PlugInResources), Name = nameof(PlugInResources.UpgradeItemLevelConfiguration_MaximumLevel_Name), Description = nameof(PlugInResources.UpgradeItemLevelConfiguration_MaximumLevel_Description))]
public byte MaximumLevel { get; set; }
/// <summary>
/// Gets or sets the amount of levels which the item will be upgraded by.
/// </summary>
[Display(ResourceType = typeof(PlugInResources), Name = nameof(PlugInResources.UpgradeItemLevelConfiguration_LevelAmount_Name), Description = nameof(PlugInResources.UpgradeItemLevelConfiguration_LevelAmount_Description))]
public byte LevelAmount { get; set; } = 1;
/// <summary>
/// Gets or sets the items which are allowed to be upgraded. If empty, all items are allowed except those in <see cref="DisallowedItems"/>.
/// </summary>
[Display(ResourceType = typeof(PlugInResources), Name = nameof(PlugInResources.UpgradeItemLevelConfiguration_AllowedItems_Name), Description = nameof(PlugInResources.UpgradeItemLevelConfiguration_AllowedItems_Description))]
public ICollection<ItemDefinition> AllowedItems { get; set; } = new List<ItemDefinition>();
/// <summary>
/// Gets or sets the items which are not allowed to be upgraded.
/// </summary>
[Display(ResourceType = typeof(PlugInResources), Name = nameof(PlugInResources.UpgradeItemLevelConfiguration_DisallowedItems_Name), Description = nameof(PlugInResources.UpgradeItemLevelConfiguration_DisallowedItems_Description))]
public ICollection<ItemDefinition> DisallowedItems { get; set; } = new List<ItemDefinition>();
/// <summary>
/// Gets or sets the item level after which the item will drop to level 0 when it fails.
/// </summary>
[Display(ResourceType = typeof(PlugInResources), Name = nameof(PlugInResources.UpgradeItemLevelConfiguration_ResetToLevel0WhenFailMinLevel_Name), Description = nameof(PlugInResources.UpgradeItemLevelConfiguration_ResetToLevel0WhenFailMinLevel_Description))]
public byte ResetToLevel0WhenFailMinLevel { get; set; }
}

View File

@@ -0,0 +1,106 @@
// -----------------------------------------------------------------------
// <copyright file="UpgradeItemLevelJewelConsumeHandlerPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
// -----------------------------------------------------------------------
namespace MUnique.OpenMU.GameLogic.PlayerActions.ItemConsumeActions;
using MUnique.OpenMU.DataModel.Configuration.Items;
using MUnique.OpenMU.Persistence;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// Base class for consume handlers which upgrade the item level by consuming a jewel.
/// </summary>
/// <typeparam name="TConfig">The type of the configuration.</typeparam>
public abstract class UpgradeItemLevelJewelConsumeHandlerPlugIn<TConfig>
: ItemModifyConsumeHandlerPlugIn, ISupportCustomConfiguration<TConfig>, ISupportDefaultCustomConfiguration
where TConfig : UpgradeItemLevelConfiguration
{
private readonly IRandomizer _randomizer;
/// <summary>
/// Initializes a new instance of the <see cref="UpgradeItemLevelJewelConsumeHandlerPlugIn{TConfig}"/> class.
/// </summary>
protected UpgradeItemLevelJewelConsumeHandlerPlugIn()
: this(Rand.GetRandomizer())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="UpgradeItemLevelJewelConsumeHandlerPlugIn{TConfig}"/> class.
/// </summary>
/// <param name="randomizer">The randomizer.</param>
protected UpgradeItemLevelJewelConsumeHandlerPlugIn(IRandomizer randomizer)
{
this._randomizer = randomizer;
}
/// <inheritdoc/>
public TConfig? Configuration { get; set; }
/// <inheritdoc />
public abstract object CreateDefaultConfig();
/// <inheritdoc/>
protected override bool ModifyItem(Item item, IContext persistenceContext)
{
if (!item.CanLevelBeUpgraded())
{
return false;
}
this.Configuration ??= (TConfig)this.CreateDefaultConfig();
if (item.Level < this.Configuration.MinimumLevel)
{
return false;
}
if (this.Configuration.DisallowedItems.Contains(item.Definition!))
{
return false;
}
if (this.Configuration.AllowedItems.Any() && !this.Configuration.AllowedItems.Contains(item.Definition!))
{
return false;
}
var maximumAllowedLevel = Math.Min(this.Configuration.MaximumLevel + 1, item.Definition!.MaximumItemLevel);
var levelAmount = Math.Min(this.Configuration.LevelAmount, maximumAllowedLevel - item.Level);
if (levelAmount <= 0)
{
return false;
}
int percent = this.Configuration.SuccessRatePercentage;
if (ItemHasLuck(item))
{
percent += this.Configuration.SuccessRateBonusWithLuckPercentage;
}
if (this._randomizer.NextRandomBool(percent))
{
item.Level += (byte)levelAmount;
item.Durability = item.GetMaximumDurabilityOfOnePiece();
return true; // true doesn't mean that it was successful, just that the consumption happened.
}
if (item.Level >= this.Configuration.ResetToLevel0WhenFailMinLevel)
{
item.Level = 0;
}
else
{
item.Level = (byte)Math.Max(item.Level - 1, 0);
}
return true;
}
private static bool ItemHasLuck(Item item)
{
return item.ItemOptions.Any(o => o.ItemOption?.OptionType == ItemOptionTypes.Luck);
}
}

Some files were not shown because too many files have changed in this diff Show More