First commit on my server, yey!

This commit is contained in:
Sebastian Bularca
2026-03-19 18:12:07 +01:00
parent 5139ec2cec
commit fedd1961a0
602 changed files with 101587 additions and 6 deletions

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,469 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Nox.Game {
public enum CharacterRole {
Protagonist,
Companion
}
[Serializable]
public sealed class CharacterAttributes {
public int might;
public int reflex;
public int knowledge;
public int Total => might + reflex + knowledge;
}
[Serializable]
public sealed class CharacterStats {
public int maxHealth;
public int maxStamina;
public float dodgeStaminaLossMultiplier;
}
[Serializable]
public sealed class PerkDefinition {
public string id;
public string name;
public string mechanicalBonus;
public string thematicPenalty;
}
[Serializable]
public sealed class CharacterData {
public string id;
public string displayName;
public CharacterRole role;
public int level;
public int experience;
public CharacterAttributes attributes;
public CharacterStats stats;
public List<PerkDefinition> perks = new List<PerkDefinition>();
public CharacterData Clone() {
return new CharacterData {
id = id,
displayName = displayName,
role = role,
level = level,
experience = experience,
attributes = new CharacterAttributes {
might = attributes?.might ?? 0,
reflex = attributes?.reflex ?? 0,
knowledge = attributes?.knowledge ?? 0
},
stats = new CharacterStats {
maxHealth = stats?.maxHealth ?? 0,
maxStamina = stats?.maxStamina ?? 0,
dodgeStaminaLossMultiplier = stats?.dodgeStaminaLossMultiplier ?? 1f
},
perks = perks.Select(p => new PerkDefinition {
id = p.id,
name = p.name,
mechanicalBonus = p.mechanicalBonus,
thematicPenalty = p.thematicPenalty
}).ToList()
};
}
}
[Serializable]
public sealed class CharacterTemplate {
public string id;
public string displayName;
public CharacterAttributes attributes;
public int level = 1;
public int experience;
public List<string> startingPerkIds = new List<string>();
}
[Serializable]
public sealed class CustomCharacterCreationRequest {
public string id;
public string displayName;
public int mightPoints;
public int reflexPoints;
public int knowledgePoints;
public List<string> startingPerkIds = new List<string>();
}
[Serializable]
public sealed class PartyData {
public List<CharacterData> members = new List<CharacterData>();
public int maxPartySize;
[JsonIgnore]
public CharacterData Protagonist => members.FirstOrDefault(m => m.role == CharacterRole.Protagonist);
[JsonIgnore]
public IReadOnlyList<CharacterData> Companions => members.Where(m => m.role == CharacterRole.Companion).ToList();
}
public interface ICharacterAttributesFactory {
CharacterAttributes Create(int might, int reflex, int knowledge);
CharacterAttributes CreateFromPointAllocation(int mightPoints, int reflexPoints, int knowledgePoints);
}
public interface ICharacterStatsFactory {
CharacterStats Create(CharacterAttributes attributes);
}
public interface IPerkFactory {
IReadOnlyCollection<PerkDefinition> GetAll();
PerkDefinition GetById(string perkId);
IReadOnlyCollection<PerkDefinition> GetAvailableFor(CharacterData character);
bool TryAddPerk(CharacterData character, string perkId);
}
public interface ICharacterFactory {
CharacterData CreateCustomProtagonist(CustomCharacterCreationRequest request);
CharacterData CreateFromTemplate(CharacterTemplate template, CharacterRole role = CharacterRole.Companion);
}
public interface IPartyFactory {
PartyData Create(CharacterData protagonist, IEnumerable<CharacterData> companions = null);
}
public interface ICharacterSystems {
IPerkFactory PerkFactory { get; }
ICharacterFactory CharacterFactory { get; }
IPartyFactory PartyFactory { get; }
}
public sealed class CharacterFactoryOptions {
public int baseMight = 1;
public int baseReflex = 1;
public int baseKnowledge = 1;
public int customAttributePointBudget = 10;
public int startingLevel = 1;
}
public sealed class CharacterStatsFactoryOptions {
public int baseHealth = 10;
public int baseStamina = 5;
public int mightHealthBonus = 3;
public int mightStaminaBonus = 1;
public int knowledgeStaminaBonus = 2;
public float baseDodgeStaminaLossMultiplier = 1f;
public float reflexDodgeStaminaLossReduction = 0.03f;
public float minDodgeStaminaLossMultiplier = 0.4f;
}
public sealed class PartyFactoryOptions {
public int minPartySize = 1;
public int maxPartySize = 4;
public bool enforceUniqueCharacterIds = true;
}
public sealed class CharacterSystems : ICharacterSystems {
public CharacterSystems(IPerkFactory perkFactory, ICharacterFactory characterFactory, IPartyFactory partyFactory) {
PerkFactory = perkFactory;
CharacterFactory = characterFactory;
PartyFactory = partyFactory;
}
public IPerkFactory PerkFactory { get; }
public ICharacterFactory CharacterFactory { get; }
public IPartyFactory PartyFactory { get; }
}
public static class DefaultCharacterSystemsFactory {
public static ICharacterSystems Create(int maxPartySize = 8) {
IPerkFactory perkFactory = new PerkFactory(CreateDefaultPerks());
ICharacterAttributesFactory attributesFactory = new CharacterAttributesFactory(new CharacterFactoryOptions {
baseMight = 1,
baseReflex = 1,
baseKnowledge = 1,
customAttributePointBudget = 10,
startingLevel = 1
});
ICharacterStatsFactory statsFactory = new CharacterStatsFactory();
ICharacterFactory characterFactory = new CharacterFactory(attributesFactory, statsFactory, perkFactory);
IPartyFactory partyFactory = new PartyFactory(new PartyFactoryOptions {
minPartySize = 1,
maxPartySize = maxPartySize,
enforceUniqueCharacterIds = true
});
return new CharacterSystems(perkFactory, characterFactory, partyFactory);
}
private static IEnumerable<PerkDefinition> CreateDefaultPerks() {
return new[] {
new PerkDefinition { id = "iron-will", name = "Iron Will", mechanicalBonus = "+1 max health per level", thematicPenalty = "-1 social flexibility in dialogue checks" },
new PerkDefinition { id = "steadfast", name = "Steadfast", mechanicalBonus = "-10% stamina loss when bracing", thematicPenalty = "-10% movement speed in retreat events" },
new PerkDefinition { id = "nimble-step", name = "Nimble Step", mechanicalBonus = "-15% dodge stamina loss", thematicPenalty = "+10% stamina loss on heavy actions" },
new PerkDefinition { id = "lorekeeper", name = "Lorekeeper", mechanicalBonus = "+15% knowledge event success", thematicPenalty = "-10% intimidation success chance" },
new PerkDefinition { id = "bulwark", name = "Bulwark", mechanicalBonus = "+2 base defense checks", thematicPenalty = "-1 reflex in stealth checks" },
new PerkDefinition { id = "pathfinder", name = "Pathfinder", mechanicalBonus = "+15% scouting event success", thematicPenalty = "-1 max health during ambush events" }
};
}
}
public sealed class CharacterAttributesFactory : ICharacterAttributesFactory {
private readonly CharacterFactoryOptions options;
public CharacterAttributesFactory(CharacterFactoryOptions options = null) {
this.options = options ?? new CharacterFactoryOptions();
}
public CharacterAttributes Create(int might, int reflex, int knowledge) {
if(might < 0 || reflex < 0 || knowledge < 0) {
throw new ArgumentOutOfRangeException(nameof(might), "attributes cannot be negative.");
}
return new CharacterAttributes {
might = might,
reflex = reflex,
knowledge = knowledge
};
}
public CharacterAttributes CreateFromPointAllocation(int mightPoints, int reflexPoints, int knowledgePoints) {
if(mightPoints < 0 || reflexPoints < 0 || knowledgePoints < 0) {
throw new ArgumentOutOfRangeException(nameof(mightPoints), "Point allocation cannot be negative.");
}
int allocated = mightPoints + reflexPoints + knowledgePoints;
if(allocated > options.customAttributePointBudget) {
throw new ArgumentException($"Allocated {allocated} points but budget is {options.customAttributePointBudget}.");
}
return new CharacterAttributes {
might = options.baseMight + mightPoints,
reflex = options.baseReflex + reflexPoints,
knowledge = options.baseKnowledge + knowledgePoints
};
}
}
public sealed class CharacterStatsFactory : ICharacterStatsFactory {
private readonly CharacterStatsFactoryOptions options;
public CharacterStatsFactory(CharacterStatsFactoryOptions options = null) {
this.options = options ?? new CharacterStatsFactoryOptions();
}
public CharacterStats Create(CharacterAttributes attributes) {
if(attributes == null) {
throw new ArgumentNullException(nameof(attributes));
}
int maxHealth = options.baseHealth + attributes.might * options.mightHealthBonus;
int maxStamina = options.baseStamina +
attributes.might * options.mightStaminaBonus +
attributes.knowledge * options.knowledgeStaminaBonus;
float dodgeMultiplier = options.baseDodgeStaminaLossMultiplier -
attributes.reflex * options.reflexDodgeStaminaLossReduction;
dodgeMultiplier = Math.Max(options.minDodgeStaminaLossMultiplier, dodgeMultiplier);
return new CharacterStats {
maxHealth = maxHealth,
maxStamina = maxStamina,
dodgeStaminaLossMultiplier = dodgeMultiplier
};
}
}
public sealed class PerkFactory : IPerkFactory {
private readonly Dictionary<string, PerkDefinition> perkPool;
public PerkFactory(IEnumerable<PerkDefinition> perkPool) {
if(perkPool == null) {
throw new ArgumentNullException(nameof(perkPool));
}
this.perkPool = perkPool
.Where(p => p != null && !string.IsNullOrWhiteSpace(p.id))
.GroupBy(p => p.id)
.ToDictionary(g => g.Key, g => g.First());
}
public IReadOnlyCollection<PerkDefinition> GetAll() {
return perkPool.Values.ToList();
}
public PerkDefinition GetById(string perkId) {
if(string.IsNullOrWhiteSpace(perkId)) {
return null;
}
perkPool.TryGetValue(perkId, out PerkDefinition perk);
return perk;
}
public IReadOnlyCollection<PerkDefinition> GetAvailableFor(CharacterData character) {
if(character == null) {
return perkPool.Values.ToList();
}
HashSet<string> ownedPerkIds = character.perks
.Where(p => p != null && !string.IsNullOrWhiteSpace(p.id))
.Select(p => p.id)
.ToHashSet();
return perkPool.Values.Where(p => !ownedPerkIds.Contains(p.id)).ToList();
}
public bool TryAddPerk(CharacterData character, string perkId) {
if(character == null || string.IsNullOrWhiteSpace(perkId)) {
return false;
}
if(character.perks.Any(p => p != null && p.id == perkId)) {
return false;
}
if(!perkPool.TryGetValue(perkId, out PerkDefinition perk)) {
return false;
}
character.perks.Add(new PerkDefinition {
id = perk.id,
name = perk.name,
mechanicalBonus = perk.mechanicalBonus,
thematicPenalty = perk.thematicPenalty
});
return true;
}
}
public sealed class CharacterFactory : ICharacterFactory {
private readonly CharacterFactoryOptions options;
private readonly ICharacterAttributesFactory attributesFactory;
private readonly ICharacterStatsFactory statsFactory;
private readonly IPerkFactory perkFactory;
public CharacterFactory(
ICharacterAttributesFactory attributesFactory,
ICharacterStatsFactory statsFactory,
IPerkFactory perkFactory,
CharacterFactoryOptions options = null) {
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.options = options ?? new CharacterFactoryOptions();
}
public CharacterData CreateCustomProtagonist(CustomCharacterCreationRequest request) {
if(request == null) {
throw new ArgumentNullException(nameof(request));
}
CharacterAttributes attributes = attributesFactory.CreateFromPointAllocation(
request.mightPoints,
request.reflexPoints,
request.knowledgePoints);
CharacterData character = new CharacterData {
id = string.IsNullOrWhiteSpace(request.id) ? Guid.NewGuid().ToString("N") : request.id,
displayName = request.displayName,
role = CharacterRole.Protagonist,
level = options.startingLevel,
experience = 0,
attributes = attributes,
stats = statsFactory.Create(attributes)
};
AddStartingPerks(character, request.startingPerkIds);
return character;
}
public CharacterData CreateFromTemplate(CharacterTemplate template, CharacterRole role = CharacterRole.Companion) {
if(template == null) {
throw new ArgumentNullException(nameof(template));
}
CharacterAttributes sourceAttributes = template.attributes ?? attributesFactory.Create(0, 0, 0);
CharacterAttributes attributes = attributesFactory.Create(
sourceAttributes.might,
sourceAttributes.reflex,
sourceAttributes.knowledge);
CharacterData character = new CharacterData {
id = string.IsNullOrWhiteSpace(template.id) ? Guid.NewGuid().ToString("N") : template.id,
displayName = template.displayName,
role = role,
level = template.level <= 0 ? options.startingLevel : template.level,
experience = template.experience,
attributes = attributes,
stats = statsFactory.Create(attributes)
};
AddStartingPerks(character, template.startingPerkIds);
return character;
}
private void AddStartingPerks(CharacterData character, IEnumerable<string> perkIds) {
if(perkIds == null) {
return;
}
foreach(string perkId in perkIds.Distinct()) {
perkFactory.TryAddPerk(character, perkId);
}
}
}
public sealed class PartyFactory : IPartyFactory {
private readonly PartyFactoryOptions options;
public PartyFactory(PartyFactoryOptions options = null) {
this.options = options ?? new PartyFactoryOptions();
}
public PartyData Create(CharacterData protagonist, IEnumerable<CharacterData> companions = null) {
if(protagonist == null) {
throw new ArgumentNullException(nameof(protagonist));
}
PartyData party = new PartyData {
maxPartySize = options.maxPartySize <= 0 ? int.MaxValue : options.maxPartySize
};
CharacterData protagonistClone = protagonist.Clone();
protagonistClone.role = CharacterRole.Protagonist;
party.members.Add(protagonistClone);
if(companions != null) {
foreach(CharacterData companion in companions.Where(c => c != null)) {
CharacterData companionClone = companion.Clone();
companionClone.role = CharacterRole.Companion;
party.members.Add(companionClone);
}
}
ValidateParty(party);
return party;
}
private void ValidateParty(PartyData party) {
if(party.members.Count < options.minPartySize) {
throw new ArgumentException($"Party size {party.members.Count} is below minimum {options.minPartySize}.");
}
if(party.members.Count > party.maxPartySize) {
throw new ArgumentException($"Party size {party.members.Count} exceeds max {party.maxPartySize}.");
}
int protagonistCount = party.members.Count(m => m.role == CharacterRole.Protagonist);
if(protagonistCount != 1) {
throw new ArgumentException($"Party must contain exactly one protagonist, found {protagonistCount}.");
}
if(options.enforceUniqueCharacterIds) {
int uniqueIds = party.members
.Where(m => !string.IsNullOrWhiteSpace(m.id))
.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,2 @@
fileFormatVersion: 2
guid: ec5c743399d71cd41b2db4f9a7e5dec1

View File

@@ -0,0 +1,5 @@
namespace v {
public interface IEntity {
}
}

View File

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

View File

@@ -0,0 +1,60 @@
namespace Nox.Game {
public class PartyCreatorModel {
private readonly ICharacterFactory characterFactory;
private readonly IPartyFactory partyFactory;
public PartyCreatorModel(ICharacterFactory characterFactory, IPartyFactory partyFactory) {
this.characterFactory = characterFactory;
this.partyFactory = partyFactory;
}
public PartyData CreatePartyForNewRun(int companionCount) {
var protagonist = characterFactory.CreateCustomProtagonist(new CustomCharacterCreationRequest {
id = "protagonist",
displayName = "The Warden",
mightPoints = 4,
reflexPoints = 3,
knowledgePoints = 3,
startingPerkIds = new System.Collections.Generic.List<string> { "iron-will" }
});
CharacterTemplate[] companionTemplates = {
new() {
id = "companion-bruiser",
displayName = "Rook",
attributes = new CharacterAttributes { might = 5, reflex = 2, knowledge = 1 },
startingPerkIds = new System.Collections.Generic.List<string> { "steadfast" }
},
new() {
id = "companion-scout",
displayName = "Sable",
attributes = new CharacterAttributes { might = 2, reflex = 5, knowledge = 1 },
startingPerkIds = new System.Collections.Generic.List<string> { "nimble-step" }
},
new() {
id = "companion-scholar",
displayName = "Quill",
attributes = new CharacterAttributes { might = 1, reflex = 2, knowledge = 5 },
startingPerkIds = new System.Collections.Generic.List<string> { "lorekeeper" }
},
new() {
id = "companion-vanguard",
displayName = "Brant",
attributes = new CharacterAttributes { might = 4, reflex = 3, knowledge = 2 },
startingPerkIds = new System.Collections.Generic.List<string> { "bulwark" }
},
new() {
id = "companion-tracker",
displayName = "Mira",
attributes = new CharacterAttributes { might = 2, reflex = 4, knowledge = 3 },
startingPerkIds = new System.Collections.Generic.List<string> { "pathfinder" }
}
};
var companions = new System.Collections.Generic.List<CharacterData>();
for(var i = 0; i < companionCount && i < companionTemplates.Length; i++) {
companions.Add(characterFactory.CreateFromTemplate(companionTemplates[i], CharacterRole.Companion));
}
return partyFactory.Create(protagonist, companions);
}
}
}

View File

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

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,79 @@
using Jovian.SaveSystem;
using Nox.Core;
using System;
using System.Linq;
using UnityEngine;
using PlayMode = Nox.Core.PlayMode;
namespace Nox.Game {
public class NoxSaveData {
public static NoxSavedDataSet RestoreSavedData(
ISaveSystem saveSystem,
GameDataState gameDataState,
ref AdventureData adventureData) {
var sessions = saveSystem.GetAllSessions().OrderByDescending(s => s.lastSaveDateUtc).ToList();
if(sessions.Count == 0) {
return null;
}
var latestSession = sessions[0];
var slots = saveSystem.GetSlots(latestSession.sessionId).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.partyData;
adventureData = saveData.adventureData;
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 PartyData partyData;
public SerializableVector3 partyPosition;
}
/// <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,174 @@
using Jovian.SaveSystem;
using Jovian.ZoneSystem;
using Nox.Core;
using Nox.Platform;
using Nox.Game.UI;
using System.Linq;
using UnityEngine;
using UnityEngine.AddressableAssets;
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 PartyData partyData;
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 AdventureSettings adventureSettings;
private TimeHandler timeHandler;
private PartyInventoryHandler partyInventoryHandler;
public AdventurePlayMode(
PlatformSettings platformSettings,
PartyData partyData,
PlayModeSettings bootstrapSettings,
GameDataState gameDataState,
ISaveSystem saveSystem,
AdventureSettings adventureSettings,
AdventureData adventureData) {
this.platformSettings = platformSettings;
this.partyData = partyData;
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.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(partyData == null) {
var sessions = saveSystem.GetAllSessions().OrderByDescending(s => s.lastSaveDateUtc).ToList();
if(sessions.Count == 0) {
return;
}
var latestSession = sessions[0];
var slots = saveSystem.GetSlots(latestSession.sessionId).OrderByDescending(s => s.timestampUtc).ToList();
if(slots.Count == 0) {
return;
}
var latestSlot = slots[0];
var saveData = saveSystem.Load<NoxSaveData>(latestSlot);
Debug.Log($"Loaded save {latestSlot.DisplayLabel}");
if(saveData == null) {
Debug.LogError("Failed to load save data");
return;
}
NoxSaveData.RestoreSavedData(saveSystem, gameDataState, ref adventureData);
Debug.LogWarning("AdventurePlayMode started from the Adventure Scene. Loading the last Autosave");
}
scenePrefabs ??= Addressables.LoadAssetAsync<AdventureModePrefabs>("AdventureMapPrefabs").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();
timeHandler ??= new TimeHandler(adventureSettings, adventureData);
zoneSystem ??= new ZoneSystem(mapRef.zonesObjectHolder);
partyMovementHandler ??= new PartyMovementHandler(partyRef, cameraController, mapLocationsReference, platformSettings.inputSettings, zoneSystem, adventureData, adventureSettings);
partyMovementHandler.Initialize();
guiReferences ??= Object.FindFirstObjectByType<GuiReferences>();
adventureView ??= new AdventureView(gameDataState, guiReferences, inputActions, adventureData, adventureSettings);
adventureView.Initialize();
IsGameModeInitialized = true;
}
public void Tick() {
if(!IsGameModeInitialized) {
return;
}
timeHandler.Tick();
partyInventoryHandler.Tick();
partyMovementHandler.Tick();
adventureView.Tick();
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,
partyData = partyData,
partyPosition = partyRef ? SerializableVector3.FromVector3(partyRef.transform.position) : SerializableVector3.Zero
};
}
public void ExitGameMode() {
inputActions.Player.Disable();
inputActions.UI.PauseMenu.Disable();
}
public void Dispose() {
cameraController.Dispose();
partyMovementHandler.Dispose();
}
}
}

View File

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

View File

@@ -0,0 +1,12 @@
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 PartyData partyData;
public CombatPlayMode(PlatformSettings platformSettings, PartyData partyData) {
this.platformSettings = platformSettings;
this.partyData = partyData;
}
public bool IsGameModeInitialized { get; private set; }
public void EnterPlayMode() {
if(partyData == 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,10 @@
#nullable enable
namespace Nox.Game.UI {
public interface IMenuView {
void Initialize();
void Show();
void Hide();
void Tick();
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e99c535c23cc449984dcd145f4c1bec8
timeCreated: 1772374016

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

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 436b68ae9b9d347469b78e52a5aa7b53

View File

@@ -0,0 +1,7 @@
using UnityEngine;
namespace Nox.Game {
public class MapLocationsReference : MonoBehaviour {
public NoxLocationInfo [] mapLocations;
}
}

View File

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

View File

@@ -0,0 +1,9 @@
using Jovian.ZoneSystem;
using UnityEngine;
namespace Nox.Game {
public class MapReference : MonoBehaviour {
public GameObject mapPlane;
public ZonesObjectHolder zonesObjectHolder;
}
}

View File

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

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 535e7fdf7ddb4c1f9edf7254c3bb0acd
timeCreated: 1771763769

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c4a1ab7b529042ca8bf42714b383f918
timeCreated: 1773614576

View File

@@ -0,0 +1,160 @@
using Jovian.ZoneSystem;
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 ZoneSystem zoneSystem;
private readonly AdventureSettings adventureSettings;
private readonly float maxDistance = 100f;
private AdventureData adventureData;
private bool partyCanMove;
private Vector3 targetPosition;
private bool shouldHover;
private MapLocation currentSelectedPoi;
private string previousZoneId;
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,
ZoneSystem zoneSystem,
AdventureData adventureData,
AdventureSettings adventureSettings) {
this.partyReference = partyReference;
this.cameraController = cameraController;
this.mapLocationsReference = mapLocationsReference;
this.zoneSystem = zoneSystem;
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(mapReference.ValidateMoveLocation(hitInfoPoint)) {
targetPosition = clickHit.point;
partyCanMove = 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);
VerifyZones(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 VerifyZones(Vector3 position) {
var zoneContext = zoneSystem.QueryZone(position);
var currentZoneId = zoneContext.resolvedZoneId;
if(currentZoneId != previousZoneId) {
if(!string.IsNullOrEmpty(currentZoneId)) {
Debug.Log($"Entered zone: {currentZoneId} (encounter: {zoneContext.encounterTableId}, safe: {zoneContext.isSafe})");
}
else if(!string.IsNullOrEmpty(previousZoneId)) {
Debug.Log($"Left zone: {previousZoneId}");
}
previousZoneId = currentZoneId;
}
}
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;
}
}
}

View File

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

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: eab5b722cabf4ec7b79af55838d24ed4
timeCreated: 1772367657

View 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 PartyData partyData;
public RestPlayMode(PlatformSettings platformSettings, PartyData partyData) {
this.platformSettings = platformSettings;
this.partyData = partyData;
}
public bool IsGameModeInitialized { get; private set; }
public void EnterPlayMode() {
if(partyData == 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() { }
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 67cb7d9e318d474694bc0addab3e8511
timeCreated: 1772367678

View File

@@ -0,0 +1,58 @@
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 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) {
this.adventureSettings = adventureSettings;
this.adventureData = adventureData;
localTime = adventureData.currentTime * adventureSettings.dayLength;
}
public void Tick() {
if (!adventureData.isPartyMoving) return;
localTime += Time.deltaTime;
if (localTime >= adventureSettings.dayLength) {
localTime -= adventureSettings.dayLength;
adventureData.currentDay++;
}
adventureData.currentTime = localTime / adventureSettings.dayLength;
adventureData.currentDayPhase = GetPhase(adventureData.currentTime);
}
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;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 071a61f838db8e640b460ce029a8d5a5

View 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 PartyData partyData;
public TownPlayMode(PlatformSettings platformSettings, PartyData partyData) {
this.platformSettings = platformSettings;
this.partyData = partyData;
}
public bool IsGameModeInitialized { get; private set; }
public void EnterPlayMode() {
if(partyData == 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() { }
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: bd57598925a54bad93fe730e15023d94
timeCreated: 1772367666

View File

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c8ae54c0cc79451c8aa69e22800962a4
timeCreated: 1771160087

View File

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

View File

@@ -0,0 +1,56 @@
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 DayPhase dayPhase = DayPhase.Midnight;
private int currentDay;
public AdventureView(
GameDataState gameDataState,
GuiReferences guiReferences,
InputSystem_Actions inputActions,
AdventureData adventureData,
AdventureSettings adventureSettings) {
this.gameDataState = gameDataState;
this.guiReferences = guiReferences;
this.inputActions = inputActions;
this.adventureData = adventureData;
this.adventureSettings = adventureSettings;
}
private void InvokePauseMenu() {
gameDataState.ChangePlayMode(PlayMode.PauseMenu);
}
public void Initialize() {
guiReferences.pauseMenuButton.onClick.AddListener(InvokePauseMenu);
guiReferences.dayText.text = $"Day {adventureData.currentDay}, {adventureData.currentDayPhase.ToString()}";
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() {
if(dayPhase != adventureData.currentDayPhase) {
dayPhase = adventureData.currentDayPhase;
guiReferences.dayText.text = $"Day {adventureData.currentDay}, {adventureData.currentDayPhase.ToString()}";
}
if(currentDay != adventureData.currentDay) {
currentDay = adventureData.currentDay;
guiReferences.dayText.text = $"Day {adventureData.currentDay}, {adventureData.currentDayPhase.ToString()}";
guiReferences.suppliesBar.fillAmount = (float)adventureData.suppliesAvailable / adventureSettings.maxSupplies;
guiReferences.suppliesText.text = $"{adventureData.suppliesAvailable}/{adventureSettings.maxSupplies}";
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: cd45a685ea554bd7ad15745f836a6de9
timeCreated: 1772572300

View File

@@ -0,0 +1,13 @@
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace Nox.Game.UI {
public class GuiReferences : MonoBehaviour {
public Transform portraitsContainer;
public Image suppliesBar;
public TextMeshProUGUI suppliesText;
public Button pauseMenuButton;
public TextMeshProUGUI dayText;
}
}

View File

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

View File

@@ -0,0 +1,9 @@
using Nox.Game.UI;
using UnityEngine;
namespace Nox.Game {
[CreateAssetMenu(fileName = "PauseMenuPrefabs", menuName = "Nox/PauseMenuPrefabs")]
public class PauseMenuPrefabs : ScenePrefabs {
public PauseMenuReferences pauseMenuReferencesPrefab;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d58af5e98665467fb7d66e4822775e75
timeCreated: 1772571122

View File

@@ -0,0 +1,10 @@
using UnityEngine;
using UnityEngine.UI;
namespace Nox.Game.UI {
public class PauseMenuReferences : MonoBehaviour {
public Button resumeButton;
public Button exitButton;
public Button saveButton;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8c881c3395da5194b926ef10f819edd7

View File

@@ -0,0 +1,68 @@
#nullable enable
using System;
using Nox.Core;
using Jovian.SaveSystem;
using UnityEngine;
using UnityEngine.AddressableAssets;
using Object = UnityEngine.Object;
using PlayMode = Nox.Core.PlayMode;
namespace Nox.Game.UI {
public class PauseMenuView: IMenuView {
private readonly GameDataState gameDataState;
private readonly ISaveSystem saveSystem;
private readonly Func<NoxSavedDataSet?> captureSaveData;
private PauseMenuReferences? pauseMenu;
private PauseMenuPrefabs? pauseMenuPrefabs;
public PauseMenuView(GameDataState gameDataState, ISaveSystem saveSystem, Func<NoxSavedDataSet?> captureSaveData) {
this.gameDataState = gameDataState;
this.saveSystem = saveSystem;
this.captureSaveData = captureSaveData;
pauseMenu ??= Object.FindFirstObjectByType<PauseMenuReferences>();
if(pauseMenu) {
Hide();
}
}
public void Initialize() {
pauseMenuPrefabs ??= Addressables.LoadAssetAsync<PauseMenuPrefabs>("PauseMenuPrefabs").WaitForCompletion();
if(!pauseMenu) {
pauseMenu = Object.Instantiate(pauseMenuPrefabs.pauseMenuReferencesPrefab);
pauseMenu?.resumeButton.onClick.AddListener(() => {gameDataState.ChangePlayMode(gameDataState.PreviousPlayMode);});
pauseMenu?.exitButton.onClick.AddListener(() => {
OnSaveRequested();
gameDataState.ChangeGameState(GameState.MainMenu);
});
pauseMenu?.saveButton.onClick.AddListener(OnSaveRequested);
}
Show();
}
private void OnSaveRequested() {
if(string.IsNullOrEmpty(gameDataState.activeSessionId)) {
Debug.LogWarning("[PauseMenuView] No active session. Cannot save.");
return;
}
var data = captureSaveData();
if(data == null) {
Debug.LogWarning("[PauseMenuView] No save data to capture.");
return;
}
saveSystem.Save(gameDataState.activeSessionId, data, SaveSlotType.Auto);
}
public void Show() {
pauseMenu?.gameObject.SetActive(true);
}
public void Hide() {
pauseMenu?.gameObject.SetActive(false);
}
public void Tick() { }
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 83affb8078c54010ab278ed4be7f15ef
timeCreated: 1772373065

View File

@@ -0,0 +1,110 @@
using System;
using Nox.Core;
using UnityEngine;
using UnityEngine.UI;
namespace Nox.Game.UI {
public class ScreenFadeTransition : MonoBehaviour, ISceneTransition {
private enum FadeState {
Idle,
FadingOut,
FadingIn
}
private const float DefaultFadeDuration = 0.3f;
private const int CanvasSortOrder = 999;
private CanvasGroup canvasGroup;
private FadeState fadeState = FadeState.Idle;
private float fadeTimer;
private float fadeDuration = DefaultFadeDuration;
private Action onFadeComplete;
public bool IsTransitioning => fadeState != FadeState.Idle;
public void Awake() {
DontDestroyOnLoad(gameObject);
CreateFadeCanvas();
canvasGroup.alpha = 0f;
canvasGroup.blocksRaycasts = false;
}
public void FadeOut(Action onComplete = null) {
if(fadeState == FadeState.FadingOut) {
return;
}
onFadeComplete = onComplete;
fadeState = FadeState.FadingOut;
fadeTimer = 0f;
canvasGroup.blocksRaycasts = true;
}
public void FadeIn(Action onComplete = null) {
if(fadeState == FadeState.FadingIn) {
return;
}
onFadeComplete = onComplete;
fadeState = FadeState.FadingIn;
fadeTimer = 0f;
}
public void Update() {
if(fadeState == FadeState.Idle) {
return;
}
fadeTimer += Time.unscaledDeltaTime;
float t = Mathf.Clamp01(fadeTimer / fadeDuration);
switch(fadeState) {
case FadeState.FadingOut:
canvasGroup.alpha = t;
if(t >= 1f) {
CompleteFade();
}
break;
case FadeState.FadingIn:
canvasGroup.alpha = 1f - t;
if(t >= 1f) {
canvasGroup.blocksRaycasts = false;
CompleteFade();
}
break;
}
}
private void CompleteFade() {
fadeState = FadeState.Idle;
fadeTimer = 0f;
var callback = onFadeComplete;
onFadeComplete = null;
callback?.Invoke();
}
private void CreateFadeCanvas() {
var canvasObject = new GameObject("FadeCanvas");
canvasObject.transform.SetParent(transform);
var canvas = canvasObject.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = CanvasSortOrder;
canvasObject.AddComponent<CanvasScaler>();
canvasGroup = canvasObject.AddComponent<CanvasGroup>();
var imageObject = new GameObject("FadeImage");
imageObject.transform.SetParent(canvasObject.transform, false);
var image = imageObject.AddComponent<Image>();
image.color = Color.black;
image.raycastTarget = true;
var rectTransform = image.rectTransform;
rectTransform.anchorMin = Vector2.zero;
rectTransform.anchorMax = Vector2.one;
rectTransform.offsetMin = Vector2.zero;
rectTransform.offsetMax = Vector2.zero;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 64d1d810d0e4d0d45bebf3cc276703af