diff --git a/EOBot/Program.cs b/EOBot/Program.cs index 6dc93d8a8..778a0c7cb 100644 --- a/EOBot/Program.cs +++ b/EOBot/Program.cs @@ -158,7 +158,7 @@ public void NotifySelfSpellCast(short playerId, short spellId, int spellHp, byte public void NotifyStartSpellCast(short playerId, short spellId) { } public void NotifyTargetOtherSpellCast(short sourcePlayerID, short targetPlayerID, short spellId, int recoveredHP, byte targetPercentHealth) { } - public void StartOtherCharacterAttackAnimation(int characterID) { } + public void StartOtherCharacterAttackAnimation(int characterID, int noteIndex) { } public void StartOtherCharacterWalkAnimation(int characterID, byte destinationX, byte destinationY, EODirection direction) { } } diff --git a/EOLib/Domain/Character/CharacterRenderProperties.cs b/EOLib/Domain/Character/CharacterRenderProperties.cs index 21ec5279b..e78481884 100644 --- a/EOLib/Domain/Character/CharacterRenderProperties.cs +++ b/EOLib/Domain/Character/CharacterRenderProperties.cs @@ -174,7 +174,11 @@ public ICharacterRenderProperties WithNextEmoteFrame() { var props = MakeCopy(this); props.EmoteFrame = (props.EmoteFrame + 1) % MAX_NUMBER_OF_EMOTE_FRAMES; - props.CurrentAction = props.EmoteFrame == 0 ? CharacterActionState.Standing : CharacterActionState.Emote; + props.CurrentAction = props.EmoteFrame == 0 + ? CharacterActionState.Standing + : props.CurrentAction == CharacterActionState.Attacking // when using an instrument keep the current state as "Attacking" + ? CharacterActionState.Attacking + : CharacterActionState.Emote; return props; } diff --git a/EOLib/Domain/Character/Emote.cs b/EOLib/Domain/Character/Emote.cs index 955c4787e..2dfe99597 100644 --- a/EOLib/Domain/Character/Emote.cs +++ b/EOLib/Domain/Character/Emote.cs @@ -21,6 +21,7 @@ public enum Emote /// /// 0 key /// - Playful = 14 + Playful = 14, + MusicNotes = 15, } } diff --git a/EOLib/Domain/Jukebox/JukeboxActions.cs b/EOLib/Domain/Jukebox/JukeboxActions.cs new file mode 100644 index 000000000..04e8ef6af --- /dev/null +++ b/EOLib/Domain/Jukebox/JukeboxActions.cs @@ -0,0 +1,51 @@ +using AutomaticTypeMapper; +using EOLib.Domain.Character; +using EOLib.IO; +using EOLib.IO.Repositories; +using EOLib.Net; +using EOLib.Net.Communication; +using Optional.Collections; +using System; + +namespace EOLib.Domain.Jukebox +{ + [AutoMappedType] + public class JukeboxActions : IJukeboxActions + { + private readonly IPacketSendService _packetSendService; + private readonly ICharacterProvider _characterProvider; + private readonly IEIFFileProvider _eifFileProvider; + + public JukeboxActions(IPacketSendService packetSendService, + ICharacterProvider characterProvider, + IEIFFileProvider eifFileProvider) + { + _packetSendService = packetSendService; + _characterProvider = characterProvider; + _eifFileProvider = eifFileProvider; + } + + public void PlayNote(int noteIndex) + { + if (noteIndex < 0 || noteIndex >= 36) + throw new ArgumentOutOfRangeException(nameof(noteIndex)); + + var weapon = _characterProvider.MainCharacter.RenderProperties.WeaponGraphic; + _eifFileProvider.EIFFile.SingleOrNone(x => x.Type == ItemType.Weapon && x.DollGraphic == weapon) + .MatchSome(rec => + { + var packet = new PacketBuilder(PacketFamily.JukeBox, PacketAction.Use) + .AddChar((byte)rec.DollGraphic) // todo: determine what GameServer expects; eoserv sends DollGraphic as a response in Character::PlayBard + .AddChar((byte)(noteIndex + 1)) + .Build(); + + _packetSendService.SendPacket(packet); + }); + } + } + + public interface IJukeboxActions + { + void PlayNote(int noteIndex); + } +} diff --git a/EOLib/Domain/Notifiers/IOtherCharacterAnimationNotifier.cs b/EOLib/Domain/Notifiers/IOtherCharacterAnimationNotifier.cs index a096be63f..827343f5e 100644 --- a/EOLib/Domain/Notifiers/IOtherCharacterAnimationNotifier.cs +++ b/EOLib/Domain/Notifiers/IOtherCharacterAnimationNotifier.cs @@ -6,7 +6,7 @@ public interface IOtherCharacterAnimationNotifier { void StartOtherCharacterWalkAnimation(int characterID, byte destinationX, byte destinationY, EODirection direction); - void StartOtherCharacterAttackAnimation(int characterID); + void StartOtherCharacterAttackAnimation(int characterID, int noteIndex = -1); void NotifyStartSpellCast(short playerId, short spellId); @@ -20,7 +20,7 @@ public class NoOpOtherCharacterAnimationNotifier : IOtherCharacterAnimationNotif { public void StartOtherCharacterWalkAnimation(int characterID, byte destinationX, byte destinationY, EODirection direction) { } - public void StartOtherCharacterAttackAnimation(int characterID) { } + public void StartOtherCharacterAttackAnimation(int characterID, int noteIndex = -1) { } public void NotifyStartSpellCast(short playerId, short spellId) { } diff --git a/EOLib/PacketHandlers/Jukebox/JukeboxMessageHandler.cs b/EOLib/PacketHandlers/Jukebox/JukeboxMessageHandler.cs new file mode 100644 index 000000000..c12e8d921 --- /dev/null +++ b/EOLib/PacketHandlers/Jukebox/JukeboxMessageHandler.cs @@ -0,0 +1,61 @@ +using AutomaticTypeMapper; +using EOLib.Domain.Character; +using EOLib.Domain.Login; +using EOLib.Domain.Map; +using EOLib.Domain.Notifiers; +using EOLib.Net; +using EOLib.Net.Handlers; +using System.Collections.Generic; + +namespace EOLib.PacketHandlers.Jukebox +{ + [AutoMappedType] + public class JukeboxMessageHandler : InGameOnlyPacketHandler + { + private readonly ICharacterRepository _characterRepository; + private readonly ICurrentMapStateRepository _currentMapStateRepository; + private readonly IEnumerable _mainCharacterEventNotifiers; + private readonly IEnumerable _otherCharacterAnimationNotifiers; + + public override PacketFamily Family => PacketFamily.JukeBox; + + public override PacketAction Action => PacketAction.Message; + + public JukeboxMessageHandler(IPlayerInfoProvider playerInfoProvider, + ICurrentMapStateRepository currentMapStateRepository, + IEnumerable otherCharacterAnimationNotifiers) + : base(playerInfoProvider) + { + _currentMapStateRepository = currentMapStateRepository; + _otherCharacterAnimationNotifiers = otherCharacterAnimationNotifiers; + } + + public override bool HandlePacket(IPacket packet) + { + var playerId = packet.ReadShort(); + var direction = (EODirection)packet.ReadChar(); + var instrument = packet.ReadChar(); + var note = packet.ReadChar(); + + if (_currentMapStateRepository.Characters.ContainsKey(playerId)) + { + var c = _currentMapStateRepository.Characters[playerId]; + + if (c.RenderProperties.WeaponGraphic == instrument) + { + c = c.WithRenderProperties(c.RenderProperties.WithDirection(direction)); + _currentMapStateRepository.Characters[playerId] = c; + + foreach (var notifier in _otherCharacterAnimationNotifiers) + notifier.StartOtherCharacterAttackAnimation(playerId, note - 1); + } + } + else + { + _currentMapStateRepository.UnknownPlayerIDs.Add(playerId); + } + + return true; + } + } +} diff --git a/EndlessClient/Controllers/BardController.cs b/EndlessClient/Controllers/BardController.cs index 8f1b47583..dd84763a9 100644 --- a/EndlessClient/Controllers/BardController.cs +++ b/EndlessClient/Controllers/BardController.cs @@ -1,12 +1,38 @@ using AutomaticTypeMapper; +using EndlessClient.Rendering.Character; +using EOLib.Domain.Character; +using EOLib.Domain.Extensions; +using EOLib.Domain.Jukebox; namespace EndlessClient.Controllers { [AutoMappedType] public class BardController : IBardController { + private readonly ICharacterAnimationActions _characterAnimationActions; + private readonly IJukeboxActions _jukeboxActions; + private readonly ICharacterProvider _characterProvider; + + public BardController(ICharacterAnimationActions characterAnimationActions, + IJukeboxActions jukeboxActions, + ICharacterProvider characterProvider) + { + _characterAnimationActions = characterAnimationActions; + _jukeboxActions = jukeboxActions; + _characterProvider = characterProvider; + } + public void PlayInstrumentNote(int noteIndex) { + _characterAnimationActions.StartAttacking(noteIndex); + _jukeboxActions.PlayNote(noteIndex); + } + + private bool CanAttackAgain() + { + var rp = _characterProvider.MainCharacter.RenderProperties; + return rp.IsActing(CharacterActionState.Standing) || + rp.RenderAttackFrame == CharacterRenderProperties.MAX_NUMBER_OF_ATTACK_FRAMES; } } diff --git a/EndlessClient/Controllers/NumPadController.cs b/EndlessClient/Controllers/NumPadController.cs index 467bf5f3d..be501e11a 100644 --- a/EndlessClient/Controllers/NumPadController.cs +++ b/EndlessClient/Controllers/NumPadController.cs @@ -1,6 +1,4 @@ using AutomaticTypeMapper; -using EndlessClient.ControlSets; -using EndlessClient.HUD.Controls; using EndlessClient.Rendering.Character; using EOLib.Domain.Character; using EOLib.Domain.Extensions; @@ -11,31 +9,25 @@ namespace EndlessClient.Controllers public class NumPadController : INumPadController { private readonly ICharacterActions _characterActions; - private readonly IHudControlProvider _hudControlProvider; - private readonly ICharacterRendererProvider _characterRendererProvider; + private readonly ICharacterAnimationActions _characterAnimationActions; + private readonly ICharacterProvider _characterProvider; public NumPadController(ICharacterActions characterActions, - IHudControlProvider hudControlProvider, - ICharacterRendererProvider characterRendererProvider) + ICharacterAnimationActions characterAnimationActions, + ICharacterProvider characterProvider) { _characterActions = characterActions; - _hudControlProvider = hudControlProvider; - _characterRendererProvider = characterRendererProvider; + _characterAnimationActions = characterAnimationActions; + _characterProvider = characterProvider; } public void Emote(Emote whichEmote) { - var mainRenderer = _characterRendererProvider.MainCharacterRenderer; - mainRenderer.MatchSome(renderer => - { - if (!renderer.Character.RenderProperties.IsActing(CharacterActionState.Standing)) - return; + if (!_characterProvider.MainCharacter.RenderProperties.IsActing(CharacterActionState.Standing)) + return; - _characterActions.Emote(whichEmote); - - var animator = _hudControlProvider.GetComponent(HudControlIdentifier.CharacterAnimator); - animator.Emote(renderer.Character.ID, whichEmote); - }); + _characterActions.Emote(whichEmote); + _characterAnimationActions.Emote(whichEmote); } } diff --git a/EndlessClient/Rendering/Character/CharacterAnimationActions.cs b/EndlessClient/Rendering/Character/CharacterAnimationActions.cs index 38ac910d1..d0fbdd7de 100644 --- a/EndlessClient/Rendering/Character/CharacterAnimationActions.cs +++ b/EndlessClient/Rendering/Character/CharacterAnimationActions.cs @@ -1,4 +1,5 @@ using AutomaticTypeMapper; +using EndlessClient.Audio; using EndlessClient.ControlSets; using EndlessClient.HUD; using EndlessClient.HUD.Controls; @@ -9,10 +10,13 @@ using EOLib.Domain.Map; using EOLib.Domain.Notifiers; using EOLib.Domain.Spells; +using EOLib.IO; using EOLib.IO.Map; using EOLib.IO.Repositories; using EOLib.Localization; using Optional; +using Optional.Collections; +using System.Linq; namespace EndlessClient.Rendering.Character { @@ -25,8 +29,9 @@ public class CharacterAnimationActions : ICharacterAnimationActions, IOtherChara private readonly ICharacterRendererProvider _characterRendererProvider; private readonly ICurrentMapProvider _currentMapProvider; private readonly ISpikeTrapActions _spikeTrapActions; - private readonly IESFFileProvider _esfFileProvider; + private readonly IPubFileProvider _pubFileProvider; private readonly IStatusLabelSetter _statusLabelSetter; + private readonly ISfxPlayer _sfxPlayer; public CharacterAnimationActions(IHudControlProvider hudControlProvider, ICharacterRepository characterRepository, @@ -34,8 +39,9 @@ public class CharacterAnimationActions : ICharacterAnimationActions, IOtherChara ICharacterRendererProvider characterRendererProvider, ICurrentMapProvider currentMapProvider, ISpikeTrapActions spikeTrapActions, - IESFFileProvider esfFileProvider, - IStatusLabelSetter statusLabelSetter) + IPubFileProvider pubFileProvider, + IStatusLabelSetter statusLabelSetter, + ISfxPlayer sfxPlayer) { _hudControlProvider = hudControlProvider; _characterRepository = characterRepository; @@ -43,8 +49,9 @@ public class CharacterAnimationActions : ICharacterAnimationActions, IOtherChara _characterRendererProvider = characterRendererProvider; _currentMapProvider = currentMapProvider; _spikeTrapActions = spikeTrapActions; - _esfFileProvider = esfFileProvider; + _pubFileProvider = pubFileProvider; _statusLabelSetter = statusLabelSetter; + _sfxPlayer = sfxPlayer; } public void Face(EODirection direction) @@ -66,14 +73,16 @@ public void StartWalking() ShowWaterSplashiesIfNeeded(CharacterActionState.Walking, _characterRepository.MainCharacter.ID); } - public void StartAttacking() + public void StartAttacking(int noteIndex = -1) { if (!_hudControlProvider.IsInGame) return; CancelSpellPrep(); - Animator.StartMainCharacterAttackAnimation(); + Animator.StartMainCharacterAttackAnimation(isBard: noteIndex >= 0); ShowWaterSplashiesIfNeeded(CharacterActionState.Attacking, _characterRepository.MainCharacter.ID); + + PlayWeaponSound(_characterRepository.MainCharacter, noteIndex); } public bool PrepareMainCharacterSpell(int spellId, ISpellTargetable spellTarget) @@ -81,11 +90,19 @@ public bool PrepareMainCharacterSpell(int spellId, ISpellTargetable spellTarget) if (!_hudControlProvider.IsInGame) return false; - var spellData = _esfFileProvider.ESFFile[spellId]; + var spellData = _pubFileProvider.ESFFile[spellId]; _characterRendererProvider.MainCharacterRenderer.MatchSome(r => r.ShoutSpellPrep(spellData.Shout)); return Animator.MainCharacterShoutSpellPrep(spellData, spellTarget); } + public void Emote(Emote whichEmote) + { + if (!_hudControlProvider.IsInGame) + return; + + Animator.Emote(_characterRepository.MainCharacter.ID, whichEmote); + } + public void StartOtherCharacterWalkAnimation(int characterID, byte destinationX, byte destinationY, EODirection direction) { if (!_hudControlProvider.IsInGame) @@ -98,13 +115,16 @@ public void StartOtherCharacterWalkAnimation(int characterID, byte destinationX, _spikeTrapActions.ShowSpikeTrap(characterID); } - public void StartOtherCharacterAttackAnimation(int characterID) + public void StartOtherCharacterAttackAnimation(int characterID, int noteIndex = -1) { if (!_hudControlProvider.IsInGame) return; - Animator.StartOtherCharacterAttackAnimation(characterID); + Animator.StartOtherCharacterAttackAnimation(characterID, noteIndex >= 0); ShowWaterSplashiesIfNeeded(CharacterActionState.Attacking, characterID); + + if (_currentMapStateProvider.Characters.ContainsKey(characterID)) + PlayWeaponSound(_currentMapStateProvider.Characters[characterID], noteIndex); } public void NotifyWarpLeaveEffect(short characterId, WarpAnimation anim) @@ -134,13 +154,13 @@ public void NotifyPotionEffect(short playerId, int effectId) public void NotifyStartSpellCast(short playerId, short spellId) { - var shoutName = _esfFileProvider.ESFFile[spellId].Shout; + var shoutName = _pubFileProvider.ESFFile[spellId].Shout; _characterRendererProvider.CharacterRenderers[playerId].ShoutSpellPrep(shoutName.ToLower()); } public void NotifySelfSpellCast(short playerId, short spellId, int spellHp, byte percentHealth) { - var spellGraphic = _esfFileProvider.ESFFile[spellId].Graphic; + var spellGraphic = _pubFileProvider.ESFFile[spellId].Graphic; if (playerId == _characterRepository.MainCharacter.ID) { @@ -172,7 +192,7 @@ public void NotifyTargetOtherSpellCast(short sourcePlayerID, short targetPlayerI _characterRendererProvider.CharacterRenderers[sourcePlayerID].ShoutSpellCast(); } - var spellData = _esfFileProvider.ESFFile[spellId]; + var spellData = _pubFileProvider.ESFFile[spellId]; if (targetPlayerID == _characterRepository.MainCharacter.ID) { @@ -246,6 +266,25 @@ private void CancelSpellPrep() _characterRendererProvider.MainCharacterRenderer.MatchSome(r => r.StopShout()); } + private void PlayWeaponSound(ICharacter character, int noteIndex = -1) + { + _pubFileProvider.EIFFile.SingleOrNone(x => x.Type == ItemType.Weapon && x.DollGraphic == character.RenderProperties.WeaponGraphic) + .MatchSome(x => + { + var ndx = Constants.InstrumentIDs.ToList().FindIndex(y => y == x.ID); + + if (ndx >= 0 && (noteIndex < 0 || noteIndex >= 36)) + return; + + switch (ndx) + { + case 0: _sfxPlayer.PlayHarpNote(noteIndex); break; + case 1: _sfxPlayer.PlayGuitarNote(noteIndex); break; + default: break; // todo: melee/bow/gun sounds + } + }); + } + private ICharacterAnimator Animator => _hudControlProvider.GetComponent(HudControlIdentifier.CharacterAnimator); } @@ -255,8 +294,10 @@ public interface ICharacterAnimationActions void StartWalking(); - void StartAttacking(); + void StartAttacking(int noteIndex = -1); bool PrepareMainCharacterSpell(int spellId, ISpellTargetable spellTarget); + + void Emote(Emote whichEmote); } } diff --git a/EndlessClient/Rendering/Character/CharacterAnimator.cs b/EndlessClient/Rendering/Character/CharacterAnimator.cs index 09dbc3a48..a0832fabf 100644 --- a/EndlessClient/Rendering/Character/CharacterAnimator.cs +++ b/EndlessClient/Rendering/Character/CharacterAnimator.cs @@ -129,7 +129,7 @@ public void StartMainCharacterWalkAnimation(Option targetCoordina _characterActions.Walk(); } - public void StartMainCharacterAttackAnimation() + public void StartMainCharacterAttackAnimation(bool isBard = false) { if (_otherPlayerStartAttackingTimes.ContainsKey(_characterRepository.MainCharacter.ID)) { @@ -139,6 +139,13 @@ public void StartMainCharacterAttackAnimation() var startAttackingTime = new RenderFrameActionTime(_characterRepository.MainCharacter.ID); _otherPlayerStartAttackingTimes.Add(_characterRepository.MainCharacter.ID, startAttackingTime); + + if (isBard) + { + var rp = _characterRepository.MainCharacter.RenderProperties.WithEmote(EOLib.Domain.Character.Emote.MusicNotes); + _characterRepository.MainCharacter = _characterRepository.MainCharacter.WithRenderProperties(rp); + _startEmoteTimes[_characterRepository.MainCharacter.ID] = new RenderFrameActionTime(_characterRepository.MainCharacter.ID); + } } public bool MainCharacterShoutSpellPrep(ESFRecord spellData, ISpellTargetable target) @@ -176,7 +183,7 @@ public void StartOtherCharacterWalkAnimation(int characterID, byte destinationX, _otherPlayerStartWalkingTimes.Add(characterID, startWalkingTimeAndID); } - public void StartOtherCharacterAttackAnimation(int characterID) + public void StartOtherCharacterAttackAnimation(int characterID, bool isBard = false) { if (_otherPlayerStartAttackingTimes.TryGetValue(characterID, out var _)) { @@ -184,8 +191,15 @@ public void StartOtherCharacterAttackAnimation(int characterID) return; } - var startAttackingTimeAndID = new RenderFrameActionTime(characterID); - _otherPlayerStartAttackingTimes.Add(characterID, startAttackingTimeAndID); + var startAttackingTime = new RenderFrameActionTime(characterID); + _otherPlayerStartAttackingTimes.Add(characterID, startAttackingTime); + + if (isBard && _currentMapStateRepository.Characters.TryGetValue(characterID, out var otherCharacter)) + { + var rp = otherCharacter.RenderProperties.WithEmote(EOLib.Domain.Character.Emote.MusicNotes); + _currentMapStateRepository.Characters[characterID] = otherCharacter.WithRenderProperties(rp); + _startEmoteTimes[characterID] = new RenderFrameActionTime(characterID); + } } public void StartOtherCharacterSpellCast(int characterID) @@ -547,7 +561,7 @@ public interface ICharacterAnimator : IGameComponent void StartMainCharacterWalkAnimation(Option targetCoordinate); - void StartMainCharacterAttackAnimation(); + void StartMainCharacterAttackAnimation(bool isBard = false); bool MainCharacterShoutSpellPrep(ESFRecord spellData, ISpellTargetable spellTarget); @@ -555,7 +569,7 @@ public interface ICharacterAnimator : IGameComponent void StartOtherCharacterWalkAnimation(int characterID, byte targetX, byte targetY, EODirection direction); - void StartOtherCharacterAttackAnimation(int characterID); + void StartOtherCharacterAttackAnimation(int characterID, bool isBard = false); void StartOtherCharacterSpellCast(int characterID); diff --git a/EndlessClient/Rendering/CharacterProperties/EmoteRenderer.cs b/EndlessClient/Rendering/CharacterProperties/EmoteRenderer.cs index ec2309d43..a1cf5a5d6 100644 --- a/EndlessClient/Rendering/CharacterProperties/EmoteRenderer.cs +++ b/EndlessClient/Rendering/CharacterProperties/EmoteRenderer.cs @@ -12,8 +12,7 @@ public class EmoteRenderer : BaseCharacterPropertyRenderer private readonly ISpriteSheet _skinSheet; private readonly SkinRenderLocationCalculator _skinRenderLocationCalculator; - public override bool CanRender => _renderProperties.IsActing(CharacterActionState.Emote) && - _renderProperties.EmoteFrame > 0; + public override bool CanRender => _renderProperties.EmoteFrame > 0; protected override bool ShouldFlip => false; diff --git a/EndlessClient/Rendering/CharacterProperties/FaceRenderer.cs b/EndlessClient/Rendering/CharacterProperties/FaceRenderer.cs index e34a708f4..97ef88c5d 100644 --- a/EndlessClient/Rendering/CharacterProperties/FaceRenderer.cs +++ b/EndlessClient/Rendering/CharacterProperties/FaceRenderer.cs @@ -16,7 +16,8 @@ public class FaceRenderer : BaseCharacterPropertyRenderer public override bool CanRender => _renderProperties.IsActing(CharacterActionState.Emote) && _renderProperties.EmoteFrame > 0 && _renderProperties.Emote != Emote.Trade && - _renderProperties.Emote != Emote.LevelUp; + _renderProperties.Emote != Emote.LevelUp && + _renderProperties.Emote != Emote.MusicNotes; public FaceRenderer(ICharacterRenderProperties renderProperties, ISpriteSheet faceSheet, diff --git a/EndlessClient/Rendering/Sprites/EmoteSpriteType.cs b/EndlessClient/Rendering/Sprites/EmoteSpriteType.cs index 94043173c..aad9b755c 100644 --- a/EndlessClient/Rendering/Sprites/EmoteSpriteType.cs +++ b/EndlessClient/Rendering/Sprites/EmoteSpriteType.cs @@ -15,6 +15,7 @@ public enum EmoteSpriteType Drunk = 10, Trade = 11, LevelUp = 12, - Playful = 13 + Playful = 13, + MusicNotes = 14, } }