added full characte creation support

This commit is contained in:
Sebastian Bularca
2026-04-06 01:05:20 +02:00
parent 419201f2a5
commit 50832c491c
20 changed files with 1037 additions and 265 deletions

View File

@@ -24,7 +24,7 @@ namespace Nox.Game {
attributes = attributes.attributes.AsValueEnumerable()
.Select(a => {
var modifiers = modifierResolver.CollectModifiers(entityDefinition, a.attribute);
return new Attribute(a.attribute, modifierResolver.Resolve(a.value, modifiers));
return new Attribute(a.attribute, modifierResolver.Resolve(a.value, modifiers, entityDefinition));
})
.ToArray()
};

View File

@@ -11,6 +11,7 @@ namespace Nox.Game {
public sealed class CharacterTemplate : IEntityDefinition {
public Guid Id { get; set; } = Guid.NewGuid();
public string Name { get; set; } = "New Character";
public int PortraitIndex { get; set; }
public CharacterRace Race { get; set; } = (CharacterRace)GetRandomInt(1, Enum.GetValues(typeof(CharacterRace)).Length-1);
public CharacterClass Class { get; set; } = (CharacterClass)GetRandomInt(1, Enum.GetValues(typeof(CharacterClass)).Length-1);
public CharacterRole Role { get; set; } = CharacterRole.Companion;
@@ -38,6 +39,7 @@ namespace Nox.Game {
public sealed class CharacterCreationRequest : IEntityDefinition {
public Guid Id { get; set; } = Guid.Empty;
public string Name { get; set; }
public int PortraitIndex { get; set; }
public CharacterRace Race { get; set; }
public CharacterClass Class { get; set; }
public CharacterRole Role { get; set; } = CharacterRole.Protagonist;
@@ -78,6 +80,7 @@ namespace Nox.Game {
Race = request.Race,
Class = request.Class,
Role = CharacterRole.Protagonist,
PortraitIndex = request.PortraitIndex,
Attributes = attributes,
Stats = stats,
Perks = request.Perks ?? new PerksData(),

View File

@@ -23,16 +23,16 @@ namespace Nox.Game {
}
var healthModifiers = modifierResolver.CollectModifiers(entityDefinition, StatType.Health);
var staminaModifiers = modifierResolver.CollectModifiers(entityDefinition, StatType.Stamina);
var staminaModifiers = modifierResolver.CollectModifiers(entityDefinition, StatType.Mana);
var levelModifiers = modifierResolver.CollectModifiers(entityDefinition, StatType.Level);
var experienceModifiers = modifierResolver.CollectModifiers(entityDefinition, StatType.Experience);
return new EntityStats {
stats = new[] {
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))
new Stat(StatType.Health, modifierResolver.Resolve(baseSettings.defaultEntityStats.GetValue(StatType.Health), healthModifiers, entityDefinition)),
new Stat(StatType.Mana, modifierResolver.Resolve(baseSettings.defaultEntityStats.GetValue(StatType.Mana), staminaModifiers, entityDefinition)),
new Stat(StatType.Level, modifierResolver.Resolve(baseSettings.defaultEntityStats.GetValue(StatType.Level), levelModifiers, entityDefinition)),
new Stat(StatType.Experience, modifierResolver.Resolve(baseSettings.defaultEntityStats.GetValue(StatType.Experience), experienceModifiers, entityDefinition))
}
};
}

View File

@@ -9,6 +9,7 @@ namespace Nox.Game {
public interface IEntityDefinition {
Guid Id { get; }
string Name { get; }
int PortraitIndex { get; }
CharacterRace Race { get; }
CharacterClass Class { get; }
CharacterRole Role { get; }
@@ -22,7 +23,7 @@ namespace Nox.Game {
public enum StatType {
None,
Health,
Stamina,
Mana,
Level,
Experience
}
@@ -129,6 +130,7 @@ namespace Nox.Game {
[Serializable]
public class CharacterDefinition : IEntityDefinition {
public Guid Id { get; set; }
public int PortraitIndex { get; set; } = 0;
[field: SerializeField] public string Name { get; set; }
[field: SerializeField] public CharacterRace Race { get; set; }
[field: SerializeField] public CharacterClass Class { get; set; }
@@ -156,17 +158,18 @@ namespace Nox.Game {
Id = p.Id,
Name = p.Name,
Modifiers = p.Modifiers
}).ToList() ?? new()
}).ToList() ?? new List<PerkDefinition>()
},
Modifiers = new ModifiersData {
modifiers = Modifiers?.modifiers?.AsValueEnumerable().Select(m => new ModifierDefinition {
Id = m.Id,
Name = m.Name,
StatType = m.StatType,
AttributeType = m.AttributeType,
Target = m.Target,
ScalingSource = m.ScalingSource,
Operation = m.Operation,
Value = m.Value
}).ToList() ?? new()
Value = m.Value,
Requirements = m.Requirements?.ToList() ?? new List<ModifierRequirement>()
}).ToList() ?? new List<ModifierDefinition>()
}
};
}

