diff --git a/Assets/Code/GameState/Entities/CharacterAttributesFactory.cs b/Assets/Code/GameState/Entities/CharacterAttributesFactory.cs index d5ae9da..49b6786 100644 --- a/Assets/Code/GameState/Entities/CharacterAttributesFactory.cs +++ b/Assets/Code/GameState/Entities/CharacterAttributesFactory.cs @@ -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() + }; } } } diff --git a/Assets/Code/GameState/Entities/CharacterFactory.cs b/Assets/Code/GameState/Entities/CharacterFactory.cs index bdc9185..4f50ad6 100644 --- a/Assets/Code/GameState/Entities/CharacterFactory.cs +++ b/Assets/Code/GameState/Entities/CharacterFactory.cs @@ -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() }; 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() }; + 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 diff --git a/Assets/Code/GameState/Entities/CharacterStatsFactory.cs b/Assets/Code/GameState/Entities/CharacterStatsFactory.cs index d161043..0136bfa 100644 --- a/Assets/Code/GameState/Entities/CharacterStatsFactory.cs +++ b/Assets/Code/GameState/Entities/CharacterStatsFactory.cs @@ -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 healthModifiers, EntityAttributes attributes) { - throw new NotImplementedException(); - } } } diff --git a/Assets/Code/GameState/Entities/CharacterSystems.cs b/Assets/Code/GameState/Entities/CharacterSystems.cs index 0b35209..5427442 100644 --- a/Assets/Code/GameState/Entities/CharacterSystems.cs +++ b/Assets/Code/GameState/Entities/CharacterSystems.cs @@ -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; } } diff --git a/Assets/Code/GameState/Entities/DefaultCharacterSystemsFactory.cs b/Assets/Code/GameState/Entities/DefaultCharacterSystemsFactory.cs index 68235e7..c94ca98 100644 --- a/Assets/Code/GameState/Entities/DefaultCharacterSystemsFactory.cs +++ b/Assets/Code/GameState/Entities/DefaultCharacterSystemsFactory.cs @@ -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); } } } diff --git a/Assets/Code/GameState/Entities/EntitiesDefinitions.cs b/Assets/Code/GameState/Entities/EntitiesDefinitions.cs index f135a54..348366c 100644 --- a/Assets/Code/GameState/Entities/EntitiesDefinitions.cs +++ b/Assets/Code/GameState/Entities/EntitiesDefinitions.cs @@ -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; } } diff --git a/Assets/Code/GameState/Entities/ModifiersHandler.cs b/Assets/Code/GameState/Entities/ModifiersHandler.cs deleted file mode 100644 index 8f96853..0000000 --- a/Assets/Code/GameState/Entities/ModifiersHandler.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Nox.Game { - - public class ModifiersHandler { - private readonly ModifiersData modifiersData; - public ModifiersHandler(ModifiersData modifiersData) { - this.modifiersData = modifiersData; - } - } -} diff --git a/Assets/Code/GameState/Entities/ModifiersResolver.cs b/Assets/Code/GameState/Entities/ModifiersResolver.cs new file mode 100644 index 0000000..c6a2e9e --- /dev/null +++ b/Assets/Code/GameState/Entities/ModifiersResolver.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Nox.Game { + + public interface IModifierResolver { + int Resolve(int baseValue, IEnumerable modifiers); + IEnumerable CollectModifiers(IEntityDefinition entity, StatType statType); + IEnumerable CollectModifiers(IEntityDefinition entity, AttributeType attributeType); + } + + /// + /// 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 + /// + public sealed class ModifierResolver : IModifierResolver { + public int Resolve(int baseValue, IEnumerable 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 CollectModifiers(IEntityDefinition entity, StatType statType) { + if(entity == null) { + return Enumerable.Empty(); + } + + var direct = entity.Modifiers?.modifiers? + .Where(m => m != null && m.StatType == statType) + ?? Enumerable.Empty(); + + 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(); + + return direct.Concat(fromPerks); + } + + public IEnumerable CollectModifiers(IEntityDefinition entity, AttributeType attributeType) { + if(entity == null) { + return Enumerable.Empty(); + } + + var direct = entity.Modifiers?.modifiers? + .Where(m => m != null && m.AttributeType == attributeType) + ?? Enumerable.Empty(); + + 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(); + + return direct.Concat(fromPerks); + } + } +} diff --git a/Assets/Code/GameState/Entities/ModifiersHandler.cs.meta b/Assets/Code/GameState/Entities/ModifiersResolver.cs.meta similarity index 100% rename from Assets/Code/GameState/Entities/ModifiersHandler.cs.meta rename to Assets/Code/GameState/Entities/ModifiersResolver.cs.meta