First commit

This commit is contained in:
sbularca
2026-05-19 15:52:04 +02:00
commit 27b7aeee46
1660 changed files with 240354 additions and 0 deletions

8
Assets/Code/Core.meta Normal file
View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: dd7c9c995eb4c449481b84fe9a667c71
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

42
Assets/Code/Core/Boot.cs Normal file
View File

@@ -0,0 +1,42 @@
using Nox.EditorCode;
using System;
using Unity.VectorGraphics;
using UnityEditor;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.SceneManagement;
namespace Nox.Core {
public class Boot {
/// <summary>
/// The is the first method called when the game starts. It will load the Initializer prefab which will initialize the project
/// </summary>
[RuntimeInitializeOnLoadMethod]
private static void Initialize() {
#if UNITY_EDITOR
switch(BootMode.BootType) {
case BootType.UnityDefault:
return;
case BootType.SceneBoot:
Addressables.InstantiateAsync("Initializer").WaitForCompletion();
break;
case BootType.FullBoot:
var loadOperation = SceneManager.LoadSceneAsync("Startup", LoadSceneMode.Single);
if(loadOperation != null) {
loadOperation.completed += OnCompleted;
}
break;
default:
throw new ArgumentOutOfRangeException();
}
#else
Addressables.InstantiateAsync("Initializer").WaitForCompletion();
#endif
}
private static void OnCompleted(AsyncOperation obj) {
obj.allowSceneActivation = true;
Addressables.InstantiateAsync("Initializer").WaitForCompletion();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d7904792340a14b458474cbdea3bbb2e

View File

@@ -0,0 +1,13 @@
using Nox.UI;
using UnityEngine;
using UnityEngine.AddressableAssets;
namespace Nox.Core {
[CreateAssetMenu(menuName = "Nox/Database/General/BoostrapReferences", fileName = "BoostrapReferences")]
public class BootstrapReferences : ScriptableObject {
public AssetReference startMenuScene;
public AssetReference splashUIReference;
public AssetReference playModeSettings;
public AssetReference menuPrefabsContainer;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 55a600591b835403c8053e4ed4b581fa

View File

@@ -0,0 +1,115 @@
using System.Collections;
using System.Collections.Generic;
using Nox.Game;
using Nox.Game.UI;
using Nox.Input;
using Nox.Platform;
using Jovian.SaveSystem;
using Jovian.InGameLogging;
using Nox.EditorCode;
using Unity.Profiling;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
namespace Nox.Core {
/// <summary>
/// The main boot class which is initializing the project, loads the platforms settings, initializes the platforms and creates the game states
/// </summary>
public class EntryPoint : MonoBehaviour {
public AssetReferenceT<InitializerSettingsFile> initializerSettingsFile;
private IPlatform platform;
private GameDataState gameDataState;
private Dictionary<GameState, IGameState> applicationStates;
private ProfilerMarker createApplicationStateMarker = new ProfilerMarker("createApplicationState");
private ISceneTransition sceneTransition;
private ISaveSystem saveSystem;
private IGameLogStore gameLogStore;
private InitializerSettingsFile initializerSettings;
private BootstrapReferences bootstrapReferences;
private MenuGameStateData menuGameStateData;
public IEnumerator Start() {
DontDestroyOnLoad(this);
// scene authoring is helping with making each scene stand-alone playable
var activeSceneReference = FindFirstObjectByType<SceneReference>();
if(activeSceneReference == null) {
Debug.LogWarning("The scene has no SceneReference script. Will start by default in Splash Application State");
var sceneRef = new GameObject {
name = "Scene Reference"
};
activeSceneReference = sceneRef.AddComponent<SceneReference>();
activeSceneReference.gameState = GameState.BootState;
yield return null;
}
// Initialize platform
gameDataState = new GameDataState {
platformSelector = new PlatformSelector(PlatformSelector.GetDevicePlatform(), PlatformSelector.GetPlatformDefaultInputMode()),
};
gameDataState.ChangeGameState(activeSceneReference.gameState);
// Load Initialization Settings
var initSettingsHandle = Addressables.LoadAssetAsync<InitializerSettingsFile>(initializerSettingsFile);
yield return new WaitUntil(() => initSettingsHandle.IsDone);
initializerSettings = initSettingsHandle.Result;
yield return CreatePlatformFactory();
var bootStrapsSettingsHandle = Addressables.LoadAssetAsync<BootstrapReferences>(initializerSettings.bootstrapSettings);
yield return new WaitUntil(() => bootStrapsSettingsHandle.IsDone);
bootstrapReferences = bootStrapsSettingsHandle.Result;
menuGameStateData = new MenuGameStateData();
var fadeObject = new GameObject("ScreenFadeTransition");
sceneTransition = fadeObject.AddComponent<ScreenFadeTransition>();
CreateApplicationStates();
var gameStateRunner = gameObject.AddComponent<GameStateRunner>();
gameStateRunner.Initialize(applicationStates, gameDataState, platform, sceneTransition);
}
private IEnumerator CreatePlatformFactory() {
platform = null;
var devicePlatform = gameDataState.platformSelector.devicePlatform;
platform = devicePlatform switch {
DevicePlatform.Desktop => new DesktopPlatform(initializerSettings.desktopPlatformSettings),
DevicePlatform.UnityEditor => new UnityEditorPlatform(initializerSettings.unityEditorPlatformSettings),
_ => platform
};
if(!Equals(gameDataState.platformSelector.devicePlatform, DevicePlatform.Desktop) &&
PlatformSelector.GetPlatformDefaultInputMode() == InputMode.Desktop) {
platform = new DesktopPlatform(initializerSettings.desktopPlatformSettings);
}
yield return platform?.Initialize(gameDataState);
}
private void CreateApplicationStates() {
createApplicationStateMarker.Begin();
// Save System
var saveSettings = SaveSystemSettings.Load();
ISaveSerializer saveSerializer = saveSettings.saveFormat == SaveFormat.Json
? new JsonSaveSerializer()
: new BinarySaveSerializer(saveSettings.obfuscationKey);
ISaveStorage saveStorage = new FileSystemSaveStorage(Application.persistentDataPath, saveSettings.saveDirectoryName);
ISaveSlotManager saveSlotManager = new SaveSlotManager(saveStorage, saveSettings);
saveSystem = new SaveSystem(saveSerializer, saveStorage, saveSlotManager, saveSettings);
gameLogStore = new GameLogStore(500);
var adventureData = new AdventureData();
applicationStates = new Dictionary<GameState, IGameState> {
[GameState.BootState] = new SplashGameState(bootstrapReferences, gameDataState),
[GameState.MainMenu] = new MainMenuGameState(gameDataState, menuGameStateData, bootstrapReferences, saveSystem, adventureData, gameLogStore),
[GameState.GameMode] = new GameModeGameState(gameDataState, bootstrapReferences, platform.PlatformSettings, saveSystem, sceneTransition, adventureData, gameLogStore),
};
createApplicationStateMarker.End();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6fd8cc6910b9e44718d0bd7733e0b83e

View File

@@ -0,0 +1,31 @@
using Nox.Game;
using UnityEngine;
namespace Nox.Core {
/// <summary>
/// This class is meant to hold application essential gameplay state
/// </summary>
public class GameDataState {
public PlatformSelector platformSelector;
public string activeSessionId;
public Vector3? savedPartyPosition;
public GameState ActiveGameState { get; set; }
public PlayMode ActivePlayMode { get; set; }
public PartyDefinition ActiveParty { get; set; }
public PlayMode PreviousPlayMode { get; private set; }
public void ChangeGameState(GameState gameState) {
ActiveGameState = gameState;
}
public void ChangePlayMode(PlayMode playMode) {
if (ActiveGameState != GameState.GameMode && playMode != PlayMode.None){
Debug.LogError("Cannot change Game Mode state in any other Application State than ApplicationState.GameMode");
return;
}
PreviousPlayMode = ActivePlayMode;
ActivePlayMode = playMode;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e6ac548e723e24ae2a81ac2d2e4cd571

View File

@@ -0,0 +1,216 @@
#nullable enable
using Nox.Game;
using Nox.Platform;
using Nox.Game.UI;
using Jovian.SaveSystem;
using Jovian.InGameLogging;
using System.Collections.Generic;
using ZLinq;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
namespace Nox.Core {
/// <summary>
/// The Game Mode Game State which is the base state in which all GameModes need to run.
/// Game Modes are any modes that involve gameplay and gameplay related scenes. This state is where the game runs.
/// </summary>
public class GameModeGameState : IGameState {
private readonly GameDataState gameDataState;
private readonly PlatformSettings platformSettings;
private readonly PlayModeSettings bootstrapSettings;
private readonly ISaveSystem saveSystem;
private readonly ISceneTransition sceneTransition;
private readonly AdventureData adventureData;
private readonly IGameLogStore gameLogStore;
private readonly Dictionary<PlayMode, IPlayMode?> playModeCache = new();
private IMenuView? pauseMenuView;
private IPlayMode? playMode;
private IPlayMode? suspendedPlayMode;
private bool isTransitioning;
private PlayMode currentPlaymode;
private bool stateEntered;
private AsyncOperationHandle<AdventureSettings> advSettingsHandler;
private AdventureSettings? adventureSettings;
public bool IsGameStateInitialized {
get => stateEntered && (playMode == null || playMode.IsGameModeInitialized);
}
public GameModeGameState(GameDataState gameDataState,
BootstrapReferences bootstrapReferences,
PlatformSettings platformSettings,
ISaveSystem saveSystem,
ISceneTransition sceneTransition,
AdventureData adventureData,
IGameLogStore gameLogStore) {
this.gameDataState = gameDataState;
this.platformSettings = platformSettings;
this.saveSystem = saveSystem;
this.sceneTransition = sceneTransition;
this.adventureData = adventureData;
this.gameLogStore = gameLogStore;
bootstrapSettings = Addressables.LoadAssetAsync<PlayModeSettings>(bootstrapReferences.playModeSettings).WaitForCompletion();
}
public void EnterGameState() {
if(gameDataState.ActivePlayMode == PlayMode.None) {
var sceneReference = Object.FindFirstObjectByType<SceneReference>();
if(bootstrapSettings.gameModeData.AsValueEnumerable().Any(g => g.playMode == sceneReference.playMode)) {
gameDataState.ChangePlayMode(sceneReference.playMode);
}
}
advSettingsHandler = Addressables.LoadAssetAsync<AdventureSettings>("AdventureSettings");
adventureSettings = advSettingsHandler.WaitForCompletion();
InitializeGameViews();
currentPlaymode = gameDataState.ActivePlayMode;
playMode?.EnterPlayMode();
Debug.Log($"Initialized Game Mode: {gameDataState.ActivePlayMode}");
stateEntered = true;
}
private void InitializeGameViews() {
SwitchGameMode();
pauseMenuView ??= new PauseMenuView(gameDataState, saveSystem, CaptureNoxSaveData);
}
private NoxSavedDataSet? CaptureNoxSaveData() {
var adventure = FindAdventurePlayMode();
var saveData = adventure?.CaptureNoxSaveData();
if(saveData != null) {
saveData.gameLogData = gameLogStore.GetSaveData();
}
return saveData;
}
private AdventurePlayMode? FindAdventurePlayMode() {
if(playMode is AdventurePlayMode active) {
return active;
}
if(suspendedPlayMode is AdventurePlayMode suspended) {
return suspended;
}
if(playModeCache.TryGetValue(PlayMode.Adventure, out var cached) && cached is AdventurePlayMode fromCache) {
return fromCache;
}
return null;
}
private void SwitchGameMode() {
var activeParty = gameDataState.ActiveParty;
if(playModeCache.TryGetValue(gameDataState.ActivePlayMode, out var cached)) {
playMode = cached;
return;
}
playMode = gameDataState.ActivePlayMode switch {
PlayMode.Adventure => new AdventurePlayMode(platformSettings, activeParty, bootstrapSettings, gameDataState, saveSystem, adventureSettings, adventureData),
PlayMode.PauseMenu => new PauseMenuPlayMode(platformSettings, gameDataState, pauseMenuView!),
PlayMode.Town => new TownPlayMode(platformSettings, activeParty),
PlayMode.Rest => new RestPlayMode(platformSettings, activeParty),
PlayMode.Combat => new CombatPlayMode(platformSettings, activeParty),
_ => playMode
};
playModeCache[gameDataState.ActivePlayMode] = playMode;
}
public GameState Tick() {
if(isTransitioning) {
return GameState.GameMode;
}
if(ShouldTransition()) {
HandlePlayModeTransition();
return GameState.GameMode;
}
if(playMode is { IsGameModeInitialized: true }) {
playMode.Tick();
}
return GameState.GameMode;
}
private bool ShouldTransition() {
return currentPlaymode != gameDataState.ActivePlayMode
&& gameDataState.ActivePlayMode != PlayMode.None;
}
private bool IsPauseTransition() {
return gameDataState.ActivePlayMode == PlayMode.PauseMenu
|| (currentPlaymode == PlayMode.PauseMenu && suspendedPlayMode != null);
}
private void HandlePlayModeTransition() {
if(IsPauseTransition()) {
ExecutePlayModeSwitch();
return;
}
isTransitioning = true;
sceneTransition.FadeOut(() => {
ExecutePlayModeSwitch();
sceneTransition.FadeIn(() => {
isTransitioning = false;
});
});
}
private void ExecutePlayModeSwitch() {
var enteringPause = gameDataState.ActivePlayMode == PlayMode.PauseMenu;
var resumingFromPause = currentPlaymode == PlayMode.PauseMenu
&& suspendedPlayMode != null;
if(enteringPause) {
suspendedPlayMode = playMode;
}
playMode?.ExitGameMode();
currentPlaymode = gameDataState.ActivePlayMode;
if(resumingFromPause) {
playMode = suspendedPlayMode;
suspendedPlayMode = null;
playMode?.EnterPlayMode();
return;
}
SwitchGameMode();
playMode?.EnterPlayMode();
}
public void LateTick() {
if(playMode is { IsGameModeInitialized: true }) {
playMode.LateTick();
}
}
public void Dispose() {
suspendedPlayMode?.Dispose();
playMode?.Dispose();
}
public void ExitGameState() {
suspendedPlayMode?.ExitGameMode();
suspendedPlayMode?.Dispose();
suspendedPlayMode = null;
playMode?.ExitGameMode();
playMode?.Dispose();
playMode = null;
foreach(var cached in playModeCache.Values) {
if(cached != null && cached != playMode && cached != suspendedPlayMode) {
cached.Dispose();
}
}
playModeCache.Clear();
stateEntered = false;
gameDataState.ChangePlayMode(PlayMode.None);
advSettingsHandler.Release();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 306626ccbd8594138af9911a5743c82e

View File

@@ -0,0 +1,110 @@
using System.Collections.Generic;
using Nox.Platform;
using Unity.Profiling;
using UnityEngine;
namespace Nox.Core {
/// <summary>
/// This is the class that will execute the update cycles for each game state. This should be the only place using Unity's Monobehaviour specific classes
/// </summary>
public class GameStateRunner : MonoBehaviour {
private IGameState currentState;
private GameState currentStateType;
private Dictionary<GameState, IGameState> applicationStates;
private GameDataState gameDataState;
private bool isRunnerUpdated;
private bool isUnloading;
private IPlatform platform;
private ISceneTransition sceneTransition;
private GameState pendingStateTransition;
private bool isStateTransitioning;
private bool waitingForFadeIn;
public void Initialize(Dictionary<GameState, IGameState> stateLookUp, GameDataState gameDataState, IPlatform selectedPlatform, ISceneTransition sceneTransition) {
DontDestroyOnLoad(gameObject);
this.platform = selectedPlatform;
this.sceneTransition = sceneTransition;
applicationStates = stateLookUp;
this.gameDataState = gameDataState;
ChangeApplicationState(gameDataState.ActiveGameState);
}
public void FixedUpdate() {
if(!currentState.IsGameStateInitialized) {
return;
}
}
public void Update() {
// Pre-ticks area
platform.Tick();
if(waitingForFadeIn) {
if(currentState.IsGameStateInitialized) {
waitingForFadeIn = false;
sceneTransition.FadeIn(() => {
isStateTransitioning = false;
});
}
return;
}
if(isStateTransitioning) {
return;
}
if(!isRunnerUpdated && currentState.IsGameStateInitialized) {
isRunnerUpdated = true;
}
var nextState = currentStateType;
if(isRunnerUpdated || isUnloading) {
nextState = currentState.Tick();
if(gameDataState.ActiveGameState != currentStateType) {
nextState = gameDataState.ActiveGameState;
}
}
if(nextState != currentStateType) {
BeginStateTransition(nextState);
}
// Ticks area
// Post-ticks area
}
public void LateUpdate() {
if(isStateTransitioning || !currentState.IsGameStateInitialized) {
return;
}
currentState.LateTick();
}
public void OnDestroy() {
currentState.Dispose();
}
private void BeginStateTransition(GameState newGameState) {
isStateTransitioning = true;
pendingStateTransition = newGameState;
sceneTransition.FadeOut(() => {
ChangeApplicationState(pendingStateTransition);
waitingForFadeIn = true;
});
}
private void ChangeApplicationState(GameState newGameState) {
if(currentState != null) {
currentState.ExitGameState();
}
currentState = applicationStates[newGameState];
currentState.EnterGameState();
currentStateType = newGameState;
gameDataState.ChangeGameState(newGameState);
isRunnerUpdated = false;
Debug.Log($"Entered {newGameState} ApplicationState");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a039e3a8262654d2b9428ec8aa6d3140

View File

@@ -0,0 +1,11 @@
namespace Nox.Core {
/// <summary>
/// Prototype for every behaviour that needs to run tick/frame updates
/// </summary>
public interface IGameLifecycle {
public void Initialize();
public void Tick();
public void Dispose();
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 55887b4438f5b42b891be19046c0c963

View File

@@ -0,0 +1,24 @@
namespace Nox.Core {
/// <summary>
/// Add or modify project specific game states
/// </summary>
public enum GameState {
Invalid,
BootState,
Loading,
MainMenu,
GameMode
}
/// <summary>
/// Prototype for creating application states
/// </summary>
public interface IGameState {
public void EnterGameState();
public GameState Tick();
public void LateTick();
public void ExitGameState();
void Dispose();
bool IsGameStateInitialized { get; }
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d01b97d5164454406931704f451edba9

View File

@@ -0,0 +1,23 @@
namespace Nox.Core {
public enum PlayMode {
None,
PauseMenu,
Adventure,
Town,
Rest,
Combat
}
/// <summary>
/// Prototype for crating new game modes
/// </summary>
public interface IPlayMode {
bool IsGameModeInitialized { get;}
public void EnterPlayMode();
public void Tick();
public void LateTick();
void ExitGameMode();
void Dispose();
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7fb4f9f9705394782b9ed1852e8f015c

View File

@@ -0,0 +1,9 @@
using System;
namespace Nox.Core {
public interface ISceneTransition {
bool IsTransitioning { get; }
void FadeOut(Action onComplete = null);
void FadeIn(Action onComplete = null);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0b503206ce8521d469bf99fd4b0df611

View File

@@ -0,0 +1,12 @@
using Nox.Platform;
using UnityEngine;
using UnityEngine.AddressableAssets;
namespace Nox.Core {
[CreateAssetMenu(menuName = "Nox/Database/General/InitializerSettingsFile", fileName = "InitializerSettingsFile")]
public class InitializerSettingsFile : ScriptableObject {
public AssetReferenceT<DesktopPlatformSettings> desktopPlatformSettings;
public AssetReferenceT<BootstrapReferences> bootstrapSettings;
public AssetReferenceT<UnityEditorPlatformSettings> unityEditorPlatformSettings;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 593adee4e2b3d4537b4d956077816eec

View File

@@ -0,0 +1,20 @@
namespace Nox.Core {
/// <summary>
/// This is a state which would normally consist of a load screen scene or anything similar
/// </summary>
public class LoadingGameState : IGameState {
public bool IsGameStateInitialized { get; set; }
public void EnterGameState() {
}
public GameState Tick() {
return GameState.Loading;
}
public void LateTick() { }
public void ExitGameState() {
}
public void Dispose() {
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e20613474534045658e4e2a0b9657ade

View File

@@ -0,0 +1,113 @@
using Nox.Game;
using Jovian.SaveSystem;
using Jovian.InGameLogging;
using Nox.UI;
using System;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceProviders;
namespace Nox.Core {
public class MenuGameStateData {
public Action<PlayMode> startGameRequests;
public Action continueGameRequest;
}
public class MainMenuGameState : IGameState {
private readonly GameDataState gameDataState;
private readonly MenuGameStateData menuGameStateData;
private readonly BootstrapReferences bootstrapReferences;
private readonly ISaveSystem saveSystem;
private readonly IGameLogStore gameLogStore;
private AdventureData adventureData;
private Action<PlayMode> onStartGameRequested;
private Action onContinueRequested;
private MainMenuView mainMenuView;
private AsyncOperationHandle<SceneInstance> loadSceneAsync;
public bool IsGameStateInitialized { get; set; }
public MainMenuGameState(GameDataState gameDataState,
MenuGameStateData menuGameStateData,
BootstrapReferences bootstrapReferences,
ISaveSystem saveSystem,
AdventureData adventureData,
IGameLogStore gameLogStore) {
this.gameDataState = gameDataState;
this.menuGameStateData = menuGameStateData;
this.bootstrapReferences = bootstrapReferences;
this.saveSystem = saveSystem;
this.adventureData = adventureData;
this.gameLogStore = gameLogStore;
}
public void EnterGameState() {
IsGameStateInitialized = false;
onStartGameRequested = mode => {
var sessions = saveSystem.GetAllSessions();
gameDataState.activeSessionId = sessions.Count > 0
? sessions[0].sessionId
: saveSystem.CreateSession();
gameDataState.savedPartyPosition = new Vector3(7.285129f, 0, 0.4297419f);
gameDataState.ChangeGameState(GameState.GameMode);
gameDataState.ChangePlayMode(mode);
};
onContinueRequested = () => {
var saveData = NoxSaveData.RestoreSavedData(saveSystem, gameDataState, ref adventureData, gameLogStore);
if(saveData == null) {
return;
}
gameDataState.ChangeGameState(GameState.GameMode);
gameDataState.ChangePlayMode(saveData.activePlayMode);
gameDataState.ActiveParty = saveData.activeParty;
};
menuGameStateData.startGameRequests += onStartGameRequested;
menuGameStateData.continueGameRequest += onContinueRequested;
_ = InitializeGameState();
}
private async Task InitializeGameState() {
var sceneHandle = Addressables.LoadSceneAsync(bootstrapReferences.startMenuScene);
await sceneHandle.Task;
await sceneHandle.Result.ActivateAsync();
var assetHandle = Addressables.LoadAssetAsync<MenuPrefabsContainer>(bootstrapReferences.menuPrefabsContainer);
await assetHandle.Task;
var characterBaseSettings = Addressables.LoadAssetAsync<StarterCharacterSettings>("CharacterBaseSettings").WaitForCompletion();
var perKRegistry = Addressables.LoadAssetAsync<PerksRegistry>("PerksRegistry").WaitForCompletion();
var characterRegistry = Addressables.LoadAssetAsync<CharacterRegistry>("CharacterRegistry").WaitForCompletion();
var modifiersRegistry = Addressables.LoadAssetAsync<ModifiersRegistry>("ModifiersRegistry").WaitForCompletion();
var partySettings = Addressables.LoadAssetAsync<PartySettings>("DefaultPartySettings").WaitForCompletion();
var portraitsHolder = Addressables.LoadAssetAsync<PortraitsHolder>(assetHandle.Result.portraitsHolder).WaitForCompletion();
var characterSystems = CharacterSystemsFactory.Create(partySettings, characterBaseSettings, perKRegistry, characterRegistry, modifiersRegistry);
mainMenuView = new MainMenuView(assetHandle.Result, menuGameStateData, saveSystem, gameDataState, partySettings, characterSystems, portraitsHolder, characterBaseSettings);
mainMenuView.Initialize();
mainMenuView.Show();
IsGameStateInitialized = true;
}
public GameState Tick() {
mainMenuView?.Tick();
return gameDataState.ActiveGameState;
}
public void LateTick() { }
public void Dispose() { }
public void ExitGameState() {
if(onStartGameRequested != null) {
menuGameStateData.startGameRequests -= onStartGameRequested;
onStartGameRequested = null;
}
if(onContinueRequested != null) {
menuGameStateData.continueGameRequest -= onContinueRequested;
onContinueRequested = null;
}
mainMenuView?.Dispose();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9453c712e94474abe8eea1a900eb12c4

View File

@@ -0,0 +1,37 @@
using Nox.Input;
using Nox.Platform;
namespace Nox.Core {
/// <summary>
/// Base class used by the platform factory to select the current platform and the input
/// </summary>
public class PlatformSelector {
public readonly DevicePlatform devicePlatform;
private InputMode inputMode;
public PlatformSelector(DevicePlatform devicePlatform, InputMode inputMode) {
this.devicePlatform = devicePlatform;
this.inputMode = inputMode;
}
public void SetInputMode(InputMode newInputMode) {
inputMode = newInputMode;
}
public static InputMode GetPlatformDefaultInputMode() {
#if UNITY_EDITOR
return InputMode.Editor;
#else
return InputMode.Desktop;
#endif
}
public static DevicePlatform GetDevicePlatform() {
#if UNITY_EDITOR
return DevicePlatform.UnityEditor;
#else
return DevicePlatform.Desktop;
#endif
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d92359b567a9c4b67882fd1c63a2d28a

View File

@@ -0,0 +1,16 @@
using System;
using UnityEngine;
using UnityEngine.AddressableAssets;
namespace Nox.Core {
[CreateAssetMenu(fileName = "GameModeSettings", menuName = "Nox/Database/General/GameModeSettings")]
public class PlayModeSettings : ScriptableObject {
public PlayModeData[] gameModeData;
}
[Serializable]
public class PlayModeData {
public PlayMode playMode;
public AssetReference scene;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 00bbbdf0f374243d986d909bc14c6773

View File

@@ -0,0 +1,12 @@
using UnityEngine;
namespace Nox.Core {
/// <summary>
/// Scene generic authoring/reference collection. Can also contain the settings for booting up a particular scene
/// </summary>
public class SceneReference : MonoBehaviour {
public GameState gameState = GameState.Invalid;
public PlayMode playMode;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 96fc2eb19de9a4236b7ca7d6a7ebd9a8

View File

@@ -0,0 +1,37 @@
using Nox.UI;
using UnityEngine;
using UnityEngine.AddressableAssets;
namespace Nox.Core {
/// <summary>
/// This is an example for the first game state which in our case would usually contain a gdpr
/// This is an example to show how it could be initialized and also how it could boot into the next game state
/// Ideally a SceneHandler/Manager of sorts should handle loading and unloading scenes.
/// </summary>
public class SplashGameState : IGameState {
private readonly BootstrapReferences bootStrapInitializer;
private readonly GameDataState gameDataState;
public bool IsGameStateInitialized { get; set; } = true;
public SplashGameState(BootstrapReferences bootStrapInitializer, GameDataState gameDataState) {
this.bootStrapInitializer = bootStrapInitializer;
this.gameDataState = gameDataState;
}
public void EnterGameState() {
DisclaimerReference gdprReference = Addressables.InstantiateAsync(bootStrapInitializer.splashUIReference).WaitForCompletion().GetComponent<DisclaimerReference>();
gdprReference.continueButton.onClick.AddListener(() => {
gameDataState.ChangeGameState(GameState.MainMenu);
Object.Destroy(gdprReference.gameObject);
});
}
public GameState Tick() {
return GameState.BootState;
}
public void LateTick() { }
public void Dispose() { }
public void ExitGameState() { }
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 607a080faa19b492baa5c1b9ad13302b

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e9a24b538d250f84b9b14564ccb4768d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 780f3ab23ddb9c24988bc45c1a8e1377
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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();
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 517c20f5b3ec4700afd07917ea316092
timeCreated: 1771075174

View File

@@ -0,0 +1,9 @@
using System;
using UnityEngine;
namespace Nox.Game {
public class CameraReference : MonoBehaviour {
public Camera mainCamera;
public CameraSettings cameraSettings;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c55bcf874b547124a861f2b9494f991e

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b508138d992c4f7280afcc0e1d6c4e8f
timeCreated: 1771153846

View File

@@ -0,0 +1,10 @@
using UnityEngine;
namespace Nox.Game {
public interface ICameraController {
CameraReference CameraReference {get;}
void Initialize();
void Tick();
void Dispose();
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e0a1d5f367bb4b3fb740541f9a0ba163
timeCreated: 1771152354

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b7671be7321935f46b97f6859276be8b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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()
};
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1c5d19a12e554fd981cec483ccfcff68
timeCreated: 1774183852

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0033c4836b0d4fbbb6e4dde0ac23a2f6
timeCreated: 1774183767

View 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 ();
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 91e3086d6951456e9a8a59fcbac0f750
timeCreated: 1774179358

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 75a241c095744a518eb8d1c1b14d24e4
timeCreated: 1774183822

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e9cec54a2252457595e3a97d3dcd755d
timeCreated: 1774184077

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 60db4014b69d403db42ee766204eb2d7
timeCreated: 1774183978

View 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();
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 98b50e969ea0412ca3575cd28b6018b7
timeCreated: 1772574533

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 882798d3e5a24150b1f11f486afdbc3e
timeCreated: 1774199617

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e6ae0d7bf0164ff793609e7f161fd7e3
timeCreated: 1774200023

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 130a7ebdbcf2417392d8fc3f9ae76eb2
timeCreated: 1774176049

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f231d6e487bc4577847de98936498bde
timeCreated: 1772644731

View 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.");
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f54ae3aaf9da4f5eaeb6d1cafe83a74f
timeCreated: 1774183779

View File

@@ -0,0 +1,6 @@
using UnityEngine;
namespace Nox.Game {
public class PartyReference : MonoBehaviour {
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 067750df0e87b834bbbb239b7c03abb4

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ff702898b3cd4100b9b8230f04711fa9
timeCreated: 1774195937

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1de2461f550e4ef8a324d5746222898f
timeCreated: 1774183789

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0adb42b13ce44d8792247246a49e9c3f
timeCreated: 1774179294

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1bbecc15c7cd4a7ca90ce17b3d3d75f0
timeCreated: 1774184829

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

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b62368c672d7fde4d94dc19ae74e133d

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 35a9294689c9454fba6e3cd8d818c38f
timeCreated: 1772367730

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

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e7cce6bb84bb20741a560389ab887769

View 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();
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: eb1c247bb12a45a587e8ae7d013038d7
timeCreated: 1771156737

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1d2424482c4843c0b7a33a9cc67411b2
timeCreated: 1771786400

View 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() { }
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 087dc559ae894cd8b9a9dee5c6e3b64c
timeCreated: 1772367690

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d6216fd41db9494aaa6c127d9d790b93
timeCreated: 1776506857

View File

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0c14350aeeed44a28d98d22c6ef048dc
timeCreated: 1777235448

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 523274f9158f453dbfac02601a77c3f7
timeCreated: 1776506833

View 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 { }
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: abf01048c404a0240ad538b65a15f5fb

View File

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7fc36c84acd04836b1280aba57a64e24
timeCreated: 1777229589

Some files were not shown because too many files have changed in this diff Show More