View File

@@ -9,10 +9,11 @@ namespace Nox.Game {
public interface IModifier {
string Name { get; }
Guid Id { get; }
StatType StatType { get; }
AttributeType AttributeType { get; }
ModifierTarget Target { get; }
ModifierTarget ScalingSource { get; }
ModifierOperation Operation { get; }
float Value { get; }
IReadOnlyList<ModifierRequirement> Requirements { get; }
}
public interface IModifiersFactory {
@@ -30,20 +31,73 @@ namespace Nox.Game {
Percentage
}
public enum ModifierTargetType {
None,
Attribute,
Stat,
CombatScore
}
[Serializable]
public sealed class ModifierTarget {
[field: SerializeField] public ModifierTargetType Type { get; set; }
[field: SerializeField] public AttributeType AttributeType { get; set; }
[field: SerializeField] public StatType StatType { get; set; }
[field: SerializeField] public CombatScoreType CombatScoreType { get; set; }
public bool Matches(StatType statType) {
return Type == ModifierTargetType.Stat && StatType == statType;
}
public bool Matches(AttributeType attributeType) {
return Type == ModifierTargetType.Attribute && AttributeType == attributeType;
}
public bool Matches(CombatScoreType combatScoreType) {
return Type == ModifierTargetType.CombatScore && CombatScoreType == combatScoreType;
}
public bool IsSet => Type != ModifierTargetType.None;
public override string ToString() {
return Type switch {
ModifierTargetType.Attribute => AttributeType.ToString(),
ModifierTargetType.Stat => StatType.ToString(),
ModifierTargetType.CombatScore => CombatScoreType.ToString(),
_ => "None"
};
}
}
[Serializable]
public sealed class ModifierRequirement {
[field: SerializeField] public AttributeType Attribute { get; set; }
[field: SerializeField] public int MinimumValue { get; set; }
public bool IsMet(EntityAttributes attributes) {
if(Attribute == AttributeType.None || attributes?.attributes == null) {
return true;
}
return attributes.GetValue(Attribute) >= MinimumValue;
}
}
[Serializable]
public sealed class ModifierDefinition : IModifier {
[field: SerializeField] public string Name { get; set; }
public Guid Id { get; set; } = Guid.NewGuid();
[field: SerializeField] public StatType StatType { get; set; }
[field: SerializeField] public AttributeType AttributeType { get; set; }
[field: SerializeField] public ModifierTarget Target { get; set; }
[field: SerializeField] public ModifierTarget ScalingSource { get; set; }
[field: SerializeField] public ModifierOperation Operation { get; set; }
[field: SerializeField] public CombatScoreType CombatScoreType { get; set; }
[field: SerializeField] public float Value { get; set; }
[field: SerializeField] public List<ModifierRequirement> Requirements { get; set; } = new();
IReadOnlyList<ModifierRequirement> IModifier.Requirements => Requirements;
}
[Serializable]
public sealed class ModifiersData {
public List<ModifierDefinition> modifiers = new ();
public List<ModifierDefinition> modifiers = new();
public override string ToString() {
return $"Modifiers: {string.Join(", ", modifiers.Select(modifier => $"{modifier.Name}"))}";
@@ -52,7 +106,7 @@ namespace Nox.Game {
public class ModifiersFactory : IModifiersFactory {
private readonly ModifiersRegistry modifiersRegistry;
private readonly Dictionary<Guid, IModifier> modifierPool = new ();
private readonly Dictionary<Guid, IModifier> modifierPool = new();
public ModifiersFactory(ModifiersRegistry modifiersRegistry) {
this.modifiersRegistry = modifiersRegistry;
@@ -92,10 +146,11 @@ namespace Nox.Game {
character.Modifiers.modifiers.Add(new ModifierDefinition {
Id = modifier.Id,
Name = modifier.Name,
StatType = modifier.StatType,
AttributeType = modifier.AttributeType,
Target = modifier.Target,
ScalingSource = modifier.ScalingSource,
Operation = modifier.Operation,
Value = modifier.Value
Value = modifier.Value,
Requirements = modifier.Requirements?.ToList() ?? new List<ModifierRequirement>()
});
return true;

View File

@@ -5,9 +5,10 @@ using ZLinq;
namespace Nox.Game {
public interface IModifierResolver {
int Resolve(int baseValue, IEnumerable<IModifier> modifiers);
int Resolve(int baseValue, IEnumerable<IModifier> modifiers, IEntityDefinition entity = null);
IEnumerable<IModifier> CollectModifiers(IEntityDefinition entity, StatType statType);
IEnumerable<IModifier> CollectModifiers(IEntityDefinition entity, AttributeType attributeType);
IEnumerable<IModifier> CollectModifiers(IEntityDefinition entity, CombatScoreType combatScoreType);
}
/// <summary>
@@ -19,9 +20,14 @@ namespace Nox.Game {
/// 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
///
/// If a modifier has a ScalingSource set, its Value is multiplied by the entity's
/// current value for that source before being applied. For example, a modifier with
/// Target=Health, ScalingSource=Might, Operation=Addition, Value=2 means "+2 Health
/// per point of Might".
/// </summary>
public sealed class ModifierResolver : IModifierResolver {
public int Resolve(int baseValue, IEnumerable<IModifier> modifiers) {
public int Resolve(int baseValue, IEnumerable<IModifier> modifiers, IEntityDefinition entity = null) {
if(modifiers == null) {
return baseValue;
}
@@ -37,19 +43,21 @@ namespace Nox.Game {
continue;
}
var effectiveValue = ResolveScaling(m, entity);
switch(m.Operation) {
case ModifierOperation.Flat:
flatSum += m.Value;
flatSum += effectiveValue;
hasFlat = true;
break;
case ModifierOperation.Addition:
addSum += m.Value;
addSum += effectiveValue;
break;
case ModifierOperation.Percentage:
pctSum += m.Value;
pctSum += effectiveValue;
break;
case ModifierOperation.Multiplication:
mulValues.Add(m.Value);
mulValues.Add(effectiveValue);
break;
}
}
@@ -65,34 +73,31 @@ namespace Nox.Game {
return (int)Math.Round(result);
}
private static float ResolveScaling(IModifier modifier, IEntityDefinition entity) {
var value = modifier.Value;
if(entity == null || modifier.ScalingSource == null || !modifier.ScalingSource.IsSet) {
return value;
}
var source = modifier.ScalingSource;
var sourceValue = source.Type switch {
ModifierTargetType.Attribute when entity.Attributes?.attributes != null =>
entity.Attributes.GetValue(source.AttributeType),
ModifierTargetType.Stat when entity.Stats?.stats != null =>
entity.Stats.GetValue(source.StatType),
_ => 0
};
return value * sourceValue;
}
public IEnumerable<IModifier> CollectModifiers(IEntityDefinition entity, StatType statType) {
if(entity == null) {
return Array.Empty<IModifier>();
}
var result = new List<IModifier>();
if(entity.Modifiers?.modifiers != null) {
foreach(var m in entity.Modifiers.modifiers) {
if(m != null && m.StatType == statType) {
result.Add(m);
}
}
}
if(entity.Perks?.perks != null) {
foreach(var p in entity.Perks.perks) {
if(p?.Modifiers?.modifiers == null) {
continue;
}
foreach(var m in p.Modifiers.modifiers) {
if(m != null && m.StatType == statType) {
result.Add(m);
}
}
}
}
CollectFromEntity(entity, result, m => m.Target != null && m.Target.Matches(statType));
return result;
}
@@ -102,10 +107,37 @@ namespace Nox.Game {
}
var result = new List<IModifier>();
CollectFromEntity(entity, result, m => m.Target != null && m.Target.Matches(attributeType));
return result;
}
public IEnumerable<IModifier> CollectModifiers(IEntityDefinition entity, CombatScoreType combatScoreType) {
if(entity == null) {
return Array.Empty<IModifier>();
}
var result = new List<IModifier>();
CollectFromEntity(entity, result, m => m.Target != null && m.Target.Matches(combatScoreType));
return result;
}
private static bool MeetsRequirements(IModifier modifier, IEntityDefinition entity) {
var requirements = modifier.Requirements;
if(requirements == null || requirements.Count == 0) {
return true;
}
for(int i = 0; i < requirements.Count; i++) {
if(!requirements[i].IsMet(entity.Attributes)) {
return false;
}
}
return true;
}
private static void CollectFromEntity(IEntityDefinition entity, List<IModifier> result, Func<IModifier, bool> predicate) {
if(entity.Modifiers?.modifiers != null) {
foreach(var m in entity.Modifiers.modifiers) {
if(m != null && m.AttributeType == attributeType) {
if(m != null && predicate(m) && MeetsRequirements(m, entity)) {
result.Add(m);
}
}
@@ -117,14 +149,12 @@ namespace Nox.Game {
continue;
}
foreach(var m in p.Modifiers.modifiers) {
if(m != null && m.AttributeType == attributeType) {
if(m != null && predicate(m)) {
result.Add(m);
}
}
}
}
return result;
}
}
}

View File

@@ -22,8 +22,7 @@ namespace Nox.Game {
throw new System.ArgumentException("Too many characters requested.");
}
var protagonist = characterFactory.CreateProtagonist(characterCreationRequests.Find(r => r.Role == CharacterRole.Protagonist));
var companions = characterCreationRequests.FindAll(r => r.Role != CharacterRole.Protagonist).Select(r => characterFactory.CreateProtagonist(r));
return partyFactory.Create(protagonist, companions);
return partyFactory.Create(protagonist);
}
}
}

View File

@@ -13,24 +13,13 @@ namespace Nox.Game {
public ModifiersData defaultModifiersData;
[Header("General Racial Bonuses and Perks per Class")]
public CharacterRace race;
public CharacterClass @class;
public RacialBonuses [] racialBonuses;
public ClassBonuses [] classBonuses;
private void OnEnable() {
race = (CharacterRace)Random.Range(0, Enum.GetNames(typeof(CharacterRace)).Length-1);
@class = (CharacterClass)Random.Range(0, Enum.GetNames(typeof(CharacterClass)).Length-1);
}
}
[Serializable]
public sealed class RacialBonuses {
public CharacterRace race;
public EntityAttributes bonusAttributes;
public EntityStats bonusStats;
public PerksData startingPerks;
public ModifiersData permanentModifiers;
}
@@ -38,8 +27,6 @@ namespace Nox.Game {
[Serializable]
public sealed class ClassBonuses {
public CharacterClass @class;
public EntityAttributes bonusAttributes;
public EntityStats bonusStats;
public PerksData startingPerks;
public ModifiersData permanentModifiers;
}