From 50832c491cae066b3afb3bbf275933180b91fed9 Mon Sep 17 00:00:00 2001 From: Sebastian Bularca Date: Mon, 6 Apr 2026 01:05:20 +0200 Subject: [PATCH] added full characte creation support --- Assets/Code/Core/MainMenuGameState.cs | 4 +- .../Entities/CharacterAttributesFactory.cs | 2 +- .../GameState/Entities/CharacterFactory.cs | 3 + .../Entities/CharacterStatsFactory.cs | 10 +- .../GameState/Entities/EntitiesDefinitions.cs | 15 +- .../GameState/Entities/ModifiersFactory.cs | 75 ++- .../GameState/Entities/ModifiersResolver.cs | 94 ++- .../GameState/Entities/PartyCreatorModel.cs | 3 +- .../Entities/StarterCharacterSettings.cs | 13 - .../Code/GameState/UI/AttributeReference.cs | 20 +- .../GameState/UI/CharacterCreationView.cs | 564 ++++++++++++++++-- Assets/Code/GameState/UI/PortraitsHolder.cs | 8 +- Assets/Code/GameState/UI/StatReference.cs | 10 +- Assets/Code/SplashMainMenuUI/MainMenuView.cs | 15 +- .../Entities/CharacterBaseSettings.asset | 260 +++++++- Assets/Prefabs/UI/Attribute.prefab | 4 + .../UI/CharacterCreationReference.prefab | 36 -- Assets/Prefabs/UI/LogContainer.prefab | 29 +- Assets/Prefabs/UI/LogEntry.prefab | 108 +--- .../Runtime/UI/GameLogView.cs | 29 +- 20 files changed, 1037 insertions(+), 265 deletions(-) diff --git a/Assets/Code/Core/MainMenuGameState.cs b/Assets/Code/Core/MainMenuGameState.cs index c8cda62..3e78e50 100644 --- a/Assets/Code/Core/MainMenuGameState.cs +++ b/Assets/Code/Core/MainMenuGameState.cs @@ -81,10 +81,10 @@ namespace Nox.Core { var characterRegistry = Addressables.LoadAssetAsync("CharacterRegistry").WaitForCompletion(); var modifiersRegistry = Addressables.LoadAssetAsync("ModifiersRegistry").WaitForCompletion(); var partySettings = Addressables.LoadAssetAsync("DefaultPartySettings").WaitForCompletion(); - var portraitsHolder = Addressables.LoadAssetAsync("PortraitsHolder").WaitForCompletion(); + var portraitsHolder = Addressables.LoadAssetAsync(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; diff --git a/Assets/Code/GameState/Entities/CharacterAttributesFactory.cs b/Assets/Code/GameState/Entities/CharacterAttributesFactory.cs index f3ea2c5..01c2885 100644 --- a/Assets/Code/GameState/Entities/CharacterAttributesFactory.cs +++ b/Assets/Code/GameState/Entities/CharacterAttributesFactory.cs @@ -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() }; diff --git a/Assets/Code/GameState/Entities/CharacterFactory.cs b/Assets/Code/GameState/Entities/CharacterFactory.cs index 8403826..deba5dd 100644 --- a/Assets/Code/GameState/Entities/CharacterFactory.cs +++ b/Assets/Code/GameState/Entities/CharacterFactory.cs @@ -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(), diff --git a/Assets/Code/GameState/Entities/CharacterStatsFactory.cs b/Assets/Code/GameState/Entities/CharacterStatsFactory.cs index b042720..cc9b829 100644 --- a/Assets/Code/GameState/Entities/CharacterStatsFactory.cs +++ b/Assets/Code/GameState/Entities/CharacterStatsFactory.cs @@ -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)) } }; } diff --git a/Assets/Code/GameState/Entities/EntitiesDefinitions.cs b/Assets/Code/GameState/Entities/EntitiesDefinitions.cs index cc1751e..13b0626 100644 --- a/Assets/Code/GameState/Entities/EntitiesDefinitions.cs +++ b/Assets/Code/GameState/Entities/EntitiesDefinitions.cs @@ -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() }, 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() + }).ToList() ?? new List() } }; } diff --git a/Assets/Code/GameState/Entities/ModifiersFactory.cs b/Assets/Code/GameState/Entities/ModifiersFactory.cs index c44eead..a0b7210 100644 --- a/Assets/Code/GameState/Entities/ModifiersFactory.cs +++ b/Assets/Code/GameState/Entities/ModifiersFactory.cs @@ -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 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 Requirements { get; set; } = new(); + + IReadOnlyList IModifier.Requirements => Requirements; } [Serializable] public sealed class ModifiersData { - public List modifiers = new (); + public List 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 modifierPool = new (); + private readonly Dictionary 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() }); return true; diff --git a/Assets/Code/GameState/Entities/ModifiersResolver.cs b/Assets/Code/GameState/Entities/ModifiersResolver.cs index 1a4d4c5..4bc6bca 100644 --- a/Assets/Code/GameState/Entities/ModifiersResolver.cs +++ b/Assets/Code/GameState/Entities/ModifiersResolver.cs @@ -5,9 +5,10 @@ using ZLinq; namespace Nox.Game { public interface IModifierResolver { - int Resolve(int baseValue, IEnumerable modifiers); + int Resolve(int baseValue, IEnumerable modifiers, IEntityDefinition entity = null); IEnumerable CollectModifiers(IEntityDefinition entity, StatType statType); IEnumerable CollectModifiers(IEntityDefinition entity, AttributeType attributeType); + IEnumerable CollectModifiers(IEntityDefinition entity, CombatScoreType combatScoreType); } /// @@ -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". /// public sealed class ModifierResolver : IModifierResolver { - public int Resolve(int baseValue, IEnumerable modifiers) { + public int Resolve(int baseValue, IEnumerable 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 CollectModifiers(IEntityDefinition entity, StatType statType) { if(entity == null) { return Array.Empty(); } var result = new List(); - - 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(); + CollectFromEntity(entity, result, m => m.Target != null && m.Target.Matches(attributeType)); + return result; + } + public IEnumerable CollectModifiers(IEntityDefinition entity, CombatScoreType combatScoreType) { + if(entity == null) { + return Array.Empty(); + } + + var result = new List(); + 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 result, Func 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; } } } diff --git a/Assets/Code/GameState/Entities/PartyCreatorModel.cs b/Assets/Code/GameState/Entities/PartyCreatorModel.cs index 2040294..3ed9d6c 100644 --- a/Assets/Code/GameState/Entities/PartyCreatorModel.cs +++ b/Assets/Code/GameState/Entities/PartyCreatorModel.cs @@ -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); } } } diff --git a/Assets/Code/GameState/Entities/StarterCharacterSettings.cs b/Assets/Code/GameState/Entities/StarterCharacterSettings.cs index c1090ea..6393fb1 100644 --- a/Assets/Code/GameState/Entities/StarterCharacterSettings.cs +++ b/Assets/Code/GameState/Entities/StarterCharacterSettings.cs @@ -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; } diff --git a/Assets/Code/GameState/UI/AttributeReference.cs b/Assets/Code/GameState/UI/AttributeReference.cs index 404f09c..e906e48 100644 --- a/Assets/Code/GameState/UI/AttributeReference.cs +++ b/Assets/Code/GameState/UI/AttributeReference.cs @@ -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; } } diff --git a/Assets/Code/GameState/UI/CharacterCreationView.cs b/Assets/Code/GameState/UI/CharacterCreationView.cs index 975c6df..d0aecf9 100644 --- a/Assets/Code/GameState/UI/CharacterCreationView.cs +++ b/Assets/Code/GameState/UI/CharacterCreationView.cs @@ -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 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 playerPerks = new(); + private List availablePerks = new(); + + // Computed state + private EntityAttributes workingAttributes; + private EntityStats workingStats; + + // Output + private List 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 { - 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(characterCreationReference.raceDropdown); + characterCreationReference.raceDropdown.onValueChanged.AddListener(OnRaceChanged); + + // Class dropdown + PopulateEnumDropdown(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(TMP_Dropdown dropdown) where T : Enum { + dropdown.ClearOptions(); + var options = new List(); + 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(characterSystems.PerkFactory.GetAll()); + + var options = new List { "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() }; + var seenIds = new HashSet(); + + 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() }; + + // 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(); + 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 { 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(); } diff --git a/Assets/Code/GameState/UI/PortraitsHolder.cs b/Assets/Code/GameState/UI/PortraitsHolder.cs index 8f36bb8..00cdcd3 100644 --- a/Assets/Code/GameState/UI/PortraitsHolder.cs +++ b/Assets/Code/GameState/UI/PortraitsHolder.cs @@ -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; + } } diff --git a/Assets/Code/GameState/UI/StatReference.cs b/Assets/Code/GameState/UI/StatReference.cs index 497cf4d..0516e18 100644 --- a/Assets/Code/GameState/UI/StatReference.cs +++ b/Assets/Code/GameState/UI/StatReference.cs @@ -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; + } } diff --git a/Assets/Code/SplashMainMenuUI/MainMenuView.cs b/Assets/Code/SplashMainMenuUI/MainMenuView.cs index d57d06e..ae6c7e6 100644 --- a/Assets/Code/SplashMainMenuUI/MainMenuView.cs +++ b/Assets/Code/SplashMainMenuUI/MainMenuView.cs @@ -22,6 +22,7 @@ namespace Nox.UI { private CharacterCreationReference characterCreationReference; private CharacterCreationView characterCreationView; private AsyncOperationHandle 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(); - 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(); } diff --git a/Assets/Database/Entities/CharacterBaseSettings.asset b/Assets/Database/Entities/CharacterBaseSettings.asset index b1b518d..3a8d2c0 100644 --- a/Assets/Database/Entities/CharacterBaseSettings.asset +++ b/Assets/Database/Entities/CharacterBaseSettings.asset @@ -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: + - k__BackingField: MGT_health_multiplier + k__BackingField: + k__BackingField: 2 + k__BackingField: 0 + k__BackingField: 1 + k__BackingField: 0 + k__BackingField: + k__BackingField: 1 + k__BackingField: 1 + k__BackingField: 0 + k__BackingField: 0 + k__BackingField: 2 + k__BackingField: 3 + k__BackingField: + - k__BackingField: 1 + k__BackingField: 0 + - k__BackingField: MGT_mana_multiplier + k__BackingField: + k__BackingField: 2 + k__BackingField: 0 + k__BackingField: 2 + k__BackingField: 0 + k__BackingField: + k__BackingField: 1 + k__BackingField: 1 + k__BackingField: 0 + k__BackingField: 0 + k__BackingField: 2 + k__BackingField: 1 + k__BackingField: + - k__BackingField: 1 + k__BackingField: 0 + - k__BackingField: KNO_mana_multiplier + k__BackingField: + k__BackingField: 2 + k__BackingField: 0 + k__BackingField: 2 + k__BackingField: 0 + k__BackingField: + k__BackingField: 1 + k__BackingField: 3 + k__BackingField: 0 + k__BackingField: 0 + k__BackingField: 2 + k__BackingField: 2 + k__BackingField: + - k__BackingField: 3 + k__BackingField: 0 + - k__BackingField: PER_ATK_multiplier + k__BackingField: + k__BackingField: 3 + k__BackingField: 0 + k__BackingField: 2 + k__BackingField: 1 + k__BackingField: + k__BackingField: 1 + k__BackingField: 4 + k__BackingField: 0 + k__BackingField: 0 + k__BackingField: 2 + k__BackingField: 1 + k__BackingField: [] + - k__BackingField: REF_ATK_multiplier + k__BackingField: + k__BackingField: 3 + k__BackingField: 0 + k__BackingField: 2 + k__BackingField: 1 + k__BackingField: + k__BackingField: 1 + k__BackingField: 2 + k__BackingField: 0 + k__BackingField: 0 + k__BackingField: 3 + k__BackingField: 1.05 + k__BackingField: [] + racialBonuses: + - race: 1 startingPerks: perks: [] - maxPartySize: 4 + permanentModifiers: + modifiers: + - k__BackingField: KNO_bonus + k__BackingField: + k__BackingField: 1 + k__BackingField: 3 + k__BackingField: 0 + k__BackingField: 0 + k__BackingField: + k__BackingField: 0 + k__BackingField: 0 + k__BackingField: 0 + k__BackingField: 0 + k__BackingField: 2 + k__BackingField: 1 + k__BackingField: [] + - race: 2 + startingPerks: + perks: [] + permanentModifiers: + modifiers: + - k__BackingField: REF_bonus + k__BackingField: + k__BackingField: 1 + k__BackingField: 2 + k__BackingField: 0 + k__BackingField: 0 + k__BackingField: + k__BackingField: 0 + k__BackingField: 0 + k__BackingField: 0 + k__BackingField: 0 + k__BackingField: 2 + k__BackingField: 1 + k__BackingField: [] + - race: 3 + startingPerks: + perks: [] + permanentModifiers: + modifiers: + - k__BackingField: MGT_bonus + k__BackingField: + k__BackingField: 0 + k__BackingField: 0 + k__BackingField: 0 + k__BackingField: 0 + k__BackingField: + k__BackingField: 0 + k__BackingField: 0 + k__BackingField: 0 + k__BackingField: 0 + k__BackingField: 2 + k__BackingField: 1 + k__BackingField: [] + classBonuses: + - class: 1 + startingPerks: + perks: [] + permanentModifiers: + modifiers: + - k__BackingField: warrior_might_health_bonus + k__BackingField: + k__BackingField: 2 + k__BackingField: 0 + k__BackingField: 1 + k__BackingField: 0 + k__BackingField: + k__BackingField: 1 + k__BackingField: 1 + k__BackingField: 0 + k__BackingField: 0 + k__BackingField: 2 + k__BackingField: 4 + k__BackingField: + - k__BackingField: 1 + k__BackingField: 5 + - class: 2 + startingPerks: + perks: [] + permanentModifiers: + modifiers: + - k__BackingField: rogue_REF_bonus + k__BackingField: + k__BackingField: 3 + k__BackingField: 0 + k__BackingField: 0 + k__BackingField: 2 + k__BackingField: + k__BackingField: 1 + k__BackingField: 2 + k__BackingField: 0 + k__BackingField: 0 + k__BackingField: 2 + k__BackingField: 1 + k__BackingField: + - k__BackingField: 2 + k__BackingField: 4 + - class: 2 + startingPerks: + perks: [] + permanentModifiers: + modifiers: + - k__BackingField: rogue_PER_bonus + k__BackingField: + k__BackingField: 3 + k__BackingField: 0 + k__BackingField: 0 + k__BackingField: 1 + k__BackingField: + k__BackingField: 1 + k__BackingField: 4 + k__BackingField: 0 + k__BackingField: 0 + k__BackingField: 2 + k__BackingField: 1 + k__BackingField: + - k__BackingField: 4 + k__BackingField: 3 + - class: 3 + startingPerks: + perks: [] + permanentModifiers: + modifiers: + - k__BackingField: rmage_KNO_bonus + k__BackingField: + k__BackingField: 3 + k__BackingField: 0 + k__BackingField: 0 + k__BackingField: 1 + k__BackingField: + k__BackingField: 1 + k__BackingField: 3 + k__BackingField: 0 + k__BackingField: 0 + k__BackingField: 2 + k__BackingField: 1 + k__BackingField: + - k__BackingField: 3 + k__BackingField: 5 + - class: 4 + startingPerks: + perks: [] + permanentModifiers: + modifiers: + - k__BackingField: herald_ALL_bonus + k__BackingField: + k__BackingField: 3 + k__BackingField: 0 + k__BackingField: 0 + k__BackingField: 3 + k__BackingField: + k__BackingField: 1 + k__BackingField: 3 + k__BackingField: 0 + k__BackingField: 0 + k__BackingField: 2 + k__BackingField: 2 + k__BackingField: + - k__BackingField: 1 + k__BackingField: 2 + - k__BackingField: 2 + k__BackingField: 2 + - k__BackingField: 3 + k__BackingField: 2 + - k__BackingField: 4 + k__BackingField: 2 diff --git a/Assets/Prefabs/UI/Attribute.prefab b/Assets/Prefabs/UI/Attribute.prefab index c793faf..2bbae63 100644 --- a/Assets/Prefabs/UI/Attribute.prefab +++ b/Assets/Prefabs/UI/Attribute.prefab @@ -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 diff --git a/Assets/Prefabs/UI/CharacterCreationReference.prefab b/Assets/Prefabs/UI/CharacterCreationReference.prefab index 03d2adc..65d4d95 100644 --- a/Assets/Prefabs/UI/CharacterCreationReference.prefab +++ b/Assets/Prefabs/UI/CharacterCreationReference.prefab @@ -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: [] diff --git a/Assets/Prefabs/UI/LogContainer.prefab b/Assets/Prefabs/UI/LogContainer.prefab index ebce87f..c1f7ec1 100644 --- a/Assets/Prefabs/UI/LogContainer.prefab +++ b/Assets/Prefabs/UI/LogContainer.prefab @@ -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: diff --git a/Assets/Prefabs/UI/LogEntry.prefab b/Assets/Prefabs/UI/LogEntry.prefab index 8a94cd7..a748925 100644 --- a/Assets/Prefabs/UI/LogEntry.prefab +++ b/Assets/Prefabs/UI/LogEntry.prefab @@ -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 diff --git a/Packages/com.jovian.ingame-logging/Runtime/UI/GameLogView.cs b/Packages/com.jovian.ingame-logging/Runtime/UI/GameLogView.cs index 4aad9ad..d9b983e 100644 --- a/Packages/com.jovian.ingame-logging/Runtime/UI/GameLogView.cs +++ b/Packages/com.jovian.ingame-logging/Runtime/UI/GameLogView.cs @@ -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 activeEntries = new(); readonly Stack 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; } }