diff --git a/EOBot/Program.cs b/EOBot/Program.cs index bacccca1c..6dc93d8a8 100644 --- a/EOBot/Program.cs +++ b/EOBot/Program.cs @@ -136,6 +136,8 @@ public void JunkItem(short id, int amountRemoved) var maxWeight = _characterProvider.MainCharacter.Stats[CharacterStat.MaxWeight]; ConsoleHelper.WriteMessage(ConsoleHelper.Type.JunkItem, $"{weight,3}/{maxWeight,3} - weight - {inventoryCount?.Amount ?? 0} in inventory"); } + + public void MakeDrunk() { } } [AutoMappedType] diff --git a/EOLib.IO/Pub/EIFRecord.cs b/EOLib.IO/Pub/EIFRecord.cs index 8c3ea5efc..61e3e1af5 100644 --- a/EOLib.IO/Pub/EIFRecord.cs +++ b/EOLib.IO/Pub/EIFRecord.cs @@ -40,6 +40,7 @@ public class EIFRecord : PubRecord public int HairColor => Get(PubRecordProperty.ItemHairColor); public int Effect => Get(PubRecordProperty.ItemEffect); public int Key => Get(PubRecordProperty.ItemKey); + public int BeerPotency => Get(PubRecordProperty.BeerPotency); public byte Gender => Get(PubRecordProperty.ItemGender); public byte ScrollX => Get(PubRecordProperty.ItemScrollX); diff --git a/EOLib.IO/Pub/PubRecordProperty.cs b/EOLib.IO/Pub/PubRecordProperty.cs index 47dfaaaa8..e4ede73b3 100644 --- a/EOLib.IO/Pub/PubRecordProperty.cs +++ b/EOLib.IO/Pub/PubRecordProperty.cs @@ -75,6 +75,8 @@ public enum PubRecordProperty : uint ItemEffect, [RecordData(32, 3)] ItemKey, + [RecordData(32, 3)] + BeerPotency, [RecordData(35, 1)] ItemGender, diff --git a/EOLib.Localization/EOResourceID.cs b/EOLib.Localization/EOResourceID.cs index 1f6cf84fe..3f77faf39 100644 --- a/EOLib.Localization/EOResourceID.cs +++ b/EOLib.Localization/EOResourceID.cs @@ -231,6 +231,7 @@ public enum EOResourceID STATUS_LABEL_ITEM_EQUIP_THIS_ITEM_REQUIRES = 411, STATUS_LABEL_UNABLE_TO_ATTACK = 423, + STATUS_LABEL_YOU_HAVE_NO_ARROWS = 424, STATUS_LABEL_YOUR_LOCATION_IS_AT = 427, STATUS_LABEL_IS_ONLINE_IN_THIS_WORLD = 428, diff --git a/EOLib/Domain/Character/AttackValidationActions.cs b/EOLib/Domain/Character/AttackValidationActions.cs index fe8d9cf32..5adab3fa1 100644 --- a/EOLib/Domain/Character/AttackValidationActions.cs +++ b/EOLib/Domain/Character/AttackValidationActions.cs @@ -1,6 +1,9 @@ using AutomaticTypeMapper; using EOLib.Domain.Extensions; using EOLib.Domain.Map; +using EOLib.IO.Repositories; +using Optional.Collections; +using System.Linq; namespace EOLib.Domain.Character { @@ -9,12 +12,15 @@ public class AttackValidationActions : IAttackValidationActions { private readonly ICharacterProvider _characterProvider; private readonly IMapCellStateProvider _mapCellStateProvider; + private readonly IEIFFileProvider _eifFileProvider; public AttackValidationActions(ICharacterProvider characterProvider, - IMapCellStateProvider mapCellStateProvider) + IMapCellStateProvider mapCellStateProvider, + IEIFFileProvider eifFileProvider) { _characterProvider = characterProvider; _mapCellStateProvider = mapCellStateProvider; + _eifFileProvider = eifFileProvider; } public AttackValidationError ValidateCharacterStateBeforeAttacking() @@ -27,6 +33,9 @@ public AttackValidationError ValidateCharacterStateBeforeAttacking() var rp = _characterProvider.MainCharacter.RenderProperties; + if (rp.IsRangedWeapon && (rp.ShieldGraphic == 0 || !_eifFileProvider.EIFFile.Any(x => x.DollGraphic == rp.ShieldGraphic && x.SubType == IO.ItemSubType.Arrows))) + return AttackValidationError.MissingArrows; + return _mapCellStateProvider .GetCellStateAt(rp.GetDestinationX(), rp.GetDestinationY()) .NPC.Match( @@ -47,6 +56,7 @@ public enum AttackValidationError OK, Overweight, Exhausted, - NotYourBattle + NotYourBattle, + MissingArrows } } diff --git a/EOLib/Domain/Character/CharacterActions.cs b/EOLib/Domain/Character/CharacterActions.cs index b79fd1ddf..86c560108 100644 --- a/EOLib/Domain/Character/CharacterActions.cs +++ b/EOLib/Domain/Character/CharacterActions.cs @@ -138,6 +138,15 @@ public void CastSpell(int spellId, ISpellTargetable target) _packetSendService.SendPacket(builder.Build()); } + + public void Emote(Emote whichEmote) + { + var packet = new PacketBuilder(PacketFamily.Emote, PacketAction.Report) + .AddChar((byte)whichEmote) + .Build(); + + _packetSendService.SendPacket(packet); + } } public interface ICharacterActions @@ -153,5 +162,7 @@ public interface ICharacterActions void PrepareCastSpell(int spellId); void CastSpell(int spellId, ISpellTargetable target); + + void Emote(Emote whichEmote); } } diff --git a/EOLib/Domain/Character/CharacterRenderProperties.cs b/EOLib/Domain/Character/CharacterRenderProperties.cs index 9892ea956..2bb09a99a 100644 --- a/EOLib/Domain/Character/CharacterRenderProperties.cs +++ b/EOLib/Domain/Character/CharacterRenderProperties.cs @@ -37,6 +37,7 @@ public class CharacterRenderProperties : ICharacterRenderProperties public bool IsHidden { get; private set; } public bool IsDead { get; private set; } + public bool IsDrunk { get; private set; } public bool IsRangedWeapon { get; private set; } @@ -232,6 +233,13 @@ public ICharacterRenderProperties WithAlive() return props; } + public ICharacterRenderProperties WithIsDrunk(bool drunk) + { + var props = MakeCopy(this); + props.IsDrunk = drunk; + return props; + } + public object Clone() { return MakeCopy(this); @@ -269,6 +277,8 @@ private static CharacterRenderProperties MakeCopy(ICharacterRenderProperties oth IsHidden = other.IsHidden, IsDead = other.IsDead, + IsDrunk = other.IsDrunk, + IsRangedWeapon = other.IsRangedWeapon }; } @@ -298,6 +308,7 @@ public override bool Equals(object obj) Emote == properties.Emote && IsHidden == properties.IsHidden && IsDead == properties.IsDead && + IsDrunk == properties.IsDrunk && IsRangedWeapon == properties.IsRangedWeapon; } @@ -326,6 +337,7 @@ public override int GetHashCode() hashCode = hashCode * -1521134295 + Emote.GetHashCode(); hashCode = hashCode * -1521134295 + IsHidden.GetHashCode(); hashCode = hashCode * -1521134295 + IsDead.GetHashCode(); + hashCode = hashCode * -1521134295 + IsDrunk.GetHashCode(); hashCode = hashCode * -1521134295 + IsRangedWeapon.GetHashCode(); return hashCode; } diff --git a/EOLib/Domain/Character/ICharacterRenderProperties.cs b/EOLib/Domain/Character/ICharacterRenderProperties.cs index 8f0f6df1e..8bf6f6c5d 100644 --- a/EOLib/Domain/Character/ICharacterRenderProperties.cs +++ b/EOLib/Domain/Character/ICharacterRenderProperties.cs @@ -33,6 +33,7 @@ public interface ICharacterRenderProperties : ICloneable bool IsHidden { get; } bool IsDead { get; } + bool IsDrunk { get; } bool IsRangedWeapon { get; } @@ -63,5 +64,7 @@ public interface ICharacterRenderProperties : ICloneable ICharacterRenderProperties WithIsHidden(bool hidden); ICharacterRenderProperties WithDead(); ICharacterRenderProperties WithAlive(); + + ICharacterRenderProperties WithIsDrunk(bool drunk); } } diff --git a/EOLib/Domain/Chat/ChatActions.cs b/EOLib/Domain/Chat/ChatActions.cs index e4ab67676..14179bbe9 100644 --- a/EOLib/Domain/Chat/ChatActions.cs +++ b/EOLib/Domain/Chat/ChatActions.cs @@ -36,14 +36,14 @@ public class ChatActions : IChatActions _chatProcessor = chatProcessor; } - public async Task SendChatToServer(string chat, string targetCharacter) + public string SendChatToServer(string chat, string targetCharacter) { var chatType = _chatTypeCalculator.CalculateChatType(chat); if (chatType == ChatType.Command) { if (HandleCommand(chat)) - return; + return chat; //treat unhandled command as local chat chatType = ChatType.Local; @@ -59,10 +59,15 @@ public async Task SendChatToServer(string chat, string targetCharacter) chat = _chatProcessor.RemoveFirstCharacterIfNeeded(chat, chatType, targetCharacter); + if (_characterProvider.MainCharacter.RenderProperties.IsDrunk) + chat = _chatProcessor.MakeDrunk(chat); + var chatPacket = _chatPacketBuilder.BuildChatPacket(chatType, chat, targetCharacter); - await _packetSendService.SendPacketAsync(chatPacket); + _packetSendService.SendPacket(chatPacket); AddChatForLocalDisplay(chatType, chat, targetCharacter); + + return chat; } /// @@ -126,6 +131,6 @@ private void AddChatForLocalDisplay(ChatType chatType, string chat, string targe public interface IChatActions { - Task SendChatToServer(string chat, string targetCharacter); + string SendChatToServer(string chat, string targetCharacter); } } \ No newline at end of file diff --git a/EOLib/Domain/Chat/ChatProcessor.cs b/EOLib/Domain/Chat/ChatProcessor.cs index ed3c07960..aecaf2354 100644 --- a/EOLib/Domain/Chat/ChatProcessor.cs +++ b/EOLib/Domain/Chat/ChatProcessor.cs @@ -1,4 +1,6 @@ using System; +using System.Linq; +using System.Text; using AutomaticTypeMapper; namespace EOLib.Domain.Chat @@ -6,6 +8,8 @@ namespace EOLib.Domain.Chat [AutoMappedType] public class ChatProcessor : IChatProcessor { + private readonly Random _random = new Random(); + public string RemoveFirstCharacterIfNeeded(string chat, ChatType chatType, string targetCharacter) { switch (chatType) @@ -33,10 +37,36 @@ public string RemoveFirstCharacterIfNeeded(string chat, ChatType chatType, strin throw new ArgumentOutOfRangeException(nameof(chatType)); } } + + public string MakeDrunk(string input) + { + // implementation from Phorophor::notepad (thanks Apollo) + // https://discord.com/channels/723989119503696013/785190349026492437/791376941822246953 + var ret = new StringBuilder(); + + foreach (var c in input) + { + var repeats = _random.Next(0, 8) < 6 ? 1 : 2; + ret.Append(c, repeats); + + if ((c == 'a' || c == 'e') && _random.NextDouble() / 1.0 < 0.555) + ret.Append('j'); + + if ((c == 'u' || c == 'o') && _random.NextDouble() / 1.0 < 0.444) + ret.Append('w'); + + if ((c == ' ') && _random.NextDouble() / 1.0 < 0.333) + ret.Append(" *hic*"); + } + + return ret.ToString(); + } } public interface IChatProcessor { string RemoveFirstCharacterIfNeeded(string input, ChatType chatType, string targetCharacter); + + string MakeDrunk(string input); } } diff --git a/EOLib/Domain/Notifiers/IEmoteNotifier.cs b/EOLib/Domain/Notifiers/IEmoteNotifier.cs index 74518b193..f29c022c0 100644 --- a/EOLib/Domain/Notifiers/IEmoteNotifier.cs +++ b/EOLib/Domain/Notifiers/IEmoteNotifier.cs @@ -6,11 +6,15 @@ namespace EOLib.Domain.Notifiers public interface IEmoteNotifier { void NotifyEmote(short playerId, Emote emote); + + void MakeMainPlayerDrunk(); } [AutoMappedType] public class NoOpEmoteNotifier : IEmoteNotifier { public void NotifyEmote(short playerId, Emote emote) { } + + public void MakeMainPlayerDrunk() { } } } diff --git a/EOLib/Net/API/Emote.cs b/EOLib/Net/API/Emote.cs deleted file mode 100644 index 4ba22fb97..000000000 --- a/EOLib/Net/API/Emote.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using EOLib.Domain.Character; -using EOLib.Net.Handlers; - -namespace EOLib.Net.API -{ - partial class PacketAPI - { - public event Action OnOtherPlayerEmote; - - private void _createEmoteMembers() - { - m_client.AddPacketHandler(new FamilyActionPair(PacketFamily.Emote, PacketAction.Player), _handleEmotePlayer, true); - } - - public bool ReportEmote(Emote emote) - { - //trade/level up happen differently - if (emote == Emote.Trade || emote == Emote.LevelUp) - return false; //signal error client-side - - if (!m_client.ConnectedAndInitialized || !Initialized) - return false; - - OldPacket pkt = new OldPacket(PacketFamily.Emote, PacketAction.Report); - pkt.AddChar((byte)emote); - - return m_client.SendPacket(pkt); - } - - private void _handleEmotePlayer(OldPacket pkt) - { - short playerID = pkt.GetShort(); - Emote emote = (Emote)pkt.GetChar(); - - if(OnOtherPlayerEmote != null) - OnOtherPlayerEmote(playerID, emote); - } - } -} diff --git a/EOLib/Net/API/PacketAPI.cs b/EOLib/Net/API/PacketAPI.cs index d2f6aee1a..421370cde 100644 --- a/EOLib/Net/API/PacketAPI.cs +++ b/EOLib/Net/API/PacketAPI.cs @@ -22,7 +22,6 @@ public PacketAPI(EOClient client) //each of these sets up members of the partial PacketAPI class relevant to a particular packet family _createBankMembers(); _createChestMembers(); - _createEmoteMembers(); _createInitMembers(); _createLockerMembers(); _createMessageMembers(); diff --git a/EOLib/PacketHandlers/Items/UseItemHandler.cs b/EOLib/PacketHandlers/Items/UseItemHandler.cs index 8748829b2..737892148 100644 --- a/EOLib/PacketHandlers/Items/UseItemHandler.cs +++ b/EOLib/PacketHandlers/Items/UseItemHandler.cs @@ -86,10 +86,9 @@ public override bool HandlePacket(IPacket packet) renderProps = renderProps.WithHairColor(hairColor); break; case ItemType.Beer: - // todo: drunk - // old logic: - // OldWorld.Instance.ActiveCharacterRenderer.MakeDrunk(); - // m_game.Hud.SetStatusLabel(EOResourceID.STATUS_LABEL_TYPE_WARNING, EOResourceID.STATUS_LABEL_ITEM_USE_DRUNK); + renderProps = renderProps.WithIsDrunk(true); + foreach (var notifier in _emoteNotifiers) + notifier.MakeMainPlayerDrunk(); break; case ItemType.EffectPotion: var potionId = packet.ReadShort(); @@ -159,7 +158,7 @@ public override bool HandlePacket(IPacket packet) .WithNewStat(CharacterStat.Armor, cureCurseArmor); break; - case ItemType.EXPReward: // todo: EXPReward has not been tested + case ItemType.EXPReward: var levelUpExp = packet.ReadInt(); var levelUpLevel = packet.ReadChar(); var levelUpStat = packet.ReadShort(); diff --git a/EOLib/PacketHandlers/OtherPlayerLevelUpHandler.cs b/EOLib/PacketHandlers/OtherPlayerLevelUpHandler.cs index 264d096e1..65c53c22b 100644 --- a/EOLib/PacketHandlers/OtherPlayerLevelUpHandler.cs +++ b/EOLib/PacketHandlers/OtherPlayerLevelUpHandler.cs @@ -1,4 +1,5 @@ -using EOLib.Domain.Character; +using AutomaticTypeMapper; +using EOLib.Domain.Character; using EOLib.Domain.Login; using EOLib.Domain.Notifiers; using EOLib.Net; @@ -7,6 +8,7 @@ namespace EOLib.PacketHandlers { + [AutoMappedType] public class OtherPlayerLevelUpHandler : InGameOnlyPacketHandler { private readonly IEnumerable _emoteNotifiers; diff --git a/EOLib/PacketHandlers/PlayerEmoteHandler.cs b/EOLib/PacketHandlers/PlayerEmoteHandler.cs new file mode 100644 index 000000000..e255bd099 --- /dev/null +++ b/EOLib/PacketHandlers/PlayerEmoteHandler.cs @@ -0,0 +1,37 @@ +using AutomaticTypeMapper; +using EOLib.Domain.Character; +using EOLib.Domain.Login; +using EOLib.Domain.Notifiers; +using EOLib.Net; +using EOLib.Net.Handlers; +using System.Collections.Generic; + +namespace EOLib.PacketHandlers +{ + [AutoMappedType] + public class PlayerEmoteHandler : InGameOnlyPacketHandler + { + private readonly IEnumerable _emoteNotifiers; + + public override PacketFamily Family => PacketFamily.Emote; + + public override PacketAction Action => PacketAction.Player; + + public PlayerEmoteHandler(IPlayerInfoProvider playerInfoProvider, + IEnumerable emoteNotifiers) + : base(playerInfoProvider) + { + _emoteNotifiers = emoteNotifiers; + } + + public override bool HandlePacket(IPacket packet) + { + var playerId = packet.ReadShort(); + var emote = (Emote)packet.ReadChar(); + foreach (var notifier in _emoteNotifiers) + notifier.NotifyEmote(playerId, emote); + + return true; + } + } +} diff --git a/EOLib/PacketHandlers/RecoverReplyHandler.cs b/EOLib/PacketHandlers/RecoverReplyHandler.cs index f6c000488..79243950b 100644 --- a/EOLib/PacketHandlers/RecoverReplyHandler.cs +++ b/EOLib/PacketHandlers/RecoverReplyHandler.cs @@ -38,15 +38,16 @@ public override bool HandlePacket(IPacket packet) .WithNewStat(CharacterStat.Karma, karma); if (level > 0) + { stats = stats.WithNewStat(CharacterStat.Level, level); + foreach (var notifier in _emoteNotifiers) + notifier.NotifyEmote((short)_characterRepository.MainCharacter.ID, Emote.LevelUp); + } - if (level > 0 && packet.ReadPosition < packet.Length) + if (packet.ReadPosition < packet.Length) { stats = stats.WithNewStat(CharacterStat.StatPoints, packet.ReadShort()) .WithNewStat(CharacterStat.SkillPoints, packet.ReadShort()); - - foreach (var notifier in _emoteNotifiers) - notifier.NotifyEmote((short)_characterRepository.MainCharacter.ID, Emote.LevelUp); } _characterRepository.MainCharacter = _characterRepository.MainCharacter.WithStats(stats); diff --git a/EndlessClient/Controllers/ChatController.cs b/EndlessClient/Controllers/ChatController.cs index 140ff5e70..8d7534bdf 100644 --- a/EndlessClient/Controllers/ChatController.cs +++ b/EndlessClient/Controllers/ChatController.cs @@ -18,45 +18,32 @@ public class ChatController : IChatController private readonly IChatTextBoxActions _chatTextBoxActions; private readonly IChatActions _chatActions; private readonly IPrivateMessageActions _privateMessageActions; - private readonly IGameStateActions _gameStateActions; - private readonly IErrorDialogDisplayAction _errorDisplayAction; private readonly IChatBubbleActions _chatBubbleActions; - private readonly ISafeNetworkOperationFactory _safeNetworkOperationFactory; private readonly IHudControlProvider _hudControlProvider; public ChatController(IChatTextBoxActions chatTextBoxActions, IChatActions chatActions, IPrivateMessageActions privateMessageActions, - IGameStateActions gameStateActions, - IErrorDialogDisplayAction errorDisplayAction, IChatBubbleActions chatBubbleActions, - ISafeNetworkOperationFactory safeNetworkOperationFactory, IHudControlProvider hudControlProvider) { _chatTextBoxActions = chatTextBoxActions; _chatActions = chatActions; _privateMessageActions = privateMessageActions; - _gameStateActions = gameStateActions; - _errorDisplayAction = errorDisplayAction; _chatBubbleActions = chatBubbleActions; - _safeNetworkOperationFactory = safeNetworkOperationFactory; _hudControlProvider = hudControlProvider; } - public async Task SendChatAndClearTextBox() + public void SendChatAndClearTextBox() { var localTypedText = ChatTextBox.Text; var targetCharacter = _privateMessageActions.GetTargetCharacter(localTypedText); - var sendChatOperation = _safeNetworkOperationFactory.CreateSafeAsyncOperation( - async () => await _chatActions.SendChatToServer(localTypedText, targetCharacter), - SetInitialStateAndShowError); - if (!await sendChatOperation.Invoke()) - return; + var updatedChat = _chatActions.SendChatToServer(localTypedText, targetCharacter); _chatTextBoxActions.ClearChatText(); - _chatBubbleActions.ShowChatBubbleForMainCharacter(localTypedText); + _chatBubbleActions.ShowChatBubbleForMainCharacter(updatedChat); } public void SelectChatTextBox() @@ -64,18 +51,12 @@ public void SelectChatTextBox() _chatTextBoxActions.FocusChatTextBox(); } - private void SetInitialStateAndShowError(NoDataSentException ex) - { - _gameStateActions.ChangeToState(GameStates.Initial); - _errorDisplayAction.ShowException(ex); - } - private ChatTextBox ChatTextBox => _hudControlProvider.GetComponent(HudControlIdentifier.ChatTextBox); } public interface IChatController { - Task SendChatAndClearTextBox(); + void SendChatAndClearTextBox(); void SelectChatTextBox(); } diff --git a/EndlessClient/Controllers/ControlKeyController.cs b/EndlessClient/Controllers/ControlKeyController.cs index 4dbd40159..8e63d958f 100644 --- a/EndlessClient/Controllers/ControlKeyController.cs +++ b/EndlessClient/Controllers/ControlKeyController.cs @@ -64,6 +64,11 @@ private void AttemptAttack() EOResourceID.STATUS_LABEL_UNABLE_TO_ATTACK); showAnimationAnyway = true; } + else if (validationResult == AttackValidationError.MissingArrows) + { + _statusLabelSetter.SetStatusLabel(EOResourceID.STATUS_LABEL_TYPE_WARNING, + EOResourceID.STATUS_LABEL_YOU_HAVE_NO_ARROWS); + } } else showAnimationAnyway = true; diff --git a/EndlessClient/Controllers/InventoryController.cs b/EndlessClient/Controllers/InventoryController.cs index b7e611017..02570ff73 100644 --- a/EndlessClient/Controllers/InventoryController.cs +++ b/EndlessClient/Controllers/InventoryController.cs @@ -5,12 +5,12 @@ using EndlessClient.Dialogs.Factories; using EndlessClient.HUD; using EndlessClient.HUD.Controls; +using EndlessClient.Rendering.Character; using EndlessClient.Rendering.Map; using EOLib.Domain.Character; using EOLib.Domain.Item; using EOLib.Domain.Map; using EOLib.IO; -using EOLib.IO.Extensions; using EOLib.IO.Pub; using EOLib.IO.Repositories; using EOLib.Localization; @@ -114,7 +114,7 @@ public void UseItem(EIFRecord record) case ItemType.HairDye: case ItemType.Beer: case ItemType.EffectPotion: - case ItemType.EXPReward: // todo: EXPReward has not been tested + case ItemType.EXPReward: useItem = true; break; @@ -127,6 +127,13 @@ public void UseItem(EIFRecord record) if (useItem) { _itemActions.UseItem((short)record.ID); + + if (record.Type == ItemType.Beer) + { + // The server does not send back the potency, it is all client-side + _hudControlProvider.GetComponent(HudControlIdentifier.PeriodicEmoteHandler) + .SetDrunkTimeout(record.BeerPotency); + } } } diff --git a/EndlessClient/Controllers/MapInteractionController.cs b/EndlessClient/Controllers/MapInteractionController.cs index a83e9bffa..a275a21cc 100644 --- a/EndlessClient/Controllers/MapInteractionController.cs +++ b/EndlessClient/Controllers/MapInteractionController.cs @@ -6,6 +6,7 @@ using EndlessClient.HUD.Controls; using EndlessClient.HUD.Inventory; using EndlessClient.HUD.Panels; +using EndlessClient.Input; using EndlessClient.Rendering; using EndlessClient.Rendering.Character; using EndlessClient.Rendering.Factories; @@ -33,6 +34,7 @@ public class MapInteractionController : IMapInteractionController private readonly IHudControlProvider _hudControlProvider; private readonly ICharacterRendererProvider _characterRendererProvider; private readonly IContextMenuRepository _contextMenuRepository; + private readonly IUserInputTimeRepository _userInputTimeRepository; private readonly IEOMessageBoxFactory _eoMessageBoxFactory; private readonly IContextMenuRendererFactory _contextMenuRendererFactory; @@ -46,6 +48,7 @@ public class MapInteractionController : IMapInteractionController IHudControlProvider hudControlProvider, ICharacterRendererProvider characterRendererProvider, IContextMenuRepository contextMenuRepository, + IUserInputTimeRepository userInputTimeRepository, IEOMessageBoxFactory eoMessageBoxFactory, IContextMenuRendererFactory contextMenuRendererFactory) { @@ -59,11 +62,12 @@ public class MapInteractionController : IMapInteractionController _hudControlProvider = hudControlProvider; _characterRendererProvider = characterRendererProvider; _contextMenuRepository = contextMenuRepository; + _userInputTimeRepository = userInputTimeRepository; _eoMessageBoxFactory = eoMessageBoxFactory; _contextMenuRendererFactory = contextMenuRendererFactory; } - public async Task LeftClickAsync(IMapCellState cellState, IMouseCursorRenderer mouseRenderer) + public void LeftClick(IMapCellState cellState, IMouseCursorRenderer mouseRenderer) { if (!InventoryPanel.NoItemsDragging()) { @@ -89,7 +93,7 @@ public async Task LeftClickAsync(IMapCellState cellState, IMouseCursorRenderer m { var sign = cellState.Sign.ValueOr(Sign.None); var messageBox = _eoMessageBoxFactory.CreateMessageBox(sign.Message, sign.Title); - await messageBox.ShowDialogAsync(); + messageBox.ShowDialog(); } else if (cellState.Chest.HasValue) { /* TODO: chest interaction */ } else if (cellState.Character.HasValue) { /* TODO: character spell cast */ } @@ -99,6 +103,8 @@ public async Task LeftClickAsync(IMapCellState cellState, IMouseCursorRenderer m _hudControlProvider.GetComponent(HudControlIdentifier.CharacterAnimator) .StartMainCharacterWalkAnimation(Option.Some(cellState.Coordinate)); } + + _userInputTimeRepository.LastInputTime = DateTime.Now; } public void RightClick(IMapCellState cellState) @@ -111,6 +117,7 @@ public void RightClick(IMapCellState cellState) if (c == _characterProvider.MainCharacter) { _inGameDialogActions.ShowPaperdollDialog(_characterProvider.MainCharacter, isMainCharacter: true); + _userInputTimeRepository.LastInputTime = DateTime.Now; } else if (_characterRendererProvider.CharacterRenderers.ContainsKey(c.ID)) { @@ -160,7 +167,7 @@ private void HandlePickupResult(ItemPickupResult pickupResult, IItem item) public interface IMapInteractionController { - Task LeftClickAsync(IMapCellState cellState, IMouseCursorRenderer mouseRenderer); + void LeftClick(IMapCellState cellState, IMouseCursorRenderer mouseRenderer); void RightClick(IMapCellState cellState); } diff --git a/EndlessClient/Controllers/NumPadController.cs b/EndlessClient/Controllers/NumPadController.cs new file mode 100644 index 000000000..467bf5f3d --- /dev/null +++ b/EndlessClient/Controllers/NumPadController.cs @@ -0,0 +1,46 @@ +using AutomaticTypeMapper; +using EndlessClient.ControlSets; +using EndlessClient.HUD.Controls; +using EndlessClient.Rendering.Character; +using EOLib.Domain.Character; +using EOLib.Domain.Extensions; + +namespace EndlessClient.Controllers +{ + [AutoMappedType] + public class NumPadController : INumPadController + { + private readonly ICharacterActions _characterActions; + private readonly IHudControlProvider _hudControlProvider; + private readonly ICharacterRendererProvider _characterRendererProvider; + + public NumPadController(ICharacterActions characterActions, + IHudControlProvider hudControlProvider, + ICharacterRendererProvider characterRendererProvider) + { + _characterActions = characterActions; + _hudControlProvider = hudControlProvider; + _characterRendererProvider = characterRendererProvider; + } + + public void Emote(Emote whichEmote) + { + var mainRenderer = _characterRendererProvider.MainCharacterRenderer; + mainRenderer.MatchSome(renderer => + { + if (!renderer.Character.RenderProperties.IsActing(CharacterActionState.Standing)) + return; + + _characterActions.Emote(whichEmote); + + var animator = _hudControlProvider.GetComponent(HudControlIdentifier.CharacterAnimator); + animator.Emote(renderer.Character.ID, whichEmote); + }); + } + } + + public interface INumPadController + { + void Emote(Emote whichEmote); + } +} diff --git a/EndlessClient/HUD/Controls/HUD.cs b/EndlessClient/HUD/Controls/HUD.cs index 131d04c84..7e7f037dd 100644 --- a/EndlessClient/HUD/Controls/HUD.cs +++ b/EndlessClient/HUD/Controls/HUD.cs @@ -35,8 +35,6 @@ public class HUD : DrawableGameComponent public DateTime SessionStartTime { get; private set; } - private List m_inputListeners; - public HUD(Game g, PacketAPI api) : base(g) { if(!api.Initialized) @@ -100,12 +98,6 @@ public override void Initialize() SessionStartTime = DateTime.Now; - m_inputListeners = new List - { - new NumPadListener() - }; - m_inputListeners.ForEach(x => x.InputTimeUpdated += OldWorld.Instance.ActiveCharacterRenderer.UpdateInputTime); - base.Initialize(); } @@ -171,12 +163,6 @@ protected override void Dispose(bool disposing) m_expInfo.Close(); m_questInfo.Close(); - - if (m_inputListeners.Count > 0) - { - m_inputListeners.ForEach(x => x.Dispose()); - m_inputListeners.Clear(); - } } base.Dispose(disposing); diff --git a/EndlessClient/HUD/Controls/HudControlIdentifier.cs b/EndlessClient/HUD/Controls/HudControlIdentifier.cs index f1181ea08..fa0bd0a2a 100644 --- a/EndlessClient/HUD/Controls/HudControlIdentifier.cs +++ b/EndlessClient/HUD/Controls/HudControlIdentifier.cs @@ -70,6 +70,7 @@ public enum HudControlIdentifier CharacterAnimator, NPCAnimator, UnknownEntitiesRequester, + PeriodicEmoteHandler, PreviousUserInputTracker = Int32.MaxValue, //this should always be last! } diff --git a/EndlessClient/HUD/Controls/HudControlsFactory.cs b/EndlessClient/HUD/Controls/HudControlsFactory.cs index ca01f980a..0fd264487 100644 --- a/EndlessClient/HUD/Controls/HudControlsFactory.cs +++ b/EndlessClient/HUD/Controls/HudControlsFactory.cs @@ -56,6 +56,7 @@ public class HudControlsFactory : IHudControlsFactory private readonly ICharacterActions _characterActions; private readonly IWalkValidationActions _walkValidationActions; private readonly IPacketSendService _packetSendService; + private readonly IUserInputTimeProvider _userInputTimeProvider; private IChatController _chatController; public HudControlsFactory(IHudButtonController hudButtonController, @@ -80,7 +81,8 @@ public class HudControlsFactory : IHudControlsFactory IPathFinder pathFinder, ICharacterActions characterActions, IWalkValidationActions walkValidationActions, - IPacketSendService packetSendService) + IPacketSendService packetSendService, + IUserInputTimeProvider userInputTimeProvider) { _hudButtonController = hudButtonController; _hudPanelFactory = hudPanelFactory; @@ -105,6 +107,7 @@ public class HudControlsFactory : IHudControlsFactory _characterActions = characterActions; _walkValidationActions = walkValidationActions; _packetSendService = packetSendService; + _userInputTimeProvider = userInputTimeProvider; } public void InjectChatController(IChatController chatController) @@ -114,6 +117,8 @@ public void InjectChatController(IChatController chatController) public IReadOnlyDictionary CreateHud() { + var characterAnimator = CreateCharacterAnimator(); + var controls = new Dictionary { {HudControlIdentifier.CurrentUserInputTracker, CreateCurrentUserInputTracker()}, @@ -161,9 +166,11 @@ public void InjectChatController(IChatController chatController) {HudControlIdentifier.UsageTracker, CreateUsageTracker()}, {HudControlIdentifier.UserInputHandler, CreateUserInputHandler()}, - {HudControlIdentifier.CharacterAnimator, CreateCharacterAnimator()}, + {HudControlIdentifier.CharacterAnimator, characterAnimator}, {HudControlIdentifier.NPCAnimator, CreateNPCAnimator()}, {HudControlIdentifier.UnknownEntitiesRequester, CreateUnknownEntitiesRequester()}, + {HudControlIdentifier.PeriodicEmoteHandler, CreatePeriodicEmoteHandler(characterAnimator)}, + {HudControlIdentifier.PreviousUserInputTracker, CreatePreviousUserInputTracker()} }; @@ -349,8 +356,8 @@ private ChatTextBox CreateChatTextBox() Visible = true, DrawOrder = HUD_CONTROL_LAYER }; - chatTextBox.OnEnterPressed += async (o, e) => await _chatController.SendChatAndClearTextBox(); - chatTextBox.OnClicked += (o, e) => _chatController.SelectChatTextBox(); + chatTextBox.OnEnterPressed += (_, _) => _chatController.SendChatAndClearTextBox(); + chatTextBox.OnClicked += (_, _) => _chatController.SelectChatTextBox(); return chatTextBox; } @@ -395,6 +402,11 @@ private INPCAnimator CreateNPCAnimator() return new NPCAnimator(_endlessGameProvider, _currentMapStateRepository); } + private IPeriodicEmoteHandler CreatePeriodicEmoteHandler(ICharacterAnimator characterAnimator) + { + return new PeriodicEmoteHandler(_endlessGameProvider, _characterActions, _userInputTimeProvider, _characterRepository, characterAnimator); + } + private PreviousUserInputTracker CreatePreviousUserInputTracker() { return new PreviousUserInputTracker(_endlessGameProvider, _userInputRepository); diff --git a/EndlessClient/Input/IUserInputTimeRepository.cs b/EndlessClient/Input/IUserInputTimeRepository.cs index 95c24e468..5f8c6c4da 100644 --- a/EndlessClient/Input/IUserInputTimeRepository.cs +++ b/EndlessClient/Input/IUserInputTimeRepository.cs @@ -8,8 +8,13 @@ public interface IUserInputTimeRepository DateTime LastInputTime { get; set; } } - [MappedType(BaseType = typeof(IUserInputTimeRepository), IsSingleton = true)] - public class UserInputTimeRepository : IUserInputTimeRepository + public interface IUserInputTimeProvider + { + DateTime LastInputTime { get; } + } + + [AutoMappedType(IsSingleton = true)] + public class UserInputTimeRepository : IUserInputTimeRepository, IUserInputTimeProvider { public DateTime LastInputTime { get; set; } diff --git a/EndlessClient/Input/NumPadHandler.cs b/EndlessClient/Input/NumPadHandler.cs new file mode 100644 index 000000000..2bd1fc30b --- /dev/null +++ b/EndlessClient/Input/NumPadHandler.cs @@ -0,0 +1,47 @@ +using EndlessClient.Controllers; +using EndlessClient.GameExecution; +using EOLib.Domain.Character; +using EOLib.Domain.Map; +using Microsoft.Xna.Framework.Input; +using Optional; + +namespace EndlessClient.Input +{ + + public class NumPadHandler : InputHandlerBase + { + private readonly INumPadController _numPadController; + + public NumPadHandler(IEndlessGameProvider endlessGameProvider, + IUserInputProvider userInputProvider, + IUserInputTimeRepository userInputTimeRepository, + ICurrentMapStateProvider mapStateProvider, + INumPadController numPadController) + : base(endlessGameProvider, userInputProvider, userInputTimeRepository, mapStateProvider) + { + _numPadController = numPadController; + } + + protected override Option HandleInput() + { + for (var key = Keys.NumPad0; key <= Keys.NumPad9; ++key) + { + if (IsKeyHeld(key)) + { + var emote = key == Keys.NumPad0 ? Emote.Playful : (Emote)(key - Keys.NumPad0); + _numPadController.Emote(emote); + return Option.Some(key); + } + } + + // Keys.Decimal == ./DEL on the num pad + if (IsKeyHeld(Keys.Decimal)) + { + _numPadController.Emote(Emote.Embarassed); + return Option.Some(Keys.Decimal); + } + + return Option.None(); + } + } +} diff --git a/EndlessClient/Input/NumPadListener.cs b/EndlessClient/Input/NumPadListener.cs deleted file mode 100644 index 6c26b19df..000000000 --- a/EndlessClient/Input/NumPadListener.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Linq; -using EOLib.Domain.Character; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Input; - -namespace EndlessClient.Input -{ - public class NumPadListener : OldInputKeyListenerBase - { - public NumPadListener() - { - if (Game.Components.Any(x => x is NumPadListener)) - throw new InvalidOperationException("The game already contains an arrow key listener"); - Game.Components.Add(this); - } - - public override void Update(GameTime gameTime) - { - if (!IgnoreInput && Character.RenderData.emoteFrame < 0) - { - UpdateInputTime(); - - bool handledPress = false; - for (int key = (int) Keys.NumPad0; key <= (int) Keys.NumPad9; ++key) - { - if (Keyboard.GetState().IsKeyHeld(PreviousKeyState, (Keys) key)) - { - var emote = key == (int)Keys.NumPad0 ? Emote.Playful : (Emote) (key - (int) Keys.NumPad0); - _doEmote(emote); - handledPress = true; - break; - } - } - - //The Decimal enumeration is 110, which is the Virtual Key code (VK_XXXX) for the 'del'/'.' key on the numpad - if (!handledPress && Keyboard.GetState().IsKeyHeld(PreviousKeyState, Keys.Decimal)) - { - _doEmote(Emote.Embarassed); - } - } - - base.Update(gameTime); - } - - private void _doEmote(Emote emote) - { - Character.Emote(emote); - Renderer.PlayerEmote(); - } - } -} diff --git a/EndlessClient/Input/OldInputKeyListenerBase.cs b/EndlessClient/Input/OldInputKeyListenerBase.cs deleted file mode 100644 index 8b1f39820..000000000 --- a/EndlessClient/Input/OldInputKeyListenerBase.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using EndlessClient.Old; -using EndlessClient.Rendering; -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Input; -using XNAControls.Old; - -namespace EndlessClient.Input -{ - public class OldInputKeyListenerBase : GameComponent - { - //input will be rate-limited to once every {x} MS - private const int INPUT_RATE_LIMIT_MS = 200; - - protected KeyboardState PreviousKeyState { get; private set; } - private DateTime? _lastInputTime; - - protected OldCharacter Character => OldWorld.Instance.MainPlayer.ActiveCharacter; - protected OldCharacterRenderer Renderer => OldWorld.Instance.ActiveCharacterRenderer; - - public event Action InputTimeUpdated; - - /// - /// Returns true if input handling for a key listener should be ignored - /// - protected bool IgnoreInput => !Game.IsActive || - (_lastInputTime != null && (DateTime.Now - _lastInputTime.Value).TotalMilliseconds < INPUT_RATE_LIMIT_MS) || - XNAControl.Dialogs.Count > 0 || - Character == null || Renderer == null; - - protected OldInputKeyListenerBase() : base(EOGame.Instance) - { - PreviousKeyState = Keyboard.GetState(); - } - - protected void UpdateInputTime() - { - _lastInputTime = DateTime.Now; - if (InputTimeUpdated != null) - InputTimeUpdated(_lastInputTime ?? DateTime.Now); - } - - public override void Update(GameTime gameTime) - { - PreviousKeyState = Keyboard.GetState(); - base.Update(gameTime); - } - } -} diff --git a/EndlessClient/Input/UserInputHandler.cs b/EndlessClient/Input/UserInputHandler.cs index a9f1b70a2..bb5e114c0 100644 --- a/EndlessClient/Input/UserInputHandler.cs +++ b/EndlessClient/Input/UserInputHandler.cs @@ -21,6 +21,7 @@ public class UserInputHandler : XNAControl, IUserInputHandler IArrowKeyController arrowKeyController, IControlKeyController controlKeyController, IFunctionKeyController functionKeyController, + INumPadController numPadController, ICurrentMapStateProvider currentMapStateProvider, IActiveDialogProvider activeDialogProvider) { @@ -41,6 +42,11 @@ public class UserInputHandler : XNAControl, IUserInputHandler userInputTimeRepository, functionKeyController, currentMapStateProvider), + new NumPadHandler(endlessGameProvider, + userInputProvider, + userInputTimeRepository, + currentMapStateProvider, + numPadController), }; _activeDialogProvider = activeDialogProvider; } diff --git a/EndlessClient/Input/UserInputHandlerFactory.cs b/EndlessClient/Input/UserInputHandlerFactory.cs index 9c924a476..9369c6b58 100644 --- a/EndlessClient/Input/UserInputHandlerFactory.cs +++ b/EndlessClient/Input/UserInputHandlerFactory.cs @@ -15,6 +15,7 @@ public class UserInputHandlerFactory : IUserInputHandlerFactory private readonly IArrowKeyController _arrowKeyController; private readonly IControlKeyController _controlKeyController; private readonly IFunctionKeyController _functionKeyController; + private readonly INumPadController _numPadController; private readonly ICurrentMapStateProvider _currentMapStateProvider; private readonly IActiveDialogProvider _activeDialogProvider; @@ -24,6 +25,7 @@ public class UserInputHandlerFactory : IUserInputHandlerFactory IArrowKeyController arrowKeyController, IControlKeyController controlKeyController, IFunctionKeyController functionKeyController, + INumPadController numPadController, ICurrentMapStateProvider currentMapStateProvider, IActiveDialogProvider activeDialogProvider) { @@ -33,6 +35,7 @@ public class UserInputHandlerFactory : IUserInputHandlerFactory _arrowKeyController = arrowKeyController; _controlKeyController = controlKeyController; _functionKeyController = functionKeyController; + _numPadController = numPadController; _currentMapStateProvider = currentMapStateProvider; _activeDialogProvider = activeDialogProvider; } @@ -45,6 +48,7 @@ public IUserInputHandler CreateUserInputHandler() _arrowKeyController, _controlKeyController, _functionKeyController, + _numPadController, _currentMapStateProvider, _activeDialogProvider); } diff --git a/EndlessClient/Old/OldCharacter.cs b/EndlessClient/Old/OldCharacter.cs index f7b0e3182..53bcf1624 100644 --- a/EndlessClient/Old/OldCharacter.cs +++ b/EndlessClient/Old/OldCharacter.cs @@ -8,10 +8,8 @@ using EOLib.IO; using EOLib.IO.Map; using EOLib.IO.Pub; -using EOLib.Localization; using EOLib.Net.API; using Microsoft.Xna.Framework; -using XNAControls.Old; namespace EndlessClient.Old { @@ -372,27 +370,6 @@ public void DoneAttacking() RenderData.SetAttackFrame(0); } - public void Emote(Emote whichEmote) - { - if (this == OldWorld.Instance.MainPlayer.ActiveCharacter && - whichEmote != EOLib.Domain.Character.Emote.LevelUp && - whichEmote != EOLib.Domain.Character.Emote.Trade) - { - if (m_packetAPI.ReportEmote(whichEmote)) - RenderData.SetEmote(whichEmote); - else - EOGame.Instance.DoShowLostConnectionDialogAndReturnToMainMenu(); - } - - RenderData.SetEmoteFrame(0); - RenderData.SetEmote(whichEmote); - } - - public void DoneEmote() - { - RenderData.SetEmoteFrame(-1); - } - public ChestKey CanOpenChest(ChestSpawnMapEntity chest) { ChestKey permission = chest.Key; diff --git a/EndlessClient/Old/PacketAPICallbackManager.cs b/EndlessClient/Old/PacketAPICallbackManager.cs index be5b12d32..4957f19e8 100644 --- a/EndlessClient/Old/PacketAPICallbackManager.cs +++ b/EndlessClient/Old/PacketAPICallbackManager.cs @@ -51,8 +51,6 @@ public void AssignCallbacks() m_packetAPI.OnLockerItemChange += _lockerItemChange; m_packetAPI.OnLockerUpgrade += _lockerUpgrade; - m_packetAPI.OnOtherPlayerEmote += _playerEmote; - //party m_packetAPI.OnPartyClose += _partyClose; m_packetAPI.OnPartyDataRefresh += _partyDataRefresh; @@ -194,12 +192,6 @@ private void _lockerUpgrade(int remaining, byte upgrades) m_game.Hud.SetStatusLabel(EOResourceID.STATUS_LABEL_TYPE_INFORMATION, EOResourceID.STATUS_LABEL_LOCKER_SPACE_INCREASED); } - private void _playerEmote(short playerID, Emote emote) - { - if (playerID != OldWorld.Instance.MainPlayer.ActiveCharacter.ID) - OldWorld.Instance.ActiveMapRenderer.OtherPlayerEmote(playerID, emote); - } - private void _partyClose() { m_game.Hud.CloseParty(); diff --git a/EndlessClient/Rendering/Character/CharacterAnimationActions.cs b/EndlessClient/Rendering/Character/CharacterAnimationActions.cs index 9cc2e3af6..55e26fa95 100644 --- a/EndlessClient/Rendering/Character/CharacterAnimationActions.cs +++ b/EndlessClient/Rendering/Character/CharacterAnimationActions.cs @@ -1,5 +1,6 @@ using AutomaticTypeMapper; using EndlessClient.ControlSets; +using EndlessClient.HUD; using EndlessClient.HUD.Controls; using EndlessClient.Rendering.Map; using EOLib; @@ -9,13 +10,12 @@ using EOLib.Domain.Notifiers; using EOLib.IO.Map; using EOLib.IO.Repositories; +using EOLib.Localization; using Optional; namespace EndlessClient.Rendering.Character { - [MappedType(BaseType = typeof(ICharacterAnimationActions))] - [MappedType(BaseType = typeof(IOtherCharacterAnimationNotifier))] - [MappedType(BaseType = typeof(IEffectNotifier))] + [AutoMappedType] public class CharacterAnimationActions : ICharacterAnimationActions, IOtherCharacterAnimationNotifier, IEffectNotifier, IEmoteNotifier { private readonly IHudControlProvider _hudControlProvider; @@ -25,6 +25,7 @@ public class CharacterAnimationActions : ICharacterAnimationActions, IOtherChara private readonly ICurrentMapProvider _currentMapProvider; private readonly ISpikeTrapActions _spikeTrapActions; private readonly IESFFileProvider _esfFileProvider; + private readonly IStatusLabelSetter _statusLabelSetter; public CharacterAnimationActions(IHudControlProvider hudControlProvider, ICharacterRepository characterRepository, @@ -32,7 +33,8 @@ public class CharacterAnimationActions : ICharacterAnimationActions, IOtherChara ICharacterRendererProvider characterRendererProvider, ICurrentMapProvider currentMapProvider, ISpikeTrapActions spikeTrapActions, - IESFFileProvider esfFileProvider) + IESFFileProvider esfFileProvider, + IStatusLabelSetter statusLabelSetter) { _hudControlProvider = hudControlProvider; _characterRepository = characterRepository; @@ -41,6 +43,7 @@ public class CharacterAnimationActions : ICharacterAnimationActions, IOtherChara _currentMapProvider = currentMapProvider; _spikeTrapActions = spikeTrapActions; _esfFileProvider = esfFileProvider; + _statusLabelSetter = statusLabelSetter; } public void Face(EODirection direction) @@ -178,6 +181,16 @@ public void NotifyEarthquake(byte strength) mapRenderer.StartEarthquake(strength); } + public void NotifyEmote(short playerId, Emote emote) + { + Animator.Emote(playerId, emote); + } + + public void MakeMainPlayerDrunk() + { + _statusLabelSetter.SetStatusLabel(EOResourceID.STATUS_LABEL_TYPE_WARNING, EOResourceID.STATUS_LABEL_ITEM_USE_DRUNK); + } + private void ShowWaterSplashiesIfNeeded(CharacterActionState action, int characterID) { var character = characterID == _characterRepository.MainCharacter.ID @@ -213,11 +226,6 @@ private void ShowWaterSplashiesIfNeeded(CharacterActionState action, int charact }); } - public void NotifyEmote(short playerId, Emote emote) - { - // todo: start emote animation - } - private ICharacterAnimator Animator => _hudControlProvider.GetComponent(HudControlIdentifier.CharacterAnimator); } diff --git a/EndlessClient/Rendering/Character/CharacterAnimator.cs b/EndlessClient/Rendering/Character/CharacterAnimator.cs index 5e21c9463..30c0f3f98 100644 --- a/EndlessClient/Rendering/Character/CharacterAnimator.cs +++ b/EndlessClient/Rendering/Character/CharacterAnimator.cs @@ -1,5 +1,6 @@ using EndlessClient.GameExecution; using EndlessClient.HUD; +using EndlessClient.Input; using EOLib; using EOLib.Domain.Character; using EOLib.Domain.Extensions; @@ -9,6 +10,7 @@ using Optional; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; namespace EndlessClient.Rendering.Character @@ -17,6 +19,7 @@ public class CharacterAnimator : GameComponent, ICharacterAnimator { public const int WALK_FRAME_TIME_MS = 120; public const int ATTACK_FRAME_TIME_MS = 100; + public const int EMOTE_FRAME_TIME_MS = 250; private readonly ICharacterRepository _characterRepository; private readonly ICurrentMapStateRepository _currentMapStateRepository; @@ -30,6 +33,7 @@ public class CharacterAnimator : GameComponent, ICharacterAnimator private readonly Dictionary _otherPlayerStartWalkingTimes; private readonly Dictionary _otherPlayerStartAttackingTimes; private readonly Dictionary _otherPlayerStartSpellCastTimes; + private readonly Dictionary _startEmoteTimes; private Queue _walkPath; private Option _targetCoordinate; @@ -54,6 +58,8 @@ public class CharacterAnimator : GameComponent, ICharacterAnimator _otherPlayerStartWalkingTimes = new Dictionary(); _otherPlayerStartAttackingTimes = new Dictionary(); _otherPlayerStartSpellCastTimes = new Dictionary(); + _startEmoteTimes = new Dictionary(); + _walkPath = new Queue(); } @@ -62,6 +68,7 @@ public override void Update(GameTime gameTime) AnimateCharacterWalking(); AnimateCharacterAttacking(); AnimateCharacterSpells(); + AnimateCharacterEmotes(); base.Update(gameTime); } @@ -166,6 +173,34 @@ public void StartOtherCharacterSpellCast(int characterID) _otherPlayerStartSpellCastTimes.Add(characterID, startAttackingTimeAndID); } + public bool Emote(int characterID, Emote whichEmote) + { + if (_otherPlayerStartWalkingTimes.ContainsKey(characterID) || + _otherPlayerStartAttackingTimes.ContainsKey(characterID) || + _otherPlayerStartSpellCastTimes.ContainsKey(characterID) || + _startEmoteTimes.ContainsKey(characterID)) + return false; + + var startEmoteTime = new RenderFrameActionTime(characterID); + if (characterID == _characterRepository.MainCharacter.ID) + { + var rp = _characterRepository.MainCharacter.RenderProperties.WithEmote(whichEmote); + _characterRepository.MainCharacter = _characterRepository.MainCharacter.WithRenderProperties(rp); + } + else if (_currentMapStateRepository.Characters.TryGetValue(characterID, out var otherCharacter)) + { + var rp = otherCharacter.RenderProperties.WithEmote(whichEmote); + _currentMapStateRepository.Characters[characterID] = otherCharacter.WithRenderProperties(rp); + } + else + { + _currentMapStateRepository.UnknownPlayerIDs.Add((short)characterID); + } + + _startEmoteTimes[characterID] = startEmoteTime; + return true; + } + public void StopAllCharacterAnimations() { _otherPlayerStartWalkingTimes.Clear(); @@ -396,6 +431,38 @@ private void AnimateCharacterSpells() #endregion + #region Emote Animation + + private void AnimateCharacterEmotes() + { + var playersDoneEmoting = new HashSet(); + foreach (var pair in _startEmoteTimes.Values) + { + if (pair.ActionTimer.ElapsedMilliseconds >= EMOTE_FRAME_TIME_MS) + { + GetCurrentCharacterFromRepository(pair).Match( + none: () => playersDoneEmoting.Add(pair.UniqueID), + some: currentCharacter => + { + var renderProperties = currentCharacter.RenderProperties; + var nextFrameRenderProperties = renderProperties.WithNextEmoteFrame(); + + pair.UpdateActionStartTime(); + if (nextFrameRenderProperties.IsActing(CharacterActionState.Standing)) + playersDoneEmoting.Add(pair.UniqueID); + + var nextFrameCharacter = currentCharacter.WithRenderProperties(nextFrameRenderProperties); + UpdateCharacterInRepository(currentCharacter, nextFrameCharacter); + }); + } + } + + foreach (var key in playersDoneEmoting) + _startEmoteTimes.Remove(key); + } + + #endregion + private Option GetCurrentCharacterFromRepository(RenderFrameActionTime pair) { return pair.UniqueID == _characterRepository.MainCharacter.ID @@ -440,6 +507,8 @@ public interface ICharacterAnimator : IGameComponent void StartOtherCharacterSpellCast(int characterID); + bool Emote(int characterID, Emote whichEmote); + void StopAllCharacterAnimations(); } } diff --git a/EndlessClient/Rendering/Character/PeriodicEmoteHandler.cs b/EndlessClient/Rendering/Character/PeriodicEmoteHandler.cs new file mode 100644 index 000000000..fd1bb2b0c --- /dev/null +++ b/EndlessClient/Rendering/Character/PeriodicEmoteHandler.cs @@ -0,0 +1,120 @@ +using EndlessClient.GameExecution; +using EndlessClient.Input; +using EOLib.Domain.Character; +using Microsoft.Xna.Framework; +using Optional; +using System; +using System.Diagnostics; + +namespace EndlessClient.Rendering.Character +{ + public class PeriodicEmoteHandler : GameComponent, IPeriodicEmoteHandler + { + private const int AFK_TIME_MINUTES = 5; + private const int AFK_TIME_BETWEEN_EMOTES_MINUTES = 1; + + private readonly ICharacterActions _characterActions; + private readonly IUserInputTimeProvider _userInputTimeProvider; + private readonly ICharacterRepository _characterRepository; + private readonly ICharacterAnimator _animator; + + private readonly Random _random; + + private Option _drunkStart; + private Option _drunkTimeSinceLastEmote; + private int _drunkIntervalSeconds; + private double _drunkTimeoutSeconds; + + private Option _afkTimeSinceLastEmote; + + public PeriodicEmoteHandler(IEndlessGameProvider endlessGameProvider, + ICharacterActions characterActions, + IUserInputTimeProvider userInputTimeProvider, + ICharacterRepository characterRepository, + ICharacterAnimator animator) + : base((Game)endlessGameProvider.Game) + { + _characterActions = characterActions; + _userInputTimeProvider = userInputTimeProvider; + _characterRepository = characterRepository; + _animator = animator; + + _random = new Random(); + } + + public override void Update(GameTime gameTime) + { + _drunkStart.Match( + some: ds => + { + if ((DateTime.Now - ds).TotalSeconds > _drunkTimeoutSeconds) + { + _drunkStart = Option.None(); + _drunkTimeSinceLastEmote = Option.None(); + _drunkIntervalSeconds = 0; + + _characterRepository.MainCharacter = _characterRepository.MainCharacter.WithRenderProperties( + _characterRepository.MainCharacter.RenderProperties.WithIsDrunk(false)); + } + else + { + _drunkTimeSinceLastEmote.MatchSome(dt => + { + if (dt.Elapsed.TotalSeconds > _drunkIntervalSeconds) + { + _drunkIntervalSeconds = _random.Next(4, 7); + _drunkTimeSinceLastEmote = Option.Some(Stopwatch.StartNew()); + + if (_animator.Emote(_characterRepository.MainCharacter.ID, Emote.Drunk)) + _characterActions.Emote(Emote.Drunk); + } + }); + } + }, + none: () => + { + if (_characterRepository.MainCharacter.RenderProperties.IsDrunk) + { + _drunkStart = Option.Some(DateTime.Now); + _drunkIntervalSeconds = _random.Next(4, 7); + _drunkTimeSinceLastEmote = Option.Some(Stopwatch.StartNew()); + + if (_animator.Emote(_characterRepository.MainCharacter.ID, Emote.Drunk)) + _characterActions.Emote(Emote.Drunk); + } + }); + + if ((DateTime.Now - _userInputTimeProvider.LastInputTime).TotalMinutes >= AFK_TIME_MINUTES) + { + _afkTimeSinceLastEmote.Match( + some: at => + { + if (at.Elapsed.TotalMinutes >= AFK_TIME_BETWEEN_EMOTES_MINUTES) + { + if (_animator.Emote(_characterRepository.MainCharacter.ID, Emote.Moon)) + _characterActions.Emote(Emote.Moon); + _afkTimeSinceLastEmote = Option.Some(Stopwatch.StartNew()); + } + }, + none: () => + { + if (_animator.Emote(_characterRepository.MainCharacter.ID, Emote.Moon)) + _characterActions.Emote(Emote.Moon); + _afkTimeSinceLastEmote = Option.Some(Stopwatch.StartNew()); + }); + } + + base.Update(gameTime); + } + + public void SetDrunkTimeout(int beerPotency) + { + _drunkTimeoutSeconds = (100 + (beerPotency * 10)) / 8.0; + } + } + + public interface IPeriodicEmoteHandler : IGameComponent, IUpdateable + { + void SetDrunkTimeout(int beerPotency); + } +} diff --git a/EndlessClient/Rendering/CharacterProperties/EmoteRenderer.cs b/EndlessClient/Rendering/CharacterProperties/EmoteRenderer.cs index e65af4aa1..ec2309d43 100644 --- a/EndlessClient/Rendering/CharacterProperties/EmoteRenderer.cs +++ b/EndlessClient/Rendering/CharacterProperties/EmoteRenderer.cs @@ -15,6 +15,8 @@ public class EmoteRenderer : BaseCharacterPropertyRenderer public override bool CanRender => _renderProperties.IsActing(CharacterActionState.Emote) && _renderProperties.EmoteFrame > 0; + protected override bool ShouldFlip => false; + public EmoteRenderer(ICharacterRenderProperties renderProperties, ISpriteSheet emoteSheet, ISpriteSheet skinSheet) @@ -28,7 +30,7 @@ public class EmoteRenderer : BaseCharacterPropertyRenderer public override void Render(SpriteBatch spriteBatch, Rectangle parentCharacterDrawArea) { var skinLoc = _skinRenderLocationCalculator.CalculateDrawLocationOfCharacterSkin(_skinSheet.SourceRectangle, parentCharacterDrawArea); - var emotePos = new Vector2(skinLoc.X - 15, parentCharacterDrawArea.Y - _emoteSheet.SheetTexture.Height + 10); + var emotePos = new Vector2(skinLoc.X - 15, parentCharacterDrawArea.Y - _emoteSheet.SheetTexture.Height); Render(spriteBatch, _emoteSheet, emotePos, 128); } } diff --git a/EndlessClient/Rendering/CharacterProperties/FaceRenderer.cs b/EndlessClient/Rendering/CharacterProperties/FaceRenderer.cs index 8a435d2ba..e34a708f4 100644 --- a/EndlessClient/Rendering/CharacterProperties/FaceRenderer.cs +++ b/EndlessClient/Rendering/CharacterProperties/FaceRenderer.cs @@ -14,7 +14,9 @@ public class FaceRenderer : BaseCharacterPropertyRenderer private readonly SkinRenderLocationCalculator _skinRenderLocationCalculator; public override bool CanRender => _renderProperties.IsActing(CharacterActionState.Emote) && - _renderProperties.EmoteFrame > 0; + _renderProperties.EmoteFrame > 0 && + _renderProperties.Emote != Emote.Trade && + _renderProperties.Emote != Emote.LevelUp; public FaceRenderer(ICharacterRenderProperties renderProperties, ISpriteSheet faceSheet, diff --git a/EndlessClient/Rendering/MouseCursorRenderer.cs b/EndlessClient/Rendering/MouseCursorRenderer.cs index ddd848ea2..5d14b39e7 100644 --- a/EndlessClient/Rendering/MouseCursorRenderer.cs +++ b/EndlessClient/Rendering/MouseCursorRenderer.cs @@ -110,7 +110,7 @@ public override void Initialize() #region Update and Helpers - public override async void Update(GameTime gameTime) + public override void Update(GameTime gameTime) { // prevents updates if there is a dialog if (!ShouldUpdate() || _activeDialogProvider.ActiveDialogs.Any(x => x.HasValue) || @@ -128,7 +128,7 @@ public override async void Update(GameTime gameTime) var cellState = _mapCellStateProvider.GetCellStateAt(_gridX, _gridY); UpdateCursorSourceRectangle(cellState); - await CheckForClicks(cellState); + CheckForClicks(cellState); } private void SetGridCoordsBasedOnMousePosition(int offsetX, int offsetY) @@ -285,7 +285,7 @@ private void UpdateCursorIndexForTileSpec(TileSpec tileSpec) } } - private async Task CheckForClicks(IMapCellState cellState) + private void CheckForClicks(IMapCellState cellState) { var currentMouseState = _userInputProvider.CurrentMouseState; var previousMouseState = _userInputProvider.PreviousMouseState; @@ -294,7 +294,7 @@ private async Task CheckForClicks(IMapCellState cellState) if (currentMouseState.LeftButton == ButtonState.Released && previousMouseState.LeftButton == ButtonState.Pressed) { - await _mapInteractionController.LeftClickAsync(cellState, this); + _mapInteractionController.LeftClick(cellState, this); } } diff --git a/EndlessClient/Rendering/OldCharacterRenderer.cs b/EndlessClient/Rendering/OldCharacterRenderer.cs index e28469a9a..c5308dff6 100644 --- a/EndlessClient/Rendering/OldCharacterRenderer.cs +++ b/EndlessClient/Rendering/OldCharacterRenderer.cs @@ -85,16 +85,12 @@ public EODirection Facing private EIFRecord shieldInfo, weaponInfo/*, bootsInfo, armorInfo*/, hatInfo; - private Timer _attackTimer, _emoteTimer, _spTimer, _spellCastTimer; + private Timer _attackTimer, _spTimer, _spellCastTimer; private readonly bool noLocUpdate; private readonly DamageCounter m_damageCounter; - private DateTime? m_deadTime, m_lastEmoteTime; - private DateTime m_lastActTime; - - private DateTime? m_drunkTime; - private int m_drunkOffset; + private DateTime? m_deadTime; private DateTime? _spellInvocationStartTime; @@ -208,7 +204,6 @@ public override void Initialize() DepthFormat.None); _attackTimer = new Timer(_attackTimerCallback); - _emoteTimer = new Timer(_emoteTimerCallback); if (Character == OldWorld.Instance.MainPlayer.ActiveCharacter) { _spTimer = new Timer(o => @@ -225,8 +220,6 @@ public override void Initialize() _spellCastTimer = new Timer(_endSpellCast, null, Timeout.Infinite, Timeout.Infinite); } - - m_lastActTime = DateTime.Now; } protected override void UnloadContent() @@ -251,9 +244,7 @@ public override void Update(GameTime gameTime) if (EOGame.Instance.State == GameStates.PlayingTheGame && this == OldWorld.Instance.ActiveCharacterRenderer) { - _adjustSP(gameTime); - _checkAFKCharacter(); - _checkHandleDrunkCharacter(); + _adjustSP(gameTime);; } } @@ -367,39 +358,6 @@ private void _adjustSP(GameTime gameTime) Character.Stats.SP = (short)(Character.Stats.SP + 1); } - private void _checkAFKCharacter() - { - //5-minute timeout: start sending emotes every minute - if ((DateTime.Now - m_lastActTime).TotalMinutes > 5 && - (m_lastEmoteTime == null || (DateTime.Now - m_lastEmoteTime.Value).TotalMinutes > 1)) - { - m_lastEmoteTime = DateTime.Now; - Character.Emote(Emote.Moon); - PlayerEmote(); - } - } - - private void _checkHandleDrunkCharacter() - { - if (m_drunkTime.HasValue && Character.IsDrunk) - { - //note: these timer values (between 1-6 seconds and 30 seconds) are completely arbitrary - if (!m_lastEmoteTime.HasValue || (DateTime.Now - m_lastEmoteTime.Value).TotalMilliseconds > m_drunkOffset) - { - m_lastEmoteTime = DateTime.Now; - Character.Emote(Emote.Drunk); - PlayerEmote(); - m_drunkOffset = (new Random()).Next(1000, 6000); //between 1-6 seconds - } - - if ((DateTime.Now - m_drunkTime.Value).TotalMilliseconds >= 30000) - { - m_drunkTime = null; - Character.IsDrunk = false; - } - } - } - private bool _getMouseOverActual() { var skinDrawLoc = _getSkinDrawLoc(); @@ -511,26 +469,6 @@ public void PlayerAttack(bool isWaterTile) catch (ObjectDisposedException) { } } - public void PlayerEmote() - { - if (OldWorld.Instance.SoundEnabled && Character.RenderData.emote == Emote.LevelUp) - EOGame.Instance.SoundManager.GetSoundEffectRef(SoundEffectID.LevelUp).Play(); - //else if (!string.IsNullOrEmpty(_shoutName)) - // _cancelSpell(false); - - const int EmoteTimeBetweenFrames = 250; - Data.SetUpdate(true); - try - { - _emoteTimer.Change(0, EmoteTimeBetweenFrames); - } - catch (ObjectDisposedException) { } - } - - public void UpdateInputTime(DateTime lastInputTime) - { - m_lastActTime = lastInputTime; - } public void Die() { @@ -592,23 +530,6 @@ private void _attackTimerCallback(object state) Data.SetUpdate(true); } - private void _emoteTimerCallback(object state) - { - if (_char == null) return; - - if (Data.emoteFrame == 3) - { - _char.DoneEmote(); - _emoteTimer.Change(Timeout.Infinite, Timeout.Infinite); - } - else - { - Data.SetEmoteFrame(Data.emoteFrame + 1); - } - - Data.SetUpdate(true); - } - //character is drawn in the following order: // - shield (if wings/arrows) // - weapon (if not melee attack frame 2 in certain directions) @@ -1113,12 +1034,6 @@ public void SetDamageCounterValue(int value, int pctHealth, bool isHeal = false) m_damageCounter.SetValue(value, pctHealth, isHeal); } - public void MakeDrunk() - { - m_drunkTime = DateTime.Now; - Character.IsDrunk = true; - } - #region Spell Casting //Workflow for spells (main player): @@ -1274,8 +1189,6 @@ protected override void Dispose(bool disposing) nameLabel.Dispose(); if (_attackTimer != null) _attackTimer.Dispose(); - if (_emoteTimer != null) - _emoteTimer.Dispose(); if (_spTimer != null) _spTimer.Dispose(); if (_charRenderTarget != null) diff --git a/EndlessClient/Rendering/OldMapRenderer.cs b/EndlessClient/Rendering/OldMapRenderer.cs index 14c96654a..6fc38774d 100644 --- a/EndlessClient/Rendering/OldMapRenderer.cs +++ b/EndlessClient/Rendering/OldMapRenderer.cs @@ -174,19 +174,6 @@ private void RemoveOtherPlayer(short id, WarpAnimation anim = WarpAnimation.None } } - public void OtherPlayerEmote(short playerID, Emote emote) - { - lock (_characterListLock) - { - OldCharacterRenderer rend = _characterRenderers.Find(cc => cc.Character.ID == playerID); - if (rend != null) - { - rend.Character.Emote(emote); - rend.PlayerEmote(); - } - } - } - public void PlayerCastSpellGroup(short fromPlayerID, short spellID, short spellHPgain, List spellTargets) { lock (_characterListLock) diff --git a/EndlessClient/Rendering/Sprites/CharacterSpriteCalculator.cs b/EndlessClient/Rendering/Sprites/CharacterSpriteCalculator.cs index c5f1670ff..8a492b124 100644 --- a/EndlessClient/Rendering/Sprites/CharacterSpriteCalculator.cs +++ b/EndlessClient/Rendering/Sprites/CharacterSpriteCalculator.cs @@ -332,7 +332,7 @@ public ISpriteSheet GetSkinTexture(ICharacterRenderProperties characterRenderPro } else if (characterRenderProperties.Gender == 0) { - if (characterRenderProperties.CurrentAction == CharacterActionState.Attacking) + if (characterRenderProperties.CurrentAction == CharacterActionState.Attacking && characterRenderProperties.RenderAttackFrame > 0) { walkExtra += 1; }