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

@@ -81,10 +81,10 @@ namespace Nox.Core {
var characterRegistry = Addressables.LoadAssetAsync<CharacterRegistry>("CharacterRegistry").WaitForCompletion();
var modifiersRegistry = Addressables.LoadAssetAsync<ModifiersRegistry>("ModifiersRegistry").WaitForCompletion();
var partySettings = Addressables.LoadAssetAsync<PartySettings>("DefaultPartySettings").WaitForCompletion();
var portraitsHolder = Addressables.LoadAssetAsync<PortraitsHolder>("PortraitsHolder").WaitForCompletion();
var portraitsHolder = Addressables.LoadAssetAsync<PortraitsHolder>(assetHandle.Result.portraitsHolder).WaitForCompletion();
var characterSystems = CharacterSystemsFactory.Create(partySettings, characterBaseSettings, perKRegistry, characterRegistry, modifiersRegistry);
mainMenuView = new MainMenuView(assetHandle.Result, menuGameStateData, saveSystem, gameDataState, partySettings, characterSystems, portraitsHolder);
mainMenuView = new MainMenuView(assetHandle.Result, menuGameStateData, saveSystem, gameDataState, partySettings, characterSystems, portraitsHolder, characterBaseSettings);
mainMenuView.Initialize();
mainMenuView.Show();
IsGameStateInitialized = true;

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;
}

View File

