First commit
This commit is contained in:
8
Assets/Code/GameState/Camera.meta
Normal file
8
Assets/Code/GameState/Camera.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 780f3ab23ddb9c24988bc45c1a8e1377
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
171
Assets/Code/GameState/Camera/CameraController.cs
Normal file
171
Assets/Code/GameState/Camera/CameraController.cs
Normal file
@@ -0,0 +1,171 @@
|
||||
#nullable enable
|
||||
using Nox.Platform;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AddressableAssets;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
namespace Nox.Game {
|
||||
public class CameraController : ICameraController {
|
||||
private readonly PlatformSettings? platformSettings;
|
||||
private readonly InputSystem_Actions? inputActions;
|
||||
private readonly MapReference? mapReference;
|
||||
|
||||
private CameraSettings? cameraSettings;
|
||||
|
||||
private InputAction? zoomAction;
|
||||
private InputAction? panAction;
|
||||
private InputAction? holdToDragAction;
|
||||
private Bounds planeBounds;
|
||||
private Vector3 dragWorldOrigin;
|
||||
private bool isDragging;
|
||||
private readonly Plane groundPlane = new Plane(Vector3.up, Vector3.zero);
|
||||
private readonly Texture2D cursorPoint;
|
||||
private readonly Texture2D cursorDrag;
|
||||
|
||||
public CameraReference? CameraReference { get; private set; }
|
||||
|
||||
public CameraController(PlatformSettings? platformSettings, MapReference? mapReference) {
|
||||
this.platformSettings = platformSettings;
|
||||
this.mapReference = mapReference;
|
||||
inputActions = platformSettings?.inputSettings.inputActions;
|
||||
|
||||
cursorPoint = Addressables.LoadAssetAsync<Texture2D>("Assets/Art/UI/Cursor_Pointer").WaitForCompletion();
|
||||
cursorDrag = Addressables.LoadAssetAsync<Texture2D>("Assets/Art/UI/Cursor_Drag").WaitForCompletion();
|
||||
Cursor.SetCursor(cursorPoint, new Vector2(12,4), CursorMode.Auto);
|
||||
}
|
||||
|
||||
public void Initialize() {
|
||||
if(inputActions == null || !mapReference) {
|
||||
return;
|
||||
}
|
||||
zoomAction = inputActions.Player.Zoom;
|
||||
panAction = inputActions.Player.PanMap;
|
||||
holdToDragAction = inputActions.Player.HoldToDrag;
|
||||
planeBounds = mapReference.mapPlane.GetComponent<BoxCollider>().bounds;
|
||||
SetupCamera();
|
||||
}
|
||||
|
||||
private void SetupCamera() {
|
||||
CameraReference = Object.FindAnyObjectByType<CameraReference>();
|
||||
|
||||
if(!CameraReference) {
|
||||
if(platformSettings?.cameraPrefab != null) {
|
||||
GameObject? go = Object.Instantiate(platformSettings.cameraPrefab);
|
||||
CameraReference = go.GetComponentInChildren<CameraReference>();
|
||||
if(!CameraReference) {
|
||||
Debug.LogError("Camera prefab does not contain a CameraReference component");
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
Debug.LogError("No camera prefab found and no camera reference found");
|
||||
}
|
||||
}
|
||||
|
||||
cameraSettings = CameraReference!.cameraSettings;
|
||||
if(!cameraSettings) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set initial zoom to maximum allowed by map boundaries
|
||||
var camera = CameraReference.mainCamera;
|
||||
if(camera.orthographic) {
|
||||
float maxHalfWidth = planeBounds.size.x * 0.5f;
|
||||
float maxHalfHeight = planeBounds.size.z * 0.5f;
|
||||
float maxOrthoSize = Mathf.Min(maxHalfHeight, maxHalfWidth / camera.aspect);
|
||||
camera.orthographicSize = Mathf.Min(cameraSettings.maxZoom, maxOrthoSize);
|
||||
}
|
||||
else {
|
||||
// Perspective camera: set height so the map fits in view
|
||||
float mapWidth = planeBounds.size.x;
|
||||
float mapHeight = planeBounds.size.z;
|
||||
float aspect = camera.aspect;
|
||||
float fovRad = camera.fieldOfView * Mathf.Deg2Rad;
|
||||
float tanFov = Mathf.Tan(fovRad / 2f);
|
||||
float requiredHeightByWidth = mapWidth / (2f * tanFov * aspect);
|
||||
float requiredHeightByHeight = mapHeight / (2f * tanFov);
|
||||
float requiredHeight = Mathf.Max(requiredHeightByWidth, requiredHeightByHeight);
|
||||
// Center camera on map and set height
|
||||
Vector3 camPos = planeBounds.center;
|
||||
camPos.y = requiredHeight;
|
||||
camera.transform.position = camPos;
|
||||
// Look straight down
|
||||
camera.transform.rotation = Quaternion.Euler(90f, 0f, 0f);
|
||||
}
|
||||
}
|
||||
|
||||
public void Tick() {
|
||||
PanCamera();
|
||||
ZoomCamera();
|
||||
}
|
||||
|
||||
private void ZoomCamera() {
|
||||
if(zoomAction == null) {
|
||||
return;
|
||||
}
|
||||
float zoomDelta = zoomAction.ReadValue<Vector2>().y;
|
||||
if(zoomDelta == 0) {
|
||||
return;
|
||||
}
|
||||
var camera = CameraReference!.mainCamera;
|
||||
if(camera.orthographic) {
|
||||
camera.orthographicSize = Mathf.Clamp(camera.orthographicSize - (zoomDelta * cameraSettings!.zoomSpeed * Time.deltaTime), cameraSettings.minZoom, cameraSettings.maxZoom);
|
||||
}
|
||||
else {
|
||||
camera.transform.Translate(Vector3.forward * zoomDelta * cameraSettings!.zoomSpeed * Time.deltaTime, Space.Self);
|
||||
}
|
||||
}
|
||||
|
||||
private void PanCamera() {
|
||||
if(holdToDragAction == null || CameraReference == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(holdToDragAction.IsInProgress()) {
|
||||
Camera camera = CameraReference.mainCamera;
|
||||
Vector2 screenPos = inputActions!.Player.Point.ReadValue<Vector2>();
|
||||
|
||||
if(!isDragging) {
|
||||
isDragging = true;
|
||||
Cursor.lockState = CursorLockMode.Confined;
|
||||
Cursor.SetCursor(cursorDrag, Vector2.zero, CursorMode.Auto);
|
||||
dragWorldOrigin = ScreenToGroundPoint(camera, screenPos);
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 currentWorldPoint = ScreenToGroundPoint(camera, screenPos);
|
||||
Vector3 offset = dragWorldOrigin - currentWorldPoint;
|
||||
|
||||
Vector3 camPos = camera.transform.position;
|
||||
camPos.x += offset.x;
|
||||
camPos.z += offset.z;
|
||||
|
||||
camPos.x = Mathf.Clamp(camPos.x, planeBounds.min.x, planeBounds.max.x);
|
||||
camPos.z = Mathf.Clamp(camPos.z, planeBounds.min.z, planeBounds.max.z);
|
||||
camera.transform.position = camPos;
|
||||
|
||||
dragWorldOrigin = ScreenToGroundPoint(camera, screenPos);
|
||||
}
|
||||
else {
|
||||
if(!isDragging) {
|
||||
return;
|
||||
}
|
||||
isDragging = false;
|
||||
Cursor.lockState = CursorLockMode.None;
|
||||
Cursor.SetCursor(cursorPoint, new Vector2(12,4), CursorMode.Auto);
|
||||
}
|
||||
}
|
||||
|
||||
private Vector3 ScreenToGroundPoint(Camera camera, Vector2 screenPos) {
|
||||
Ray ray = camera.ScreenPointToRay(new Vector3(screenPos.x, screenPos.y, 0f));
|
||||
if(groundPlane.Raycast(ray, out float distance)) {
|
||||
return ray.GetPoint(distance);
|
||||
}
|
||||
return camera.transform.position;
|
||||
}
|
||||
public void Dispose() {
|
||||
inputActions?.Disable();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
3
Assets/Code/GameState/Camera/CameraController.cs.meta
Normal file
3
Assets/Code/GameState/Camera/CameraController.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 517c20f5b3ec4700afd07917ea316092
|
||||
timeCreated: 1771075174
|
||||
9
Assets/Code/GameState/Camera/CameraReference.cs
Normal file
9
Assets/Code/GameState/Camera/CameraReference.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Nox.Game {
|
||||
public class CameraReference : MonoBehaviour {
|
||||
public Camera mainCamera;
|
||||
public CameraSettings cameraSettings;
|
||||
}
|
||||
}
|
||||
2
Assets/Code/GameState/Camera/CameraReference.cs.meta
Normal file
2
Assets/Code/GameState/Camera/CameraReference.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c55bcf874b547124a861f2b9494f991e
|
||||
10
Assets/Code/GameState/Camera/CameraSettings.cs
Normal file
10
Assets/Code/GameState/Camera/CameraSettings.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Nox.Game {
|
||||
[CreateAssetMenu(fileName = "CameraSettings", menuName = "Nox/Camera Settings")]
|
||||
public class CameraSettings : ScriptableObject {
|
||||
public float zoomSpeed = 2f;
|
||||
public float minZoom = 5f;
|
||||
public float maxZoom = 20f;
|
||||
}
|
||||
}
|
||||
3
Assets/Code/GameState/Camera/CameraSettings.cs.meta
Normal file
3
Assets/Code/GameState/Camera/CameraSettings.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b508138d992c4f7280afcc0e1d6c4e8f
|
||||
timeCreated: 1771153846
|
||||
10
Assets/Code/GameState/Camera/ICameraController.cs
Normal file
10
Assets/Code/GameState/Camera/ICameraController.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Nox.Game {
|
||||
public interface ICameraController {
|
||||
CameraReference CameraReference {get;}
|
||||
void Initialize();
|
||||
void Tick();
|
||||
void Dispose();
|
||||
}
|
||||
}
|
||||
3
Assets/Code/GameState/Camera/ICameraController.cs.meta
Normal file
3
Assets/Code/GameState/Camera/ICameraController.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e0a1d5f367bb4b3fb740541f9a0ba163
|
||||
timeCreated: 1771152354
|
||||
8
Assets/Code/GameState/Entities.meta
Normal file
8
Assets/Code/GameState/Entities.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b7671be7321935f46b97f6859276be8b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
33
Assets/Code/GameState/Entities/CharacterAttributesFactory.cs
Normal file
33
Assets/Code/GameState/Entities/CharacterAttributesFactory.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using ZLinq;
|
||||
|
||||
namespace Nox.Game {
|
||||
public interface ICharacterAttributesFactory {
|
||||
EntityAttributes Create(IEntityDefinition entityDefinition);
|
||||
}
|
||||
|
||||
public sealed class CharacterAttributesFactory : ICharacterAttributesFactory {
|
||||
private readonly IModifierResolver modifierResolver;
|
||||
|
||||
public CharacterAttributesFactory(IModifierResolver modifierResolver) {
|
||||
this.modifierResolver = modifierResolver ?? throw new ArgumentNullException(nameof(modifierResolver));
|
||||
}
|
||||
|
||||
public EntityAttributes Create(IEntityDefinition entityDefinition) {
|
||||
var attributes = entityDefinition.Attributes;
|
||||
|
||||
if(attributes.attributes.AsValueEnumerable().Any(a => a.value <= 0)) {
|
||||
throw new ArgumentOutOfRangeException( "attributes cannot be zero or negative.", new ArgumentException() );
|
||||
}
|
||||
|
||||
return new EntityAttributes {
|
||||
attributes = attributes.attributes.AsValueEnumerable()
|
||||
.Select(a => {
|
||||
var modifiers = modifierResolver.CollectModifiers(entityDefinition, a.attribute);
|
||||
return new Attribute(a.attribute, modifierResolver.Resolve(a.value, modifiers, entityDefinition));
|
||||
})
|
||||
.ToArray()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1c5d19a12e554fd981cec483ccfcff68
|
||||
timeCreated: 1774183852
|
||||
139
Assets/Code/GameState/Entities/CharacterFactory.cs
Normal file
139
Assets/Code/GameState/Entities/CharacterFactory.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using System;
|
||||
using ZLinq;
|
||||
|
||||
namespace Nox.Game {
|
||||
public interface ICharacterFactory {
|
||||
CharacterDefinition CreateProtagonist(CharacterCreationRequest request);
|
||||
CharacterDefinition CreateFromTemplate(CharacterTemplate template, CharacterRole role = CharacterRole.Companion);
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
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;
|
||||
public EntityAttributes Attributes { get; set; } = GetDefaultAttributes();
|
||||
public EntityStats Stats { get; set; } = new() { stats = Array.Empty<Stat>() };
|
||||
public PerksData Perks { get; set; } = new();
|
||||
public ModifiersData Modifiers { get; set; } = new();
|
||||
|
||||
private static int GetRandomInt(int start, int end) => new Random().Next(start, end);
|
||||
private static EntityAttributes GetDefaultAttributes() {
|
||||
var attributes = new EntityAttributes {
|
||||
attributes = new Attribute[] {
|
||||
new (AttributeType.Might, 1),
|
||||
new (AttributeType.Knowledge, 1),
|
||||
new (AttributeType.Perception, 1),
|
||||
new (AttributeType.Reflex, 1)
|
||||
}
|
||||
};
|
||||
|
||||
return attributes;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
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;
|
||||
public EntityAttributes Attributes { get; set; }
|
||||
public EntityStats Stats { get; set; } = new() { stats = Array.Empty<Stat>() };
|
||||
public PerksData Perks { get; set; } = new();
|
||||
public ModifiersData Modifiers { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class CharacterFactory : ICharacterFactory {
|
||||
private readonly ICharacterAttributesFactory attributesFactory;
|
||||
private readonly ICharacterStatsFactory statsFactory;
|
||||
private readonly IPerkFactory perkFactory;
|
||||
private readonly IModifiersFactory modifiersFactory;
|
||||
|
||||
public CharacterFactory(
|
||||
ICharacterAttributesFactory attributesFactory,
|
||||
ICharacterStatsFactory statsFactory,
|
||||
IPerkFactory perkFactory,
|
||||
IModifiersFactory modifiersFactory) {
|
||||
this.attributesFactory = attributesFactory ?? throw new ArgumentNullException(nameof(attributesFactory));
|
||||
this.statsFactory = statsFactory ?? throw new ArgumentNullException(nameof(statsFactory));
|
||||
this.perkFactory = perkFactory ?? throw new ArgumentNullException(nameof(perkFactory));
|
||||
this.modifiersFactory = modifiersFactory ?? throw new ArgumentNullException(nameof(modifiersFactory));
|
||||
}
|
||||
|
||||
public CharacterDefinition CreateProtagonist(CharacterCreationRequest request) {
|
||||
if(request == null) {
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
}
|
||||
|
||||
var attributes = attributesFactory.Create(request);
|
||||
var stats = statsFactory.Create(request);
|
||||
|
||||
var character = new CharacterDefinition {
|
||||
Id = request.Id == Guid.Empty ? Guid.NewGuid() : request.Id,
|
||||
Name = request.Name,
|
||||
Race = request.Race,
|
||||
Class = request.Class,
|
||||
Role = CharacterRole.Protagonist,
|
||||
PortraitIndex = request.PortraitIndex,
|
||||
Attributes = attributes,
|
||||
Stats = stats,
|
||||
Perks = request.Perks ?? new PerksData(),
|
||||
Modifiers = request.Modifiers ?? new ModifiersData()
|
||||
};
|
||||
|
||||
AddStartingPerks(character, request.Perks);
|
||||
return character;
|
||||
}
|
||||
|
||||
public CharacterDefinition CreateFromTemplate(CharacterTemplate template, CharacterRole role = CharacterRole.Companion) {
|
||||
if(template == null) {
|
||||
throw new ArgumentNullException(nameof(template));
|
||||
}
|
||||
|
||||
template.Perks ??= new PerksData();
|
||||
template.Modifiers ??= new ModifiersData();
|
||||
|
||||
var character = new CharacterDefinition {
|
||||
Id = template.Id == Guid.Empty ? Guid.NewGuid() : template.Id,
|
||||
Name = template.Name,
|
||||
Race = template.Race,
|
||||
Class = template.Class,
|
||||
Role = role,
|
||||
Attributes = attributesFactory.Create(template),
|
||||
Stats = statsFactory.Create(template),
|
||||
Perks = template.Perks,
|
||||
Modifiers = template.Modifiers
|
||||
};
|
||||
|
||||
AddStartingPerks(character, template.Perks);
|
||||
AddStartingModifiers(character, template.Modifiers);
|
||||
return character;
|
||||
}
|
||||
|
||||
private void AddStartingPerks(CharacterDefinition character, PerksData perkData) {
|
||||
if(perkData?.perks == null || perkData.perks.Count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach(var perkId in perkData.perks.AsValueEnumerable().Distinct()) {
|
||||
perkFactory.TryAddPerk(character, perkId.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddStartingModifiers(CharacterDefinition character, ModifiersData modifiersData) {
|
||||
if(modifiersData?.modifiers == null || modifiersData.modifiers.Count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach(var modifierId in modifiersData.modifiers.AsValueEnumerable().Distinct()) {
|
||||
modifiersFactory.TryAddModifier(character, modifierId.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Code/GameState/Entities/CharacterFactory.cs.meta
Normal file
3
Assets/Code/GameState/Entities/CharacterFactory.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0033c4836b0d4fbbb6e4dde0ac23a2f6
|
||||
timeCreated: 1774183767
|
||||
10
Assets/Code/GameState/Entities/CharacterRegistry.cs
Normal file
10
Assets/Code/GameState/Entities/CharacterRegistry.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Nox.Game {
|
||||
[CreateAssetMenu(fileName = "CharacterRegistry", menuName = "Nox/Database/Entities/Character Registry")]
|
||||
public class CharacterRegistry: ScriptableObject {
|
||||
public CharacterDefinition defaultCharacter;
|
||||
public List<CharacterDefinition> characters = new ();
|
||||
}
|
||||
}
|
||||
3
Assets/Code/GameState/Entities/CharacterRegistry.cs.meta
Normal file
3
Assets/Code/GameState/Entities/CharacterRegistry.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 91e3086d6951456e9a8a59fcbac0f750
|
||||
timeCreated: 1774179358
|
||||
40
Assets/Code/GameState/Entities/CharacterStatsFactory.cs
Normal file
40
Assets/Code/GameState/Entities/CharacterStatsFactory.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using ZLinq;
|
||||
|
||||
namespace Nox.Game {
|
||||
public interface ICharacterStatsFactory {
|
||||
EntityStats Create(IEntityDefinition entityDefinition);
|
||||
}
|
||||
|
||||
public sealed class CharacterStatsFactory : ICharacterStatsFactory {
|
||||
private readonly StarterCharacterSettings baseSettings;
|
||||
private readonly IModifierResolver modifierResolver;
|
||||
|
||||
public CharacterStatsFactory(StarterCharacterSettings baseSettings, IModifierResolver modifierResolver) {
|
||||
this.baseSettings = baseSettings;
|
||||
this.modifierResolver = modifierResolver ?? throw new ArgumentNullException(nameof(modifierResolver));
|
||||
}
|
||||
|
||||
public EntityStats Create(IEntityDefinition entityDefinition) {
|
||||
var attributes = entityDefinition.Attributes;
|
||||
|
||||
if(attributes.attributes.AsValueEnumerable().Any(a => a.value <= 0)) {
|
||||
throw new ArgumentOutOfRangeException( "attributes cannot be zero or negative.", new ArgumentException() );
|
||||
}
|
||||
|
||||
var healthModifiers = modifierResolver.CollectModifiers(entityDefinition, StatType.Health);
|
||||
var manaModifiers = 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, entityDefinition)),
|
||||
new Stat(StatType.Mana, modifierResolver.Resolve(baseSettings.defaultEntityStats.GetValue(StatType.Mana), manaModifiers, 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))
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 75a241c095744a518eb8d1c1b14d24e4
|
||||
timeCreated: 1774183822
|
||||
26
Assets/Code/GameState/Entities/CharacterSystems.cs
Normal file
26
Assets/Code/GameState/Entities/CharacterSystems.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
namespace Nox.Game {
|
||||
|
||||
public interface ICharacterSystems {
|
||||
IPerkFactory PerkFactory { get; }
|
||||
IModifiersFactory ModifiersFactory { get; }
|
||||
IModifierResolver ModifierResolver { get; }
|
||||
ICharacterFactory CharacterFactory { get; }
|
||||
IPartyFactory PartyFactory { get; }
|
||||
}
|
||||
|
||||
public sealed class CharacterSystems : ICharacterSystems {
|
||||
public CharacterSystems(IPerkFactory perkFactory, IModifiersFactory modifiersFactory, IModifierResolver modifierResolver, ICharacterFactory characterFactory, IPartyFactory partyFactory) {
|
||||
ModifiersFactory = modifiersFactory;
|
||||
ModifierResolver = modifierResolver;
|
||||
PerkFactory = perkFactory;
|
||||
CharacterFactory = characterFactory;
|
||||
PartyFactory = partyFactory;
|
||||
}
|
||||
|
||||
public IPerkFactory PerkFactory { get; }
|
||||
public IModifiersFactory ModifiersFactory { get; }
|
||||
public IModifierResolver ModifierResolver { get; }
|
||||
public ICharacterFactory CharacterFactory { get; }
|
||||
public IPartyFactory PartyFactory { get; }
|
||||
}
|
||||
}
|
||||
3
Assets/Code/GameState/Entities/CharacterSystems.cs.meta
Normal file
3
Assets/Code/GameState/Entities/CharacterSystems.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e9cec54a2252457595e3a97d3dcd755d
|
||||
timeCreated: 1774184077
|
||||
22
Assets/Code/GameState/Entities/CharacterSystemsFactory.cs
Normal file
22
Assets/Code/GameState/Entities/CharacterSystemsFactory.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
|
||||
namespace Nox.Game {
|
||||
public static class CharacterSystemsFactory {
|
||||
public static ICharacterSystems Create(
|
||||
PartySettings partySettings,
|
||||
StarterCharacterSettings starterCharacterSettings,
|
||||
PerksRegistry perksRegistry,
|
||||
CharacterRegistry characterRegistry,
|
||||
ModifiersRegistry modifiersRegistry) {
|
||||
IPerkFactory perkFactory = new PerkFactory(perksRegistry);
|
||||
IModifiersFactory modifiersFactory = new ModifiersFactory(modifiersRegistry);
|
||||
IModifierResolver modifierResolver = new ModifierResolver();
|
||||
ICharacterAttributesFactory attributesFactory = new CharacterAttributesFactory(modifierResolver);
|
||||
ICharacterStatsFactory statsFactory = new CharacterStatsFactory(starterCharacterSettings, modifierResolver);
|
||||
ICharacterFactory characterFactory = new CharacterFactory(attributesFactory, statsFactory, perkFactory, modifiersFactory);
|
||||
IPartyFactory partyFactory = new PartyFactory(partySettings, starterCharacterSettings);
|
||||
|
||||
return new CharacterSystems(perkFactory, modifiersFactory, modifierResolver, characterFactory, partyFactory);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 60db4014b69d403db42ee766204eb2d7
|
||||
timeCreated: 1774183978
|
||||
192
Assets/Code/GameState/Entities/EntitiesDefinitions.cs
Normal file
192
Assets/Code/GameState/Entities/EntitiesDefinitions.cs
Normal file
@@ -0,0 +1,192 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ZLinq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Nox.Game {
|
||||
public interface IEntityDefinition {
|
||||
Guid Id { get; }
|
||||
string Name { get; }
|
||||
int PortraitIndex { get; }
|
||||
CharacterRace Race { get; }
|
||||
CharacterClass Class { get; }
|
||||
CharacterRole Role { get; }
|
||||
EntityAttributes Attributes { get; }
|
||||
EntityStats Stats { get; }
|
||||
PerksData Perks { get; }
|
||||
ModifiersData Modifiers { get; }
|
||||
|
||||
}
|
||||
|
||||
public enum StatType {
|
||||
None,
|
||||
Health,
|
||||
Mana,
|
||||
Level,
|
||||
Experience
|
||||
}
|
||||
|
||||
public enum AttributeType {
|
||||
None,
|
||||
Might,
|
||||
Reflex,
|
||||
Knowledge,
|
||||
Perception
|
||||
}
|
||||
|
||||
public enum CombatScoreType {
|
||||
None,
|
||||
ATK,
|
||||
DEF,
|
||||
INIT,
|
||||
SPD,
|
||||
RES
|
||||
}
|
||||
|
||||
public enum CharacterRole {
|
||||
None,
|
||||
Protagonist,
|
||||
Companion
|
||||
}
|
||||
|
||||
public enum CharacterClass {
|
||||
None,
|
||||
Warrior,
|
||||
Rogue,
|
||||
Mage,
|
||||
Herald
|
||||
}
|
||||
|
||||
public enum CharacterRace {
|
||||
None,
|
||||
Human,
|
||||
DarkElf,
|
||||
Tunneler
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed record Stat {
|
||||
public StatType stat;
|
||||
public int value;
|
||||
public Stat(StatType stat, int value) {
|
||||
this.stat = stat;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed record Attribute {
|
||||
public AttributeType attribute;
|
||||
public int value;
|
||||
public Attribute(AttributeType attribute, int value) {
|
||||
this.attribute = attribute;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class EntityAttributes {
|
||||
public Attribute[] attributes;
|
||||
|
||||
public static EntityAttributes operator +(EntityAttributes a, EntityAttributes b) {
|
||||
return new EntityAttributes {
|
||||
attributes = a.attributes.AsValueEnumerable()
|
||||
.Select(attr => {
|
||||
var match = b.attributes?.AsValueEnumerable().FirstOrDefault(attr2 => attr2.attribute == attr.attribute);
|
||||
return new Attribute(attr.attribute, attr.value + (match?.value ?? 0));
|
||||
})
|
||||
.ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
public int GetValue(AttributeType attributeType) {
|
||||
return attributes.AsValueEnumerable().First(attr => attr.attribute == attributeType).value;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return $"Attributes: {string.Join(", ", attributes.Select(attr => $"{attr.attribute}: {attr.value}"))}";
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class EntityStats {
|
||||
public Stat[] stats;
|
||||
|
||||
public int GetValue(StatType statType) {
|
||||
if(stats == null) {
|
||||
return 0;
|
||||
}
|
||||
var match = stats.AsValueEnumerable().FirstOrDefault(stat => stat.stat == statType);
|
||||
return match?.value ?? 0;
|
||||
}
|
||||
|
||||
public override string ToString() {
|
||||
return $"Stats: {string.Join(", ", stats.Select(stat => $"{stat.stat}: {stat.value}"))}";
|
||||
}
|
||||
}
|
||||
|
||||
[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; }
|
||||
[field: SerializeField] public CharacterRole Role { get; set; }
|
||||
[field: SerializeField] public EntityAttributes Attributes { get; set; }
|
||||
[field: SerializeField] public EntityStats Stats { get; set; }
|
||||
[field: SerializeField] public PerksData Perks { get; set; }
|
||||
[field: SerializeField] public ModifiersData Modifiers { get; set; }
|
||||
|
||||
public CharacterDefinition Clone() {
|
||||
return new CharacterDefinition {
|
||||
Id = Guid.NewGuid(),
|
||||
Name = Name,
|
||||
Role = Role,
|
||||
Race = Race,
|
||||
Class = Class,
|
||||
PortraitIndex = PortraitIndex,
|
||||
Attributes = new EntityAttributes {
|
||||
attributes = Attributes?.attributes?.AsValueEnumerable().Select(a => new Attribute(a.attribute, a.value)).ToArray()
|
||||
},
|
||||
Stats = new EntityStats {
|
||||
stats = Stats?.stats?.AsValueEnumerable().Select(s => new Stat(s.stat, s.value)).ToArray()
|
||||
},
|
||||
Perks = new PerksData {
|
||||
perks = Perks?.perks?.AsValueEnumerable().Select(p => new PerkDefinition {
|
||||
Id = p.Id,
|
||||
Name = p.Name,
|
||||
Modifiers = p.Modifiers
|
||||
}).ToList() ?? new List<PerkDefinition>()
|
||||
},
|
||||
Modifiers = new ModifiersData {
|
||||
modifiers = Modifiers?.modifiers?.AsValueEnumerable().Select(m => new ModifierDefinition {
|
||||
Id = m.Id,
|
||||
Name = m.Name,
|
||||
Target = m.Target,
|
||||
ScalingSource = m.ScalingSource,
|
||||
Operation = m.Operation,
|
||||
Value = m.Value,
|
||||
Requirements = m.Requirements?.ToList() ?? new List<ModifierRequirement>()
|
||||
}).ToList() ?? new List<ModifierDefinition>()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class PartyDefinition {
|
||||
public int maxPartySize;
|
||||
public List<CharacterDefinition> members = new();
|
||||
|
||||
[JsonIgnore]
|
||||
public CharacterDefinition Protagonist => members.AsValueEnumerable().FirstOrDefault(m => m.Role == CharacterRole.Protagonist);
|
||||
|
||||
[JsonIgnore]
|
||||
public IReadOnlyList<CharacterDefinition> Companions => members.AsValueEnumerable().Where(m => m.Role == CharacterRole.Companion).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 98b50e969ea0412ca3575cd28b6018b7
|
||||
timeCreated: 1772574533
|
||||
159
Assets/Code/GameState/Entities/ModifiersFactory.cs
Normal file
159
Assets/Code/GameState/Entities/ModifiersFactory.cs
Normal file
@@ -0,0 +1,159 @@
|
||||
using Jovian.Logger;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ZLinq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Nox.Game {
|
||||
public interface IModifier {
|
||||
string Name { get; }
|
||||
Guid Id { get; }
|
||||
ModifierTarget Target { get; }
|
||||
ModifierTarget ScalingSource { get; }
|
||||
ModifierOperation Operation { get; }
|
||||
float Value { get; }
|
||||
IReadOnlyList<ModifierRequirement> Requirements { get; }
|
||||
}
|
||||
|
||||
public interface IModifiersFactory {
|
||||
IReadOnlyCollection<IModifier> GetAll();
|
||||
IModifier GetById(Guid modifierId);
|
||||
IReadOnlyCollection<IModifier> GetModifiersFor(IEntityDefinition character);
|
||||
bool TryAddModifier(IEntityDefinition character, Guid modiferId);
|
||||
}
|
||||
|
||||
public enum ModifierOperation {
|
||||
None,
|
||||
Flat,
|
||||
Addition,
|
||||
Multiplication,
|
||||
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 ModifierTarget Target { get; set; }
|
||||
[field: SerializeField] public ModifierTarget ScalingSource { get; set; }
|
||||
[field: SerializeField] public ModifierOperation Operation { 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 override string ToString() {
|
||||
return $"Modifiers: {string.Join(", ", modifiers.Select(modifier => $"{modifier.Name}"))}";
|
||||
}
|
||||
}
|
||||
|
||||
public class ModifiersFactory : IModifiersFactory {
|
||||
private readonly ModifiersRegistry modifiersRegistry;
|
||||
private readonly Dictionary<Guid, IModifier> modifierPool = new();
|
||||
|
||||
public ModifiersFactory(ModifiersRegistry modifiersRegistry) {
|
||||
this.modifiersRegistry = modifiersRegistry;
|
||||
var allAvailableModifiers = modifiersRegistry.modifiersData;
|
||||
foreach(var modifier in allAvailableModifiers.modifiers) {
|
||||
modifierPool.Add(modifier.Id, modifier);
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<IModifier> GetAll() {
|
||||
return modifiersRegistry.modifiersData.modifiers;
|
||||
}
|
||||
public IModifier GetById(Guid modifierId) {
|
||||
return modifiersRegistry.modifiersData.modifiers.AsValueEnumerable().FirstOrDefault(m => m.Id == modifierId);
|
||||
}
|
||||
public IReadOnlyCollection<IModifier> GetModifiersFor(IEntityDefinition character) {
|
||||
return character.Modifiers.modifiers;
|
||||
}
|
||||
public bool TryAddModifier(IEntityDefinition character, Guid modifierId) {
|
||||
if(character == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(modifierId == Guid.Empty) {
|
||||
GlobalLogger.LogException("Cannot add a modifier with an empty ID!", LogCategory.GameLogic);
|
||||
return false;
|
||||
}
|
||||
|
||||
if(character.Modifiers.modifiers.AsValueEnumerable().Any(p => p != null && p.Id == modifierId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!modifierPool.TryGetValue(modifierId, out var modifier)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
character.Modifiers.modifiers.Add(new ModifierDefinition {
|
||||
Id = modifier.Id,
|
||||
Name = modifier.Name,
|
||||
Target = modifier.Target,
|
||||
ScalingSource = modifier.ScalingSource,
|
||||
Operation = modifier.Operation,
|
||||
Value = modifier.Value,
|
||||
Requirements = modifier.Requirements?.ToList() ?? new List<ModifierRequirement>()
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Code/GameState/Entities/ModifiersFactory.cs.meta
Normal file
3
Assets/Code/GameState/Entities/ModifiersFactory.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 882798d3e5a24150b1f11f486afdbc3e
|
||||
timeCreated: 1774199617
|
||||
8
Assets/Code/GameState/Entities/ModifiersRegistry.cs
Normal file
8
Assets/Code/GameState/Entities/ModifiersRegistry.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Nox.Game {
|
||||
[CreateAssetMenu(fileName = "ModifiersRegistry", menuName = "Nox/Database/Entities/Modifiers Registry")]
|
||||
public class ModifiersRegistry : ScriptableObject {
|
||||
public ModifiersData modifiersData;
|
||||
}
|
||||
}
|
||||
3
Assets/Code/GameState/Entities/ModifiersRegistry.cs.meta
Normal file
3
Assets/Code/GameState/Entities/ModifiersRegistry.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e6ae0d7bf0164ff793609e7f161fd7e3
|
||||
timeCreated: 1774200023
|
||||
160
Assets/Code/GameState/Entities/ModifiersResolver.cs
Normal file
160
Assets/Code/GameState/Entities/ModifiersResolver.cs
Normal file
@@ -0,0 +1,160 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ZLinq;
|
||||
|
||||
namespace Nox.Game {
|
||||
|
||||
public interface IModifierResolver {
|
||||
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>
|
||||
/// Collects modifiers from an entity's direct modifiers and perk-granted modifiers,
|
||||
/// then resolves them against a base value.
|
||||
///
|
||||
/// Resolution order:
|
||||
/// 1. Flat — sum of all flat values replaces the base
|
||||
/// 2. Addition — summed and added to the running total
|
||||
/// 3. Percentage — summed into a single multiplier applied to the post-addition total
|
||||
/// 4. Multiplication — each factor applied sequentially to the running total
|
||||
///
|
||||
/// 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, IEntityDefinition entity = null) {
|
||||
if(modifiers == null) {
|
||||
return baseValue;
|
||||
}
|
||||
|
||||
float flatSum = 0f;
|
||||
float addSum = 0f;
|
||||
float pctSum = 0f;
|
||||
var mulValues = new List<float>();
|
||||
var hasFlat = false;
|
||||
|
||||
foreach(var m in modifiers) {
|
||||
if(m == null || m.Operation == ModifierOperation.None) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var effectiveValue = ResolveScaling(m, entity);
|
||||
|
||||
switch(m.Operation) {
|
||||
case ModifierOperation.Flat:
|
||||
flatSum += effectiveValue;
|
||||
hasFlat = true;
|
||||
break;
|
||||
case ModifierOperation.Addition:
|
||||
addSum += effectiveValue;
|
||||
break;
|
||||
case ModifierOperation.Percentage:
|
||||
pctSum += effectiveValue;
|
||||
break;
|
||||
case ModifierOperation.Multiplication:
|
||||
mulValues.Add(effectiveValue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
float result = hasFlat ? flatSum : baseValue;
|
||||
result += addSum;
|
||||
result *= 1f + (pctSum / 100f);
|
||||
|
||||
foreach(var mul in mulValues) {
|
||||
result *= mul;
|
||||
}
|
||||
|
||||
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>();
|
||||
CollectFromEntity(entity, result, m => m.Target != null && m.Target.Matches(statType));
|
||||
return result;
|
||||
}
|
||||
|
||||
public IEnumerable<IModifier> CollectModifiers(IEntityDefinition entity, AttributeType attributeType) {
|
||||
if(entity == null) {
|
||||
return Array.Empty<IModifier>();
|
||||
}
|
||||
|
||||
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 && predicate(m) && MeetsRequirements(m, entity)) {
|
||||
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 && predicate(m)) {
|
||||
result.Add(m);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Code/GameState/Entities/ModifiersResolver.cs.meta
Normal file
3
Assets/Code/GameState/Entities/ModifiersResolver.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 130a7ebdbcf2417392d8fc3f9ae76eb2
|
||||
timeCreated: 1774176049
|
||||
28
Assets/Code/GameState/Entities/PartyCreatorModel.cs
Normal file
28
Assets/Code/GameState/Entities/PartyCreatorModel.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Nox.Game {
|
||||
public class PartyCreatorModel {
|
||||
private readonly ICharacterFactory characterFactory;
|
||||
private readonly IPartyFactory partyFactory;
|
||||
private readonly List<CharacterCreationRequest> characterCreationRequests;
|
||||
private readonly PartySettings partySettings;
|
||||
|
||||
public PartyCreatorModel(ICharacterFactory characterFactory,
|
||||
IPartyFactory partyFactory,
|
||||
List<CharacterCreationRequest> characterCreationRequests,
|
||||
PartySettings partySettings) {
|
||||
this.characterFactory = characterFactory;
|
||||
this.partyFactory = partyFactory;
|
||||
this.characterCreationRequests = characterCreationRequests;
|
||||
this.partySettings = partySettings;
|
||||
}
|
||||
public PartyDefinition CreatePartyForNewRun() {
|
||||
if(characterCreationRequests.Count > partySettings.maxPartySize) {
|
||||
throw new System.ArgumentException("Too many characters requested.");
|
||||
}
|
||||
var protagonist = characterFactory.CreateProtagonist(characterCreationRequests.Find(r => r.Role == CharacterRole.Protagonist));
|
||||
return partyFactory.Create(protagonist);
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Code/GameState/Entities/PartyCreatorModel.cs.meta
Normal file
3
Assets/Code/GameState/Entities/PartyCreatorModel.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f231d6e487bc4577847de98936498bde
|
||||
timeCreated: 1772644731
|
||||
64
Assets/Code/GameState/Entities/PartyFactory.cs
Normal file
64
Assets/Code/GameState/Entities/PartyFactory.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ZLinq;
|
||||
|
||||
namespace Nox.Game {
|
||||
public interface IPartyFactory {
|
||||
PartyDefinition Create(CharacterDefinition protagonist, IEnumerable<CharacterDefinition> companions = null);
|
||||
}
|
||||
|
||||
public sealed class PartyFactory : IPartyFactory {
|
||||
private readonly PartySettings partySettings;
|
||||
private readonly StarterCharacterSettings starterCharacterSettings;
|
||||
|
||||
public PartyFactory(PartySettings partySettings, StarterCharacterSettings starterCharacterSettings) {
|
||||
this.partySettings = partySettings;
|
||||
this.starterCharacterSettings = starterCharacterSettings;
|
||||
}
|
||||
|
||||
public PartyDefinition Create(CharacterDefinition protagonist, IEnumerable<CharacterDefinition> companions = null) {
|
||||
if(protagonist == null) {
|
||||
throw new ArgumentNullException(nameof(protagonist));
|
||||
}
|
||||
|
||||
var party = new PartyDefinition {
|
||||
maxPartySize = partySettings.maxPartySize <= 0 ? int.MaxValue : partySettings.maxPartySize
|
||||
};
|
||||
|
||||
var protagonistClone = protagonist.Clone();
|
||||
protagonistClone.Role = CharacterRole.Protagonist;
|
||||
party.members.Add(protagonistClone);
|
||||
|
||||
if(companions != null) {
|
||||
foreach(var companion in companions.AsValueEnumerable().Where(c => c != null)) {
|
||||
var companionClone = companion.Clone();
|
||||
companionClone.Role = CharacterRole.Companion;
|
||||
party.members.Add(companionClone);
|
||||
}
|
||||
}
|
||||
|
||||
ValidateParty(party);
|
||||
return party;
|
||||
}
|
||||
|
||||
private void ValidateParty(PartyDefinition party) {
|
||||
if(party.members.Count > party.maxPartySize) {
|
||||
throw new ArgumentException($"Party size {party.members.Count} exceeds max {party.maxPartySize}.");
|
||||
}
|
||||
|
||||
var protagonistCount = party.members.AsValueEnumerable().Count(m => m.Role == CharacterRole.Protagonist);
|
||||
if(protagonistCount != 1) {
|
||||
throw new ArgumentException($"Party must contain exactly one protagonist, found {protagonistCount}.");
|
||||
}
|
||||
|
||||
var uniqueIds = party.members.AsValueEnumerable()
|
||||
.Where(m => m.Id != Guid.Empty)
|
||||
.Select(m => m.Id)
|
||||
.Distinct()
|
||||
.Count();
|
||||
if(uniqueIds != party.members.Count) {
|
||||
throw new ArgumentException("Party contains duplicate or missing character ids.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Code/GameState/Entities/PartyFactory.cs.meta
Normal file
3
Assets/Code/GameState/Entities/PartyFactory.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f54ae3aaf9da4f5eaeb6d1cafe83a74f
|
||||
timeCreated: 1774183779
|
||||
6
Assets/Code/GameState/Entities/PartyReference.cs
Normal file
6
Assets/Code/GameState/Entities/PartyReference.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Nox.Game {
|
||||
public class PartyReference : MonoBehaviour {
|
||||
}
|
||||
}
|
||||
2
Assets/Code/GameState/Entities/PartyReference.cs.meta
Normal file
2
Assets/Code/GameState/Entities/PartyReference.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 067750df0e87b834bbbb239b7c03abb4
|
||||
50
Assets/Code/GameState/Entities/PartySettings.cs
Normal file
50
Assets/Code/GameState/Entities/PartySettings.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ZLinq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Nox.Game {
|
||||
[CreateAssetMenu(fileName = "DefaultPartySettings", menuName = "Nox/Database/Entities/Default Party Settings")]
|
||||
public class PartySettings : ScriptableObject {
|
||||
[Header("Party System Defaults")]
|
||||
public int maxPartySize = 4;
|
||||
|
||||
[Header("This will be default starting set")]
|
||||
public string testStartingSetId;
|
||||
|
||||
[Header("Party Definition Sets")]
|
||||
public List<PartyDefinitionSet> testPartyDefinitionSets;
|
||||
|
||||
private void OnValidate() {
|
||||
if(String.IsNullOrEmpty(testStartingSetId)) {
|
||||
Debug.LogError("DefaultPartySettings: startingSetId cannot be null or empty");
|
||||
return;
|
||||
}
|
||||
foreach(var partyDefinitionSet in testPartyDefinitionSets) {
|
||||
var partyDefinition = partyDefinitionSet.partyDefinition;
|
||||
if(partyDefinition == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for(var i = 0; i < partyDefinition.maxPartySize; i++) {
|
||||
if (partyDefinition.members.Count <= i) {
|
||||
partyDefinition.members.Add(new CharacterDefinition());
|
||||
}
|
||||
}
|
||||
|
||||
if(partyDefinition.members.Count <= partyDefinition.maxPartySize) {
|
||||
continue;
|
||||
}
|
||||
Debug.LogError($"Party definition '{partyDefinitionSet.id}' has more members than the maximum allowed size.Removing extra members.");
|
||||
partyDefinition.members.RemoveRange(partyDefinition.maxPartySize, partyDefinition.members.Count - partyDefinition.maxPartySize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class PartyDefinitionSet {
|
||||
public string id;
|
||||
public bool isTestingSet;
|
||||
public PartyDefinition partyDefinition;
|
||||
}
|
||||
}
|
||||
3
Assets/Code/GameState/Entities/PartySettings.cs.meta
Normal file
3
Assets/Code/GameState/Entities/PartySettings.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ff702898b3cd4100b9b8230f04711fa9
|
||||
timeCreated: 1774195937
|
||||
99
Assets/Code/GameState/Entities/PerkFactory.cs
Normal file
99
Assets/Code/GameState/Entities/PerkFactory.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using Jovian.Logger;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ZLinq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Nox.Game {
|
||||
|
||||
public interface IPerk {
|
||||
Guid Id { get; }
|
||||
string Name { get; }
|
||||
ModifiersData Modifiers { get; }
|
||||
}
|
||||
|
||||
public interface IPerkFactory {
|
||||
IReadOnlyCollection<IPerk> GetAll();
|
||||
IPerk GetById(Guid perkId);
|
||||
IReadOnlyCollection<IPerk> GetPerksFor(IEntityDefinition character);
|
||||
bool TryAddPerk(IEntityDefinition character, Guid perkId);
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class PerkDefinition : IPerk {
|
||||
[field: SerializeField] public string Name { get; set; }
|
||||
[field: SerializeField] public ModifiersData Modifiers { get; set; }
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class PerksData {
|
||||
public List<PerkDefinition> perks = new ();
|
||||
|
||||
public override string ToString() {
|
||||
return $"Perks: {string.Join(", ", perks.Select(perk => $"{perk.Name}"))}";
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class PerkFactory : IPerkFactory {
|
||||
private readonly Dictionary<Guid, IPerk> perkPool = new ();
|
||||
|
||||
public PerkFactory(PerksRegistry perksRegistry) {
|
||||
if(!perksRegistry) {
|
||||
throw new ArgumentNullException(nameof(perksRegistry));
|
||||
}
|
||||
var allAvailablePerks = perksRegistry.perksData;
|
||||
foreach(var perk in allAvailablePerks.perks) {
|
||||
perkPool.Add(perk.Id, perk);
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<IPerk> GetAll() {
|
||||
return perkPool.Values.AsValueEnumerable().ToList();
|
||||
}
|
||||
|
||||
public IPerk GetById(Guid perkId) {
|
||||
perkPool.TryGetValue(perkId, out var perk);
|
||||
return perk;
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<IPerk> GetPerksFor(IEntityDefinition character) {
|
||||
if(character == null) {
|
||||
return perkPool.Values.AsValueEnumerable().ToList();
|
||||
}
|
||||
|
||||
var ownedPerkIds = character.Perks.perks.AsValueEnumerable()
|
||||
.Select(p => p.Id)
|
||||
.ToHashSet();
|
||||
|
||||
return perkPool.Values.AsValueEnumerable().Where(p => !ownedPerkIds.Contains(p.Id)).ToList();
|
||||
}
|
||||
|
||||
public bool TryAddPerk(IEntityDefinition character, Guid perkId) {
|
||||
if(character == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(perkId == Guid.Empty) {
|
||||
GlobalLogger.LogException("Cannot add a perk with an empty Id!", LogCategory.GameLogic);
|
||||
}
|
||||
|
||||
if(character.Perks.perks.AsValueEnumerable().Any(p => p != null && p.Id == perkId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!perkPool.TryGetValue(perkId, out var perk)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
character.Perks.perks.Add(new PerkDefinition {
|
||||
Id = perk.Id,
|
||||
Name = perk.Name,
|
||||
Modifiers = perk.Modifiers
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Code/GameState/Entities/PerkFactory.cs.meta
Normal file
3
Assets/Code/GameState/Entities/PerkFactory.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1de2461f550e4ef8a324d5746222898f
|
||||
timeCreated: 1774183789
|
||||
8
Assets/Code/GameState/Entities/PerksRegistry.cs
Normal file
8
Assets/Code/GameState/Entities/PerksRegistry.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Nox.Game {
|
||||
[CreateAssetMenu(fileName = "PerksRegistry", menuName = "Nox/Database/Entities/Perks Registry")]
|
||||
public class PerksRegistry : ScriptableObject {
|
||||
public PerksData perksData;
|
||||
}
|
||||
}
|
||||
3
Assets/Code/GameState/Entities/PerksRegistry.cs.meta
Normal file
3
Assets/Code/GameState/Entities/PerksRegistry.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0adb42b13ce44d8792247246a49e9c3f
|
||||
timeCreated: 1774179294
|
||||
39
Assets/Code/GameState/Entities/StarterCharacterSettings.cs
Normal file
39
Assets/Code/GameState/Entities/StarterCharacterSettings.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using Random = UnityEngine.Random;
|
||||
|
||||
namespace Nox.Game {
|
||||
[CreateAssetMenu(fileName = "CharacterBaseSettings", menuName = "Nox/Database/Entities/CharacterBaseSettings")]
|
||||
public class StarterCharacterSettings: ScriptableObject {
|
||||
[Header("Character Creation Defaults")]
|
||||
public DistributionPointsPerClass[] distributionPointsPerClass;
|
||||
public EntityAttributes defaultEntityAttributes;
|
||||
public EntityStats defaultEntityStats;
|
||||
public PerksData defaultPerksData;
|
||||
public ModifiersData defaultModifiersData;
|
||||
|
||||
[Header("General Racial Bonuses and Perks per Class")]
|
||||
public RacialBonuses [] racialBonuses;
|
||||
public ClassBonuses [] classBonuses;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class RacialBonuses {
|
||||
public CharacterRace race;
|
||||
public PerksData startingPerks;
|
||||
public ModifiersData permanentModifiers;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class ClassBonuses {
|
||||
public CharacterClass @class;
|
||||
public PerksData startingPerks;
|
||||
public ModifiersData permanentModifiers;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public sealed class DistributionPointsPerClass {
|
||||
public CharacterClass @class;
|
||||
public int points;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1bbecc15c7cd4a7ca90ce17b3d3d75f0
|
||||
timeCreated: 1774184829
|
||||
88
Assets/Code/GameState/NoxSaveData.cs
Normal file
88
Assets/Code/GameState/NoxSaveData.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using Jovian.SaveSystem;
|
||||
using Jovian.InGameLogging;
|
||||
using Nox.Core;
|
||||
using System;
|
||||
using ZLinq;
|
||||
using UnityEngine;
|
||||
using PlayMode = Nox.Core.PlayMode;
|
||||
|
||||
namespace Nox.Game {
|
||||
|
||||
public class NoxSaveData {
|
||||
public static NoxSavedDataSet RestoreSavedData(
|
||||
ISaveSystem saveSystem,
|
||||
GameDataState gameDataState,
|
||||
ref AdventureData adventureData,
|
||||
IGameLogStore gameLogStore = null) {
|
||||
var sessions = saveSystem.GetAllSessions().AsValueEnumerable().OrderByDescending(s => s.lastSaveDateUtc).ToList();
|
||||
if(sessions.Count == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var latestSession = sessions[0];
|
||||
var slots = saveSystem.GetSlots(latestSession.sessionId).AsValueEnumerable().OrderByDescending(s => s.timestampUtc).ToList();
|
||||
if(slots.Count == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var latestSlot = slots[0];
|
||||
var saveData = saveSystem.Load<NoxSavedDataSet>(latestSlot);
|
||||
Debug.Log($"Loaded save {latestSlot.DisplayLabel}");
|
||||
if(saveData == null) {
|
||||
Debug.LogError("Failed to load save data");
|
||||
return null;
|
||||
}
|
||||
|
||||
gameDataState.activeSessionId = latestSession.sessionId;
|
||||
gameDataState.savedPartyPosition = saveData.partyPosition.ToVector3();
|
||||
gameDataState.ActiveParty = saveData.activeParty;
|
||||
adventureData = saveData.adventureData;
|
||||
|
||||
if(gameLogStore != null && saveData.gameLogData != null) {
|
||||
gameLogStore.RestoreFromSaveData(saveData.gameLogData);
|
||||
}
|
||||
|
||||
return saveData;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The game's save data snapshot. Contains all state needed to restore a game session.
|
||||
/// This is the TData passed to the save system package.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public sealed class NoxSavedDataSet {
|
||||
// game state
|
||||
public PlayMode activePlayMode;
|
||||
|
||||
//game mode specific data
|
||||
public AdventureData adventureData;
|
||||
|
||||
// Party
|
||||
public PartyDefinition activeParty;
|
||||
public SerializableVector3 partyPosition;
|
||||
|
||||
// In-game log
|
||||
public GameLogSaveData gameLogData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON-friendly Vector3 representation for save data.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public struct SerializableVector3 {
|
||||
public float x;
|
||||
public float y;
|
||||
public float z;
|
||||
|
||||
public static SerializableVector3 Zero => new SerializableVector3 { x = 0, y = 0, z = 0 };
|
||||
|
||||
public static SerializableVector3 FromVector3(Vector3 value) {
|
||||
return new SerializableVector3 { x = value.x, y = value.y, z = value.z };
|
||||
}
|
||||
|
||||
public Vector3 ToVector3() {
|
||||
return new Vector3(x, y, z);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Code/GameState/NoxSaveData.cs.meta
Normal file
2
Assets/Code/GameState/NoxSaveData.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b62368c672d7fde4d94dc19ae74e133d
|
||||
3
Assets/Code/GameState/PlayModes.meta
Normal file
3
Assets/Code/GameState/PlayModes.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 35a9294689c9454fba6e3cd8d818c38f
|
||||
timeCreated: 1772367730
|
||||
12
Assets/Code/GameState/PlayModes/AdventureModePrefabs.cs
Normal file
12
Assets/Code/GameState/PlayModes/AdventureModePrefabs.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Nox.Game.UI;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Nox.Game {
|
||||
[CreateAssetMenu(fileName = "GameModePrefabs", menuName = "Nox/AdventureMapPrefabs")]
|
||||
public class AdventureModePrefabs: ScenePrefabs {
|
||||
public GuiReferences guiReferencesPrefab;
|
||||
public MapReference mapReferencePrefab;
|
||||
public MapLocationsReference mapLocationsReferencePrefab;
|
||||
public PartyReference partyReferencePrefab;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e7cce6bb84bb20741a560389ab887769
|
||||
204
Assets/Code/GameState/PlayModes/AdventurePlayMode.cs
Normal file
204
Assets/Code/GameState/PlayModes/AdventurePlayMode.cs
Normal file
@@ -0,0 +1,204 @@
|
||||
using Jovian.Calendar;
|
||||
using Jovian.EncounterSystem;
|
||||
using Jovian.PopupSystem;
|
||||
using Jovian.PopupSystem.UI;
|
||||
using Jovian.SaveSystem;
|
||||
using Jovian.ZoneSystem;
|
||||
using Nox.Core;
|
||||
using Nox.Platform;
|
||||
using Nox.Game.UI;
|
||||
using Nox.UI;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AddressableAssets;
|
||||
using ZLinq;
|
||||
using PlayMode = Nox.Core.PlayMode;
|
||||
|
||||
namespace Nox.Game {
|
||||
public class AdventureData {
|
||||
public bool isPartyMoving;
|
||||
public int currentDay = 0;
|
||||
public int suppliesAvailable = -1;
|
||||
public float currentTime = -1f;
|
||||
public DayPhase currentDayPhase = DayPhase.Morning;
|
||||
}
|
||||
|
||||
public class AdventurePlayMode : IPlayMode {
|
||||
private readonly PlatformSettings platformSettings;
|
||||
private readonly PlayModeSettings bootstrapSettings;
|
||||
private readonly GameDataState gameDataState;
|
||||
private readonly ISaveSystem saveSystem;
|
||||
private readonly PartyDefinition partyDefinition;
|
||||
private readonly AdventureSettings adventureSettings;
|
||||
private AdventureData adventureData;
|
||||
private AdventureModePrefabs scenePrefabs;
|
||||
private ICameraController cameraController;
|
||||
private MapReference mapRef;
|
||||
private PartyMovementHandler partyMovementHandler;
|
||||
private PartyReference partyRef;
|
||||
private MapLocationsReference mapLocationsReference;
|
||||
private InputSystem_Actions inputActions;
|
||||
private AdventureView adventureView;
|
||||
private ZoneSystem zoneSystem;
|
||||
private GuiReferences guiReferences;
|
||||
private TimeHandler timeHandler;
|
||||
private PartyInventoryHandler partyInventoryHandler;
|
||||
private PartyGuiView partyGuiView;
|
||||
private IPopupSystem popupSystem;
|
||||
private EncounterRegistry encounterRegistry;
|
||||
private EncounterHandler encounterHandler;
|
||||
private EncounterPrefabs encounterPrefabs;
|
||||
|
||||
public AdventurePlayMode(
|
||||
PlatformSettings platformSettings,
|
||||
PartyDefinition partyDefinition,
|
||||
PlayModeSettings bootstrapSettings,
|
||||
GameDataState gameDataState,
|
||||
ISaveSystem saveSystem,
|
||||
AdventureSettings adventureSettings,
|
||||
AdventureData adventureData) {
|
||||
this.platformSettings = platformSettings;
|
||||
this.partyDefinition = partyDefinition;
|
||||
this.bootstrapSettings = bootstrapSettings;
|
||||
this.gameDataState = gameDataState;
|
||||
this.saveSystem = saveSystem;
|
||||
this.adventureSettings = adventureSettings;
|
||||
this.adventureData = adventureData;
|
||||
}
|
||||
|
||||
public bool IsGameModeInitialized { get; private set; }
|
||||
|
||||
public void EnterPlayMode() {
|
||||
inputActions = platformSettings.inputSettings.inputActions;
|
||||
if(IsGameModeInitialized) {
|
||||
inputActions.Player.Enable();
|
||||
inputActions.UI.PauseMenu.Enable();
|
||||
partyMovementHandler.ConsumeNextClick();
|
||||
return;
|
||||
}
|
||||
Addressables.LoadSceneAsync(bootstrapSettings.gameModeData.AsValueEnumerable().FirstOrDefault(g => g.playMode == PlayMode.Adventure)?.scene)
|
||||
.WaitForCompletion().ActivateAsync().completed += InitializeGameMode;
|
||||
}
|
||||
|
||||
private void InitializeGameMode(AsyncOperation obj) {
|
||||
inputActions.Player.Enable();
|
||||
inputActions.UI.PauseMenu.Enable();
|
||||
Debug.Log("Entering Adventure Play Mode");
|
||||
if(partyDefinition == null) {
|
||||
var restoreResult = NoxSaveData.RestoreSavedData(saveSystem, gameDataState, ref adventureData);
|
||||
if(restoreResult == null) {
|
||||
Debug.LogError("AdventurePlayMode: no save data found for auto-recovery. Party will be null.");
|
||||
}
|
||||
}
|
||||
|
||||
encounterRegistry ??= Addressables.LoadAssetAsync<EncounterRegistry>("EncounterRegistry").WaitForCompletion();
|
||||
scenePrefabs ??= Addressables.LoadAssetAsync<AdventureModePrefabs>("AdventureMapPrefabs").WaitForCompletion();
|
||||
encounterPrefabs = Addressables.LoadAssetAsync<EncounterPrefabs>("EncounterPrefabs").WaitForCompletion();
|
||||
mapRef ??= Object.FindFirstObjectByType<MapReference>();
|
||||
partyRef ??= Object.FindFirstObjectByType<PartyReference>();
|
||||
if(partyRef && gameDataState.savedPartyPosition.HasValue) {
|
||||
partyRef.transform.position = gameDataState.savedPartyPosition.Value;
|
||||
}
|
||||
mapLocationsReference ??= Object.FindFirstObjectByType<MapLocationsReference>();
|
||||
if(!mapRef) {
|
||||
mapRef ??= Object.Instantiate(scenePrefabs.mapReferencePrefab);
|
||||
}
|
||||
cameraController ??= new CameraController(platformSettings, mapRef);
|
||||
cameraController.Initialize();
|
||||
|
||||
if(adventureData.suppliesAvailable == -1) {
|
||||
adventureData.suppliesAvailable = adventureSettings.maxSupplies;
|
||||
}
|
||||
if(Mathf.Approximately(adventureData.currentTime, -1f)) {
|
||||
adventureData.currentTime = 0.25f;
|
||||
}
|
||||
|
||||
partyInventoryHandler ??= new PartyInventoryHandler(adventureData, adventureSettings);
|
||||
partyInventoryHandler.Initialize();
|
||||
|
||||
var calendarSettings = Addressables.LoadAssetAsync<CalendarSettings>("CalendarSettings").WaitForCompletion();
|
||||
var worldClock = new WorldClock(calendarSettings);
|
||||
timeHandler ??= new TimeHandler(adventureSettings, adventureData, worldClock);
|
||||
|
||||
zoneSystem ??= new ZoneSystem(mapRef.zonesObjectHolder);
|
||||
encounterHandler = new EncounterHandler(zoneSystem, encounterRegistry, encounterPrefabs);
|
||||
partyMovementHandler ??= new PartyMovementHandler(
|
||||
partyRef,
|
||||
cameraController,
|
||||
mapLocationsReference,
|
||||
platformSettings.inputSettings,
|
||||
encounterHandler,
|
||||
adventureData,
|
||||
adventureSettings);
|
||||
partyMovementHandler.Initialize();
|
||||
|
||||
guiReferences ??= Object.FindFirstObjectByType<GuiReferences>();
|
||||
adventureView ??= new AdventureView(gameDataState, guiReferences, inputActions, adventureData, adventureSettings, worldClock);
|
||||
adventureView.Initialize();
|
||||
|
||||
if(partyGuiView == null && guiReferences.partyMemberSlotPrefab != null) {
|
||||
var portraitsHolder = Addressables.LoadAssetAsync<PortraitsHolder>("PortraitsHolder").WaitForCompletion();
|
||||
|
||||
if(popupSystem == null) {
|
||||
var popupSettings = Addressables.LoadAssetAsync<PopupSettings>("PopupSettings").WaitForCompletion();
|
||||
var popupViewPrefab = Addressables.LoadAssetAsync<GameObject>("PopupReferencePrefab").WaitForCompletion().GetComponent<PopupReference>();
|
||||
var guiCanvas = guiReferences.GetComponentInParent<Canvas>().transform;
|
||||
popupSystem = new PopupSystem(popupSettings, popupViewPrefab, guiCanvas);
|
||||
popupSystem.RegisterCategory(PopupCategory.Character, priority: 10);
|
||||
}
|
||||
|
||||
partyGuiView = new PartyGuiView(guiReferences.portraitsContainer, guiReferences.partyMemberSlotPrefab, portraitsHolder, popupSystem);
|
||||
partyGuiView.Initialize(partyDefinition);
|
||||
}
|
||||
|
||||
IsGameModeInitialized = true;
|
||||
}
|
||||
|
||||
public void Tick() {
|
||||
if(!IsGameModeInitialized || gameDataState.ActiveParty == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
timeHandler.Tick();
|
||||
partyInventoryHandler.Tick();
|
||||
partyMovementHandler.Tick();
|
||||
encounterHandler.Tick();
|
||||
adventureView.Tick();
|
||||
partyGuiView?.Tick();
|
||||
popupSystem?.Tick(Time.deltaTime);
|
||||
|
||||
if(inputActions.UI.PauseMenu.WasPerformedThisFrame()) {
|
||||
gameDataState.ChangePlayMode(PlayMode.PauseMenu);
|
||||
}
|
||||
}
|
||||
public void LateTick() {
|
||||
if(!IsGameModeInitialized) {
|
||||
return;
|
||||
}
|
||||
cameraController.Tick();
|
||||
}
|
||||
public NoxSavedDataSet CaptureNoxSaveData() {
|
||||
return new NoxSavedDataSet {
|
||||
activePlayMode = PlayMode.Adventure,
|
||||
activeParty = partyDefinition,
|
||||
partyPosition = partyRef ? SerializableVector3.FromVector3(partyRef.transform.position) : SerializableVector3.Zero,
|
||||
adventureData = this.adventureData
|
||||
};
|
||||
}
|
||||
|
||||
public void ExitGameMode() {
|
||||
inputActions.Player.Disable();
|
||||
inputActions.UI.PauseMenu.Disable();
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
partyGuiView?.Dispose();
|
||||
popupSystem?.Dispose();
|
||||
cameraController?.Dispose();
|
||||
partyMovementHandler?.Dispose();
|
||||
encounterHandler?.Dispose();
|
||||
encounterRegistry = null;
|
||||
Resources.UnloadUnusedAssets();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eb1c247bb12a45a587e8ae7d013038d7
|
||||
timeCreated: 1771156737
|
||||
13
Assets/Code/GameState/PlayModes/AdventureSettings.cs
Normal file
13
Assets/Code/GameState/PlayModes/AdventureSettings.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Nox.Game {
|
||||
[CreateAssetMenu(fileName = "AdventureSettings", menuName = "Nox/AdventureSettings")]
|
||||
public class AdventureSettings : ScriptableObject {
|
||||
public int dayLength = 10;
|
||||
public int maxSupplies = 20;
|
||||
|
||||
[Header("Party Data")]
|
||||
public float partyBaseSpeed = 3f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1d2424482c4843c0b7a33a9cc67411b2
|
||||
timeCreated: 1771786400
|
||||
31
Assets/Code/GameState/PlayModes/CombatPlayMode.cs
Normal file
31
Assets/Code/GameState/PlayModes/CombatPlayMode.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Nox.Core;
|
||||
using Nox.Platform;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Nox.Game {
|
||||
public class CombatPlayMode : IPlayMode {
|
||||
private readonly PlatformSettings platformSettings;
|
||||
private readonly PartyDefinition partyDefinition;
|
||||
|
||||
public CombatPlayMode(PlatformSettings platformSettings, PartyDefinition partyDefinition) {
|
||||
this.platformSettings = platformSettings;
|
||||
this.partyDefinition = partyDefinition;
|
||||
}
|
||||
|
||||
public bool IsGameModeInitialized { get; private set; }
|
||||
|
||||
public void EnterPlayMode() {
|
||||
if(partyDefinition == null) {
|
||||
Debug.LogWarning("CombatPlayMode started without PartyData.");
|
||||
}
|
||||
|
||||
Debug.Log("Entering Combat Play Mode");
|
||||
IsGameModeInitialized = true;
|
||||
}
|
||||
|
||||
public void Tick() { }
|
||||
public void LateTick() { }
|
||||
public void ExitGameMode() { }
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
3
Assets/Code/GameState/PlayModes/CombatPlayMode.cs.meta
Normal file
3
Assets/Code/GameState/PlayModes/CombatPlayMode.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 087dc559ae894cd8b9a9dee5c6e3b64c
|
||||
timeCreated: 1772367690
|
||||
3
Assets/Code/GameState/PlayModes/Encounters.meta
Normal file
3
Assets/Code/GameState/PlayModes/Encounters.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d6216fd41db9494aaa6c127d9d790b93
|
||||
timeCreated: 1776506857
|
||||
@@ -0,0 +1,11 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Nox.Game {
|
||||
public class AnswerReference : MonoBehaviour {
|
||||
public TextMeshProUGUI number;
|
||||
public TextMeshProUGUI dialogText;
|
||||
public Button button;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0c14350aeeed44a28d98d22c6ef048dc
|
||||
timeCreated: 1777235448
|
||||
146
Assets/Code/GameState/PlayModes/Encounters/EncounterHandler.cs
Normal file
146
Assets/Code/GameState/PlayModes/Encounters/EncounterHandler.cs
Normal file
@@ -0,0 +1,146 @@
|
||||
using Jovian.EncounterSystem;
|
||||
using Jovian.ZoneSystem;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Nox.Game {
|
||||
public class EncounterHandler {
|
||||
private readonly ZoneSystem zoneSystem;
|
||||
private readonly EncounterRegistry encounterRegistry;
|
||||
private readonly EncounterView encounterView;
|
||||
private string previousZoneId;
|
||||
private IEncounter activeEncounter;
|
||||
|
||||
public EncounterHandler(ZoneSystem zoneSystem, EncounterRegistry encounterRegistry, EncounterPrefabs encounterPrefabs) {
|
||||
this.zoneSystem = zoneSystem;
|
||||
this.encounterRegistry = encounterRegistry;
|
||||
encounterView = new EncounterView(encounterPrefabs);
|
||||
encounterView.OptionSelected += OnOptionSelected;
|
||||
}
|
||||
|
||||
public bool AskForRandomEncounter(ZoneContext zoneContext, string encounterTableId, out IEncounter encounter) {
|
||||
return ResolveEncounter(zoneContext, encounterTableId, out encounter);
|
||||
}
|
||||
|
||||
public bool AskForRandomEncounterOfType(ZoneContext zoneContext, string encounterTableId, out IEncounter encounter, IEncounterKind encounterKind) {
|
||||
return ResolveEncounter(zoneContext, encounterTableId, out encounter, encounterKind);
|
||||
}
|
||||
|
||||
public bool AskForEncounter(string encounterId, out IEncounter encounter) {
|
||||
return encounterRegistry.GetEncounters().TryGetValue(encounterId, out encounter);
|
||||
}
|
||||
|
||||
private bool ResolveEncounter(ZoneContext zoneContext, string encounterTableId, out IEncounter encounter, IEncounterKind encounterKind = null) {
|
||||
encounter = null;
|
||||
if(zoneContext.isSafe) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var randomChance = Random.Range(0f, 1f);
|
||||
var shouldTrigger = randomChance <= zoneContext.finalEncounterChance;
|
||||
|
||||
if(!shouldTrigger) {
|
||||
Debug.Log($"Rolled for encounter '{encounterTableId}': {randomChance:F2}/{zoneContext.finalEncounterChance:F2} -> none");
|
||||
return false;
|
||||
}
|
||||
|
||||
encounter = encounterKind == null
|
||||
? encounterRegistry.GetRandomEncounter(encounterTableId)
|
||||
: encounterRegistry.GetRandomEncounter(encounterTableId, encounterKind);
|
||||
|
||||
var resultName = encounter?.EncounterDefinition?.name ?? "none";
|
||||
Debug.Log($"Rolled for encounter '{encounterTableId}': {randomChance:F2}/{zoneContext.finalEncounterChance:F2} -> {resultName}");
|
||||
return encounter != null;
|
||||
}
|
||||
|
||||
private void VerifyZones(Vector3 position) {
|
||||
var zoneContext = zoneSystem.QueryZone(position);
|
||||
if(string.IsNullOrEmpty(zoneContext.resolvedZoneId)) {
|
||||
return;
|
||||
}
|
||||
var currentZoneId = zoneContext.resolvedZoneId;
|
||||
|
||||
if(currentZoneId != previousZoneId) {
|
||||
if(!string.IsNullOrEmpty(currentZoneId)) {
|
||||
Debug.Log($"Entered zone: {currentZoneId} (encounter: {zoneContext.encounterTableId}, safe: {zoneContext.isSafe})");
|
||||
if(ResolveEncounter(zoneContext, zoneContext.encounterTableId, out var encounter)) {
|
||||
TriggerEncounter(encounter);
|
||||
}
|
||||
|
||||
}
|
||||
else if(!string.IsNullOrEmpty(previousZoneId)) {
|
||||
Debug.Log($"Left zone: {previousZoneId}");
|
||||
}
|
||||
previousZoneId = currentZoneId;
|
||||
}
|
||||
}
|
||||
|
||||
private void TriggerEncounter(IEncounter encounter) {
|
||||
switch(encounter.EncounterDefinition.Kind) {
|
||||
case CombatKind:
|
||||
return;
|
||||
default:
|
||||
activeEncounter = encounter;
|
||||
encounterView?.SetCurrentEncounter(encounter);
|
||||
encounterView?.Show();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOptionSelected(int optionIndex) {
|
||||
if(activeEncounter == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var options = activeEncounter.EncounterDialogOptionSet?.options;
|
||||
if(options == null || optionIndex < 0 || optionIndex >= options.Count) {
|
||||
return;
|
||||
}
|
||||
|
||||
ResolveOption(activeEncounter, options[optionIndex]);
|
||||
encounterView?.Hide();
|
||||
activeEncounter = null;
|
||||
}
|
||||
|
||||
private void ResolveOption(IEncounter encounter, EncounterDialogOption option) {
|
||||
if(option?.events == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for(var i = 0; i < option.events.Count; i++) {
|
||||
var encounterEvent = option.events[i];
|
||||
if(encounterEvent == null) {
|
||||
continue;
|
||||
}
|
||||
switch(encounterEvent) {
|
||||
case ChainToEncounterEvent chain:
|
||||
if(AskForEncounter(chain.nextEncounterId, out var next)) {
|
||||
TriggerEncounter(next);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case LogEvent log:
|
||||
Debug.Log($"[Encounter '{encounter.EncounterDefinition.id}'] {log.message}");
|
||||
break;
|
||||
case StartCombatEvent _:
|
||||
case GiveRewardEvent _:
|
||||
Debug.Log($"[Encounter] unhandled event {encounterEvent.GetType().Name}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void CheckForEncounters(Vector3 position) {
|
||||
VerifyZones(position);
|
||||
}
|
||||
|
||||
public void Tick() { }
|
||||
|
||||
public void Dispose() {
|
||||
if(encounterView != null) {
|
||||
encounterView.OptionSelected -= OnOptionSelected;
|
||||
encounterView.Dispose();
|
||||
}
|
||||
activeEncounter = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 523274f9158f453dbfac02601a77c3f7
|
||||
timeCreated: 1776506833
|
||||
25
Assets/Code/GameState/PlayModes/Encounters/EncounterKind.cs
Normal file
25
Assets/Code/GameState/PlayModes/Encounters/EncounterKind.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Jovian.EncounterSystem;
|
||||
using System;
|
||||
|
||||
namespace Nox.Game {
|
||||
[Serializable]
|
||||
public class CombatKind : IEncounterKind { }
|
||||
|
||||
[Serializable]
|
||||
public class SocialKind : IEncounterKind { }
|
||||
|
||||
[Serializable]
|
||||
public class PuzzleKind : IEncounterKind { }
|
||||
|
||||
[Serializable]
|
||||
public class ExplorationKind : IEncounterKind { }
|
||||
|
||||
[Serializable]
|
||||
public class TutorialKind : IEncounterKind { }
|
||||
|
||||
[Serializable]
|
||||
public class HazardKind : IEncounterKind { }
|
||||
|
||||
[Serializable]
|
||||
public class OtherKind : IEncounterKind { }
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: abf01048c404a0240ad538b65a15f5fb
|
||||
@@ -0,0 +1,18 @@
|
||||
using Jovian.EncounterSystem;
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Nox.Game {
|
||||
[CreateAssetMenu(fileName = "EncounterPrefabs", menuName = "Nox/EncounterPrefabs")]
|
||||
public class EncounterPrefabs : ScriptableObject {
|
||||
public EncounterSet[] encounterSets;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class EncounterSet {
|
||||
[field: SerializeReference, SubclassSelector]
|
||||
public IEncounterKind encounterKind;
|
||||
public EncounterReference encounterReference;
|
||||
public AnswerReference answerReference;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7fc36c84acd04836b1280aba57a64e24
|
||||
timeCreated: 1777229589
|
||||
167
Assets/Code/GameState/PlayModes/Encounters/EncounterView.cs
Normal file
167
Assets/Code/GameState/PlayModes/Encounters/EncounterView.cs
Normal file
@@ -0,0 +1,167 @@
|
||||
using Jovian.EncounterSystem;
|
||||
using Nox.Game.UI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Nox.Game {
|
||||
public class EncounterView : IMenuView {
|
||||
private const int MaxAnswers = 4;
|
||||
|
||||
private readonly EncounterPrefabs encounterPrefabs;
|
||||
private readonly Dictionary<Type, EncounterReference> kindToReference = new();
|
||||
private readonly Dictionary<Type, List<AnswerReference>> kindToAnswerPool = new();
|
||||
|
||||
private IEncounter currentEncounter;
|
||||
private EncounterReference currentReference;
|
||||
private List<AnswerReference> currentAnswerPool;
|
||||
|
||||
public event Action<int> OptionSelected;
|
||||
|
||||
public EncounterView(EncounterPrefabs encounterPrefabs) {
|
||||
this.encounterPrefabs = encounterPrefabs;
|
||||
}
|
||||
|
||||
public void SetCurrentEncounter(IEncounter encounter) {
|
||||
currentEncounter = encounter;
|
||||
}
|
||||
|
||||
public void Initialize() { }
|
||||
|
||||
public void Show() {
|
||||
if(currentEncounter?.EncounterDefinition?.Kind == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(currentReference) {
|
||||
currentReference.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
var kindType = currentEncounter.EncounterDefinition.Kind.GetType();
|
||||
var set = encounterPrefabs.encounterSets
|
||||
.FirstOrDefault(s => s.encounterKind != null && s.encounterKind.GetType() == kindType);
|
||||
if(set == null || !set.encounterReference) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!kindToReference.TryGetValue(kindType, out var reference) || !reference) {
|
||||
reference = UnityEngine.Object.Instantiate(set.encounterReference);
|
||||
kindToReference[kindType] = reference;
|
||||
}
|
||||
|
||||
currentReference = reference;
|
||||
currentAnswerPool = GetOrBuildAnswerPool(kindType, set);
|
||||
|
||||
PopulateEncounterReference();
|
||||
currentReference.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
public void Hide() {
|
||||
if(currentReference) {
|
||||
currentReference.gameObject.SetActive(false);
|
||||
}
|
||||
DeactivateAnswers(currentAnswerPool);
|
||||
}
|
||||
|
||||
public void Tick() { }
|
||||
|
||||
private List<AnswerReference> GetOrBuildAnswerPool(Type kindType, EncounterSet set) {
|
||||
if(kindToAnswerPool.TryGetValue(kindType, out var pool) && pool != null) {
|
||||
return pool;
|
||||
}
|
||||
|
||||
pool = new List<AnswerReference>(MaxAnswers);
|
||||
kindToAnswerPool[kindType] = pool;
|
||||
|
||||
if(!set.answerReference || !currentReference.encounterOptionsContainer) {
|
||||
return pool;
|
||||
}
|
||||
|
||||
for(var i = 0; i < MaxAnswers; i++) {
|
||||
var answer = UnityEngine.Object.Instantiate(set.answerReference, currentReference.encounterOptionsContainer);
|
||||
answer.gameObject.SetActive(false);
|
||||
pool.Add(answer);
|
||||
}
|
||||
return pool;
|
||||
}
|
||||
|
||||
private void PopulateEncounterReference() {
|
||||
var definition = currentEncounter.EncounterDefinition;
|
||||
var visuals = currentEncounter.EncounterVisuals;
|
||||
|
||||
if(currentReference.encounterName) {
|
||||
currentReference.encounterName.text = definition.name;
|
||||
}
|
||||
if(currentReference.encounterDescription) {
|
||||
currentReference.encounterDescription.text = definition.description;
|
||||
}
|
||||
if(currentReference.encounterArt && visuals != null) {
|
||||
currentReference.encounterArt.sprite = visuals.encounterArt;
|
||||
}
|
||||
|
||||
PopulateAnswers();
|
||||
}
|
||||
|
||||
private void PopulateAnswers() {
|
||||
DeactivateAnswers(currentAnswerPool);
|
||||
|
||||
var optionSet = currentEncounter.EncounterDialogOptionSet;
|
||||
if(currentAnswerPool == null || optionSet?.options == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var count = Mathf.Min(optionSet.options.Count, currentAnswerPool.Count);
|
||||
for(var i = 0; i < count; i++) {
|
||||
var option = optionSet.options[i];
|
||||
var answer = currentAnswerPool[i];
|
||||
if(option == null || !answer) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(answer.number) {
|
||||
answer.number.text = (i + 1).ToString();
|
||||
}
|
||||
if(answer.dialogText) {
|
||||
answer.dialogText.text = option.text.Resolve(optionSet.library);
|
||||
}
|
||||
BindAnswerButton(answer, i);
|
||||
answer.gameObject.SetActive(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void BindAnswerButton(AnswerReference answer, int optionIndex) {
|
||||
var button = answer.button ? answer.button : answer.GetComponentInChildren<UnityEngine.UI.Button>(true);
|
||||
if(!button) {
|
||||
return;
|
||||
}
|
||||
button.onClick.RemoveAllListeners();
|
||||
button.onClick.AddListener(() => OptionSelected?.Invoke(optionIndex));
|
||||
}
|
||||
|
||||
private static void DeactivateAnswers(List<AnswerReference> pool) {
|
||||
if(pool == null) {
|
||||
return;
|
||||
}
|
||||
for(var i = 0; i < pool.Count; i++) {
|
||||
if(pool[i]) {
|
||||
pool[i].gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
foreach(var reference in kindToReference.Values) {
|
||||
if(reference) {
|
||||
UnityEngine.Object.Destroy(reference.gameObject);
|
||||
}
|
||||
}
|
||||
kindToReference.Clear();
|
||||
kindToAnswerPool.Clear();
|
||||
currentReference = null;
|
||||
currentAnswerPool = null;
|
||||
currentEncounter = null;
|
||||
OptionSelected = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b69490c9f1d8471a84b4594d1b1be117
|
||||
timeCreated: 1776590016
|
||||
30
Assets/Code/GameState/PlayModes/Encounters/RewardKind.cs
Normal file
30
Assets/Code/GameState/PlayModes/Encounters/RewardKind.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Jovian.EncounterSystem;
|
||||
using System;
|
||||
|
||||
namespace Nox.Game {
|
||||
[Serializable]
|
||||
public class CurrencyRewardKind : IRewardKind {
|
||||
public string currencyId;
|
||||
public int amount;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ItemRewardKind : IRewardKind {
|
||||
public string itemId;
|
||||
public int quantity;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ExperienceRewardKind : IRewardKind {
|
||||
public int amount;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class UnlockableRewardKind : IRewardKind {
|
||||
public string unlockableId;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class OtherRewardKind : IRewardKind {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7895041af87249e4c8458a07b0bc6b2c
|
||||
10
Assets/Code/GameState/PlayModes/IMenuView.cs
Normal file
10
Assets/Code/GameState/PlayModes/IMenuView.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
#nullable enable
|
||||
|
||||
namespace Nox.Game.UI {
|
||||
public interface IMenuView {
|
||||
void Initialize();
|
||||
void Show();
|
||||
void Hide();
|
||||
void Tick();
|
||||
}
|
||||
}
|
||||
3
Assets/Code/GameState/PlayModes/IMenuView.cs.meta
Normal file
3
Assets/Code/GameState/PlayModes/IMenuView.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e99c535c23cc449984dcd145f4c1bec8
|
||||
timeCreated: 1772374016
|
||||
10
Assets/Code/GameState/PlayModes/MapLocation.cs
Normal file
10
Assets/Code/GameState/PlayModes/MapLocation.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Nox.Game {
|
||||
public class MapLocation : MonoBehaviour {
|
||||
public SpriteRenderer icon;
|
||||
public SpriteRenderer highlight;
|
||||
public TextMeshProUGUI nameText;
|
||||
}
|
||||
}
|
||||
2
Assets/Code/GameState/PlayModes/MapLocation.cs.meta
Normal file
2
Assets/Code/GameState/PlayModes/MapLocation.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 436b68ae9b9d347469b78e52a5aa7b53
|
||||
7
Assets/Code/GameState/PlayModes/MapLocationsReference.cs
Normal file
7
Assets/Code/GameState/PlayModes/MapLocationsReference.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Nox.Game {
|
||||
public class MapLocationsReference : MonoBehaviour {
|
||||
public NoxLocationInfo [] mapLocations;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bdd66cc132efc3640b46bf53dedcbcb4
|
||||
9
Assets/Code/GameState/PlayModes/MapReference.cs
Normal file
9
Assets/Code/GameState/PlayModes/MapReference.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Jovian.ZoneSystem;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Nox.Game {
|
||||
public class MapReference : MonoBehaviour {
|
||||
public GameObject mapPlane;
|
||||
public ZonesObjectHolder zonesObjectHolder;
|
||||
}
|
||||
}
|
||||
2
Assets/Code/GameState/PlayModes/MapReference.cs.meta
Normal file
2
Assets/Code/GameState/PlayModes/MapReference.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7c173f913efc6004faf5dd75e96b32fe
|
||||
21
Assets/Code/GameState/PlayModes/NoxMapData.cs
Normal file
21
Assets/Code/GameState/PlayModes/NoxMapData.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Nox.Game {
|
||||
public enum NoxMapLocations {
|
||||
None,
|
||||
City1,
|
||||
City2,
|
||||
Village1,
|
||||
Village2,
|
||||
Ruin1,
|
||||
Ruin2
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class NoxLocationInfo {
|
||||
public NoxMapLocations locationId;
|
||||
public MapLocation mapLocation;
|
||||
}
|
||||
|
||||
}
|
||||
3
Assets/Code/GameState/PlayModes/NoxMapData.cs.meta
Normal file
3
Assets/Code/GameState/PlayModes/NoxMapData.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 535e7fdf7ddb4c1f9edf7254c3bb0acd
|
||||
timeCreated: 1771763769
|
||||
25
Assets/Code/GameState/PlayModes/PartyInventoryHandler.cs
Normal file
25
Assets/Code/GameState/PlayModes/PartyInventoryHandler.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
namespace Nox.Game {
|
||||
public class PartyInventoryHandler {
|
||||
private readonly AdventureData adventureData;
|
||||
private readonly AdventureSettings adventureSettings;
|
||||
private int currentDay;
|
||||
|
||||
public PartyInventoryHandler(AdventureData adventureData, AdventureSettings adventureSettings) {
|
||||
this.adventureData = adventureData;
|
||||
this.adventureSettings = adventureSettings;
|
||||
}
|
||||
|
||||
public void Initialize() {
|
||||
currentDay = adventureData.currentDay;
|
||||
}
|
||||
|
||||
public void Tick() {
|
||||
if(currentDay != adventureData.currentDay) {
|
||||
currentDay = adventureData.currentDay;
|
||||
if(adventureData.suppliesAvailable > 0) {
|
||||
adventureData.suppliesAvailable--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c4a1ab7b529042ca8bf42714b383f918
|
||||
timeCreated: 1773614576
|
||||
149
Assets/Code/GameState/PlayModes/PartyMovementHandler.cs
Normal file
149
Assets/Code/GameState/PlayModes/PartyMovementHandler.cs
Normal file
@@ -0,0 +1,149 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
namespace Nox.Game {
|
||||
public class PartyMovementHandler {
|
||||
private readonly PartyReference partyReference;
|
||||
private readonly ICameraController cameraController;
|
||||
private readonly InputSystem_Actions inputActions;
|
||||
private readonly MapLocationsReference mapLocationsReference;
|
||||
private readonly EncounterHandler encounterHandler;
|
||||
private readonly AdventureSettings adventureSettings;
|
||||
private readonly float maxDistance = 100f;
|
||||
private readonly AdventureData adventureData;
|
||||
|
||||
private bool partyCanMove;
|
||||
private Vector3 targetPosition;
|
||||
private bool shouldHover;
|
||||
private MapLocation currentSelectedPoi;
|
||||
private bool hasClicked;
|
||||
private bool skipNextClick;
|
||||
|
||||
public LayerMask clickToMoveMask = LayerMask.GetMask("Clickable");
|
||||
public LayerMask hoverOverMask = LayerMask.GetMask("Hoverable");
|
||||
|
||||
public PartyMovementHandler(
|
||||
PartyReference partyReference,
|
||||
ICameraController cameraController,
|
||||
MapLocationsReference mapLocationsReference,
|
||||
Input.InputSettings inputSettings,
|
||||
EncounterHandler encounterHandler,
|
||||
AdventureData adventureData,
|
||||
AdventureSettings adventureSettings) {
|
||||
this.partyReference = partyReference;
|
||||
this.cameraController = cameraController;
|
||||
this.mapLocationsReference = mapLocationsReference;
|
||||
this.encounterHandler = encounterHandler;
|
||||
this.adventureData = adventureData;
|
||||
this.adventureSettings = adventureSettings;
|
||||
inputActions = inputSettings.inputActions;
|
||||
}
|
||||
|
||||
public void Initialize() {
|
||||
inputActions.Player.ClickOnMap.performed += OnClickOnMap;
|
||||
}
|
||||
|
||||
public void ConsumeNextClick() {
|
||||
skipNextClick = true;
|
||||
}
|
||||
|
||||
private void OnClickOnMap(InputAction.CallbackContext obj) {
|
||||
if(!obj.action.WasReleasedThisFrame()) {
|
||||
return;
|
||||
}
|
||||
hasClicked = true;
|
||||
}
|
||||
|
||||
private void HandleHoverableLocation(RaycastHit hitInfo) {
|
||||
var go = hitInfo.collider.gameObject;
|
||||
if(!go.transform.parent.TryGetComponent(out MapLocation mapLocation)) {
|
||||
return;
|
||||
}
|
||||
currentSelectedPoi = mapLocation;
|
||||
mapLocation.highlight.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
private void HandleClickableLocation() {
|
||||
var screenPos = inputActions.Player.Point.ReadValue<Vector2>();
|
||||
var ray = cameraController.CameraReference.mainCamera.ScreenPointToRay(screenPos);
|
||||
if(!Physics.Raycast(ray, out var clickHit, maxDistance, clickToMoveMask, QueryTriggerInteraction.Ignore)) {
|
||||
return;
|
||||
}
|
||||
if(!clickHit.collider.gameObject.transform.parent.TryGetComponent(out MapReference mapReference)) {
|
||||
return;
|
||||
}
|
||||
if(ValidateMoveLocation(clickHit)) {
|
||||
targetPosition = clickHit.point;
|
||||
partyCanMove = true;
|
||||
}
|
||||
}
|
||||
|
||||
private bool ValidateMoveLocation(object hitInfoPoint) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Tick() {
|
||||
HandleHover();
|
||||
|
||||
if(hasClicked) {
|
||||
hasClicked = false;
|
||||
// should it skip next click to avoid click-through from the pause menu?
|
||||
if(skipNextClick) {
|
||||
skipNextClick = false;
|
||||
return;
|
||||
}
|
||||
if(EventSystem.current.IsPointerOverGameObject()) {
|
||||
return;
|
||||
}
|
||||
HandleClickableLocation();
|
||||
}
|
||||
|
||||
if(!partyCanMove) {
|
||||
adventureData.isPartyMoving = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if(Vector3.Distance(partyReference.transform.position, targetPosition) < 0.05f) {
|
||||
partyReference.transform.position = targetPosition;
|
||||
VerifyPointsOfInterest();
|
||||
partyCanMove = false;
|
||||
}
|
||||
|
||||
adventureData.isPartyMoving = true;
|
||||
partyReference.transform.position = Vector3.MoveTowards(
|
||||
new Vector3(partyReference.transform.position.x, 0,
|
||||
partyReference.transform.position.z), targetPosition,
|
||||
Time.deltaTime * adventureSettings.partyBaseSpeed);
|
||||
encounterHandler?.CheckForEncounters(partyReference.transform.position);
|
||||
}
|
||||
|
||||
private void HandleHover() {
|
||||
if(EventSystem.current.IsPointerOverGameObject()) {
|
||||
return;
|
||||
}
|
||||
var screenPos = inputActions.Player.Point.ReadValue<Vector2>();
|
||||
var ray = cameraController.CameraReference.mainCamera.ScreenPointToRay(screenPos);
|
||||
if(Physics.Raycast(ray, out var hoverHit, maxDistance, hoverOverMask, QueryTriggerInteraction.Ignore)) {
|
||||
HandleHoverableLocation(hoverHit);
|
||||
}
|
||||
else {
|
||||
currentSelectedPoi?.highlight.gameObject.SetActive(false);
|
||||
currentSelectedPoi = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void VerifyPointsOfInterest() {
|
||||
foreach(var location in mapLocationsReference.mapLocations) {
|
||||
if(Vector3.Distance(location.mapLocation.transform.position, partyReference.transform.position) < 0.4f) {
|
||||
Debug.Log($"Arrived at {location.locationId}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
inputActions.Player.ClickOnMap.performed -= OnClickOnMap;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d2f697207298f2242aa2a8d36ef6a9b4
|
||||
47
Assets/Code/GameState/PlayModes/PauseMenuPlayMode.cs
Normal file
47
Assets/Code/GameState/PlayModes/PauseMenuPlayMode.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
#nullable enable
|
||||
using Nox.Core;
|
||||
using Nox.Platform;
|
||||
using Nox.Game.UI;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Nox.Game {
|
||||
public class PauseMenuPlayMode : IPlayMode {
|
||||
private readonly PlatformSettings platformSettings;
|
||||
private readonly GameDataState gameDataState;
|
||||
private PauseMenuReferences? pauseMenuReference;
|
||||
private readonly IMenuView? pauseMenuView;
|
||||
private readonly InputSystem_Actions inputActions;
|
||||
|
||||
public PauseMenuPlayMode(PlatformSettings platformSettings, GameDataState gameDataState, IMenuView pauseMenuView) {
|
||||
this.platformSettings = platformSettings;
|
||||
this.gameDataState = gameDataState;
|
||||
this.pauseMenuView = pauseMenuView;
|
||||
inputActions = platformSettings.inputSettings.inputActions;
|
||||
inputActions.UI.Enable();
|
||||
}
|
||||
|
||||
public bool IsGameModeInitialized { get; private set; }
|
||||
|
||||
public void EnterPlayMode() {
|
||||
Debug.Log("Entering PauseMenu Play Mode");
|
||||
pauseMenuView?.Initialize();
|
||||
IsGameModeInitialized = true;
|
||||
}
|
||||
|
||||
public void Tick() {
|
||||
if(inputActions.UI.PauseMenu.WasPerformedThisFrame()) {
|
||||
pauseMenuView?.Hide();
|
||||
gameDataState.ChangePlayMode(gameDataState.PreviousPlayMode);
|
||||
}
|
||||
pauseMenuView?.Tick();
|
||||
}
|
||||
public void LateTick() { }
|
||||
public void ExitGameMode() {
|
||||
pauseMenuView?.Hide();
|
||||
}
|
||||
public void Dispose() {
|
||||
inputActions.UI.Disable();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eab5b722cabf4ec7b79af55838d24ed4
|
||||
timeCreated: 1772367657
|
||||
31
Assets/Code/GameState/PlayModes/RestPlayMode.cs
Normal file
31
Assets/Code/GameState/PlayModes/RestPlayMode.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Nox.Core;
|
||||
using Nox.Platform;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Nox.Game {
|
||||
public class RestPlayMode : IPlayMode {
|
||||
private readonly PlatformSettings platformSettings;
|
||||
private readonly PartyDefinition partyDefinition;
|
||||
|
||||
public RestPlayMode(PlatformSettings platformSettings, PartyDefinition partyDefinition) {
|
||||
this.platformSettings = platformSettings;
|
||||
this.partyDefinition = partyDefinition;
|
||||
}
|
||||
|
||||
public bool IsGameModeInitialized { get; private set; }
|
||||
|
||||
public void EnterPlayMode() {
|
||||
if(partyDefinition == null) {
|
||||
Debug.LogWarning("RestPlayMode started without PartyData.");
|
||||
}
|
||||
|
||||
Debug.Log("Entering Rest Play Mode");
|
||||
IsGameModeInitialized = true;
|
||||
}
|
||||
|
||||
public void Tick() { }
|
||||
public void LateTick() { }
|
||||
public void ExitGameMode() { }
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
3
Assets/Code/GameState/PlayModes/RestPlayMode.cs.meta
Normal file
3
Assets/Code/GameState/PlayModes/RestPlayMode.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 67cb7d9e318d474694bc0addab3e8511
|
||||
timeCreated: 1772367678
|
||||
70
Assets/Code/GameState/PlayModes/TimeHandler.cs
Normal file
70
Assets/Code/GameState/PlayModes/TimeHandler.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using Jovian.Calendar;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Nox.Game {
|
||||
public enum DayPhase {
|
||||
Midnight,
|
||||
Dawn,
|
||||
Morning,
|
||||
Afternoon,
|
||||
Dusk,
|
||||
Night
|
||||
}
|
||||
|
||||
public class TimeHandler {
|
||||
private readonly AdventureSettings adventureSettings;
|
||||
private readonly AdventureData adventureData;
|
||||
private readonly WorldClock worldClock;
|
||||
|
||||
private float localTime;
|
||||
|
||||
private static readonly (float start, DayPhase phase)[] PhaseThresholds = {
|
||||
(0.00f, DayPhase.Midnight),
|
||||
(0.05f, DayPhase.Night),
|
||||
(0.17f, DayPhase.Dawn),
|
||||
(0.25f, DayPhase.Morning),
|
||||
(0.50f, DayPhase.Afternoon),
|
||||
(0.75f, DayPhase.Dusk),
|
||||
(0.90f, DayPhase.Night)
|
||||
};
|
||||
|
||||
public TimeHandler(AdventureSettings adventureSettings, AdventureData adventureData, WorldClock worldClock) {
|
||||
this.adventureSettings = adventureSettings;
|
||||
this.adventureData = adventureData;
|
||||
this.worldClock = worldClock;
|
||||
localTime = adventureData.currentTime * adventureSettings.dayLength;
|
||||
}
|
||||
|
||||
public void Tick() {
|
||||
if(!adventureData.isPartyMoving) {
|
||||
return;
|
||||
}
|
||||
|
||||
localTime += Time.deltaTime;
|
||||
|
||||
if(localTime >= adventureSettings.dayLength) {
|
||||
localTime -= adventureSettings.dayLength;
|
||||
}
|
||||
|
||||
var normalized = localTime / adventureSettings.dayLength;
|
||||
|
||||
worldClock.Tick(normalized);
|
||||
|
||||
adventureData.currentTime = normalized;
|
||||
adventureData.currentDayPhase = GetPhase(normalized);
|
||||
}
|
||||
|
||||
private static DayPhase GetPhase(float t) {
|
||||
var phase = DayPhase.Midnight;
|
||||
foreach(var (start, p) in PhaseThresholds) {
|
||||
if(t >= start) {
|
||||
phase = p;
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return phase;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Code/GameState/PlayModes/TimeHandler.cs.meta
Normal file
2
Assets/Code/GameState/PlayModes/TimeHandler.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 071a61f838db8e640b460ce029a8d5a5
|
||||
31
Assets/Code/GameState/PlayModes/TownPlayMode.cs
Normal file
31
Assets/Code/GameState/PlayModes/TownPlayMode.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Nox.Core;
|
||||
using Nox.Platform;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Nox.Game {
|
||||
public class TownPlayMode : IPlayMode {
|
||||
private readonly PlatformSettings platformSettings;
|
||||
private readonly PartyDefinition partyDefinition;
|
||||
|
||||
public TownPlayMode(PlatformSettings platformSettings, PartyDefinition partyDefinition) {
|
||||
this.platformSettings = platformSettings;
|
||||
this.partyDefinition = partyDefinition;
|
||||
}
|
||||
|
||||
public bool IsGameModeInitialized { get; private set; }
|
||||
|
||||
public void EnterPlayMode() {
|
||||
if(partyDefinition == null) {
|
||||
Debug.LogWarning("TownPlayMode started without PartyData.");
|
||||
}
|
||||
|
||||
Debug.Log("Entering Town Play Mode");
|
||||
IsGameModeInitialized = true;
|
||||
}
|
||||
|
||||
public void Tick() { }
|
||||
public void LateTick() { }
|
||||
public void ExitGameMode() { }
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
3
Assets/Code/GameState/PlayModes/TownPlayMode.cs.meta
Normal file
3
Assets/Code/GameState/PlayModes/TownPlayMode.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bd57598925a54bad93fe730e15023d94
|
||||
timeCreated: 1772367666
|
||||
6
Assets/Code/GameState/ScenePrefabs.cs
Normal file
6
Assets/Code/GameState/ScenePrefabs.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Nox.Game {
|
||||
public class ScenePrefabs : ScriptableObject {
|
||||
}
|
||||
}
|
||||
3
Assets/Code/GameState/ScenePrefabs.cs.meta
Normal file
3
Assets/Code/GameState/ScenePrefabs.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c8ae54c0cc79451c8aa69e22800962a4
|
||||
timeCreated: 1771160087
|
||||
8
Assets/Code/GameState/UI.meta
Normal file
8
Assets/Code/GameState/UI.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6db6b719820b72d42912937d4b633a05
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
60
Assets/Code/GameState/UI/AdventureView.cs
Normal file
60
Assets/Code/GameState/UI/AdventureView.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using Jovian.Calendar;
|
||||
using Nox.Core;
|
||||
|
||||
namespace Nox.Game.UI {
|
||||
public class AdventureView : IMenuView {
|
||||
private readonly GameDataState gameDataState;
|
||||
private readonly GuiReferences guiReferences;
|
||||
private readonly InputSystem_Actions inputActions;
|
||||
private readonly AdventureData adventureData;
|
||||
private readonly AdventureSettings adventureSettings;
|
||||
private readonly WorldClock worldClock;
|
||||
private int currentDay;
|
||||
private int previousTime;
|
||||
|
||||
public AdventureView(GameDataState gameDataState,
|
||||
GuiReferences guiReferences,
|
||||
InputSystem_Actions inputActions,
|
||||
AdventureData adventureData,
|
||||
AdventureSettings adventureSettings,
|
||||
WorldClock worldClock) {
|
||||
this.gameDataState = gameDataState;
|
||||
this.guiReferences = guiReferences;
|
||||
this.inputActions = inputActions;
|
||||
this.adventureData = adventureData;
|
||||
this.adventureSettings = adventureSettings;
|
||||
this.worldClock = worldClock;
|
||||
}
|
||||
private void InvokePauseMenu() {
|
||||
gameDataState.ChangePlayMode(PlayMode.PauseMenu);
|
||||
}
|
||||
|
||||
public void Initialize() {
|
||||
guiReferences.pauseMenuButton.onClick.AddListener(InvokePauseMenu);
|
||||
guiReferences.dayText.text = $"{worldClock.FullStringNamed()}";
|
||||
previousTime = -1;
|
||||
guiReferences.suppliesBar.fillAmount = (float)adventureData.suppliesAvailable / adventureSettings.maxSupplies;
|
||||
guiReferences.suppliesText.text = $"{adventureData.suppliesAvailable}/{adventureSettings.maxSupplies}";
|
||||
}
|
||||
|
||||
public void Show() {
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
public void Hide() {
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
public void Tick() {
|
||||
var time = worldClock.Now.minute;
|
||||
if (time != previousTime) {
|
||||
previousTime = time;
|
||||
}
|
||||
guiReferences.dayText.text = $"{worldClock.FullStringNamed()}";
|
||||
|
||||
if(currentDay != adventureData.currentDay) {
|
||||
currentDay = adventureData.currentDay;
|
||||
guiReferences.suppliesBar.fillAmount = (float)adventureData.suppliesAvailable / adventureSettings.maxSupplies;
|
||||
guiReferences.suppliesText.text = $"{adventureData.suppliesAvailable}/{adventureSettings.maxSupplies}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
Assets/Code/GameState/UI/AdventureView.cs.meta
Normal file
3
Assets/Code/GameState/UI/AdventureView.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cd45a685ea554bd7ad15745f836a6de9
|
||||
timeCreated: 1772572300
|
||||
12
Assets/Code/GameState/UI/AttributeReference.cs
Normal file
12
Assets/Code/GameState/UI/AttributeReference.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Nox.UI {
|
||||
public class AttributeReference : MonoBehaviour {
|
||||
public Button removePointsButton;
|
||||
public Button addPointsButton;
|
||||
public TextMeshProUGUI attributeName;
|
||||
public TextMeshProUGUI attributeValue;
|
||||
}
|
||||
}
|
||||
2
Assets/Code/GameState/UI/AttributeReference.cs.meta
Normal file
2
Assets/Code/GameState/UI/AttributeReference.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f51aaf11e81876845b289f5f7d310469
|
||||
34
Assets/Code/GameState/UI/CharacterCreationReference.cs
Normal file
34
Assets/Code/GameState/UI/CharacterCreationReference.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using Jovian.InGameLogging.UI;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Nox.UI {
|
||||
public class CharacterCreationReference : MonoBehaviour {
|
||||
public Canvas canvas;
|
||||
|
||||
//top
|
||||
public Button backButton;
|
||||
public Button settingsButton;
|
||||
|
||||
//left
|
||||
public TMP_Dropdown raceDropdown;
|
||||
public TMP_Dropdown classDropdown;
|
||||
public TMP_Dropdown perksDropdown;
|
||||
public TextMeshProUGUI pointsToDistribute;
|
||||
public AttributeReference[] attributeReference;
|
||||
public StatReference[] statReference;
|
||||
|
||||
//center
|
||||
public Button backButtonCenter;
|
||||
public Button acceptButton;
|
||||
public GameLogView gameLogView;
|
||||
|
||||
//right
|
||||
public Image portraitImage;
|
||||
public Button portraitSelectionLeftButton;
|
||||
public Button portraitSelectionRightButton;
|
||||
public TMP_InputField nameInputField;
|
||||
public Button startGameButton;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 15f5fb19d1765bb49b9dd5956f172398
|
||||
604
Assets/Code/GameState/UI/CharacterCreationView.cs
Normal file
604
Assets/Code/GameState/UI/CharacterCreationView.cs
Normal file
@@ -0,0 +1,604 @@
|
||||
using Jovian.InGameLogging;
|
||||
using Jovian.InGameLogging.UI;
|
||||
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;
|
||||
|
||||
// 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 previousMana;
|
||||
|
||||
// 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,
|
||||
StarterCharacterSettings starterCharacterSettings) {
|
||||
SaveSystem = saveSystem;
|
||||
this.characterCreationReference = characterCreationReference;
|
||||
this.menuGameStateData = menuGameStateData;
|
||||
this.gameDataState = gameDataState;
|
||||
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.onClick.AddListener(() => {
|
||||
if(characterCreationRequests == null || characterCreationRequests.Count == 0) {
|
||||
inGameLogger.Log("You must accept your character before starting the game.", "#FF4444");
|
||||
return;
|
||||
}
|
||||
Hide();
|
||||
menuGameStateData.startGameRequests?.Invoke(PlayMode.Adventure);
|
||||
});
|
||||
|
||||
// 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);
|
||||
previousMana = 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 newMana = 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(newMana != previousMana && previousMana != 0) {
|
||||
var delta = newMana - previousMana;
|
||||
var sign = delta > 0 ? "+" : "";
|
||||
inGameLogger.Log($"Mana: {previousMana} -> {newMana} ({sign}{delta})", "#FFFF99");
|
||||
}
|
||||
previousHealth = newHealth;
|
||||
previousMana = newMana;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// --- Lifecycle ---
|
||||
|
||||
public void Tick() {
|
||||
}
|
||||
|
||||
public void Show() {
|
||||
characterCreationReference.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
public void Hide() {
|
||||
characterCreationReference.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
inGameLogger.Disable();
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user