yuanshen/Yuanshen.Gameserver/Game/CombatManager.cs
wisedev 61cc2055cf First working version of the server
KCP gameserver with packet dispatch, scene streaming, combat, gacha and the
shop, plus a small HTTP server for the login flow and the wish history page.

Game data under assets is pulled out of the client instead of being typed by
hand. The excel tables come from the packed bin container and the spawn points
from the compiled scene scripts.
2026-09-04 02:06:53 +03:00

169 lines
5.6 KiB
C#

using Microsoft.Extensions.Logging;
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 ActionReasonMonsterDie = 37;
private const uint ActionReasonMonsterEnvAnimal = 8;
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 ILogger _logger;
public CombatManager(NetSession session, Player player, PlayerPersistence persistence, DropTable drops,
ILogger<CombatManager> logger)
{
_session = session;
_player = player;
_persistence = persistence;
_drops = drops;
_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 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;
StoreItemChangeNotify store = new() { StoreType = StoreType.StorePack };
ItemAddHintNotify hint = new()
{
Reason = monster.IsWildlife ? ActionReasonMonsterEnvAnimal : ActionReasonMonsterDie,
Position = monster.Pos,
IsPositionValid = true
};
List<string> granted = new();
bool currencyChanged = false;
foreach ((uint itemId, uint count) in rolled)
{
switch (_drops.TypeOf(itemId))
{
case "weapon":
for (uint made = 0; made < Math.Min(count, 10u); made++)
store.ItemList.Add(_player.CreateWeapon(itemId).AsItem());
break;
case "virtual" when itemId == MoraItemId:
_player.Scoin += count;
currencyChanged = true;
break;
case "virtual" when itemId == PrimogemItemId:
_player.Hcoin += count;
currencyChanged = true;
break;
case "virtual":
continue;
case "reliquary":
continue;
default:
_player.GiveMaterial(itemId, count);
store.ItemList.Add(new Item
{
ItemId = itemId,
Guid = _player.MaterialGuid(itemId),
Material = new Material { Count = _player.Materials.GetValueOrDefault(itemId) }
});
break;
}
hint.ItemList.Add(new ItemHint { ItemId = itemId, Count = count, IsNew = false });
granted.Add($"{itemId}x{count}");
}
if (hint.ItemList.Count == 0)
return;
if (store.ItemList.Count != 0)
await _session.NotifyAsync(CmdType.StoreItemChangeNotify, store);
if (currencyChanged)
await _session.NotifyAsync(CmdType.PlayerPropNotify, _player.BuildCurrencyProps());
await _session.NotifyAsync(CmdType.ItemAddHintNotify, hint);
_logger.LogInformation("Monster {monsterId} lvl {level} dropped {items}",
monster.MonsterId, monster.Level, string.Join(", ", granted));
}
}