Added resolver

This commit is contained in:
Sebastian Bularca
2026-03-30 01:17:25 +02:00
parent e7d5acac7c
commit cfac76ed25
9 changed files with 147 additions and 45 deletions

View File

@@ -3,22 +3,31 @@ using System.Linq;
namespace Nox.Game {
public interface ICharacterAttributesFactory {
EntityAttributes Create(EntityAttributes entityAttributes);
EntityAttributes Create(IEntityDefinition entityDefinition);
}
public sealed class CharacterAttributesFactory : ICharacterAttributesFactory {
private readonly CharacterRegistry characterRegistry;
private readonly IModifierResolver modifierResolver;
public CharacterAttributesFactory(CharacterRegistry characterRegistry) {
this.characterRegistry = characterRegistry;
public CharacterAttributesFactory(IModifierResolver modifierResolver) {
this.modifierResolver = modifierResolver ?? throw new ArgumentNullException(nameof(modifierResolver));
}
public EntityAttributes Create(EntityAttributes entityAttributes) {
if(entityAttributes.attributes.Any(a => a.value <= 0)) {
public EntityAttributes Create(IEntityDefinition entityDefinition) {
var attributes = entityDefinition.Attributes;
if(attributes.attributes.Any(a => a.value <= 0)) {
throw new ArgumentOutOfRangeException( "attributes cannot be zero or negative.", new ArgumentException() );
}
//TODO: Handle attributes modifiers and perks
return entityAttributes;
return new EntityAttributes {
attributes = attributes.attributes
.Select(a => {
var modifiers = modifierResolver.CollectModifiers(entityDefinition, a.attribute);
return new Attribute(a.attribute, modifierResolver.Resolve(a.value, modifiers));
})
.ToArray()
};
}
}
}

View File

@@ -15,7 +15,7 @@ namespace Nox.Game {
public CharacterClass Class { get; set; } = (CharacterClass)GetRandomInt(1, Enum.GetValues(typeof(CharacterClass)).Length-1);
public CharacterRole Role { get; set; } = CharacterRole.Companion;
public EntityAttributes Attributes { get; set; } = GetDefaultAttributes();
public EntityStats Stats { get; set; } = new();
public EntityStats Stats { get; set; } = new() { stats = Array.Empty<Stat>() };
public PerksData Perks { get; set; } = new();
public ModifiersData Modifiers { get; set; } = new();
@@ -36,15 +36,15 @@ namespace Nox.Game {
[Serializable]
public sealed class CustomCharacterCreationRequest : IEntityDefinition {
public Guid Id { get; set; }
public Guid Id { get; set; } = Guid.Empty;
public string Name { get; set; }
public CharacterRace Race { get; set; }
public CharacterClass Class { get; set; }
public CharacterRole Role { get; set; }
public CharacterRole Role { get; set; } = CharacterRole.Protagonist;
public EntityAttributes Attributes { get; set; }
public EntityStats Stats { get; set; }
public PerksData Perks { get; set; }
public ModifiersData Modifiers { get; set; }
public EntityStats Stats { get; set; } = new() { stats = Array.Empty<Stat>() };
public PerksData Perks { get; set; } = new();
public ModifiersData Modifiers { get; set; } = new();
}
public sealed class CharacterFactory : ICharacterFactory {
@@ -69,7 +69,7 @@ namespace Nox.Game {
throw new ArgumentNullException(nameof(request));
}
var attributes = attributesFactory.Create(request.Attributes);
var attributes = attributesFactory.Create(request);
var stats = statsFactory.Create(request);
var character = new CharacterDefinition {
@@ -79,6 +79,7 @@ namespace Nox.Game {
Class = request.Class,
Role = CharacterRole.Protagonist,
Attributes = attributes,
Stats = stats,
Perks = request.Perks ?? new PerksData(),
Modifiers = request.Modifiers ?? new ModifiersData()
};
@@ -101,7 +102,7 @@ namespace Nox.Game {
Race = template.Race,
Class = template.Class,
Role = role,
Attributes = attributesFactory.Create(template.Attributes),
Attributes = attributesFactory.Create(template),
Stats = statsFactory.Create(template),
Perks = template.Perks,
Modifiers = template.Modifiers

View File

@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Nox.Game {
@@ -8,33 +7,34 @@ namespace Nox.Game {
}
public sealed class CharacterStatsFactory : ICharacterStatsFactory {
private readonly CharacterRegistry characterRegistry;
private readonly StarterCharacterSettings baseSettings;
private readonly IModifierResolver modifierResolver;
public CharacterStatsFactory(CharacterRegistry characterRegistry) {
this.characterRegistry = characterRegistry;
public CharacterStatsFactory(StarterCharacterSettings baseSettings, IModifierResolver modifierResolver) {
this.baseSettings = baseSettings;
this.modifierResolver = modifierResolver ?? throw new ArgumentNullException(nameof(modifierResolver));
}
public EntityStats Create(IEntityDefinition entityDefinition) {
var attributes = entityDefinition.Attributes;
var stats = entityDefinition.Stats;
var perks = entityDefinition.Perks;
var modifiers = entityDefinition.Modifiers;
if(attributes.attributes.Any(a => a.value <= 0)) {
throw new ArgumentOutOfRangeException( "attributes cannot be zero or negative.", new ArgumentException() );
}
var healthModifiers = modifiers.modifiers.Where(m => m.StatType == StatType.Health);
var health = stats.GetValue(StatType.Health) + attributes.GetValue(AttributeType.Might) * ResolveModifiersValue(healthModifiers, attributes);
var healthModifiers = modifierResolver.CollectModifiers(entityDefinition, StatType.Health);
var staminaModifiers = modifierResolver.CollectModifiers(entityDefinition, StatType.Stamina);
var levelModifiers = modifierResolver.CollectModifiers(entityDefinition, StatType.Level);
var experienceModifiers = modifierResolver.CollectModifiers(entityDefinition, StatType.Experience);
return new EntityStats {
stats = new[] {
new Stat(StatType.Health, health)
new Stat(StatType.Health, modifierResolver.Resolve(baseSettings.defaultEntityStats.GetValue(StatType.Health), healthModifiers)),
new Stat(StatType.Stamina, modifierResolver.Resolve(baseSettings.defaultEntityStats.GetValue(StatType.Stamina), staminaModifiers)),
new Stat(StatType.Level, modifierResolver.Resolve(baseSettings.defaultEntityStats.GetValue(StatType.Level), levelModifiers)),
new Stat(StatType.Experience, modifierResolver.Resolve(baseSettings.defaultEntityStats.GetValue(StatType.Experience), experienceModifiers))
}
};
}
private int ResolveModifiersValue(IEnumerable<ModifierDefinition> healthModifiers, EntityAttributes attributes) {
throw new NotImplementedException();
}
}
}

View File

@@ -3,13 +3,15 @@ namespace Nox.Game {
public interface ICharacterSystems {
IPerkFactory PerkFactory { get; }
IModifiersFactory ModifiersFactory { get; }
IModifierResolver ModifierResolver { get; }
ICharacterFactory CharacterFactory { get; }
IPartyFactory PartyFactory { get; }
}
public sealed class CharacterSystems : ICharacterSystems {
public CharacterSystems(IPerkFactory perkFactory, IModifiersFactory modifiersFactory, ICharacterFactory characterFactory, IPartyFactory partyFactory) {
public CharacterSystems(IPerkFactory perkFactory, IModifiersFactory modifiersFactory, IModifierResolver modifierResolver, ICharacterFactory characterFactory, IPartyFactory partyFactory) {
ModifiersFactory = modifiersFactory;
ModifierResolver = modifierResolver;
PerkFactory = perkFactory;
CharacterFactory = characterFactory;
PartyFactory = partyFactory;
@@ -17,6 +19,7 @@ namespace Nox.Game {
public IPerkFactory PerkFactory { get; }
public IModifiersFactory ModifiersFactory { get; }
public IModifierResolver ModifierResolver { get; }
public ICharacterFactory CharacterFactory { get; }
public IPartyFactory PartyFactory { get; }
}

View File

@@ -9,12 +9,13 @@ namespace Nox.Game {
ModifiersRegistry modifiersRegistry) {
IPerkFactory perkFactory = new PerkFactory(perksRegistry);
IModifiersFactory modifiersFactory = new ModifiersFactory(modifiersRegistry);
ICharacterAttributesFactory attributesFactory = new CharacterAttributesFactory(characterRegistry);
ICharacterStatsFactory statsFactory = new CharacterStatsFactory(characterRegistry);
IModifierResolver modifierResolver = new ModifierResolver();
ICharacterAttributesFactory attributesFactory = new CharacterAttributesFactory(modifierResolver);
ICharacterStatsFactory statsFactory = new CharacterStatsFactory(starterCharacterSettings, modifierResolver);
ICharacterFactory characterFactory = new CharacterFactory(attributesFactory, statsFactory, perkFactory, modifiersFactory);
IPartyFactory partyFactory = new PartyFactory(starterCharacterSettings);
return new CharacterSystems(perkFactory, modifiersFactory, characterFactory, partyFactory);
return new CharacterSystems(perkFactory, modifiersFactory, modifierResolver, characterFactory, partyFactory);
}
}
}

View File

@@ -100,7 +100,8 @@ namespace Nox.Game {
public Stat[] stats;
public int GetValue(StatType statType) {
return stats.First(stat => stat.stat == statType).value;
var match = stats?.FirstOrDefault(stat => stat.stat == statType);
return match?.value ?? 0;
}
}

View File

@@ -1,11 +0,0 @@
using System;
namespace Nox.Game {
public class ModifiersHandler {
private readonly ModifiersData modifiersData;
public ModifiersHandler(ModifiersData modifiersData) {
this.modifiersData = modifiersData;
}
}
}

View File

@@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Nox.Game {
public interface IModifierResolver {
int Resolve(int baseValue, IEnumerable<IModifier> modifiers);
IEnumerable<IModifier> CollectModifiers(IEntityDefinition entity, StatType statType);
IEnumerable<IModifier> CollectModifiers(IEntityDefinition entity, AttributeType attributeType);
}
/// <summary>
/// Collects modifiers from an entity's direct modifiers and perk-granted modifiers,
/// then resolves them against a base value.
///
/// Resolution order:
/// 1. Flat — sum of all flat values replaces the base
/// 2. Addition — summed and added to the running total
/// 3. Percentage — summed into a single multiplier applied to the post-addition total
/// 4. Multiplication — each factor applied sequentially to the running total
/// </summary>
public sealed class ModifierResolver : IModifierResolver {
public int Resolve(int baseValue, IEnumerable<IModifier> modifiers) {
if(modifiers == null) {
return baseValue;
}
var grouped = modifiers
.Where(m => m != null && m.Operation != ModifierOperation.None)
.GroupBy(m => m.Operation)
.ToDictionary(g => g.Key, g => g.ToList());
float result = baseValue;
// 1. Flat — if any flat modifiers exist, they replace the base entirely
if(grouped.TryGetValue(ModifierOperation.Flat, out var flatMods)) {
result = flatMods.Sum(m => m.Value);
}
// 2. Addition — sum all additive bonuses
if(grouped.TryGetValue(ModifierOperation.Addition, out var addMods)) {
result += addMods.Sum(m => m.Value);
}
// 3. Percentage — summed into one multiplier: result * (1 + totalPercent / 100)
if(grouped.TryGetValue(ModifierOperation.Percentage, out var pctMods)) {
var totalPercent = pctMods.Sum(m => m.Value);
result *= 1f + (totalPercent / 100f);
}
// 4. Multiplication — each factor applied sequentially
if(grouped.TryGetValue(ModifierOperation.Multiplication, out var mulMods)) {
foreach(var mod in mulMods) {
result *= mod.Value;
}
}
return (int)Math.Round(result);
}
public IEnumerable<IModifier> CollectModifiers(IEntityDefinition entity, StatType statType) {
if(entity == null) {
return Enumerable.Empty<IModifier>();
}
var direct = entity.Modifiers?.modifiers?
.Where(m => m != null && m.StatType == statType)
?? Enumerable.Empty<IModifier>();
var fromPerks = entity.Perks?.perks?
.Where(p => p?.Modifiers?.modifiers != null)
.SelectMany(p => p.Modifiers.modifiers)
.Where(m => m != null && m.StatType == statType)
?? Enumerable.Empty<IModifier>();
return direct.Concat(fromPerks);
}
public IEnumerable<IModifier> CollectModifiers(IEntityDefinition entity, AttributeType attributeType) {
if(entity == null) {
return Enumerable.Empty<IModifier>();
}
var direct = entity.Modifiers?.modifiers?
.Where(m => m != null && m.AttributeType == attributeType)
?? Enumerable.Empty<IModifier>();
var fromPerks = entity.Perks?.perks?
.Where(p => p?.Modifiers?.modifiers != null)
.SelectMany(p => p.Modifiers.modifiers)
.Where(m => m != null && m.AttributeType == attributeType)
?? Enumerable.Empty<IModifier>();
return direct.Concat(fromPerks);
}
}
}