Added Furnance and smelting
This commit is contained in:
@@ -123,7 +123,10 @@ public sealed class NetworkPhysicalLeverInteractable :
|
||||
}
|
||||
|
||||
if (TryCalculateTargetValue(playerObject, out float targetValue))
|
||||
{
|
||||
MoveTowardsValue(targetValue);
|
||||
UpdateActivationAtEndpoint();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -198,8 +201,7 @@ public sealed class NetworkPhysicalLeverInteractable :
|
||||
hasSnapTarget = false;
|
||||
bool snappedToMaximum = snapTargetValue >= 0.5f;
|
||||
|
||||
if (isActivated.Value != snappedToMaximum)
|
||||
isActivated.Value = snappedToMaximum;
|
||||
SetActivation(snappedToMaximum);
|
||||
|
||||
bool movedAwayFromInitialPosition =
|
||||
!Mathf.Approximately(snapTargetValue, InitialPositionValue);
|
||||
@@ -209,6 +211,22 @@ public sealed class NetworkPhysicalLeverInteractable :
|
||||
: -1f;
|
||||
}
|
||||
|
||||
private void UpdateActivationAtEndpoint()
|
||||
{
|
||||
const float endpointTolerance = 0.001f;
|
||||
|
||||
if (synchronizedValue.Value >= 1f - endpointTolerance)
|
||||
SetActivation(true);
|
||||
else if (synchronizedValue.Value <= endpointTolerance)
|
||||
SetActivation(false);
|
||||
}
|
||||
|
||||
private void SetActivation(bool activated)
|
||||
{
|
||||
if (isActivated.Value != activated)
|
||||
isActivated.Value = activated;
|
||||
}
|
||||
|
||||
private bool IsPlayerWithinControlDistance(NetworkObject playerObject)
|
||||
{
|
||||
float maximumDistanceSquared = maximumControlDistance * maximumControlDistance;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 49b83c56f1d5e6d46b448c92e658fbe9
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,22 @@
|
||||
using UnityEngine;
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
[RequireComponent(typeof(NetworkItem))]
|
||||
public sealed class FurnaceMetalSource : MonoBehaviour
|
||||
{
|
||||
[SerializeField, Min(0.01f)] private float metalAmount = 25f;
|
||||
|
||||
private bool isConsumed;
|
||||
|
||||
public float MetalAmount => metalAmount;
|
||||
public bool IsConsumed => isConsumed;
|
||||
|
||||
public bool TryMarkConsumed()
|
||||
{
|
||||
if (isConsumed)
|
||||
return false;
|
||||
|
||||
isConsumed = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9dd3ea21361871345b5a9d21839c6da1
|
||||
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
[CreateAssetMenu(fileName = "ItemCatalog", menuName = "FORT-BO/Items/Item Catalog")]
|
||||
public sealed class ItemCatalog : ScriptableObject
|
||||
{
|
||||
[SerializeField] private List<ItemDefinition> items = new();
|
||||
|
||||
private readonly Dictionary<string, ItemDefinition> itemsById =
|
||||
new(StringComparer.Ordinal);
|
||||
|
||||
public IReadOnlyList<ItemDefinition> Items => items;
|
||||
|
||||
private void OnEnable() => RebuildIndex();
|
||||
private void OnValidate() => RebuildIndex();
|
||||
|
||||
public bool TryGetItem(string itemId, out ItemDefinition definition)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(itemId))
|
||||
{
|
||||
definition = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return itemsById.TryGetValue(itemId.Trim().ToLowerInvariant(), out definition);
|
||||
}
|
||||
|
||||
private void RebuildIndex()
|
||||
{
|
||||
itemsById.Clear();
|
||||
|
||||
foreach (ItemDefinition definition in items)
|
||||
{
|
||||
if (definition == null || string.IsNullOrWhiteSpace(definition.ItemId))
|
||||
continue;
|
||||
|
||||
if (!itemsById.TryAdd(definition.ItemId, definition))
|
||||
Debug.LogError($"Duplicate item id '{definition.ItemId}' in {name}.", this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0928368adb57acb4987bb8fabda3a927
|
||||
@@ -0,0 +1,31 @@
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
public enum ItemCategory
|
||||
{
|
||||
Scrap,
|
||||
SmeltedMaterial,
|
||||
RocketPart,
|
||||
Tool,
|
||||
Consumable
|
||||
}
|
||||
|
||||
[CreateAssetMenu(fileName = "ItemDefinition", menuName = "FORT-BO/Items/Item Definition")]
|
||||
public sealed class ItemDefinition : ScriptableObject
|
||||
{
|
||||
[SerializeField] private string itemId;
|
||||
[SerializeField] private string displayName;
|
||||
[SerializeField] private ItemCategory category;
|
||||
[SerializeField] private NetworkObject worldPrefab;
|
||||
|
||||
public string ItemId => itemId;
|
||||
public string DisplayName => string.IsNullOrWhiteSpace(displayName) ? name : displayName;
|
||||
public ItemCategory Category => category;
|
||||
public NetworkObject WorldPrefab => worldPrefab;
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
itemId = itemId?.Trim().ToLowerInvariant();
|
||||
displayName = displayName?.Trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b0c79cebdb4201743be8612545ee180e
|
||||
@@ -0,0 +1,14 @@
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
[RequireComponent(typeof(NetworkObject))]
|
||||
public sealed class NetworkItem : NetworkBehaviour
|
||||
{
|
||||
[SerializeField] private ItemDefinition definition;
|
||||
|
||||
public ItemDefinition Definition => definition;
|
||||
public string ItemId => definition != null ? definition.ItemId : string.Empty;
|
||||
public string DisplayName => definition != null ? definition.DisplayName : name;
|
||||
public ItemCategory Category => definition != null ? definition.Category : default;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 323282b7a87fc4344bd33250e88822e8
|
||||
@@ -12,9 +12,18 @@ using Unity.Services.Relay.Models;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
public enum NetworkConnectionMode
|
||||
{
|
||||
None,
|
||||
Relay,
|
||||
Lan
|
||||
}
|
||||
|
||||
[RequireComponent(typeof(NetworkManager), typeof(UnityTransport))]
|
||||
public sealed class NetworkSessionController : MonoBehaviour
|
||||
{
|
||||
public const ushort DefaultLanPort = 7777;
|
||||
|
||||
private const string PlayerNamePreferenceKey = "player_name";
|
||||
private const string RelayCodeDataKey = "relay_code";
|
||||
|
||||
@@ -26,9 +35,12 @@ public sealed class NetworkSessionController : MonoBehaviour
|
||||
[SerializeField, Min(1)] private int maxClientCount = 3;
|
||||
[SerializeField] private bool useSecureRelay = false;
|
||||
|
||||
public string Status { get; private set; } = "Relay is not initialized.";
|
||||
public string Status { get; private set; } = "Ready.";
|
||||
public string JoinCode { get; private set; } = string.Empty;
|
||||
public string LocalPlayerName { get; private set; }
|
||||
public string LanAddress { get; private set; } = "127.0.0.1";
|
||||
public ushort LanPort { get; private set; } = DefaultLanPort;
|
||||
public NetworkConnectionMode ConnectionMode { get; private set; }
|
||||
public bool IsBusy { get; private set; }
|
||||
public bool IsOnline => NetworkManager.Singleton != null && NetworkManager.Singleton.IsListening;
|
||||
public bool IsHost => networkManager != null && networkManager.IsHost;
|
||||
@@ -85,12 +97,15 @@ public sealed class NetworkSessionController : MonoBehaviour
|
||||
allocation.AllocationIdBytes, allocation.Key, allocation.ConnectionData, useSecureRelay);
|
||||
JoinCode = await RelayService.Instance.GetJoinCodeAsync(allocation.AllocationId);
|
||||
await PublishLobbyAsync();
|
||||
ConnectionMode = NetworkConnectionMode.Relay;
|
||||
loadFactoryAfterServerStart = true;
|
||||
if (!networkManager.StartHost()) throw new InvalidOperationException("Netcode host could not start.");
|
||||
Status = "Host started.";
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ConnectionMode = NetworkConnectionMode.None;
|
||||
loadFactoryAfterServerStart = false;
|
||||
Status = $"Relay host error: {exception.Message}";
|
||||
Debug.LogException(exception);
|
||||
}
|
||||
@@ -108,17 +123,93 @@ public sealed class NetworkSessionController : MonoBehaviour
|
||||
JoinAllocation allocation = await RelayService.Instance.JoinAllocationAsync(joinCode.Trim().ToUpperInvariant());
|
||||
transport.SetClientRelayData(allocation.RelayServer.IpV4, (ushort)allocation.RelayServer.Port,
|
||||
allocation.AllocationIdBytes, allocation.Key, allocation.ConnectionData, allocation.HostConnectionData, useSecureRelay);
|
||||
ConnectionMode = NetworkConnectionMode.Relay;
|
||||
if (!networkManager.StartClient()) throw new InvalidOperationException("Netcode client could not start.");
|
||||
Status = "Connecting to host...";
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ConnectionMode = NetworkConnectionMode.None;
|
||||
Status = $"Relay join error: {exception.Message}";
|
||||
Debug.LogException(exception);
|
||||
}
|
||||
finally { IsBusy = false; }
|
||||
}
|
||||
|
||||
public bool StartLanHost(ushort port = DefaultLanPort)
|
||||
{
|
||||
if (IsBusy || IsOnline)
|
||||
return false;
|
||||
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
JoinCode = string.Empty;
|
||||
LanAddress = "127.0.0.1";
|
||||
LanPort = port;
|
||||
ConnectionMode = NetworkConnectionMode.Lan;
|
||||
transport.SetConnectionData(LanAddress, LanPort, "0.0.0.0");
|
||||
loadFactoryAfterServerStart = true;
|
||||
|
||||
if (!networkManager.StartHost())
|
||||
throw new InvalidOperationException("Netcode LAN host could not start.");
|
||||
|
||||
Status = $"LAN host started on port {LanPort}.";
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ConnectionMode = NetworkConnectionMode.None;
|
||||
loadFactoryAfterServerStart = false;
|
||||
Status = $"LAN host error: {exception.Message}";
|
||||
Debug.LogException(exception);
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool StartLanClient(string address, ushort port = DefaultLanPort)
|
||||
{
|
||||
if (IsBusy || IsOnline)
|
||||
return false;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(address))
|
||||
{
|
||||
Status = "LAN address is empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
JoinCode = string.Empty;
|
||||
LanAddress = address.Trim();
|
||||
LanPort = port;
|
||||
ConnectionMode = NetworkConnectionMode.Lan;
|
||||
transport.SetConnectionData(LanAddress, LanPort);
|
||||
|
||||
if (!networkManager.StartClient())
|
||||
throw new InvalidOperationException("Netcode LAN client could not start.");
|
||||
|
||||
Status = $"Connecting directly to {LanAddress}:{LanPort}...";
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
ConnectionMode = NetworkConnectionMode.None;
|
||||
Status = $"LAN join error: {exception.Message}";
|
||||
Debug.LogException(exception);
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RefreshPublicLobbiesAsync()
|
||||
{
|
||||
if (IsBusy) return;
|
||||
|
||||
@@ -60,6 +60,24 @@ public sealed class FirstPersonCameraController : NetworkBehaviour, IPlayerSyste
|
||||
|
||||
if (isInitialized && !IsOwner)
|
||||
ApplyCameraPitch(synchronizedPitch.Value);
|
||||
|
||||
if (isInitialized && IsOwner)
|
||||
CaptureCursor();
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (!isInitialized || !IsOwner || !Application.isFocused)
|
||||
return;
|
||||
|
||||
if (Cursor.lockState != CursorLockMode.Locked || Cursor.visible)
|
||||
CaptureCursor();
|
||||
}
|
||||
|
||||
private void OnApplicationFocus(bool hasFocus)
|
||||
{
|
||||
if (hasFocus && isInitialized && IsOwner)
|
||||
CaptureCursor();
|
||||
}
|
||||
|
||||
public void ApplyPitch(float pitchDelta)
|
||||
@@ -98,4 +116,10 @@ public sealed class FirstPersonCameraController : NetworkBehaviour, IPlayerSyste
|
||||
if (playerCamera != null)
|
||||
playerCamera.transform.localRotation = Quaternion.Euler(pitch, 0f, 0f);
|
||||
}
|
||||
|
||||
private static void CaptureCursor()
|
||||
{
|
||||
Cursor.lockState = CursorLockMode.Locked;
|
||||
Cursor.visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,12 +11,23 @@ public sealed class FactoryLobbyCodeView : MonoBehaviour
|
||||
GUILayout.BeginArea(new Rect(42, 42, 304, 76));
|
||||
if (session.IsHost)
|
||||
{
|
||||
GUILayout.Label("LOBBY CODE");
|
||||
GUILayout.Label(string.IsNullOrEmpty(session.JoinCode) ? "Generating..." : session.JoinCode);
|
||||
if (session.ConnectionMode == NetworkConnectionMode.Lan)
|
||||
{
|
||||
GUILayout.Label("LAN HOST");
|
||||
GUILayout.Label($"Same PC: 127.0.0.1:{session.LanPort}");
|
||||
}
|
||||
else
|
||||
{
|
||||
GUILayout.Label("LOBBY CODE");
|
||||
GUILayout.Label(string.IsNullOrEmpty(session.JoinCode) ? "Generating..." : session.JoinCode);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
GUILayout.Label("CONNECTED TO FACTORY");
|
||||
string connectionName = session.ConnectionMode == NetworkConnectionMode.Lan
|
||||
? "LAN"
|
||||
: "RELAY";
|
||||
GUILayout.Label($"CONNECTED VIA {connectionName}");
|
||||
}
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
public interface IGlobalUISectionController { }
|
||||
|
||||
public sealed class GlobalUIController : MonoBehaviour
|
||||
{
|
||||
public static GlobalUIController Instance { get; private set; }
|
||||
|
||||
private readonly HashSet<IGlobalUISectionController> sectionControllers = new();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
@@ -13,11 +18,43 @@ public sealed class GlobalUIController : MonoBehaviour
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
|
||||
foreach (MonoBehaviour behaviour in FindObjectsByType<MonoBehaviour>(
|
||||
FindObjectsInactive.Include,
|
||||
FindObjectsSortMode.None))
|
||||
{
|
||||
if (behaviour is IGlobalUISectionController controller)
|
||||
Register(controller);
|
||||
}
|
||||
}
|
||||
|
||||
public void Register(IGlobalUISectionController controller)
|
||||
{
|
||||
if (controller != null)
|
||||
sectionControllers.Add(controller);
|
||||
}
|
||||
|
||||
public void Unregister(IGlobalUISectionController controller)
|
||||
{
|
||||
if (controller != null)
|
||||
sectionControllers.Remove(controller);
|
||||
}
|
||||
|
||||
public IEnumerable<T> GetControllers<T>() where T : class, IGlobalUISectionController
|
||||
{
|
||||
foreach (IGlobalUISectionController controller in sectionControllers)
|
||||
{
|
||||
if (controller is T typedController)
|
||||
yield return typedController;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (Instance == this)
|
||||
{
|
||||
sectionControllers.Clear();
|
||||
Instance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,17 @@ using UnityEngine;
|
||||
public sealed class MainMenuController : MonoBehaviour
|
||||
{
|
||||
private string joinCode = string.Empty;
|
||||
private string lanAddress = "127.0.0.1";
|
||||
private string lanPort = NetworkSessionController.DefaultLanPort.ToString();
|
||||
private string playerName = string.Empty;
|
||||
private string lanValidationMessage = string.Empty;
|
||||
private Vector2 lobbyScrollPosition;
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
NetworkSessionController session = NetworkSessionController.Instance;
|
||||
GUI.Box(new Rect(24, 24, 480, 620), string.Empty);
|
||||
GUILayout.BeginArea(new Rect(44, 44, 440, 580));
|
||||
GUI.Box(new Rect(24, 24, 480, 700), string.Empty);
|
||||
GUILayout.BeginArea(new Rect(44, 44, 440, 660));
|
||||
GUILayout.Label("FRIENDSLOP // MOUNTAIN FACTORY");
|
||||
GUILayout.Space(10);
|
||||
|
||||
@@ -38,6 +41,27 @@ public sealed class MainMenuController : MonoBehaviour
|
||||
|
||||
GUILayout.Space(10);
|
||||
GUI.enabled = !session.IsBusy;
|
||||
|
||||
GUILayout.Label("LAN // DIRECT CONNECTION");
|
||||
GUILayout.BeginHorizontal();
|
||||
GUILayout.Label("IP", GUILayout.Width(24));
|
||||
lanAddress = GUILayout.TextField(lanAddress);
|
||||
GUILayout.Label("Port", GUILayout.Width(32));
|
||||
lanPort = GUILayout.TextField(lanPort, GUILayout.Width(64));
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("Start LAN Host", GUILayout.Height(32)) && TryReadLanPort(out ushort hostPort))
|
||||
session.StartLanHost(hostPort);
|
||||
if (GUILayout.Button("Join LAN", GUILayout.Height(32)) && TryReadLanPort(out ushort clientPort))
|
||||
session.StartLanClient(lanAddress, clientPort);
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
if (!string.IsNullOrEmpty(lanValidationMessage))
|
||||
GUILayout.Label(lanValidationMessage);
|
||||
|
||||
GUILayout.Space(12);
|
||||
GUILayout.Label("RELAY // ONLINE");
|
||||
if (GUILayout.Button("Create Relay Host", GUILayout.Height(34)))
|
||||
_ = session.StartHostAsync();
|
||||
|
||||
@@ -54,7 +78,7 @@ public sealed class MainMenuController : MonoBehaviour
|
||||
_ = session.RefreshPublicLobbiesAsync();
|
||||
GUILayout.EndHorizontal();
|
||||
|
||||
lobbyScrollPosition = GUILayout.BeginScrollView(lobbyScrollPosition, GUILayout.Height(220));
|
||||
lobbyScrollPosition = GUILayout.BeginScrollView(lobbyScrollPosition, GUILayout.Height(160));
|
||||
foreach (Lobby lobby in session.PublicLobbies)
|
||||
{
|
||||
GUILayout.BeginHorizontal(GUI.skin.box);
|
||||
@@ -72,4 +96,11 @@ public sealed class MainMenuController : MonoBehaviour
|
||||
GUI.enabled = true;
|
||||
GUILayout.EndArea();
|
||||
}
|
||||
|
||||
private bool TryReadLanPort(out ushort port)
|
||||
{
|
||||
bool isValid = ushort.TryParse(lanPort, out port) && port > 0;
|
||||
lanValidationMessage = isValid ? string.Empty : "LAN port must be between 1 and 65535.";
|
||||
return isValid;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d65437d286974b44bb6df1a726ef6d23
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d965f4d1841b6bf41b6345426ad113b4
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,11 @@
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
[RequireComponent(typeof(NetworkObject))]
|
||||
public abstract class NetworkWorkstationController : NetworkBehaviour
|
||||
{
|
||||
[SerializeField] private string workstationName = "Workstation";
|
||||
|
||||
public string WorkstationName => workstationName;
|
||||
public abstract bool IsOperating { get; }
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b35ba1fb1e9287646b0e3bdf150e98db
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1af1f8302444cae4e8c981b8d01cfcbb
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,291 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
public enum FurnaceState : byte
|
||||
{
|
||||
Idle,
|
||||
Smelting
|
||||
}
|
||||
|
||||
public sealed class FurnaceController : NetworkWorkstationController
|
||||
{
|
||||
[Header("Storage")]
|
||||
[SerializeField, Min(1f)] private float metalCapacity = 100f;
|
||||
|
||||
[Header("Production")]
|
||||
[SerializeField] private Transform outputPoint;
|
||||
[SerializeField] private List<SmeltingRecipe> recipes = new();
|
||||
[SerializeField, Min(0f)] private float outputSpacing = 0.35f;
|
||||
|
||||
private readonly NetworkVariable<float> storedMetal = new(
|
||||
0f,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server);
|
||||
|
||||
private readonly NetworkVariable<int> selectedRecipeIndex = new(
|
||||
-1,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server);
|
||||
|
||||
private readonly NetworkVariable<FurnaceState> furnaceState = new(
|
||||
FurnaceState.Idle,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server);
|
||||
|
||||
private readonly NetworkVariable<double> processStartedAt = new(
|
||||
0d,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server);
|
||||
|
||||
private readonly NetworkVariable<double> processEndsAt = new(
|
||||
0d,
|
||||
NetworkVariableReadPermission.Everyone,
|
||||
NetworkVariableWritePermission.Server);
|
||||
|
||||
public override bool IsOperating => furnaceState.Value == FurnaceState.Smelting;
|
||||
public FurnaceState State => furnaceState.Value;
|
||||
public float StoredMetal => storedMetal.Value;
|
||||
public float MetalCapacity => metalCapacity;
|
||||
public float MetalNormalized => metalCapacity > 0f ? storedMetal.Value / metalCapacity : 0f;
|
||||
public int SelectedRecipeIndex => selectedRecipeIndex.Value;
|
||||
public IReadOnlyList<SmeltingRecipe> Recipes => recipes;
|
||||
public SmeltingRecipe SelectedRecipe => TryGetRecipe(selectedRecipeIndex.Value, out SmeltingRecipe recipe)
|
||||
? recipe
|
||||
: null;
|
||||
public bool CanStartSelectedRecipe =>
|
||||
!IsOperating &&
|
||||
SelectedRecipe != null &&
|
||||
SelectedRecipe.OutputItem != null &&
|
||||
SelectedRecipe.OutputItem.WorldPrefab != null &&
|
||||
storedMetal.Value >= SelectedRecipe.MetalCost;
|
||||
public float ProcessProgress
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!IsOperating)
|
||||
return 0f;
|
||||
|
||||
double duration = processEndsAt.Value - processStartedAt.Value;
|
||||
if (duration <= 0d)
|
||||
return 1f;
|
||||
|
||||
double now = NetworkManager != null
|
||||
? NetworkManager.ServerTime.Time
|
||||
: Time.timeAsDouble;
|
||||
return Mathf.Clamp01((float)((now - processStartedAt.Value) / duration));
|
||||
}
|
||||
}
|
||||
public float RemainingSeconds
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!IsOperating)
|
||||
return 0f;
|
||||
|
||||
double now = NetworkManager != null
|
||||
? NetworkManager.ServerTime.Time
|
||||
: Time.timeAsDouble;
|
||||
return Mathf.Max(0f, (float)(processEndsAt.Value - now));
|
||||
}
|
||||
}
|
||||
|
||||
public event Action Changed;
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
storedMetal.OnValueChanged += HandleStoredMetalChanged;
|
||||
selectedRecipeIndex.OnValueChanged += HandleSelectedRecipeChanged;
|
||||
furnaceState.OnValueChanged += HandleStateChanged;
|
||||
processStartedAt.OnValueChanged += HandleProcessTimeChanged;
|
||||
processEndsAt.OnValueChanged += HandleProcessTimeChanged;
|
||||
|
||||
if (IsServer)
|
||||
{
|
||||
storedMetal.Value = Mathf.Clamp(storedMetal.Value, 0f, metalCapacity);
|
||||
selectedRecipeIndex.Value = recipes.Count > 0 ? 0 : -1;
|
||||
furnaceState.Value = FurnaceState.Idle;
|
||||
processStartedAt.Value = 0d;
|
||||
processEndsAt.Value = 0d;
|
||||
}
|
||||
|
||||
Changed?.Invoke();
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
storedMetal.OnValueChanged -= HandleStoredMetalChanged;
|
||||
selectedRecipeIndex.OnValueChanged -= HandleSelectedRecipeChanged;
|
||||
furnaceState.OnValueChanged -= HandleStateChanged;
|
||||
processStartedAt.OnValueChanged -= HandleProcessTimeChanged;
|
||||
processEndsAt.OnValueChanged -= HandleProcessTimeChanged;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!IsServer || !IsSpawned || !IsOperating)
|
||||
return;
|
||||
|
||||
if (NetworkManager.ServerTime.Time >= processEndsAt.Value)
|
||||
CompleteSmeltingOnServer();
|
||||
}
|
||||
|
||||
public bool TryConsumeMetalSource(FurnaceMetalSource source)
|
||||
{
|
||||
if (!IsServer || !IsSpawned || source == null || storedMetal.Value >= metalCapacity)
|
||||
return false;
|
||||
|
||||
NetworkItem item = source.GetComponent<NetworkItem>();
|
||||
if (item == null || !item.IsSpawned || item.NetworkObject == null)
|
||||
return false;
|
||||
|
||||
if (source.TryGetComponent(out NetworkHeldInteractable heldObject) && heldObject.IsHeld)
|
||||
return false;
|
||||
|
||||
if (source.TryGetComponent(out NetworkHandItemInteractable handItem) && handItem.IsHeld)
|
||||
return false;
|
||||
|
||||
if (!source.TryMarkConsumed())
|
||||
return false;
|
||||
|
||||
storedMetal.Value = Mathf.Min(metalCapacity, storedMetal.Value + source.MetalAmount);
|
||||
item.NetworkObject.Despawn(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryAddMetal(float amount)
|
||||
{
|
||||
if (!IsServer || !IsSpawned || amount <= 0f || storedMetal.Value >= metalCapacity)
|
||||
return false;
|
||||
|
||||
storedMetal.Value = Mathf.Min(metalCapacity, storedMetal.Value + amount);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void SelectRecipe(int recipeIndex)
|
||||
{
|
||||
if (!IsSpawned)
|
||||
return;
|
||||
|
||||
if (IsServer)
|
||||
SelectRecipeOnServer(recipeIndex);
|
||||
else
|
||||
RequestSelectRecipeRpc(recipeIndex);
|
||||
}
|
||||
|
||||
public void StartSmelting()
|
||||
{
|
||||
if (!IsSpawned)
|
||||
return;
|
||||
|
||||
if (IsServer)
|
||||
TryStartSmeltingOnServer();
|
||||
else
|
||||
RequestStartSmeltingRpc();
|
||||
}
|
||||
|
||||
public void SelectAndStartRecipe(int recipeIndex)
|
||||
{
|
||||
if (!IsSpawned)
|
||||
return;
|
||||
|
||||
if (IsServer)
|
||||
{
|
||||
if (SelectRecipeOnServer(recipeIndex))
|
||||
TryStartSmeltingOnServer();
|
||||
}
|
||||
else
|
||||
{
|
||||
RequestSelectAndStartRecipeRpc(recipeIndex);
|
||||
}
|
||||
}
|
||||
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||
private void RequestSelectRecipeRpc(int recipeIndex) =>
|
||||
SelectRecipeOnServer(recipeIndex);
|
||||
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||
private void RequestStartSmeltingRpc() =>
|
||||
TryStartSmeltingOnServer();
|
||||
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||
private void RequestSelectAndStartRecipeRpc(int recipeIndex)
|
||||
{
|
||||
if (SelectRecipeOnServer(recipeIndex))
|
||||
TryStartSmeltingOnServer();
|
||||
}
|
||||
|
||||
private bool SelectRecipeOnServer(int recipeIndex)
|
||||
{
|
||||
if (!IsServer || IsOperating || !TryGetRecipe(recipeIndex, out _))
|
||||
return false;
|
||||
|
||||
selectedRecipeIndex.Value = recipeIndex;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryStartSmeltingOnServer()
|
||||
{
|
||||
if (!IsServer || !CanStartSelectedRecipe)
|
||||
return false;
|
||||
|
||||
SmeltingRecipe recipe = SelectedRecipe;
|
||||
storedMetal.Value -= recipe.MetalCost;
|
||||
double now = NetworkManager.ServerTime.Time;
|
||||
processStartedAt.Value = now;
|
||||
processEndsAt.Value = now + recipe.ProcessingDuration;
|
||||
furnaceState.Value = FurnaceState.Smelting;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void CompleteSmeltingOnServer()
|
||||
{
|
||||
if (!TryGetRecipe(selectedRecipeIndex.Value, out SmeltingRecipe recipe) ||
|
||||
recipe.OutputItem == null ||
|
||||
recipe.OutputItem.WorldPrefab == null)
|
||||
{
|
||||
Debug.LogError($"{name} cannot complete smelting: output prefab is missing.", this);
|
||||
StopProcessingOnServer();
|
||||
return;
|
||||
}
|
||||
|
||||
Transform spawnTransform = outputPoint != null ? outputPoint : transform;
|
||||
for (int index = 0; index < recipe.OutputAmount; index++)
|
||||
{
|
||||
Vector3 sideOffset = spawnTransform.right * outputSpacing * index;
|
||||
NetworkObject output = Instantiate(
|
||||
recipe.OutputItem.WorldPrefab,
|
||||
spawnTransform.position + sideOffset,
|
||||
spawnTransform.rotation);
|
||||
output.Spawn();
|
||||
}
|
||||
|
||||
StopProcessingOnServer();
|
||||
}
|
||||
|
||||
private void StopProcessingOnServer()
|
||||
{
|
||||
furnaceState.Value = FurnaceState.Idle;
|
||||
processStartedAt.Value = 0d;
|
||||
processEndsAt.Value = 0d;
|
||||
}
|
||||
|
||||
private bool TryGetRecipe(int index, out SmeltingRecipe recipe)
|
||||
{
|
||||
bool isValid = index >= 0 && index < recipes.Count && recipes[index] != null;
|
||||
recipe = isValid ? recipes[index] : null;
|
||||
return isValid;
|
||||
}
|
||||
|
||||
private void HandleStoredMetalChanged(float _, float __) => Changed?.Invoke();
|
||||
private void HandleSelectedRecipeChanged(int _, int __) => Changed?.Invoke();
|
||||
private void HandleStateChanged(FurnaceState _, FurnaceState __) => Changed?.Invoke();
|
||||
private void HandleProcessTimeChanged(double _, double __) => Changed?.Invoke();
|
||||
|
||||
[ContextMenu("Debug/Add 25 Metal (Server Play Mode)")]
|
||||
private void DebugAddMetal() => TryAddMetal(25f);
|
||||
|
||||
[ContextMenu("Debug/Start Selected Recipe (Server Play Mode)")]
|
||||
private void DebugStartSelectedRecipe() => StartSmelting();
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 41af49db79eeaf84ea8bc7c3dd75ae0f
|
||||
@@ -0,0 +1,88 @@
|
||||
using UnityEngine;
|
||||
|
||||
public sealed class FurnaceMetalLevelView : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private FurnaceController furnace;
|
||||
[SerializeField] private Transform metalVisual;
|
||||
[SerializeField] private Transform emptyLocalPosition;
|
||||
[SerializeField] private Transform fullLocalPosition;
|
||||
[SerializeField, Min(0f)] private float movementSpeed = 1.5f;
|
||||
|
||||
private Vector3 targetLocalPosition;
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
furnace = GetComponentInParent<FurnaceController>();
|
||||
metalVisual = transform;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (furnace == null)
|
||||
furnace = GetComponentInParent<FurnaceController>();
|
||||
|
||||
if (furnace != null)
|
||||
furnace.Changed += RefreshTarget;
|
||||
|
||||
RefreshTarget();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (furnace != null)
|
||||
furnace.Changed -= RefreshTarget;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (metalVisual == null)
|
||||
return;
|
||||
|
||||
metalVisual.localPosition = movementSpeed <= 0f
|
||||
? targetLocalPosition
|
||||
: Vector3.MoveTowards(
|
||||
metalVisual.localPosition,
|
||||
targetLocalPosition,
|
||||
movementSpeed * Time.deltaTime);
|
||||
}
|
||||
|
||||
private void RefreshTarget()
|
||||
{
|
||||
float normalized = furnace != null ? furnace.MetalNormalized : 0f;
|
||||
targetLocalPosition = Vector3.Lerp(
|
||||
GetLocalPoint(emptyLocalPosition),
|
||||
GetLocalPoint(fullLocalPosition),
|
||||
normalized);
|
||||
}
|
||||
|
||||
private Vector3 GetLocalPoint(Transform point)
|
||||
{
|
||||
if (point == null || metalVisual == null)
|
||||
return metalVisual != null ? metalVisual.localPosition : Vector3.zero;
|
||||
|
||||
Transform space = metalVisual.parent;
|
||||
return space != null
|
||||
? space.InverseTransformPoint(point.position)
|
||||
: point.position;
|
||||
}
|
||||
|
||||
[ContextMenu("Preview/Empty")]
|
||||
private void PreviewEmpty() => ApplyPreview(0f);
|
||||
|
||||
[ContextMenu("Preview/Half")]
|
||||
private void PreviewHalf() => ApplyPreview(0.5f);
|
||||
|
||||
[ContextMenu("Preview/Full")]
|
||||
private void PreviewFull() => ApplyPreview(1f);
|
||||
|
||||
private void ApplyPreview(float normalized)
|
||||
{
|
||||
if (metalVisual == null)
|
||||
return;
|
||||
|
||||
metalVisual.localPosition = Vector3.Lerp(
|
||||
GetLocalPoint(emptyLocalPosition),
|
||||
GetLocalPoint(fullLocalPosition),
|
||||
normalized);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d51e4e087bfc8f14d8df1b4da69a6d56
|
||||
@@ -0,0 +1,31 @@
|
||||
using UnityEngine;
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
[RequireComponent(typeof(Collider))]
|
||||
public sealed class FurnaceScrapReceiver : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private FurnaceController furnace;
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
furnace = GetComponentInParent<FurnaceController>();
|
||||
Collider receiverCollider = GetComponent<Collider>();
|
||||
receiverCollider.isTrigger = true;
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (furnace == null)
|
||||
furnace = GetComponentInParent<FurnaceController>();
|
||||
}
|
||||
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (furnace == null || !furnace.IsServer || !furnace.IsSpawned)
|
||||
return;
|
||||
|
||||
FurnaceMetalSource source = other.GetComponentInParent<FurnaceMetalSource>();
|
||||
if (source != null)
|
||||
furnace.TryConsumeMetalSource(source);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7a72230fcd966f84d967ab39b124447d
|
||||
@@ -0,0 +1,25 @@
|
||||
using UnityEngine;
|
||||
|
||||
[CreateAssetMenu(fileName = "SmeltingRecipe", menuName = "FORT-BO/Workstations/Smelting Recipe")]
|
||||
public sealed class SmeltingRecipe : ScriptableObject
|
||||
{
|
||||
[SerializeField] private string recipeId;
|
||||
[SerializeField] private string displayName;
|
||||
[SerializeField] private ItemDefinition outputItem;
|
||||
[SerializeField, Min(1)] private int outputAmount = 1;
|
||||
[SerializeField, Min(0.01f)] private float metalCost = 25f;
|
||||
[SerializeField, Min(0.05f)] private float processingDuration = 3f;
|
||||
|
||||
public string RecipeId => recipeId;
|
||||
public string DisplayName => string.IsNullOrWhiteSpace(displayName) ? name : displayName;
|
||||
public ItemDefinition OutputItem => outputItem;
|
||||
public int OutputAmount => outputAmount;
|
||||
public float MetalCost => metalCost;
|
||||
public float ProcessingDuration => processingDuration;
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
recipeId = recipeId?.Trim().ToLowerInvariant();
|
||||
displayName = displayName?.Trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dbdeccc1573d9204f858f7c114972d91
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 46d9fb1e1a0fed4479d8298a3ccff544
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,72 @@
|
||||
using UnityEngine;
|
||||
|
||||
public sealed class FurnacePanelUIController : MonoBehaviour, IGlobalUISectionController
|
||||
{
|
||||
[SerializeField] private FurnaceController furnace;
|
||||
[SerializeField] private FurnacePanelUIView view;
|
||||
|
||||
private void Reset()
|
||||
{
|
||||
furnace = GetComponentInParent<FurnaceController>();
|
||||
view = GetComponent<FurnacePanelUIView>();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (furnace == null)
|
||||
furnace = GetComponentInParent<FurnaceController>();
|
||||
if (view == null)
|
||||
view = GetComponent<FurnacePanelUIView>();
|
||||
|
||||
if (furnace != null)
|
||||
furnace.Changed += Refresh;
|
||||
|
||||
GlobalUIController.Instance?.Register(this);
|
||||
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (furnace != null)
|
||||
furnace.Changed -= Refresh;
|
||||
|
||||
GlobalUIController.Instance?.Unregister(this);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (furnace != null && furnace.IsOperating)
|
||||
Refresh();
|
||||
}
|
||||
|
||||
public void SelectRecipe(int recipeIndex)
|
||||
{
|
||||
furnace?.SelectRecipe(recipeIndex);
|
||||
}
|
||||
|
||||
public void SelectAndStartRecipe(int recipeIndex)
|
||||
{
|
||||
furnace?.SelectAndStartRecipe(recipeIndex);
|
||||
}
|
||||
|
||||
public void StartSmelting()
|
||||
{
|
||||
furnace?.StartSmelting();
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
if (furnace == null || view == null)
|
||||
return;
|
||||
|
||||
view.Render(
|
||||
furnace.StoredMetal,
|
||||
furnace.MetalCapacity,
|
||||
furnace.SelectedRecipe,
|
||||
furnace.State,
|
||||
furnace.ProcessProgress,
|
||||
furnace.RemainingSeconds,
|
||||
furnace.CanStartSelectedRecipe);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6cce45d5e80938340961575a1e91eb41
|
||||
@@ -0,0 +1,51 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public sealed class FurnacePanelUIView : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private TMP_Text metalText;
|
||||
[SerializeField] private TMP_Text selectedRecipeText;
|
||||
[SerializeField] private TMP_Text statusText;
|
||||
[SerializeField] private TMP_Text remainingTimeText;
|
||||
[SerializeField] private Image metalImage;
|
||||
[SerializeField] private Image progressImage;
|
||||
[SerializeField] private Button startButton;
|
||||
|
||||
public void Render(
|
||||
float storedMetal,
|
||||
float metalCapacity,
|
||||
SmeltingRecipe selectedRecipe,
|
||||
FurnaceState state,
|
||||
float processProgress,
|
||||
float remainingSeconds,
|
||||
bool canStart)
|
||||
{
|
||||
if (metalText != null)
|
||||
metalText.text = $"METAL {storedMetal:0.#} / {metalCapacity:0.#}";
|
||||
|
||||
if (metalImage != null)
|
||||
metalImage.fillAmount = metalCapacity > 0f ? storedMetal / metalCapacity : 0f;
|
||||
|
||||
if (selectedRecipeText != null)
|
||||
{
|
||||
selectedRecipeText.text = selectedRecipe != null
|
||||
? $"{selectedRecipe.DisplayName} | {selectedRecipe.MetalCost:0.#} metal"
|
||||
: "NO RECIPE";
|
||||
}
|
||||
|
||||
if (statusText != null)
|
||||
statusText.text = state == FurnaceState.Smelting ? "SMELTING" : "READY";
|
||||
|
||||
if (progressImage != null)
|
||||
progressImage.fillAmount = processProgress;
|
||||
|
||||
if (remainingTimeText != null)
|
||||
remainingTimeText.text = state == FurnaceState.Smelting
|
||||
? $"{remainingSeconds:0.0}s"
|
||||
: string.Empty;
|
||||
|
||||
if (startButton != null)
|
||||
startButton.interactable = canStart;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 871599a5b7d016b45a74a2005a6e22e5
|
||||
Reference in New Issue
Block a user