Drops now land on the ground as gadgets and are picked up with F instead of appearing in the bag out of nowhere. Chests, ore, crystals and plants all spawn and can be interacted with. Monsters load by scene group rather than by a radius around each one, so a camp shows up as a camp and nothing pops into existence mid fight. The group anchors come out of the scene Lua, which also killed four invented tuning constants. The gateway was creating a whole new session for every handshake datagram, and the client retransmits those until it gets an answer. That is where the piles of duplicated monsters and the chests coming back from the dead were coming from. Also: monster level scales to the party now, path queries return an actual two point path instead of an empty one, NpcTalk closes properly, and the ability and combat invocations are no longer echoed back to the client that sent them.
171 lines
5.5 KiB
C#
171 lines
5.5 KiB
C#
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<CombatManager> 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<uint, uint> rolled = _drops.Roll(dropId, Random);
|
|
if (rolled.Count == 0)
|
|
return;
|
|
|
|
List<string> granted = new();
|
|
int scattered = 0;
|
|
|
|
foreach ((uint itemId, uint count) in rolled)
|
|
{
|
|
uint gadgetId = _excel.GetTable(ExcelType.Material).Items.OfType<MaterialExcel>()
|
|
.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));
|
|
}
|
|
}
|