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 { namespace Nox.Game {
public interface ICharacterAttributesFactory { public interface ICharacterAttributesFactory {
EntityAttributes Create(EntityAttributes entityAttributes); EntityAttributes Create(IEntityDefinition entityDefinition);
} }
public sealed class CharacterAttributesFactory : ICharacterAttributesFactory { public sealed class CharacterAttributesFactory : ICharacterAttributesFactory {
private readonly CharacterRegistry characterRegistry; private readonly IModifierResolver modifierResolver;
public CharacterAttributesFactory(CharacterRegistry characterRegistry) { public CharacterAttributesFactory(IModifierResolver modifierResolver) {
this.characterRegistry = characterRegistry; this.modifierResolver = modifierResolver ?? throw new ArgumentNullException(nameof(modifierResolver));
} }
public EntityAttributes Create(EntityAttributes entityAttributes) { public EntityAttributes Create(IEntityDefinition entityDefinition) {
if(entityAttributes.attributes.Any(a => a.value <= 0)) { var attributes = entityDefinition.Attributes;
if(attributes.attributes.Any(a => a.value <= 0)) {
throw new ArgumentOutOfRangeException( "attributes cannot be zero or negative.", new ArgumentException() ); 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 CharacterClass Class { get; set; } = (CharacterClass)GetRandomInt(1, Enum.GetValues(typeof(CharacterClass)).Length-1);
public CharacterRole Role { get; set; } = CharacterRole.Companion; public CharacterRole Role { get; set; } = CharacterRole.Companion;
public EntityAttributes Attributes { get; set; } = GetDefaultAttributes(); 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 PerksData Perks { get; set; } = new();
public ModifiersData Modifiers { get; set; } = new(); public ModifiersData Modifiers { get; set; } = new();
@@ -36,15 +36,15 @@ namespace Nox.Game {
[Serializable] [Serializable]
public sealed class CustomCharacterCreationRequest : IEntityDefinition { public sealed class CustomCharacterCreationRequest : IEntityDefinition {
public Guid Id { get; set; } public Guid Id { get; set; } = Guid.Empty;
public string Name { get; set; } public string Name { get; set; }
public CharacterRace Race { get; set; } public CharacterRace Race { get; set; }
public CharacterClass Class { 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 EntityAttributes Attributes { get; set; }
public EntityStats Stats { get; set; } public EntityStats Stats { get; set; } = new() { stats = Array.Empty<Stat>() };
public PerksData Perks { get; set; } public PerksData Perks { get; set; } = new();
public ModifiersData Modifiers { get; set; } public ModifiersData Modifiers { get; set; } = new();
} }
public sealed class CharacterFactory : ICharacterFactory { public sealed class CharacterFactory : ICharacterFactory {
@@ -69,7 +69,7 @@ namespace Nox.Game {
throw new ArgumentNullException(nameof(request)); throw new ArgumentNullException(nameof(request));
} }
var attributes = attributesFactory.Create(request.Attributes); var attributes = attributesFactory.Create(request);
var stats = statsFactory.Create(request); var stats = statsFactory.Create(request);
var character = new CharacterDefinition { var character = new CharacterDefinition {
@@ -79,6 +79,7 @@ namespace Nox.Game {
Class = request.Class, Class = request.Class,
Role = CharacterRole.Protagonist, Role = CharacterRole.Protagonist,
Attributes = attributes, Attributes = attributes,
Stats = stats,
Perks = request.Perks ?? new PerksData(), Perks = request.Perks ?? new PerksData(),
Modifiers = request.Modifiers ?? new ModifiersData() Modifiers = request.Modifiers ?? new ModifiersData()
}; };
@@ -101,7 +102,7 @@ namespace Nox.Game {
Race = template.Race, Race = template.Race,
Class = template.Class, Class = template.Class,
Role = role, Role = role,
Attributes = attributesFactory.Create(template.Attributes), Attributes = attributesFactory.Create(template),
Stats = statsFactory.Create(template), Stats = statsFactory.Create(template),
Perks = template.Perks, Perks = template.Perks,
Modifiers = template.Modifiers Modifiers = template.Modifiers

View File

@@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
namespace Nox.Game { namespace Nox.Game {
@@ -8,33 +7,34 @@ namespace Nox.Game {
} }
public sealed class CharacterStatsFactory : ICharacterStatsFactory { public sealed class CharacterStatsFactory : ICharacterStatsFactory {
private readonly CharacterRegistry characterRegistry; private readonly StarterCharacterSettings baseSettings;
private readonly IModifierResolver modifierResolver;
public CharacterStatsFactory(CharacterRegistry characterRegistry) { public CharacterStatsFactory(StarterCharacterSettings baseSettings, IModifierResolver modifierResolver) {
this.characterRegistry = characterRegistry; this.baseSettings = baseSettings;
this.modifierResolver = modifierResolver ?? throw new ArgumentNullException(nameof(modifierResolver));
} }
public EntityStats Create(IEntityDefinition entityDefinition) { public EntityStats Create(IEntityDefinition entityDefinition) {
var attributes = entityDefinition.Attributes; var attributes = entityDefinition.Attributes;
var stats = entityDefinition.Stats;
var perks = entityDefinition.Perks;
var modifiers = entityDefinition.Modifiers;
if(attributes.attributes.Any(a => a.value <= 0)) { if(attributes.attributes.Any(a => a.value <= 0)) {
throw new ArgumentOutOfRangeException( "attributes cannot be zero or negative.", new ArgumentException() ); throw new ArgumentOutOfRangeException( "attributes cannot be zero or negative.", new ArgumentException() );
} }
var healthModifiers = modifiers.modifiers.Where(m => m.StatType == StatType.Health); var healthModifiers = modifierResolver.CollectModifiers(entityDefinition, StatType.Health);
var health = stats.GetValue(StatType.Health) + attributes.GetValue(AttributeType.Might) * ResolveModifiersValue(healthModifiers, attributes); var staminaModifiers = modifierResolver.CollectModifiers(entityDefinition, StatType.Stamina);
var levelModifiers = modifierResolver.CollectModifiers(entityDefinition, StatType.Level);
var experienceModifiers = modifierResolver.CollectModifiers(entityDefinition, StatType.Experience);
return new EntityStats { return new EntityStats {
stats = new[] { 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 { public interface ICharacterSystems {
IPerkFactory PerkFactory { get; } IPerkFactory PerkFactory { get; }
IModifiersFactory ModifiersFactory { get; } IModifiersFactory ModifiersFactory { get; }
IModifierResolver ModifierResolver { get; }
ICharacterFactory CharacterFactory { get; } ICharacterFactory CharacterFactory { get; }
IPartyFactory PartyFactory { get; } IPartyFactory PartyFactory { get; }
} }
public sealed class CharacterSystems : ICharacterSystems { 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; ModifiersFactory = modifiersFactory;
ModifierResolver = modifierResolver;
PerkFactory = perkFactory; PerkFactory = perkFactory;
CharacterFactory = characterFactory; CharacterFactory = characterFactory;
PartyFactory = partyFactory; PartyFactory = partyFactory;
@@ -17,6 +19,7 @@ namespace Nox.Game {
public IPerkFactory PerkFactory { get; } public IPerkFactory PerkFactory { get; }
public IModifiersFactory ModifiersFactory { get; } public IModifiersFactory ModifiersFactory { get; }
public IModifierResolver ModifierResolver { get; }
public ICharacterFactory CharacterFactory { get; } public ICharacterFactory CharacterFactory { get; }
public IPartyFactory PartyFactory { get; } public IPartyFactory PartyFactory { get; }
} }

View File

@@ -9,12 +9,13 @@ namespace Nox.Game {
ModifiersRegistry modifiersRegistry) { ModifiersRegistry modifiersRegistry) {
IPerkFactory perkFactory = new PerkFactory(perksRegistry); IPerkFactory perkFactory = new PerkFactory(perksRegistry);
IModifiersFactory modifiersFactory = new ModifiersFactory(modifiersRegistry); IModifiersFactory modifiersFactory = new ModifiersFactory(modifiersRegistry);
ICharacterAttributesFactory attributesFactory = new CharacterAttributesFactory(characterRegistry); IModifierResolver modifierResolver = new ModifierResolver();
ICharacterStatsFactory statsFactory = new CharacterStatsFactory(characterRegistry); ICharacterAttributesFactory attributesFactory = new CharacterAttributesFactory(modifierResolver);
ICharacterStatsFactory statsFactory = new CharacterStatsFactory(starterCharacterSettings, modifierResolver);
ICharacterFactory characterFactory = new CharacterFactory(attributesFactory, statsFactory, perkFactory, modifiersFactory); ICharacterFactory characterFactory = new CharacterFactory(attributesFactory, statsFactory, perkFactory, modifiersFactory);
IPartyFactory partyFactory = new PartyFactory(starterCharacterSettings); 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 Stat[] stats;
public int GetValue(StatType statType) { 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);
}
}
}