Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ public sealed class WearHandler(
ILogger<WearHandler> logger,
IClock clock,
ILogLanguageLocalizer<LogLanguageKey> logLanguage,
IOptions<WorldConfiguration> worldConfiguration)
IOptions<WorldConfiguration> worldConfiguration,
Services.BattleService.IVitalityService vitalityService)
{
[UsedImplicitly]
public async Task Handle(ItemUsedEvent evt)
Expand Down Expand Up @@ -192,6 +193,8 @@ await session.Character.MapInstance.SendPacketAsync(
itemInstance.ItemInstance.ItemDeleteTime =
clock.GetCurrentInstant().Plus(Duration.FromSeconds(itemInstance.ItemInstance.Item.ItemValidTime));
}

await vitalityService.RefreshAndNotifyAsync(session.Character).ConfigureAwait(false);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ private static IEnumerable<IReadOnlyList<BCardDto>> CardSources(
yield return equipment;
}

private static int ScaleByLevel(BCardDto card, int level)
public static int ScaleByLevel(BCardDto card, int level)
{
// Matches OpenNos: IsLevelScaled + IsLevelDivided together means "first/level",
// IsLevelScaled alone means "first * level". Default path uses FirstData as-is.
Expand Down
23 changes: 19 additions & 4 deletions src/NosCore.GameObject/Services/BattleService/HitQueue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public sealed class HitQueue(
IBattleStatsProvider statsProvider,
IBuffService buffService,
IRegenerationService regenerationService,
IVitalityService vitalityService,
ILogger<HitQueue> logger) : IHitQueue, ISingletonService
{
private readonly ConcurrentDictionary<Entity, Channel<HitRequest>> _channels = new();
Expand All @@ -59,6 +60,14 @@ public Task<HitOutcome> EnqueueAsync(HitRequest request)
return request.Completion.Task;
}

private async Task RefreshVitalityAsync(IAliveEntity entity)
{
if (entity is ICharacterEntity character)
{
await vitalityService.RefreshAndNotifyAsync(character).ConfigureAwait(false);
}
}

private Channel<HitRequest> CreateChannel(IAliveEntity target)
{
var channel = Channel.CreateUnbounded<HitRequest>(new UnboundedChannelOptions
Expand Down Expand Up @@ -175,12 +184,18 @@ await ApplySpecialActionsAsync(request.Skill.BCards, request.Origin, target)
regenerationService.NotifyDamaged(hurtCharacter.CharacterId);
}

// Skill BCards that don't describe damage (i.e. stat modifiers) become a
// buff on the target lasting the skill's Duration. Fire-and-forget is fine:
// the worker is already serialising per-target, so ordering is preserved.
if (!killed && request.Skill.Duration > 0 && request.Skill.BCards.Count > 0)
{
_ = buffService.ApplySkillBuffAsync(target, request.Skill.SkillVnum, request.Skill.Duration, request.Skill.BCards, request.Origin);
await buffService
.ApplySkillBuffAsync(target, request.Skill.SkillVnum, request.Skill.Duration,
request.Skill.BCards, request.Origin)
.ConfigureAwait(false);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need the configureawait


await RefreshVitalityAsync(target).ConfigureAwait(false);
if (!ReferenceEquals(request.Origin, target))
{
await RefreshVitalityAsync(request.Origin).ConfigureAwait(false);
}
}

request.Completion.TrySetResult(new HitOutcome(HitStatus.Landed, damage.Damage, damage.HitMode, killed));
Expand Down
17 changes: 17 additions & 0 deletions src/NosCore.GameObject/Services/BattleService/IVitalityService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// __ _ __ __ ___ __ ___ ___
// | \| |/__\ /' _/ / _//__\| _ \ __|
// | | ' | \/ |`._`.| \_| \/ | v / _|
// |_|\__|\__/ |___/ \__/\__/|_|_\___|
//

using System.Threading.Tasks;
using NosCore.GameObject.Ecs.Interfaces;

namespace NosCore.GameObject.Services.BattleService;

public interface IVitalityService
{
bool Refresh(ICharacterEntity character);

Task RefreshAndNotifyAsync(ICharacterEntity character);
}
143 changes: 143 additions & 0 deletions src/NosCore.GameObject/Services/BattleService/VitalityService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// __ _ __ __ ___ __ ___ ___
// | \| |/__\ /' _/ / _//__\| _ \ __|
// | | ' | \/ |`._`.| \_| \/ | v / _|
// |_|\__|\__/ |___/ \__/\__/|_|_\___|
//

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NosCore.Algorithm.HpService;
using NosCore.Algorithm.MpService;
using NosCore.Data.Enumerations.Buff;
using NosCore.Data.StaticEntities;
using NosCore.GameObject.Ecs.Extensions;
using NosCore.GameObject.Ecs.Interfaces;
using NosCore.GameObject.Infastructure;
using NosCore.Networking;

namespace NosCore.GameObject.Services.BattleService;

public sealed class VitalityService(
IHpService hpService,
IMpService mpService,
EquipmentService.IEquipmentStatsService equipmentStatsService,
IBuffService buffService) : IVitalityService, ISingletonService
{
public bool Refresh(ICharacterEntity character)
{
if (character is not Ecs.PlayerComponentBundle player)
{
return false;
}

var gear = equipmentStatsService.Resolve(character);
var cards = gear.BCards.Concat(buffService.GetActiveBuffs(character).SelectMany(b => b.BCards));

var baseHp = (int)hpService.GetHp(character.Class, character.Level);
var baseMp = (int)mpService.GetMp(character.Class, character.Level);
var hp = baseHp + gear.Hp;
var mp = baseMp + gear.Mp;

int hpPercent = 0, mpPercent = 0;

int additionalHpPercent = 0, additionalHpCap = 0;
int additionalMpPercent = 0, additionalMpCap = 0;

foreach (var card in cards)
{
if ((BCardType.CardType)card.Type == BCardType.CardType.Quest)
{
var first = BattleStatsProvider.ScaleByLevel(card, character.Level);
switch ((AdditionalTypes.Quest)card.SubType)

Check failure on line 53 in src/NosCore.GameObject/Services/BattleService/VitalityService.cs

View workflow job for this annotation

GitHub Actions / build

The type or namespace name 'AdditionalTypes' could not be found (are you missing a using directive or an assembly reference?)
{
case AdditionalTypes.Quest.AdditionalHpPercent:

Check failure on line 55 in src/NosCore.GameObject/Services/BattleService/VitalityService.cs

View workflow job for this annotation

GitHub Actions / build

The name 'AdditionalTypes' does not exist in the current context
additionalHpPercent += first;
additionalHpCap = Math.Max(additionalHpCap, card.SecondData);
break;
case AdditionalTypes.Quest.AdditionalMpPercent:

Check failure on line 59 in src/NosCore.GameObject/Services/BattleService/VitalityService.cs

View workflow job for this annotation

GitHub Actions / build

The name 'AdditionalTypes' does not exist in the current context
additionalMpPercent += first;
additionalMpCap = Math.Max(additionalMpCap, card.SecondData);
break;
}

continue;
}

if ((BCardType.CardType)card.Type != BCardType.CardType.MaxHpmp)
{
continue;
}

var value = BattleStatsProvider.ScaleByLevel(card, character.Level);
switch ((AdditionalTypes.MaxHpmp)card.SubType)

Check failure on line 74 in src/NosCore.GameObject/Services/BattleService/VitalityService.cs

View workflow job for this annotation

GitHub Actions / build

The type or namespace name 'AdditionalTypes' could not be found (are you missing a using directive or an assembly reference?)
{
case AdditionalTypes.MaxHpmp.MaximumHpIncreased: hp += value; break;

Check failure on line 76 in src/NosCore.GameObject/Services/BattleService/VitalityService.cs

View workflow job for this annotation

GitHub Actions / build

The name 'AdditionalTypes' does not exist in the current context
case AdditionalTypes.MaxHpmp.MaximumHpDecreased: hp -= value; break;

Check failure on line 77 in src/NosCore.GameObject/Services/BattleService/VitalityService.cs

View workflow job for this annotation

GitHub Actions / build

The name 'AdditionalTypes' does not exist in the current context
case AdditionalTypes.MaxHpmp.MaximumMpIncreased: mp += value; break;

Check failure on line 78 in src/NosCore.GameObject/Services/BattleService/VitalityService.cs

View workflow job for this annotation

GitHub Actions / build

The name 'AdditionalTypes' does not exist in the current context
case AdditionalTypes.MaxHpmp.MaximumMpDecreased: mp -= value; break;

Check failure on line 79 in src/NosCore.GameObject/Services/BattleService/VitalityService.cs

View workflow job for this annotation

GitHub Actions / build

The name 'AdditionalTypes' does not exist in the current context
case AdditionalTypes.MaxHpmp.IncreasesMaximumHp: hpPercent += value; break;

Check failure on line 80 in src/NosCore.GameObject/Services/BattleService/VitalityService.cs

View workflow job for this annotation

GitHub Actions / build

The name 'AdditionalTypes' does not exist in the current context
case AdditionalTypes.MaxHpmp.DecreasesMaximumHp: hpPercent -= value; break;

Check failure on line 81 in src/NosCore.GameObject/Services/BattleService/VitalityService.cs

View workflow job for this annotation

GitHub Actions / build

The name 'AdditionalTypes' does not exist in the current context
case AdditionalTypes.MaxHpmp.IncreasesMaximumMp: mpPercent += value; break;
case AdditionalTypes.MaxHpmp.DecreasesMaximumMp: mpPercent -= value; break;

// Subtype 51 moves both maxima together.
case AdditionalTypes.MaxHpmp.MaximumHpmpIncreased: hp += value; mp += value; break;
case AdditionalTypes.MaxHpmp.MaximumHpmpDecreased: hp -= value; mp -= value; break;
}
}

hp += hp * hpPercent / 100;
mp += mp * mpPercent / 100;

hp += BoostedAddition(hp - baseHp, hp, additionalHpPercent, additionalHpCap);
mp += BoostedAddition(mp - baseMp, mp, additionalMpPercent, additionalMpCap);

hp = Math.Max(1, hp);
mp = Math.Max(1, mp);

if (player.MaxHp == hp && player.MaxMp == mp)
{
return false;
}

player.MaxHp = hp;
player.MaxMp = mp;

player.Hp = Math.Min(player.Hp, hp);
player.Mp = Math.Min(player.Mp, mp);
return true;
}

public async Task RefreshAndNotifyAsync(ICharacterEntity character)
{
if (!Refresh(character))
{
return;
}

if (character is Ecs.PlayerComponentBundle player)
{
await player.SendPacketAsync(player.GenerateStat()).ConfigureAwait(false);
}
}

public static int BoostedAddition(int additional, int maximum, int percent, int capPercent)
{
if (percent <= 0 || additional <= 0)
{
return 0;
}

var boost = additional * percent / 100;
if (capPercent <= 0)
{
return boost;
}

// Already at or over the ceiling: the effect adds nothing rather than taking away.
var ceiling = maximum * capPercent / 100;
return Math.Min(boost, Math.Max(0, ceiling - additional));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ public sealed class ExperienceProgressionService(
IHeroExperienceService heroExperienceService,
ISpExperienceService spExperienceService,
IFairyExperienceService fairyExperienceService,
ISkillService skillService) : IExperienceProgressionService
ISkillService skillService,
BattleService.IVitalityService vitalityService) : IExperienceProgressionService
{
private const byte MaxLevel = 99;
private const byte MaxJobLevel = 80;
Expand Down Expand Up @@ -196,6 +197,8 @@ public async Task AddExperienceAsync(PlayerComponentBundle player,

if (characterLeveledUp)
{
vitalityService.Refresh(player);

// Full heal on any character / SP / job / hero level-up (trace `stat 256 256 78 78`).
player.Hp = player.MaxHp;
player.Mp = player.MaxMp;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public class MapInstance : IBroadcastable, IDisposable
private readonly IBuffService? _buffService;
private readonly IRegenerationService? _regenerationService;
private readonly IBattleService? _battleService;
private readonly IVitalityService? _vitalityService;
private readonly ConcurrentDictionary<long, (MonsterComponentBundle Monster, Instant RespawnAt)> _pendingRespawns = new();

public MapWorld EcsWorld { get; }
Expand All @@ -72,7 +73,7 @@ public MapInstance(Map.Map map, Guid guid, bool shopAllowed, MapInstanceType typ
IMapItemGenerationService mapItemGenerationService, ILogger<MapInstance> logger, IClock clock, IMapChangeService mapChangeService,
ISessionGroupFactory sessionGroupFactory, ISessionRegistry sessionRegistry, IHeuristic distanceCalculator,
IMonsterAi? monsterAi = null, IBuffService? buffService = null, IRegenerationService? regenerationService = null,
IBattleService? battleService = null)
IBattleService? battleService = null, IVitalityService? vitalityService = null)
{
LastPackets = new ConcurrentQueue<IPacket>();
XpRate = 1;
Expand All @@ -98,6 +99,7 @@ public MapInstance(Map.Map map, Guid guid, bool shopAllowed, MapInstanceType typ
_buffService = buffService;
_regenerationService = regenerationService;
_battleService = battleService;
_vitalityService = vitalityService;
EcsWorld = new MapWorld();
}

Expand Down Expand Up @@ -415,9 +417,17 @@ async Task LifeAsync()
foreach (var npc in Npcs) await _buffService.TickAsync(npc).ConfigureAwait(false);
foreach (var session in _sessionRegistry.GetClientSessionsByMapInstance(MapInstanceId))
{
if (session.HasPlayerEntity)
if (!session.HasPlayerEntity)
{
await _buffService.TickAsync(session.Character).ConfigureAwait(false);
continue;
}

var expired = await _buffService.TickAsync(session.Character).ConfigureAwait(false);

if (expired.Count > 0 && _vitalityService != null)
{
await _vitalityService.RefreshAndNotifyAsync(session.Character)
.ConfigureAwait(false);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ public class MapInstanceGeneratorService(List<MapDto> maps, List<NpcMonsterDto>
NosCore.GameObject.Services.BattleService.IMonsterAi monsterAi,
NosCore.GameObject.Services.BattleService.IBuffService buffService,
NosCore.GameObject.Services.BattleService.IRegenerationService regenerationService,
NosCore.GameObject.Services.BattleService.IBattleService battleService)
NosCore.GameObject.Services.BattleService.IBattleService battleService,
NosCore.GameObject.Services.BattleService.IVitalityService vitalityService)
: IMapInstanceGeneratorService
{
public Task AddMapInstanceAsync(MapInstance mapInstance)
Expand Down Expand Up @@ -129,7 +130,7 @@ public MapInstance CreateMapInstance(Map.Map map, Guid guid, bool shopAllowed, M
{
return new MapInstance(map, guid, shopAllowed, normalInstance, mapItemGenerationService,
loggerFactory.CreateLogger<MapInstance>(), clock,
mapChangeService, sessionGroupFactory, sessionRegistry, distanceCalculator, monsterAi, buffService, regenerationService, battleService);
mapChangeService, sessionGroupFactory, sessionRegistry, distanceCalculator, monsterAi, buffService, regenerationService, battleService, vitalityService);
}

private async Task LoadPortalsAsync(MapInstance mapInstance, List<PortalDto> portals)
Expand Down
13 changes: 13 additions & 0 deletions src/NosCore.PacketHandlers/CharacterScreen/SelectPacketHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public class SelectPacketHandler(IDao<CharacterDto, long> characterDao, ILogger<
IOptions<WorldConfiguration> configuration, ILogLanguageLocalizer<LogLanguageKey> logLanguage,
IPubSubHub pubSubHub, IClock clock,
List<ItemDto> items, IHpService hpService, IMpService mpService, ISpeedService speedService,
NosCore.GameObject.Services.BattleService.IVitalityService vitalityService,
ISessionGroupFactory sessionGroupFactory,
ICharacterInitializationService characterInitializationService, IMessageBus messageBus)
: PacketHandler<SelectPacket>, IWorldPacketHandler
Expand Down Expand Up @@ -207,6 +208,18 @@ await pubSubHub.SubscribeAsync(new Subscriber
#pragma warning restore CS0618
await clientSession.SendPacketAsync(character.GenerateMlobjlst());

vitalityService.Refresh(character);

if (character.Hp > character.MaxHp)
{
character.Hp = character.MaxHp;
}

if (character.Mp > character.MaxMp)
{
character.Mp = character.MaxMp;
}

if (character.Hp > character.MaxHp)
{
character.Hp = character.MaxHp;
Expand Down
5 changes: 4 additions & 1 deletion src/NosCore.PacketHandlers/Inventory/RemovePacketHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@

namespace NosCore.PacketHandlers.Inventory
{
public class RemovePacketHandler : PacketHandler<RemovePacket>, IWorldPacketHandler
public class RemovePacketHandler(NosCore.GameObject.Services.BattleService.IVitalityService vitalityService)
: PacketHandler<RemovePacket>, IWorldPacketHandler
{
public override async Task ExecuteAsync(RemovePacket removePacket, ClientSession clientSession)
{
Expand Down Expand Up @@ -66,6 +67,8 @@ await clientSession.SendPacketAsync(new MsgiPacket
await clientSession.Character.MapInstance.SendPacketAsync(
clientSession.Character.GeneratePairy(null));
}

await vitalityService.RefreshAndNotifyAsync(clientSession.Character).ConfigureAwait(false);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ public async Task SetupAsync()
new Mock<ILogger<WearHandler>>().Object,
TestHelpers.Instance.Clock,
TestHelpers.Instance.LogLanguageLocalizer,
TestHelpers.Instance.WorldConfiguration);
TestHelpers.Instance.WorldConfiguration,
new Mock<NosCore.GameObject.Services.BattleService.IVitalityService>().Object);
}

[TestMethod]
Expand Down
Loading
Loading