// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.GameLogic.PlayerActions.MiniGames; using MUnique.OpenMU.DataModel.Configuration.Items; using MUnique.OpenMU.GameLogic.Attributes; using MUnique.OpenMU.GameLogic.GuildWar; using MUnique.OpenMU.GameLogic.MiniGames; using MUnique.OpenMU.GameLogic.PlayerActions.PlayerStore; using MUnique.OpenMU.GameLogic.PlugIns.PeriodicTasks; using MUnique.OpenMU.GameLogic.Views.Inventory; /// /// Player action which implements entering a mini game. /// public class EnterMiniGameAction { private const int UndefinedTicketSlot = 0xFF; /// /// Tries to enter the mini game event of the specified type and level with the specified ticket item. /// /// The player. /// The mini game type. /// The mini game level. /// The inventory index of the ticket. public async ValueTask TryEnterMiniGameAsync(Player player, MiniGameType miniGameType, int gameLevel, byte gameTicketInventoryIndex) { if (player.SelectedCharacter?.CharacterClass is null) { throw new InvalidOperationException("Character not selected or initialized."); } var miniGameDefinition = player.GameContext.Configuration.MiniGameDefinitions .FirstOrDefault(def => def.Type == miniGameType && def.GameLevel == gameLevel); if (miniGameDefinition is null || (miniGameDefinition.RequiresMasterClass && !player.SelectedCharacter.CharacterClass.IsMasterClass) || player.CurrentMiniGame is not null) { await player.InvokeViewPlugInAsync(p => p.ShowResultAsync(miniGameType, EnterResult.Failed)).ConfigureAwait(false); return; } var isSpecialCharacter = player.SelectedCharacter.IsSpecialCharacter(); var characterLevel = player.Attributes![Stats.Level]; var minLevel = isSpecialCharacter ? miniGameDefinition.MinimumSpecialCharacterLevel : miniGameDefinition.MinimumCharacterLevel; var maxLevel = isSpecialCharacter ? miniGameDefinition.MaximumSpecialCharacterLevel : miniGameDefinition.MaximumCharacterLevel; var requiresMasterLevel = miniGameDefinition.RequiresMasterClass; if (characterLevel < minLevel || (requiresMasterLevel && player.SelectedCharacter?.CharacterClass?.IsMasterClass is not true)) { await player.InvokeViewPlugInAsync(p => p.ShowResultAsync(miniGameType, EnterResult.CharacterLevelTooLow)).ConfigureAwait(false); return; } if (characterLevel > maxLevel) { await player.InvokeViewPlugInAsync(p => p.ShowResultAsync(miniGameType, EnterResult.CharacterLevelTooHigh)).ConfigureAwait(false); return; } if (!this.CheckTicketItem(miniGameDefinition, player, gameTicketInventoryIndex, out var ticketItem)) { await player.InvokeViewPlugInAsync(p => p.ShowResultAsync(miniGameType, EnterResult.Failed)).ConfigureAwait(false); return; } if (!this.CheckEntranceFee(miniGameDefinition, player, out var entranceFee)) { await player.InvokeViewPlugInAsync(p => p.ShowResultAsync(miniGameType, EnterResult.NotEnoughMoney)).ConfigureAwait(false); return; } if (!this.CheckPlayerKillState(miniGameDefinition, player)) { await player.InvokeViewPlugInAsync(p => p.ShowResultAsync(miniGameType, EnterResult.PlayerKillerCantEnter)).ConfigureAwait(false); return; } if (player.GuildWarContext is { State: GuildWarState.Started or GuildWarState.Requested }) { await player.InvokeViewPlugInAsync(p => p.ShowResultAsync(miniGameType, EnterResult.Failed)).ConfigureAwait(false); return; } // TODO: Check Duel state when duels are implemented if (miniGameDefinition.MapCreationPolicy == MiniGameMapCreationPolicy.Shared) { var miniGameStrategy = player.GameContext.PlugInManager.GetStrategy(miniGameDefinition.Type); if (miniGameStrategy is not null && await miniGameStrategy.GetDurationUntilNextStartAsync(player.GameContext, miniGameDefinition).ConfigureAwait(false) != TimeSpan.Zero) { await player.InvokeViewPlugInAsync(p => p.ShowResultAsync(miniGameType, EnterResult.NotOpen)).ConfigureAwait(false); return; } } var entrance = miniGameDefinition.Entrance ?? throw new InvalidOperationException("mini game entrance not defined"); var miniGame = await player.GameContext.GetMiniGameAsync(miniGameDefinition, player).ConfigureAwait(false); // Snapshot before entering: an event which disallows parties (Chaos Castle) kicks the // entering player out of its party below, losing the knowledge of who was going to follow. var partyBots = Bots.BotMiniGameHandler.SnapshotPartyBots(player); var enterResult = await miniGame.TryEnterAsync(player).ConfigureAwait(false); if (enterResult == EnterResult.Success) { if (entranceFee > 0) { player.TryRemoveMoney(entranceFee); } await this.ConsumeTicketItemAsync(ticketItem, player).ConfigureAwait(false); if (!miniGameDefinition.AllowParty && player.Party is { } party) { await party.KickMySelfAsync(player).ConfigureAwait(false); } await player.MagicEffectList.ClearEffectsAfterDeathAsync().ConfigureAwait(false); if (player.ShopStorage?.StoreOpen ?? false) { var storeCloseAction = new CloseStoreAction(); await storeCloseAction.CloseStoreAsync(player).ConfigureAwait(false); } await player.RemoveSummonAsync().ConfigureAwait(false); await player.MagicEffectList.ClearEffectsAfterDeathAsync().ConfigureAwait(false); await player.WarpToAsync(entrance).ConfigureAwait(false); // The bots of the entering party leader follow them in (each checked against the same // entry restrictions, no ticket needed - the leader's own ticket legitimizes the visit). Bots.BotMiniGameHandler.BringPartyBotsAlong(player, partyBots, miniGameDefinition, miniGame); } else { await player.InvokeViewPlugInAsync(p => p.ShowResultAsync(miniGameType, enterResult)).ConfigureAwait(false); } } private bool CheckPlayerKillState(MiniGameDefinition miniGameDefinition, Player player) { if (miniGameDefinition.ArePlayerKillersAllowedToEnter) { return true; } return player.SelectedCharacter?.State < HeroState.PlayerKiller1stStage; } private async ValueTask ConsumeTicketItemAsync(Item? ticketItem, Player player) { if (ticketItem is null) { return; } if (ticketItem.Durability > 0) { ticketItem.Durability -= 1; } if (ticketItem.Durability == 0) { if (player.Inventory is { } inventory) { await inventory.RemoveItemAsync(ticketItem).ConfigureAwait(false); } await player.DestroyInventoryItemAsync(ticketItem).ConfigureAwait(false); } else { await player.InvokeViewPlugInAsync(p => p.ItemDurabilityChangedAsync(ticketItem, false)).ConfigureAwait(false); } } private bool CheckTicketItem(MiniGameDefinition miniGameDefinition, Player player, byte gameTicketInventoryIndex, out Item? ticketItem) { ticketItem = null; if (miniGameDefinition.TicketItem is not { } ticketItemDefinition) { return true; } if (gameTicketInventoryIndex != UndefinedTicketSlot) { ticketItem = player.Inventory?.GetItem(gameTicketInventoryIndex); if (this.IsValidTicket(ticketItem, ticketItemDefinition, miniGameDefinition)) { return true; } } foreach (var candidateItem in player.Inventory?.FindItemsByDefinition(ticketItemDefinition) ?? Enumerable.Empty()) { if (this.IsValidTicket(candidateItem, ticketItemDefinition, miniGameDefinition)) { ticketItem = candidateItem; return true; } } return false; } private bool IsValidTicket(Item? ticketItem, ItemDefinition requiredItemDefinition, MiniGameDefinition miniGameDefinition) { return ticketItem is not null && requiredItemDefinition.Equals(ticketItem.Definition) && ticketItem.Durability > 0 && ticketItem.Level == miniGameDefinition.TicketItemLevel; } private bool CheckEntranceFee(MiniGameDefinition miniGameDefinition, Player player, out int entranceFee) { entranceFee = Math.Max(miniGameDefinition.EntranceFee, 0); return entranceFee <= player.Money; } }