using Microsoft.Extensions.Logging; using Yuanshen.Common.Data.Excel; using Yuanshen.Gameserver.Database; using Yuanshen.Gameserver.Network.Session; using Yuanshen.Protocol; namespace Yuanshen.Gameserver.Game; internal sealed class CombatManager { private const uint FightPropCurHp = 1010; private const uint FightPropMaxHp = 2000; private const uint MoraItemId = 202; private const uint PrimogemItemId = 201; private static readonly Random Random = new(); private readonly NetSession _session; private readonly Player _player; private readonly PlayerPersistence _persistence; private readonly DropTable _drops; private readonly SceneManager _scene; private readonly ExcelTableCollection _excel; private readonly ILogger _logger; public CombatManager(NetSession session, Player player, PlayerPersistence persistence, DropTable drops, SceneManager scene, ExcelTableCollection excel, ILogger logger) { _session = session; _player = player; _persistence = persistence; _drops = drops; _scene = scene; _excel = excel; _logger = logger; } public async ValueTask HandleAttackAsync(AttackResult attack) { GameMonster? monster = _player.Monsters.FirstOrDefault(m => m.EntityId == attack.DefenseId); if (monster is null) { _logger.LogDebug("hit on non-monster entity {id}", attack.DefenseId); return; } if (!monster.IsAlive || attack.Damage <= 0f) { _logger.LogDebug("hit ignored on {id}: alive={alive} damage={dmg}", attack.DefenseId, monster.IsAlive, attack.Damage); return; } _logger.LogInformation("monster {mid} takes {dmg} damage ({hp} -> {left})", monster.MonsterId, attack.Damage, monster.CurrentHp, monster.CurrentHp - attack.Damage); float remaining = monster.ApplyDamage(attack.Damage); await _session.NotifyAsync(CmdType.EntityFightPropUpdateNotify, new EntityFightPropUpdateNotify { EntityId = monster.EntityId, FightPropMap = { { FightPropCurHp, remaining } } }); if (remaining > 0f) return; _logger.LogInformation("Monster {monsterId} killed by entity {attacker}", monster.MonsterId, attack.AttackerId); if (monster.NodeId != 0) _player.MarkNodeKilled(monster.NodeId); await GrantDropsAsync(monster); if (monster.NodeId != 0) await _persistence.SaveAsync(); await _session.NotifyAsync(CmdType.LifeStateChangeNotify, new LifeStateChangeNotify { EntityId = monster.EntityId, LifeState = 2, SourceEntityId = attack.AttackerId, DieType = PlayerDieType.PlayerDieKillByMonster }); await _session.NotifyAsync(CmdType.SceneEntityDisappearNotify, new SceneEntityDisappearNotify { DisappearType = VisionType.VisionDie, EntityList = { monster.EntityId } }); _player.Monsters.Remove(monster); } private static Vector Scatter(Vector origin, int index) { double angle = index * 2.39996; double radius = 0.6 + index * 0.35; return new Vector { X = origin.X + (float)(Math.Cos(angle) * radius), Y = origin.Y + 0.4f, Z = origin.Z + (float)(Math.Sin(angle) * radius) }; } private async ValueTask GrantDirectAsync(uint itemId, uint count) { if (itemId == MoraItemId) { _player.Scoin += count; } else if (itemId == PrimogemItemId) { _player.Hcoin += count; } else { _player.GiveMaterial(itemId, count); StoreItemChangeNotify store = new() { StoreType = StoreType.StorePack }; store.ItemList.Add(new Item { ItemId = itemId, Guid = _player.MaterialGuid(itemId), Material = new Material { Count = _player.Materials.GetValueOrDefault(itemId) } }); await _session.NotifyAsync(CmdType.StoreItemChangeNotify, store); return; } await _session.NotifyAsync(CmdType.PlayerPropNotify, _player.BuildCurrencyProps()); } private async ValueTask GrantDropsAsync(GameMonster monster) { uint dropId = _drops.DropIdFor(monster.MonsterId, monster.Level); if (dropId == 0) return; Dictionary rolled = _drops.Roll(dropId, Random); if (rolled.Count == 0) return; List granted = new(); int scattered = 0; foreach ((uint itemId, uint count) in rolled) { uint gadgetId = _excel.GetTable(ExcelType.Material).Items.OfType() .FirstOrDefault(candidate => candidate.Id == itemId)?.GadgetId ?? 0; if (gadgetId == 0) { await GrantDirectAsync(itemId, count); } else { GameGadget drop = _player.SpawnDrop(gadgetId, itemId, count, Scatter(monster.Pos, scattered++)); await _scene.AppearGadgetAsync(drop); } granted.Add($"{itemId}x{count}"); } _logger.LogInformation("Monster {monsterId} lvl {level} dropped {items}", monster.MonsterId, monster.Level, string.Join(", ", granted)); } }