@@ -1,16 +1,12 @@
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class AttributeReference : MonoBehaviour
{
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
}
// Update is called once per frame
void Update()
{
namespace Nox.UI {
public class AttributeReference : MonoBehaviour {
public Button removePointsButton;
public Button addPointsButton;
public TextMeshProUGUI attributeName;
public TextMeshProUGUI attributeValue;
}
}

View File

@@ -1,37 +1,69 @@
using Jovian.InGameLogging;
using Jovian.InGameLogging.UI;
using Jovian.Logger;
using Jovian.SaveSystem;
using Nox.Core;
using Nox.Game;
using Nox.Game.UI;
using System;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using ZLinq;
using Attribute = Nox.Game.Attribute;
using PlayMode = Nox.Core.PlayMode;
namespace Nox.UI {
public class CharacterCreationView : IGameLifecycle, IMenuView {
public ISaveSystem SaveSystem { get; }
private readonly CharacterCreationReference characterCreationReference;
private readonly MenuGameStateData menuGameStateData;
private readonly GameDataState gameDataState;
private readonly PartySettings partySettings;
private readonly ICharacterSystems characterSystems;
private readonly PortraitsHolder portraitsHolder;
private readonly StarterCharacterSettings starterCharacterSettings;
private List<CharacterCreationRequest> characterCreationRequests;
private Action canStartCheck;
// Logger
private GameLogView gameLogView;
private InGameLogger inGameLogger;
// Working state
private CharacterRace selectedRace;
private CharacterClass selectedClass;
private int currentPortraitIndex;
private int remainingPoints;
private readonly int[] allocatedPoints = new int[4]; // Might, Reflex, Knowledge, Perception (AttributeType 1-4)
private int previousHealth;
private int previousStamina;
// Modifier source tracking
private PerksData racialPerks = new();
private ModifiersData racialModifiers = new();
private PerksData classPerks = new();
private ModifiersData classModifiers = new();
private readonly List<PerkDefinition> playerPerks = new();
private List<IPerk> availablePerks = new();
// Computed state
private EntityAttributes workingAttributes;
private EntityStats workingStats;
// Output
private List<CharacterCreationRequest> characterCreationRequests;
private Action canStartCheck;
// Back confirmation (null until popup is implemented)
private Action confirmBackAction;
public CharacterCreationView(CharacterCreationReference characterCreationReference,
MenuGameStateData menuGameStateData,
ISaveSystem saveSystem,
GameDataState gameDataState,
PartySettings partySettings,
ICharacterSystems characterSystems,
PortraitsHolder portraitsHolder) {
PortraitsHolder portraitsHolder,
StarterCharacterSettings starterCharacterSettings) {
SaveSystem = saveSystem;
this.characterCreationReference = characterCreationReference;
this.menuGameStateData = menuGameStateData;
@@ -39,77 +71,529 @@ namespace Nox.UI {
this.partySettings = partySettings;
this.characterSystems = characterSystems;
this.portraitsHolder = portraitsHolder;
this.starterCharacterSettings = starterCharacterSettings;
}
public void Initialize() {
// Logger
var store = new GameLogStore(500);
gameLogView = characterCreationReference.gameLogView;
gameLogView.Initialize(store);
inGameLogger = new InGameLogger(store, LogChannel.CharacterCreation);
inGameLogger.Enable();
// Start Game button
canStartCheck = () => {
var canStart = characterCreationRequests is { Count: > 0 };
characterCreationReference.startGameButton.interactable = canStart;
};
characterCreationReference.startGameButton.interactable = false;
characterCreationReference.startGameButton.onClick.AddListener(() => {
Hide();
menuGameStateData.startGameRequests?.Invoke(PlayMode.Adventure);
});
characterCreationReference.backButton.onClick.AddListener(Hide);
characterCreationReference.backButtonCenter.onClick.AddListener(Hide);
characterCreationReference.acceptButton.onClick.AddListener(() => {
if(characterCreationRequests == null || characterCreationRequests.Count == 0) {
GlobalLogger.LogWarning("No characters selected. Creating party from the test party definition sets", LogCategory.GameLogic);
var randomIndex = UnityEngine.Random.Range(0, partySettings.testPartyDefinitionSets.Count - 1);
var protagonist = partySettings.testPartyDefinitionSets[randomIndex].partyDefinition.Protagonist;
characterCreationRequests = new List<CharacterCreationRequest> {
new() {
Id = Guid.NewGuid(),
Name = protagonist.Name,
Race = protagonist.Race,
Class = protagonist.Class,
Role = CharacterRole.Protagonist,
Attributes = protagonist.Attributes,
Stats = protagonist.Stats,
Perks = protagonist.Perks,
Modifiers = protagonist.Modifiers
}
};
}
CreateParty();
canStartCheck.Invoke();
});
// Back buttons with popup check
characterCreationReference.backButton.onClick.AddListener(OnBackClicked);
characterCreationReference.backButtonCenter.onClick.AddListener(OnBackClicked);
// Accept button
characterCreationReference.acceptButton.onClick.AddListener(OnAcceptClicked);
// Race dropdown
PopulateEnumDropdown<CharacterRace>(characterCreationReference.raceDropdown);
characterCreationReference.raceDropdown.onValueChanged.AddListener(OnRaceChanged);
// Class dropdown
PopulateEnumDropdown<CharacterClass>(characterCreationReference.classDropdown);
characterCreationReference.classDropdown.onValueChanged.AddListener(OnClassChanged);
// Perks dropdown
PopulatePerksDropdown();
characterCreationReference.perksDropdown.onValueChanged.AddListener(OnPerkSelected);
// Attribute +/- buttons
var attrTypes = new[] { AttributeType.Might, AttributeType.Reflex, AttributeType.Knowledge, AttributeType.Perception };
var attrRefs = characterCreationReference.attributeReference;
for(int i = 0; i < attrRefs.Length && i < attrTypes.Length; i++) {
var type = attrTypes[i];
attrRefs[i].attributeName.text = type.ToString();
attrRefs[i].addPointsButton.onClick.AddListener(() => OnAttributeAdd(type));
attrRefs[i].removePointsButton.onClick.AddListener(() => OnAttributeRemove(type));
}
// Portrait navigation
currentPortraitIndex = 0;
if(portraitsHolder != null && portraitsHolder.portraits.Length > 0) {
characterCreationReference.portraitImage.sprite = portraitsHolder.portraits[0];
}
characterCreationReference.portraitSelectionLeftButton.onClick.AddListener(OnPortraitLeft);
characterCreationReference.portraitSelectionRightButton.onClick.AddListener(OnPortraitRight);
// Initial state
selectedRace = CharacterRace.Human;
selectedClass = CharacterClass.Warrior;
characterCreationReference.raceDropdown.SetValueWithoutNotify(0);
characterCreationReference.classDropdown.SetValueWithoutNotify(0);
ResetWorkingState();
}
// --- Dropdown helpers ---
private void PopulateEnumDropdown<T>(TMP_Dropdown dropdown) where T : Enum {
dropdown.ClearOptions();
var options = new List<string>();
foreach(T value in Enum.GetValues(typeof(T))) {
if(Convert.ToInt32(value) == 0) {
continue; // skip None
}
options.Add(value.ToString());
}
dropdown.AddOptions(options);
}
private void PopulatePerksDropdown() {
var dropdown = characterCreationReference.perksDropdown;
dropdown.ClearOptions();
availablePerks = new List<IPerk>(characterSystems.PerkFactory.GetAll());
var options = new List<string> { "Select a perk..." };
foreach(var perk in availablePerks) {
options.Add(perk.Name);
}
dropdown.AddOptions(options);
dropdown.SetValueWithoutNotify(0);
}
// --- State management ---
private void ResetWorkingState() {
Array.Clear(allocatedPoints, 0, allocatedPoints.Length);
racialPerks = new PerksData();
racialModifiers = new ModifiersData();
classPerks = new PerksData();
classModifiers = new ModifiersData();
playerPerks.Clear();
ApplyRacialBonuses();
ApplyClassBonuses();
UpdateRemainingPoints();
RecalculateAll();
// Initialize previous values so first change doesn't log a delta from 0
previousHealth = workingStats.GetValue(StatType.Health);
previousStamina = workingStats.GetValue(StatType.Mana);
}
private void ApplyRacialBonuses() {
racialPerks = new PerksData();
racialModifiers = new ModifiersData();
var bonuses = starterCharacterSettings.racialBonuses;
if(bonuses == null) {
return;
}
foreach(var rb in bonuses) {
if(rb.race == selectedRace) {
racialPerks = rb.startingPerks ?? new PerksData();
racialModifiers = rb.permanentModifiers ?? new ModifiersData();
break;
}
}
}
private void ApplyClassBonuses() {
classPerks = new PerksData();
classModifiers = new ModifiersData();
var bonuses = starterCharacterSettings.classBonuses;
if(bonuses == null) {
return;
}
foreach(var cb in bonuses) {
if(cb.@class == selectedClass) {
classPerks = cb.startingPerks ?? new PerksData();
classModifiers = cb.permanentModifiers ?? new ModifiersData();
break;
}
}
}
private void UpdateRemainingPoints() {
var totalPoints = 0;
if(starterCharacterSettings.distributionPointsPerClass != null) {
foreach(var dpc in starterCharacterSettings.distributionPointsPerClass) {
if(dpc.@class == selectedClass) {
totalPoints = dpc.points;
break;
}
}
}
var spent = 0;
for(int i = 0; i < allocatedPoints.Length; i++) {
spent += allocatedPoints[i];
}
remainingPoints = totalPoints - spent;
}
// --- Core calculation ---
private void RecalculateAll() {
// 1. Start from default attributes + player allocations
var baseAttrs = starterCharacterSettings.defaultEntityAttributes;
var attrTypes = new[] { AttributeType.Might, AttributeType.Reflex, AttributeType.Knowledge, AttributeType.Perception };
var finalAttrs = new Attribute[attrTypes.Length];
for(int i = 0; i < attrTypes.Length; i++) {
var baseVal = baseAttrs.GetValue(attrTypes[i]);
finalAttrs[i] = new Attribute(attrTypes[i], baseVal + allocatedPoints[i]);
}
workingAttributes = new EntityAttributes { attributes = finalAttrs };
// 2. Build combined perks and modifiers (defaults + racial + class + player)
// Racial/class attribute and stat bonuses flow through permanentModifiers
var combinedPerks = BuildCombinedPerks();
var combinedModifiers = BuildCombinedModifiers();
// 3. Build temp entity for modifier collection
var tempEntity = new CharacterCreationRequest {
Id = Guid.NewGuid(),
Race = selectedRace,
Class = selectedClass,
Role = CharacterRole.Protagonist,
Attributes = workingAttributes,
Perks = combinedPerks,
Modifiers = combinedModifiers
};
// 4. Resolve attributes through modifiers (racial/class attribute bonuses come via modifiers)
var resolver = characterSystems.ModifierResolver;
var resolvedAttrs = new Attribute[attrTypes.Length];
for(int i = 0; i < attrTypes.Length; i++) {
var mods = resolver.CollectModifiers(tempEntity, attrTypes[i]);
resolvedAttrs[i] = new Attribute(attrTypes[i], resolver.Resolve(finalAttrs[i].value, mods, tempEntity));
}
workingAttributes = new EntityAttributes { attributes = resolvedAttrs };
tempEntity.Attributes = workingAttributes;
// 5. Calculate stats through modifiers (racial/class stat bonuses come via modifiers)
var baseStats = starterCharacterSettings.defaultEntityStats;
var statTypes = new[] { StatType.Health, StatType.Mana, StatType.Level, StatType.Experience };
var resolvedStats = new Stat[statTypes.Length];
for(int i = 0; i < statTypes.Length; i++) {
var baseVal = baseStats.GetValue(statTypes[i]);
var mods = resolver.CollectModifiers(tempEntity, statTypes[i]);
resolvedStats[i] = new Stat(statTypes[i], resolver.Resolve(baseVal, mods, tempEntity));
}
workingStats = new EntityStats { stats = resolvedStats };
// 9. Update UI
UpdateAttributeUI();
UpdateStatUI();
UpdatePointsDisplay();
// 10. Log stat deltas
var newHealth = workingStats.GetValue(StatType.Health);
var newStamina = workingStats.GetValue(StatType.Mana);
if(newHealth != previousHealth && previousHealth != 0) {
var delta = newHealth - previousHealth;
var sign = delta > 0 ? "+" : "";
inGameLogger.Log($"Health: {previousHealth} -> {newHealth} ({sign}{delta})", "#87CEEB");
}
if(newStamina != previousStamina && previousStamina != 0) {
var delta = newStamina - previousStamina;
var sign = delta > 0 ? "+" : "";
inGameLogger.Log($"Stamina: {previousStamina} -> {newStamina} ({sign}{delta})", "#FFFF99");
}
previousHealth = newHealth;
previousStamina = newStamina;
}
private PerksData BuildCombinedPerks() {
// Start with defaults, add racial/class/player perks (deduplicate by Id)
var combined = new PerksData { perks = new List<PerkDefinition>() };
var seenIds = new HashSet<Guid>();
void AddPerks(PerksData source) {
if(source?.perks == null) {
return;
}
foreach(var perk in source.perks) {
if(perk != null && seenIds.Add(perk.Id)) {
combined.perks.Add(perk);
}
}
}
AddPerks(starterCharacterSettings.defaultPerksData);
AddPerks(racialPerks);
AddPerks(classPerks);
foreach(var perk in playerPerks) {
if(perk != null && seenIds.Add(perk.Id)) {
combined.perks.Add(perk);
}
}
return combined;
}
private ModifiersData BuildCombinedModifiers() {
// Start with defaults. Racial/class modifiers override defaults that target the same
// thing, but ONLY if the override's requirements are currently met. If requirements
// are not met, the default stays and the override is still added — the resolver will
// skip the unqualified override at resolution time, leaving the default active.
var combined = new ModifiersData { modifiers = new List<ModifierDefinition>() };
// Seed with defaults
if(starterCharacterSettings.defaultModifiersData?.modifiers != null) {
combined.modifiers.AddRange(starterCharacterSettings.defaultModifiersData.modifiers);
}
// Override with racial modifiers
OverrideModifiers(combined, racialModifiers, workingAttributes);
// Override with class modifiers
OverrideModifiers(combined, classModifiers, workingAttributes);
return combined;
}
private static void OverrideModifiers(ModifiersData combined, ModifiersData overrides, EntityAttributes currentAttributes) {
if(overrides?.modifiers == null) {
return;
}
foreach(var mod in overrides.modifiers) {
if(mod?.Target == null) {
continue;
}
// Only remove the default if the override's requirements are currently met.
// Both are added regardless — the resolver skips unqualified modifiers at
// resolution time, so if requirements aren't met, the default still applies.
var requirementsMet = AreRequirementsMet(mod, currentAttributes);
if(requirementsMet) {
for(int i = combined.modifiers.Count - 1; i >= 0; i--) {
var existing = combined.modifiers[i];
if(existing?.Target != null && TargetsMatch(existing.Target, mod.Target)) {
combined.modifiers.RemoveAt(i);
}
}
}
combined.modifiers.Add(mod);
}
}
private static bool AreRequirementsMet(ModifierDefinition mod, EntityAttributes attributes) {
if(mod.Requirements == null || mod.Requirements.Count == 0) {
return true;
}
foreach(var req in mod.Requirements) {
if(!req.IsMet(attributes)) {
return false;
}
}
return true;
}
private static bool TargetsMatch(ModifierTarget a, ModifierTarget b) {
if(a.Type != b.Type) {
return false;
}
return a.Type switch {
ModifierTargetType.Attribute => a.AttributeType == b.AttributeType,
ModifierTargetType.Stat => a.StatType == b.StatType,
ModifierTargetType.CombatScore => a.CombatScoreType == b.CombatScoreType,
_ => false
};
}
// --- UI updates ---
private void UpdateAttributeUI() {
var attrTypes = new[] { AttributeType.Might, AttributeType.Reflex, AttributeType.Knowledge, AttributeType.Perception };
var attrRefs = characterCreationReference.attributeReference;
for(int i = 0; i < attrRefs.Length && i < attrTypes.Length; i++) {
attrRefs[i].attributeValue.text = workingAttributes.GetValue(attrTypes[i]).ToString();
attrRefs[i].addPointsButton.interactable = remainingPoints > 0;
attrRefs[i].removePointsButton.interactable = allocatedPoints[i] > 0;
}
}
private void UpdateStatUI() {
var statTypes = new[] { StatType.Health, StatType.Mana };
var statRefs = characterCreationReference.statReference;
for(int i = 0; i < statRefs.Length && i < statTypes.Length; i++) {
var value = workingStats.GetValue(statTypes[i]);
statRefs[i].statName.text = statTypes[i].ToString();
statRefs[i].statValue.text = value.ToString();
statRefs[i].statBar.fillAmount = Mathf.Clamp01(value / 200f);
}
}
private void UpdatePointsDisplay() {
characterCreationReference.pointsToDistribute.text = remainingPoints.ToString();
}
// --- Event handlers ---
private void OnRaceChanged(int index) {
selectedRace = (CharacterRace)(index + 1);
Array.Clear(allocatedPoints, 0, allocatedPoints.Length);
ApplyRacialBonuses();
UpdateRemainingPoints();
RecalculateAll();
inGameLogger.Log($"Race changed to {selectedRace}");
}
private void OnClassChanged(int index) {
selectedClass = (CharacterClass)(index + 1);
Array.Clear(allocatedPoints, 0, allocatedPoints.Length);
ApplyClassBonuses();
UpdateRemainingPoints();
RecalculateAll();
inGameLogger.Log($"Class changed to {selectedClass}");
}
private void OnPerkSelected(int index) {
if(index <= 0 || index > availablePerks.Count) {
return; // "Select a perk..." placeholder
}
var perkIndex = index - 1; // offset for placeholder
var perk = availablePerks[perkIndex];
playerPerks.Add(new PerkDefinition {
Id = perk.Id,
Name = perk.Name,
Modifiers = perk.Modifiers
});
availablePerks.RemoveAt(perkIndex);
PopulatePerksDropdown();
RecalculateAll();
inGameLogger.Log($"Perk added: {perk.Name}");
}
private void OnAttributeAdd(AttributeType type) {
if(remainingPoints <= 0) {
return;
}
var idx = (int)type - 1;
allocatedPoints[idx]++;
remainingPoints--;
RecalculateAll();
}
private void OnAttributeRemove(AttributeType type) {
var idx = (int)type - 1;
if(allocatedPoints[idx] <= 0) {
return;
}
allocatedPoints[idx]--;
remainingPoints++;
RecalculateAll();
}
private void OnPortraitLeft() {
if(portraitsHolder == null || portraitsHolder.portraits.Length == 0) {
return;
}
currentPortraitIndex--;
if(currentPortraitIndex < 0) {
currentPortraitIndex = portraitsHolder.portraits.Length - 1;
}
characterCreationReference.portraitImage.sprite = portraitsHolder.portraits[currentPortraitIndex];
}
private void OnPortraitRight() {
if(portraitsHolder == null || portraitsHolder.portraits.Length == 0) {
return;
}
currentPortraitIndex++;
if(currentPortraitIndex >= portraitsHolder.portraits.Length) {
currentPortraitIndex = 0;
}
characterCreationReference.portraitImage.sprite = portraitsHolder.portraits[currentPortraitIndex];
}
private void OnBackClicked() {
if(confirmBackAction != null) {
confirmBackAction();
return;
}
Hide();
}
// --- Accept ---
private void OnAcceptClicked() {
var errors = new List<string>();
if(selectedRace == CharacterRace.None) {
errors.Add("Race must be selected");
}
if(selectedClass == CharacterClass.None) {
errors.Add("Class must be selected");
}
if(remainingPoints > 0) {
errors.Add($"{remainingPoints} distribution points remaining");
}
var characterName = characterCreationReference.nameInputField.text;
if(string.IsNullOrWhiteSpace(characterName)) {
errors.Add("Name cannot be empty");
}
if(errors.Count > 0) {
foreach(var error in errors) {
inGameLogger.Log(error, "#FF4444");
}
return;
}
var request = new CharacterCreationRequest {
Id = Guid.NewGuid(),
Name = characterName,
Race = selectedRace,
Class = selectedClass,
Role = CharacterRole.Protagonist,
PortraitIndex = currentPortraitIndex,
Attributes = workingAttributes,
Stats = workingStats,
Perks = BuildCombinedPerks(),
Modifiers = BuildCombinedModifiers()
};
characterCreationRequests = new List<CharacterCreationRequest> { request };
// Log full breakdown
inGameLogger.Log("--- Character Accepted ---");
inGameLogger.Log($"Name: {request.Name}", "#FFBF00");
inGameLogger.Log($"Race: {request.Race}");
inGameLogger.Log($"Class: {request.Class}");
inGameLogger.Log($"Portrait: #{request.PortraitIndex}");
inGameLogger.Log($"{request.Attributes}");
inGameLogger.Log($"{request.Stats}");
if(request.Perks?.perks != null && request.Perks.perks.Count > 0) {
foreach(var perk in request.Perks.perks) {
inGameLogger.Log($"Perk: {perk.Name}");
}
}
CreateParty();
canStartCheck.Invoke();
}
private void CreateParty() {
var partyCreatorModel = new PartyCreatorModel(characterSystems.CharacterFactory, characterSystems.PartyFactory, characterCreationRequests, partySettings);
var party = partyCreatorModel.CreatePartyForNewRun();
gameDataState.ActiveParty = party;
inGameLogger.Log("Character Creation Results:");
inGameLogger.Log($"Protagonist: {party.Protagonist.Name}", "#FFBF00");
inGameLogger.Log($"Protagonist Race: {party.Protagonist.Race}");
inGameLogger.Log($"Protagonist Class: {party.Protagonist.Class}");
inGameLogger.Log($"Companions: {party.Companions.Count}");
inGameLogger.Log($"{party.Protagonist.Attributes}");
inGameLogger.Log($"{party.Protagonist.Stats}");
inGameLogger.Log($"{party.Protagonist.Perks}");
inGameLogger.Log($"{party.Protagonist.Modifiers}");
}
// --- Lifecycle ---
public void Tick() {
return;
}
public void Show() {
characterCreationReference.gameObject.SetActive(true);
}
public void Hide() {
characterCreationReference.gameObject.SetActive(false);
}
public void Dispose() {
inGameLogger.Disable();
}

View File

@@ -1,6 +1,8 @@
using UnityEngine;
[CreateAssetMenu(fileName = "PortraitsHolder", menuName = "Nox/Database/UI/PortraitsHolder")]
public class PortraitsHolder : ScriptableObject {
public Sprite[] portraits;
namespace Nox.UI {
[CreateAssetMenu(fileName = "PortraitsHolder", menuName = "Nox/Database/UI/PortraitsHolder")]
public class PortraitsHolder : ScriptableObject {
public Sprite[] portraits;
}
}

View File

@@ -2,8 +2,10 @@ using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class StatReference : MonoBehaviour {
public TextMeshProUGUI statName;
public TextMeshProUGUI statValue;
public Image statBar;
namespace Nox.UI {
public class StatReference : MonoBehaviour {
public TextMeshProUGUI statName;
public TextMeshProUGUI statValue;
public Image statBar;
}
}

View File

@@ -22,6 +22,7 @@ namespace Nox.UI {
private CharacterCreationReference characterCreationReference;
private CharacterCreationView characterCreationView;
private AsyncOperationHandle<GameObject> charCreationHandle;
private readonly StarterCharacterSettings starterCharacterSettings;
public MainMenuView(MenuPrefabsContainer menuPrefabsContainer,
MenuGameStateData menuGameStateData,
@@ -29,7 +30,8 @@ namespace Nox.UI {
GameDataState gameDataState,
PartySettings partySettings,
ICharacterSystems characterSystems,
PortraitsHolder portraitsHolder) {
PortraitsHolder portraitsHolder,
StarterCharacterSettings starterCharacterSettings) {
this.menuPrefabsContainer = menuPrefabsContainer;
this.menuGameStateData = menuGameStateData;
this.saveSystem = saveSystem;
@@ -37,6 +39,7 @@ namespace Nox.UI {
this.partySettings = partySettings;
this.characterSystems = characterSystems;
this.portraitsHolder = portraitsHolder;
this.starterCharacterSettings = starterCharacterSettings;
}
public void Initialize() {
if(!mainMenuReference) {
@@ -65,7 +68,15 @@ namespace Nox.UI {
charCreationHandle = Addressables.InstantiateAsync(menuPrefabsContainer.characterCreationReference);
var result = charCreationHandle.WaitForCompletion();
characterCreationReference =result.GetComponent<CharacterCreationReference>();
characterCreationView = new CharacterCreationView(characterCreationReference, menuGameStateData, saveSystem, gameDataState, partySettings, characterSystems, portraitsHolder);
characterCreationView = new CharacterCreationView(
characterCreationReference,
menuGameStateData,
saveSystem,
gameDataState,
partySettings,
characterSystems,
portraitsHolder,
starterCharacterSettings);
characterCreationView.Initialize();
characterCreationView.Show();
}

View File

@@ -36,20 +36,258 @@ MonoBehaviour:
- stat: 1
value: 1
- stat: 2
value: 0
value: 1
- stat: 3
value: 0
value: 1
- stat: 4
value: 0
value: 1
defaultPerksData:
perks: []
racialBonuses: []
classBonuses:
- class: 1
bonusAttributes:
attributes: []
bonusStats:
stats: []
defaultModifiersData:
modifiers:
- <Name>k__BackingField: MGT_health_multiplier
<Target>k__BackingField:
<Type>k__BackingField: 2
<AttributeType>k__BackingField: 0
<StatType>k__BackingField: 1
<CombatScoreType>k__BackingField: 0
<ScalingSource>k__BackingField:
<Type>k__BackingField: 1
<AttributeType>k__BackingField: 1
<StatType>k__BackingField: 0
<CombatScoreType>k__BackingField: 0
<Operation>k__BackingField: 2
<Value>k__BackingField: 3
<Requirements>k__BackingField:
- <Attribute>k__BackingField: 1
<MinimumValue>k__BackingField: 0
- <Name>k__BackingField: MGT_mana_multiplier
<Target>k__BackingField:
<Type>k__BackingField: 2
<AttributeType>k__BackingField: 0
<StatType>k__BackingField: 2
<CombatScoreType>k__BackingField: 0
<ScalingSource>k__BackingField:
<Type>k__BackingField: 1
<AttributeType>k__BackingField: 1
<StatType>k__BackingField: 0
<CombatScoreType>k__BackingField: 0
<Operation>k__BackingField: 2
<Value>k__BackingField: 1
<Requirements>k__BackingField:
- <Attribute>k__BackingField: 1
<MinimumValue>k__BackingField: 0
- <Name>k__BackingField: KNO_mana_multiplier
<Target>k__BackingField:
<Type>k__BackingField: 2
<AttributeType>k__BackingField: 0
<StatType>k__BackingField: 2
<CombatScoreType>k__BackingField: 0
<ScalingSource>k__BackingField:
<Type>k__BackingField: 1
<AttributeType>k__BackingField: 3
<StatType>k__BackingField: 0
<CombatScoreType>k__BackingField: 0
<Operation>k__BackingField: 2
<Value>k__BackingField: 2
<Requirements>k__BackingField:
- <Attribute>k__BackingField: 3
<MinimumValue>k__BackingField: 0
- <Name>k__BackingField: PER_ATK_multiplier
<Target>k__BackingField:
<Type>k__BackingField: 3
<AttributeType>k__BackingField: 0
<StatType>k__BackingField: 2
<CombatScoreType>k__BackingField: 1
<ScalingSource>k__BackingField:
<Type>k__BackingField: 1
<AttributeType>k__BackingField: 4
<StatType>k__BackingField: 0
<CombatScoreType>k__BackingField: 0
<Operation>k__BackingField: 2
<Value>k__BackingField: 1
<Requirements>k__BackingField: []
- <Name>k__BackingField: REF_ATK_multiplier
<Target>k__BackingField:
<Type>k__BackingField: 3
<AttributeType>k__BackingField: 0
<StatType>k__BackingField: 2
<CombatScoreType>k__BackingField: 1
<ScalingSource>k__BackingField:
<Type>k__BackingField: 1
<AttributeType>k__BackingField: 2
<StatType>k__BackingField: 0
<CombatScoreType>k__BackingField: 0
<Operation>k__BackingField: 3
<Value>k__BackingField: 1.05
<Requirements>k__BackingField: []
racialBonuses:
- race: 1
startingPerks:
perks: []
maxPartySize: 4
permanentModifiers:
modifiers:
- <Name>k__BackingField: KNO_bonus
<Target>k__BackingField:
<Type>k__BackingField: 1
<AttributeType>k__BackingField: 3
<StatType>k__BackingField: 0
<CombatScoreType>k__BackingField: 0
<ScalingSource>k__BackingField:
<Type>k__BackingField: 0
<AttributeType>k__BackingField: 0
<StatType>k__BackingField: 0
<CombatScoreType>k__BackingField: 0
<Operation>k__BackingField: 2
<Value>k__BackingField: 1
<Requirements>k__BackingField: []
- race: 2
startingPerks:
perks: []
permanentModifiers:
modifiers:
- <Name>k__BackingField: REF_bonus
<Target>k__BackingField:
<Type>k__BackingField: 1
<AttributeType>k__BackingField: 2
<StatType>k__BackingField: 0
<CombatScoreType>k__BackingField: 0
<ScalingSource>k__BackingField:
<Type>k__BackingField: 0
<AttributeType>k__BackingField: 0
<StatType>k__BackingField: 0
<CombatScoreType>k__BackingField: 0
<Operation>k__BackingField: 2
<Value>k__BackingField: 1
<Requirements>k__BackingField: []
- race: 3
startingPerks:
perks: []
permanentModifiers:
modifiers:
- <Name>k__BackingField: MGT_bonus
<Target>k__BackingField:
<Type>k__BackingField: 0
<AttributeType>k__BackingField: 0
<StatType>k__BackingField: 0
<CombatScoreType>k__BackingField: 0
<ScalingSource>k__BackingField:
<Type>k__BackingField: 0
<AttributeType>k__BackingField: 0
<StatType>k__BackingField: 0
<CombatScoreType>k__BackingField: 0
<Operation>k__BackingField: 2
<Value>k__BackingField: 1
<Requirements>k__BackingField: []
classBonuses:
- class: 1
startingPerks:
perks: []
permanentModifiers:
modifiers:
- <Name>k__BackingField: warrior_might_health_bonus
<Target>k__BackingField:
<Type>k__BackingField: 2
<AttributeType>k__BackingField: 0
<StatType>k__BackingField: 1
<CombatScoreType>k__BackingField: 0
<ScalingSource>k__BackingField:
<Type>k__BackingField: 1
<AttributeType>k__BackingField: 1
<StatType>k__BackingField: 0
<CombatScoreType>k__BackingField: 0
<Operation>k__BackingField: 2
<Value>k__BackingField: 4
<Requirements>k__BackingField:
- <Attribute>k__BackingField: 1
<MinimumValue>k__BackingField: 5
- class: 2
startingPerks:
perks: []
permanentModifiers:
modifiers:
- <Name>k__BackingField: rogue_REF_bonus
<Target>k__BackingField:
<Type>k__BackingField: 3
<AttributeType>k__BackingField: 0
<StatType>k__BackingField: 0
<CombatScoreType>k__BackingField: 2
<ScalingSource>k__BackingField:
<Type>k__BackingField: 1
<AttributeType>k__BackingField: 2
<StatType>k__BackingField: 0
<CombatScoreType>k__BackingField: 0
<Operation>k__BackingField: 2
<Value>k__BackingField: 1
<Requirements>k__BackingField:
- <Attribute>k__BackingField: 2
<MinimumValue>k__BackingField: 4
- class: 2
startingPerks:
perks: []
permanentModifiers:
modifiers:
- <Name>k__BackingField: rogue_PER_bonus
<Target>k__BackingField:
<Type>k__BackingField: 3
<AttributeType>k__BackingField: 0
<StatType>k__BackingField: 0
<CombatScoreType>k__BackingField: 1
<ScalingSource>k__BackingField:
<Type>k__BackingField: 1
<AttributeType>k__BackingField: 4
<StatType>k__BackingField: 0
<CombatScoreType>k__BackingField: 0
<Operation>k__BackingField: 2
<Value>k__BackingField: 1
<Requirements>k__BackingField:
- <Attribute>k__BackingField: 4
<MinimumValue>k__BackingField: 3
- class: 3
startingPerks:
perks: []
permanentModifiers:
modifiers:
- <Name>k__BackingField: rmage_KNO_bonus
<Target>k__BackingField:
<Type>k__BackingField: 3
<AttributeType>k__BackingField: 0
<StatType>k__BackingField: 0
<CombatScoreType>k__BackingField: 1
<ScalingSource>k__BackingField:
<Type>k__BackingField: 1
<AttributeType>k__BackingField: 3
<StatType>k__BackingField: 0
<CombatScoreType>k__BackingField: 0
<Operation>k__BackingField: 2
<Value>k__BackingField: 1
<Requirements>k__BackingField:
- <Attribute>k__BackingField: 3
<MinimumValue>k__BackingField: 5
- class: 4
startingPerks:
perks: []
permanentModifiers:
modifiers:
- <Name>k__BackingField: herald_ALL_bonus
<Target>k__BackingField:
<Type>k__BackingField: 3
<AttributeType>k__BackingField: 0
<StatType>k__BackingField: 0
<CombatScoreType>k__BackingField: 3
<ScalingSource>k__BackingField:
<Type>k__BackingField: 1
<AttributeType>k__BackingField: 3
<StatType>k__BackingField: 0
<CombatScoreType>k__BackingField: 0
<Operation>k__BackingField: 2
<Value>k__BackingField: 2
<Requirements>k__BackingField:
- <Attribute>k__BackingField: 1
<MinimumValue>k__BackingField: 2
- <Attribute>k__BackingField: 2
<MinimumValue>k__BackingField: 2
- <Attribute>k__BackingField: 3
<MinimumValue>k__BackingField: 2
- <Attribute>k__BackingField: 4
<MinimumValue>k__BackingField: 2

View File

@@ -52,6 +52,10 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: f51aaf11e81876845b289f5f7d310469, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::AttributeReference
removePointsButton: {fileID: 111231332404835723}
addPointsButton: {fileID: 7007629831261223577}
attributeName: {fileID: 6232133109646223137}
attributeValue: {fileID: 7864923222111671344}
--- !u!1 &602367721906152228
GameObject:
m_ObjectHideFlags: 0

View File

@@ -1876,10 +1876,6 @@ PrefabInstance:
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 374280998538979084, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_AnchorMin.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 1525412503934350410, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_Name
value: LogContainer
@@ -1972,38 +1968,6 @@ PrefabInstance:
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2688140319784364735, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_AnchorMax.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2688140319784364735, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4070132176653801724, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 4070132176653801724, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6286131802514671469, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_AnchorMax.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6286131802514671469, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6286131802514671469, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6286131802514671469, guid: 1b41f907ca960b644ae3af6e1942b9fb, type: 3}
propertyPath: m_SizeDelta.y
value: 0
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []

View File

@@ -142,7 +142,7 @@ MonoBehaviour:
scrollRect: {fileID: 2554617498295089481}
content: {fileID: 3059084814696477984}
entryPrefab: {fileID: 4832918257971952213, guid: 9d1c7837b0b5a9f45baa84f326fc247c, type: 3}
poolSize: 20
poolSize: 200
--- !u!1 &2405629305058117087
GameObject:
m_ObjectHideFlags: 0
@@ -153,6 +153,7 @@ GameObject:
m_Component:
- component: {fileID: 3059084814696477984}
- component: {fileID: 1744225011043606462}
- component: {fileID: 4069423691573659896}
m_Layer: 5
m_Name: Content
m_TagString: Untagged
@@ -176,8 +177,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 20.186, y: 0.00002861023}
m_SizeDelta: {x: -40.374, y: 1085.3}
m_AnchoredPosition: {x: 20.186, y: 0}
m_SizeDelta: {x: -40.374, y: 0}
m_Pivot: {x: 0, y: 1}
--- !u!114 &1744225011043606462
MonoBehaviour:
@@ -197,7 +198,7 @@ MonoBehaviour:
m_Top: 0
m_Bottom: 0
m_ChildAlignment: 0
m_Spacing: 2
m_Spacing: 4.6
m_ChildForceExpandWidth: 1
m_ChildForceExpandHeight: 0
m_ChildControlWidth: 1
@@ -205,6 +206,20 @@ MonoBehaviour:
m_ChildScaleWidth: 0
m_ChildScaleHeight: 0
m_ReverseArrangement: 0
--- !u!114 &4069423691573659896
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2405629305058117087}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.ContentSizeFitter
m_HorizontalFit: 0
m_VerticalFit: 2
--- !u!1 &3952660362247579954
GameObject:
m_ObjectHideFlags: 0
@@ -537,7 +552,7 @@ MonoBehaviour:
m_Horizontal: 0
m_Vertical: 1
m_MovementType: 2
m_Elasticity: 0.23
m_Elasticity: 0.05
m_Inertia: 1
m_DecelerationRate: 0.135
m_ScrollSensitivity: 20
@@ -671,8 +686,8 @@ MonoBehaviour:
m_TargetGraphic: {fileID: 6879256843609484833}
m_HandleRect: {fileID: 374280998538979084}
m_Direction: 2
m_Value: 1
m_Size: 0.9999999
m_Value: 0
m_Size: 1
m_NumberOfSteps: 0
m_OnValueChanged:
m_PersistentCalls:

View File

@@ -10,7 +10,9 @@ GameObject:
m_Component:
- component: {fileID: 382400732949652569}
- component: {fileID: 4832918257971952213}
- component: {fileID: 1727094465477629914}
- component: {fileID: 4055037707474935581}
- component: {fileID: 5541913713772788578}
- component: {fileID: 440716831877687606}
m_Layer: 0
m_Name: LogEntry
m_TagString: Untagged
@@ -29,14 +31,13 @@ RectTransform:
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 8597194705437786868}
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 0, y: 10.8254}
m_SizeDelta: {x: 1093, y: 65.3898}
m_AnchoredPosition: {x: 499.7, y: -12}
m_SizeDelta: {x: 999.4, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &4832918257971952213
MonoBehaviour:
@@ -50,8 +51,16 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 5526887cdf77a54439357be8b5754ffc, type: 3}
m_Name:
m_EditorClassIdentifier: Jovian.InGameLogging::Jovian.InGameLogging.UI.LogEntryView
messageText: {fileID: 4259574353180979179}
--- !u!114 &1727094465477629914
messageText: {fileID: 5541913713772788578}
--- !u!222 &4055037707474935581
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6501593483943143564}
m_CullTransparentMesh: 1
--- !u!114 &5541913713772788578
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@@ -60,71 +69,6 @@ MonoBehaviour:
m_GameObject: {fileID: 6501593483943143564}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: -1
m_PreferredHeight: -1
m_FlexibleWidth: 1
m_FlexibleHeight: 1
m_LayoutPriority: 1
--- !u!1 &8033193440249993941
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 8597194705437786868}
- component: {fileID: 8331827018725656388}
- component: {fileID: 4259574353180979179}
m_Layer: 0
m_Name: Text (TMP)
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &8597194705437786868
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8033193440249993941}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 382400732949652569}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &8331827018725656388
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8033193440249993941}
m_CullTransparentMesh: 1
--- !u!114 &4259574353180979179
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8033193440249993941}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI
@@ -163,10 +107,10 @@ MonoBehaviour:
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 24.55
m_fontSizeBase: 36
m_fontSize: 24
m_fontSizeBase: 24
m_fontWeight: 400
m_enableAutoSizing: 1
m_enableAutoSizing: 0
m_fontSizeMin: 5
m_fontSizeMax: 24.55
m_fontStyle: 0
@@ -208,3 +152,17 @@ MonoBehaviour:
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!114 &440716831877687606
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6501593483943143564}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.ContentSizeFitter
m_HorizontalFit: 0
m_VerticalFit: 2

