baseline: OpenMU upstream b5a0961 (fresh source)
This commit is contained in:
158
src/GameLogic/PlayerActions/Character/AddMasterPointAction.cs
Normal file
158
src/GameLogic/PlayerActions/Character/AddMasterPointAction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
152
src/GameLogic/PlayerActions/Character/CreateCharacterAction.cs
Normal file
152
src/GameLogic/PlayerActions/Character/CreateCharacterAction.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
66
src/GameLogic/PlayerActions/Character/IncreaseStatsAction.cs
Normal file
66
src/GameLogic/PlayerActions/Character/IncreaseStatsAction.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
105
src/GameLogic/PlayerActions/Chat/ChatMessageAction.cs
Normal file
105
src/GameLogic/PlayerActions/Chat/ChatMessageAction.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/GameLogic/PlayerActions/Chat/IChatMessageProcessor.cs
Normal file
19
src/GameLogic/PlayerActions/Chat/IChatMessageProcessor.cs
Normal 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);
|
||||
}
|
||||
50
src/GameLogic/PlayerActions/CloseNpcDialogAction.cs
Normal file
50
src/GameLogic/PlayerActions/CloseNpcDialogAction.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
122
src/GameLogic/PlayerActions/Craftings/BaseEventTicketCrafting.cs
Normal file
122
src/GameLogic/PlayerActions/Craftings/BaseEventTicketCrafting.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
40
src/GameLogic/PlayerActions/Craftings/DarkHorseCrafting.cs
Normal file
40
src/GameLogic/PlayerActions/Craftings/DarkHorseCrafting.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
88
src/GameLogic/PlayerActions/Craftings/DinorantCrafting.cs
Normal file
88
src/GameLogic/PlayerActions/Craftings/DinorantCrafting.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
122
src/GameLogic/PlayerActions/Craftings/FenrirUpgradeCrafting.cs
Normal file
122
src/GameLogic/PlayerActions/Craftings/FenrirUpgradeCrafting.cs
Normal 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 };
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
164
src/GameLogic/PlayerActions/Craftings/MountSeedSphereCrafting.cs
Normal file
164
src/GameLogic/PlayerActions/Craftings/MountSeedSphereCrafting.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
177
src/GameLogic/PlayerActions/Craftings/RefineStoneCrafting.cs
Normal file
177
src/GameLogic/PlayerActions/Craftings/RefineStoneCrafting.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
45
src/GameLogic/PlayerActions/Craftings/RestoreItemCrafting.cs
Normal file
45
src/GameLogic/PlayerActions/Craftings/RestoreItemCrafting.cs
Normal 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 };
|
||||
}
|
||||
}
|
||||
55
src/GameLogic/PlayerActions/Craftings/SecondWingsCrafting.cs
Normal file
55
src/GameLogic/PlayerActions/Craftings/SecondWingsCrafting.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
63
src/GameLogic/PlayerActions/Craftings/SeedSphereCrafting.cs
Normal file
63
src/GameLogic/PlayerActions/Craftings/SeedSphereCrafting.cs
Normal 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 };
|
||||
}
|
||||
}
|
||||
83
src/GameLogic/PlayerActions/Craftings/ThirdWingsCrafting.cs
Normal file
83
src/GameLogic/PlayerActions/Craftings/ThirdWingsCrafting.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
323
src/GameLogic/PlayerActions/Duel/DuelActions.cs
Normal file
323
src/GameLogic/PlayerActions/Duel/DuelActions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
47
src/GameLogic/PlayerActions/Duel/UpdateDuelScorePlugIn.cs
Normal file
47
src/GameLogic/PlayerActions/Duel/UpdateDuelScorePlugIn.cs
Normal 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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/GameLogic/PlayerActions/EnterBalgassRefugeeAction.cs
Normal file
29
src/GameLogic/PlayerActions/EnterBalgassRefugeeAction.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
31
src/GameLogic/PlayerActions/EnterBarracksOfBalgassAction.cs
Normal file
31
src/GameLogic/PlayerActions/EnterBarracksOfBalgassAction.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
54
src/GameLogic/PlayerActions/EnterMarketPlaceAction.cs
Normal file
54
src/GameLogic/PlayerActions/EnterMarketPlaceAction.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
98
src/GameLogic/PlayerActions/EnterQuestMapAction.cs
Normal file
98
src/GameLogic/PlayerActions/EnterQuestMapAction.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
54
src/GameLogic/PlayerActions/GameMapDefinitionExtensions.cs
Normal file
54
src/GameLogic/PlayerActions/GameMapDefinitionExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
52
src/GameLogic/PlayerActions/Guild/GuildCreateAction.cs
Normal file
52
src/GameLogic/PlayerActions/Guild/GuildCreateAction.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/GameLogic/PlayerActions/Guild/GuildInfoRequestAction.cs
Normal file
23
src/GameLogic/PlayerActions/Guild/GuildInfoRequestAction.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
71
src/GameLogic/PlayerActions/Guild/GuildKickPlayerAction.cs
Normal file
71
src/GameLogic/PlayerActions/Guild/GuildKickPlayerAction.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
33
src/GameLogic/PlayerActions/Guild/GuildListRequestAction.cs
Normal file
33
src/GameLogic/PlayerActions/Guild/GuildListRequestAction.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
50
src/GameLogic/PlayerActions/Guild/GuildMasterAnswerAction.cs
Normal file
50
src/GameLogic/PlayerActions/Guild/GuildMasterAnswerAction.cs
Normal 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.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
51
src/GameLogic/PlayerActions/Guild/GuildRequestAction.cs
Normal file
51
src/GameLogic/PlayerActions/Guild/GuildRequestAction.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
208
src/GameLogic/PlayerActions/Guild/GuildWarAnswerAction.cs
Normal file
208
src/GameLogic/PlayerActions/Guild/GuildWarAnswerAction.cs
Normal 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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
91
src/GameLogic/PlayerActions/Guild/GuildWarRequestAction.cs
Normal file
91
src/GameLogic/PlayerActions/Guild/GuildWarRequestAction.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
123
src/GameLogic/PlayerActions/HitAction.cs
Normal file
123
src/GameLogic/PlayerActions/HitAction.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
26
src/GameLogic/PlayerActions/ItemConsumeActions/FruitUsage.cs
Normal file
26
src/GameLogic/PlayerActions/ItemConsumeActions/FruitUsage.cs
Normal 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,
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user