View File

@@ -1,3 +1,4 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
@@ -12,6 +13,8 @@ namespace Jovian.InGameLogging.UI {
IGameLogStore store;
LogChannel? channelFilter;
bool autoScroll = true;
bool scrollingToBottom;
Coroutine scrollCoroutine;
readonly List<LogEntryView> activeEntries = new();
readonly Stack<LogEntryView> pool = new();
@@ -76,11 +79,27 @@ namespace Jovian.InGameLogging.UI {
activeEntries.Add(view);
if(autoScroll) {
Canvas.ForceUpdateCanvases();
scrollRect.verticalNormalizedPosition = 0f;
RequestScrollToBottom();
}
}
void RequestScrollToBottom() {
if(scrollCoroutine != null) {
StopCoroutine(scrollCoroutine);
}
scrollCoroutine = StartCoroutine(ScrollToBottomRoutine());
}
IEnumerator ScrollToBottomRoutine() {
scrollingToBottom = true;
yield return null;
LayoutRebuilder.ForceRebuildLayoutImmediate(content);
scrollRect.verticalNormalizedPosition = 0f;
yield return null;
scrollingToBottom = false;
scrollCoroutine = null;
}
void HandleCleared() {
for(int i = activeEntries.Count - 1; i >= 0; i--) {
ReturnToPool(activeEntries[i]);
@@ -104,12 +123,14 @@ namespace Jovian.InGameLogging.UI {
}
if(autoScroll) {
Canvas.ForceUpdateCanvases();
scrollRect.verticalNormalizedPosition = 0f;
RequestScrollToBottom();
}
}
void HandleScrollChanged(Vector2 position) {
if(scrollingToBottom) {
return;
}
autoScroll = position.y <= 0.01f;
}
}