Added RocketAssemblyStation and ReadyRocket

This commit is contained in:
2026-08-18 16:12:00 +03:00
parent 7a0ee6b519
commit 63d2836dc9
85 changed files with 15087 additions and 3562 deletions
@@ -14,5 +14,4 @@ MonoBehaviour:
m_EditorClassIdentifier:
itemId: fish
displayName: Fish
category: 0
worldPrefab: {fileID: 6820144355512337958, guid: 04a6d9d9e28782a4a9f8f71ce4438fc0, type: 3}
@@ -15,3 +15,4 @@ MonoBehaviour:
items:
- {fileID: 11400000, guid: dc1f8087c2120614fa0ddcf92cde7f0d, type: 2}
- {fileID: 11400000, guid: 28dfa90ac37ac884d84b4f3754b33546, type: 2}
- {fileID: 11400000, guid: 5353380be6a58a041bc52d32971d1d9b, type: 2}
@@ -9,10 +9,9 @@ MonoBehaviour:
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: b0c79cebdb4201743be8612545ee180e, type: 3}
m_Script: {fileID: 11500000, guid: d1e6d4d81ab34d2e9ee3a1bfa201f101, type: 3}
m_Name: MetalPlate
m_EditorClassIdentifier:
itemId: metal_plate
displayName: Metal Plate
category: 1
worldPrefab: {fileID: 2888887303011377321, guid: 29519dc5d2cfae441b261f3ca68f2290, type: 3}
@@ -9,10 +9,9 @@ MonoBehaviour:
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: b0c79cebdb4201743be8612545ee180e, type: 3}
m_Script: {fileID: 11500000, guid: d1e6d4d81ab34d2e9ee3a1bfa201f101, type: 3}
m_Name: MetalRod
m_EditorClassIdentifier:
itemId: metal_rod
displayName: Metal Rod
category: 1
worldPrefab: {fileID: 6071798070859371582, guid: 5cf4d9e867c084847ba82c56bddc456b, type: 3}
@@ -9,11 +9,11 @@ MonoBehaviour:
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: b0c79cebdb4201743be8612545ee180e, type: 3}
m_Script: {fileID: 11500000, guid: d1e6d4d81ab34d2e9ee3a1bfa201f102, type: 3}
m_Name: Rocket-Back-Standart-Ready
m_EditorClassIdentifier:
itemId: rocket-back-standart-ready
displayName: Rocket-Back-Standart-Ready
category: 2
isAssemblyReady: 1
partTypeId: rocket-back-standard
processingState: 1
worldPrefab: {fileID: 5195529150077527847, guid: ea8e439364ce6aa4dab3586bae0edfed, type: 3}
@@ -9,11 +9,11 @@ MonoBehaviour:
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: b0c79cebdb4201743be8612545ee180e, type: 3}
m_Script: {fileID: 11500000, guid: d1e6d4d81ab34d2e9ee3a1bfa201f102, type: 3}
m_Name: Rocket-Base-Avia-Ready
m_EditorClassIdentifier:
itemId: rocket-base-avia-ready
displayName: Rocket-Base-Avia-Ready
category: 2
isAssemblyReady: 1
partTypeId: rocket-base-avia
processingState: 1
worldPrefab: {fileID: 1256934312238460503, guid: 9712e2459410a6d43b55ea9d3847e29a, type: 3}
@@ -9,11 +9,11 @@ MonoBehaviour:
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: b0c79cebdb4201743be8612545ee180e, type: 3}
m_Script: {fileID: 11500000, guid: d1e6d4d81ab34d2e9ee3a1bfa201f102, type: 3}
m_Name: Rocket-Base-Standart-Ready
m_EditorClassIdentifier:
itemId: rocket-base-standart-ready
displayName: Rocket-Base-Standart-Ready
category: 2
isAssemblyReady: 1
partTypeId: rocket-base-standard
processingState: 1
worldPrefab: {fileID: 875723161486981202, guid: 6e681ca5358fff648ba85ae9e494fa77, type: 3}
@@ -9,11 +9,11 @@ MonoBehaviour:
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: b0c79cebdb4201743be8612545ee180e, type: 3}
m_Script: {fileID: 11500000, guid: d1e6d4d81ab34d2e9ee3a1bfa201f102, type: 3}
m_Name: Rocket-Cone-Standart-Ready
m_EditorClassIdentifier:
itemId: rocket-cone-standart-ready
displayName: Rocket-Cone-Standart-Ready
category: 2
isAssemblyReady: 1
partTypeId: rocket-cone-standard
processingState: 1
worldPrefab: {fileID: 7151289907260619783, guid: a31204f69b185d54ab9e12b0a3a1e98b, type: 3}
@@ -9,10 +9,11 @@ MonoBehaviour:
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: b0c79cebdb4201743be8612545ee180e, type: 3}
m_Script: {fileID: 11500000, guid: d1e6d4d81ab34d2e9ee3a1bfa201f102, type: 3}
m_Name: Rocket-Back-Standart
m_EditorClassIdentifier:
itemId: rocket-back-standart
displayName: Rocket-Back-Standart
category: 2
partTypeId: rocket-back-standard
processingState: 0
worldPrefab: {fileID: 8591287727625443167, guid: be54182e751ec4b48b7db90e8598dce1, type: 3}
@@ -9,10 +9,11 @@ MonoBehaviour:
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: b0c79cebdb4201743be8612545ee180e, type: 3}
m_Script: {fileID: 11500000, guid: d1e6d4d81ab34d2e9ee3a1bfa201f102, type: 3}
m_Name: Rocket-Base-Avia
m_EditorClassIdentifier:
itemId: rocket-base-avia
displayName: Rocket-Base-Avia
category: 2
partTypeId: rocket-base-avia
processingState: 0
worldPrefab: {fileID: 1256934312238460503, guid: 9712e2459410a6d43b55ea9d3847e29a, type: 3}
@@ -9,10 +9,11 @@ MonoBehaviour:
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: b0c79cebdb4201743be8612545ee180e, type: 3}
m_Script: {fileID: 11500000, guid: d1e6d4d81ab34d2e9ee3a1bfa201f102, type: 3}
m_Name: Rocket-Base-Standart
m_EditorClassIdentifier:
itemId: rocket-base-standart
displayName: Rocket-Base-Standart
category: 2
partTypeId: rocket-base-standard
processingState: 0
worldPrefab: {fileID: 875723161486981202, guid: 6e681ca5358fff648ba85ae9e494fa77, type: 3}
@@ -9,10 +9,11 @@ MonoBehaviour:
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: b0c79cebdb4201743be8612545ee180e, type: 3}
m_Script: {fileID: 11500000, guid: d1e6d4d81ab34d2e9ee3a1bfa201f102, type: 3}
m_Name: Rocket-Cone-Standart
m_EditorClassIdentifier:
itemId: rocket-cone-standart
displayName: Rocket-Cone-Standart
category: 2
partTypeId: rocket-cone-standard
processingState: 0
worldPrefab: {fileID: 7151289907260619783, guid: a31204f69b185d54ab9e12b0a3a1e98b, type: 3}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5d20a47f4accbbc4a993a1373834d882
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,19 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d1e6d4d81ab34d2e9ee3a1bfa201f103, type: 3}
m_Name: Ready-Rocket-Standart
m_EditorClassIdentifier:
itemId: full-rocket-standart
displayName: Full-Rocket-Standart
worldPrefab: {fileID: 7456648717704822871, guid: 746f04d9ba6c87d4bae8a09287c5d090, type: 3}
rocketTypeId: standard
criticalImpactSpeed: 7
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5353380be6a58a041bc52d32971d1d9b
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
+1 -2
View File
@@ -9,10 +9,9 @@ MonoBehaviour:
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: b0c79cebdb4201743be8612545ee180e, type: 3}
m_Script: {fileID: 11500000, guid: d1e6d4d81ab34d2e9ee3a1bfa201f101, type: 3}
m_Name: Scrap-1
m_EditorClassIdentifier:
itemId: scrap-1
displayName: Scrap-1
category: 0
worldPrefab: {fileID: 9104915302393667013, guid: 6eee6df5dab550446b41008fa6fff89a, type: 3}
+1 -2
View File
@@ -9,10 +9,9 @@ MonoBehaviour:
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: b0c79cebdb4201743be8612545ee180e, type: 3}
m_Script: {fileID: 11500000, guid: d1e6d4d81ab34d2e9ee3a1bfa201f101, type: 3}
m_Name: Scrap-2
m_EditorClassIdentifier:
itemId: scrap-2
displayName: Scrap-2
category: 0
worldPrefab: {fileID: 3823145821987330810, guid: 8cc9c4d7d9e04d640bbd0616e7926670, type: 3}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2973313a334da044a84ce698504a5751
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 016d686fabe553e4e87d7531e66451ba
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,24 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 04f3334793c14ee791f78059d0c2854c, type: 3}
m_Name: Ready-Rocket-Standart
m_EditorClassIdentifier:
recipeId: full-rocket-standart
displayName: Full-Rocket-Standart
requiredPartsBottomToTop:
- {fileID: 11400000, guid: 203427fd9de0c514887bec7c046a6f69, type: 2}
- {fileID: 11400000, guid: 218df70f83b5574428eb2159664b9b6e, type: 2}
- {fileID: 11400000, guid: a97b695af4781b34fa8cab9a97ba9d9d, type: 2}
outputItem: {fileID: 11400000, guid: 5353380be6a58a041bc52d32971d1d9b, type: 2}
assemblyDuration: 5
blueprintTitle:
blueprintImage: {fileID: 21300000, guid: 10f35c691ba84cc43a31218838a402a2, type: 3}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1cfa63174ab38cc40bb4aa962831fc25
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:
@@ -21,16 +21,16 @@ public static class FurnacePipelinePrototypeSetup
EnsureFolder(ItemPrefabFolder);
EnsureFolder(WorkstationPrefabFolder);
ItemDefinition scrapDefinition = GetOrCreateAsset<ItemDefinition>(
MaterialItemDefinition scrapDefinition = GetOrCreateAsset<MaterialItemDefinition>(
$"{ItemDataFolder}/Scrap.asset");
ItemDefinition plateDefinition = GetOrCreateAsset<ItemDefinition>(
MaterialItemDefinition plateDefinition = GetOrCreateAsset<MaterialItemDefinition>(
$"{ItemDataFolder}/MetalPlate.asset");
ItemDefinition rodDefinition = GetOrCreateAsset<ItemDefinition>(
MaterialItemDefinition rodDefinition = GetOrCreateAsset<MaterialItemDefinition>(
$"{ItemDataFolder}/MetalRod.asset");
ConfigureItemDefinition(scrapDefinition, "scrap", "Scrap", ItemCategory.Scrap);
ConfigureItemDefinition(plateDefinition, "metal_plate", "Metal Plate", ItemCategory.SmeltedMaterial);
ConfigureItemDefinition(rodDefinition, "metal_rod", "Metal Rod", ItemCategory.SmeltedMaterial);
ConfigureItemDefinition(scrapDefinition, "scrap", "Scrap");
ConfigureItemDefinition(plateDefinition, "metal_plate", "Metal Plate");
ConfigureItemDefinition(rodDefinition, "metal_rod", "Metal Rod");
GameObject scrapPrefab = CreateNetworkItemPrefab(
$"{ItemPrefabFolder}/Scrap.prefab",
@@ -195,12 +195,10 @@ public static class FurnacePipelinePrototypeSetup
private static void ConfigureItemDefinition(
ItemDefinition definition,
string itemId,
string displayName,
ItemCategory category)
string displayName)
{
SetString(definition, "itemId", itemId);
SetString(definition, "displayName", displayName);
SetInt(definition, "category", (int)category);
}
private static void ConfigureRecipe(
@@ -0,0 +1,752 @@
using System;
using System.IO;
using Unity.Netcode;
using Unity.Netcode.Components;
using UnityEditor;
using UnityEngine;
public enum ItemCreationDefinitionKind
{
Generic,
Material,
Part,
Rocket,
Usable
}
public enum ItemCreationMovementKind
{
Carry,
Drag,
Hand,
Static
}
public sealed class ItemCreationWindow : EditorWindow
{
private const string DefaultDefinitionFolder = "Assets/CONTENT/Game/Data/Items";
private const string DefaultPrefabFolder = "Assets/CONTENT/Game/Prefabs/Items";
private const string DefaultCatalogPath =
"Assets/CONTENT/Game/Data/Items/ItemCatalog.asset";
private const string DefaultNetworkPrefabsPath = "Assets/DefaultNetworkPrefabs.asset";
[SerializeField] private string assetName = "NewItem";
[SerializeField] private string itemId = "new-item";
[SerializeField] private string displayName = "New Item";
[SerializeField] private ItemDefinition existingDefinition;
[SerializeField] private ItemCreationDefinitionKind definitionKind;
[SerializeField] private ItemCreationMovementKind movementKind =
ItemCreationMovementKind.Carry;
[SerializeField] private GameObject existingWorldPrefab;
[SerializeField] private GameObject visualPrefab;
[SerializeField] private string definitionFolder = DefaultDefinitionFolder;
[SerializeField] private string prefabFolder = DefaultPrefabFolder;
[SerializeField] private string partTypeId = "part";
[SerializeField] private PartProcessingState partProcessingState;
[SerializeField] private string rocketTypeId = "rocket";
[SerializeField] private float criticalImpactSpeed = 7f;
[SerializeField] private float dragWeight = 35f;
[SerializeField] private MonoScript handItemBehaviour;
[SerializeField] private bool addFurnaceMetalSource;
[SerializeField] private float furnaceMetalAmount = 25f;
[SerializeField] private ItemCatalog catalog;
[SerializeField] private NetworkPrefabsList networkPrefabsList;
[MenuItem("FORT-BO/Items/Item Creator")]
private static void Open()
{
ItemCreationWindow window = GetWindow<ItemCreationWindow>("Item Creator");
window.minSize = new Vector2(470f, 650f);
}
private void OnEnable()
{
if (catalog == null)
catalog = AssetDatabase.LoadAssetAtPath<ItemCatalog>(DefaultCatalogPath);
if (networkPrefabsList == null)
{
networkPrefabsList =
AssetDatabase.LoadAssetAtPath<NetworkPrefabsList>(DefaultNetworkPrefabsPath);
}
}
private void OnGUI()
{
EditorGUILayout.LabelField("Identity", EditorStyles.boldLabel);
existingDefinition = (ItemDefinition)EditorGUILayout.ObjectField(
"Existing Definition",
existingDefinition,
typeof(ItemDefinition),
false);
using (new EditorGUI.DisabledScope(existingDefinition != null))
{
assetName = EditorGUILayout.TextField("Asset Name", assetName);
itemId = EditorGUILayout.TextField("Item Id", itemId);
displayName = EditorGUILayout.TextField("Display Name", displayName);
definitionKind = (ItemCreationDefinitionKind)EditorGUILayout.EnumPopup(
"Definition Kind",
definitionKind);
}
if (existingDefinition != null)
{
EditorGUILayout.LabelField(
"Definition Kind",
GetDefinitionKind(existingDefinition).ToString());
}
movementKind = (ItemCreationMovementKind)EditorGUILayout.EnumPopup(
"Movement Kind",
movementKind);
EditorGUILayout.Space();
EditorGUILayout.LabelField("Prefab", EditorStyles.boldLabel);
existingWorldPrefab = (GameObject)EditorGUILayout.ObjectField(
"Existing World Prefab",
existingWorldPrefab,
typeof(GameObject),
false);
bool hasWorldPrefab = ResolveExistingWorldPrefab() != null;
using (new EditorGUI.DisabledScope(hasWorldPrefab))
{
visualPrefab = (GameObject)EditorGUILayout.ObjectField(
"Visual Prefab",
visualPrefab,
typeof(GameObject),
false);
prefabFolder = EditorGUILayout.TextField("Prefab Folder", prefabFolder);
}
EditorGUILayout.HelpBox(
hasWorldPrefab
? "The existing prefab is configured in place. Existing components are preserved."
: "A new network prefab is created. Visual Prefab is added as a child and may be empty.",
MessageType.Info);
if (existingDefinition == null)
DrawDefinitionSpecificFields();
DrawMovementSpecificFields();
EditorGUILayout.Space();
EditorGUILayout.LabelField("Optional capabilities", EditorStyles.boldLabel);
addFurnaceMetalSource = EditorGUILayout.Toggle(
"Furnace Metal Source",
addFurnaceMetalSource);
if (addFurnaceMetalSource)
{
furnaceMetalAmount = EditorGUILayout.FloatField(
"Metal Amount",
furnaceMetalAmount);
}
EditorGUILayout.Space();
EditorGUILayout.LabelField("Registration", EditorStyles.boldLabel);
using (new EditorGUI.DisabledScope(existingDefinition != null))
{
definitionFolder = EditorGUILayout.TextField(
"Definition Folder",
definitionFolder);
}
catalog = (ItemCatalog)EditorGUILayout.ObjectField(
"Item Catalog",
catalog,
typeof(ItemCatalog),
false);
networkPrefabsList = (NetworkPrefabsList)EditorGUILayout.ObjectField(
"Network Prefabs List",
networkPrefabsList,
typeof(NetworkPrefabsList),
false);
EditorGUILayout.Space();
EditorGUILayout.HelpBox(
"Definition Kind describes what the item is. Movement Kind describes how its prefab " +
"is handled. They are intentionally independent.",
MessageType.None);
using (new EditorGUI.DisabledScope(EditorApplication.isPlayingOrWillChangePlaymode))
{
string buttonLabel = existingDefinition == null
? "Create and Register Item"
: "Configure and Register Item";
if (GUILayout.Button(buttonLabel, GUILayout.Height(34f)))
CreateAndRegisterItem();
}
}
private void DrawDefinitionSpecificFields()
{
switch (definitionKind)
{
case ItemCreationDefinitionKind.Part:
EditorGUILayout.Space();
EditorGUILayout.LabelField("Part", EditorStyles.boldLabel);
partTypeId = EditorGUILayout.TextField("Part Type Id", partTypeId);
partProcessingState = (PartProcessingState)EditorGUILayout.EnumPopup(
"Processing State",
partProcessingState);
break;
case ItemCreationDefinitionKind.Rocket:
EditorGUILayout.Space();
EditorGUILayout.LabelField("Rocket", EditorStyles.boldLabel);
rocketTypeId = EditorGUILayout.TextField("Rocket Type Id", rocketTypeId);
criticalImpactSpeed = EditorGUILayout.FloatField(
"Critical Impact Speed",
criticalImpactSpeed);
break;
}
}
private void DrawMovementSpecificFields()
{
switch (movementKind)
{
case ItemCreationMovementKind.Drag:
EditorGUILayout.Space();
EditorGUILayout.LabelField("Drag", EditorStyles.boldLabel);
dragWeight = EditorGUILayout.FloatField("Weight", dragWeight);
break;
case ItemCreationMovementKind.Hand:
EditorGUILayout.Space();
EditorGUILayout.LabelField("Hand", EditorStyles.boldLabel);
handItemBehaviour = (MonoScript)EditorGUILayout.ObjectField(
"Custom Hand Interactable",
handItemBehaviour,
typeof(MonoScript),
false);
EditorGUILayout.HelpBox(
"Leave empty to use NetworkUsableHandItemInteractable. Add a separate " +
"IHandItemUseAction component for the actual effect.",
MessageType.Info);
break;
}
}
private void CreateAndRegisterItem()
{
try
{
ValidateInput();
ItemDefinition definition;
string definitionPath;
string safeAssetName;
if (existingDefinition != null)
{
definition = existingDefinition;
definitionPath = AssetDatabase.GetAssetPath(definition);
safeAssetName = SanitizeFileName(definition.name);
}
else
{
EnsureFolder(definitionFolder);
safeAssetName = SanitizeFileName(assetName.Trim());
definitionPath = $"{definitionFolder}/{safeAssetName}.asset";
if (AssetDatabase.LoadMainAssetAtPath(definitionPath) != null)
{
throw new InvalidOperationException(
$"Asset already exists: {definitionPath}");
}
EnsureUniqueItemId(itemId);
definition = CreateDefinition(definitionPath);
}
GameObject worldPrefab = ConfigureOrCreateWorldPrefab(safeAssetName, definition);
SetObjectReference(
definition,
"worldPrefab",
worldPrefab.GetComponent<NetworkObject>());
RegisterInCatalog(definition);
RegisterNetworkPrefab(worldPrefab);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Selection.activeObject = definition;
EditorGUIUtility.PingObject(worldPrefab);
Debug.Log(
$"Created '{definition.DisplayName}': {definitionPath} + " +
$"{AssetDatabase.GetAssetPath(worldPrefab)}.",
definition);
}
catch (Exception exception)
{
Debug.LogException(exception);
EditorUtility.DisplayDialog("Item creation failed", exception.Message, "OK");
}
}
private void ValidateInput()
{
if (existingDefinition == null && string.IsNullOrWhiteSpace(assetName))
throw new InvalidOperationException("Asset Name is required.");
if (existingDefinition == null && string.IsNullOrWhiteSpace(itemId))
throw new InvalidOperationException("Item Id is required.");
if (existingDefinition == null &&
(string.IsNullOrWhiteSpace(definitionFolder) ||
!definitionFolder.StartsWith("Assets", StringComparison.Ordinal)))
{
throw new InvalidOperationException("Definition Folder must be inside Assets.");
}
GameObject resolvedWorldPrefab = ResolveExistingWorldPrefab();
if (resolvedWorldPrefab != null &&
!PrefabUtility.IsPartOfPrefabAsset(resolvedWorldPrefab))
{
throw new InvalidOperationException("Existing World Prefab must be a prefab asset.");
}
if (resolvedWorldPrefab != null)
ValidateExistingPrefabMovement(resolvedWorldPrefab);
if (ResolveExistingWorldPrefab() == null &&
(string.IsNullOrWhiteSpace(prefabFolder) ||
!prefabFolder.StartsWith("Assets", StringComparison.Ordinal)))
{
throw new InvalidOperationException("Prefab Folder must be inside Assets.");
}
if (movementKind == ItemCreationMovementKind.Hand)
ResolveHandItemBehaviourType();
if (EffectiveDefinitionKind == ItemCreationDefinitionKind.Rocket &&
movementKind is not ItemCreationMovementKind.Carry and
not ItemCreationMovementKind.Drag)
{
throw new InvalidOperationException(
"A rocket assembled by the workstation must use Carry or Drag movement.");
}
if (ResolveExistingWorldPrefab() == null && visualPrefab != null &&
(visualPrefab.GetComponentInChildren<NetworkObject>(true) != null ||
visualPrefab.GetComponentInChildren<Rigidbody>(true) != null))
{
throw new InvalidOperationException(
"Visual Prefab must contain visuals/colliders only, without NetworkObject or Rigidbody.");
}
if (existingDefinition == null && definitionKind == ItemCreationDefinitionKind.Part &&
string.IsNullOrWhiteSpace(partTypeId))
{
throw new InvalidOperationException("Part Type Id is required for a part.");
}
if (existingDefinition == null && definitionKind == ItemCreationDefinitionKind.Rocket &&
string.IsNullOrWhiteSpace(rocketTypeId))
{
throw new InvalidOperationException("Rocket Type Id is required for a rocket.");
}
}
private void ValidateExistingPrefabMovement(GameObject prefabAsset)
{
string prefabPath = AssetDatabase.GetAssetPath(prefabAsset);
GameObject root = PrefabUtility.LoadPrefabContents(prefabPath);
try
{
EnsureSingleMovementBehaviour(root);
}
finally
{
PrefabUtility.UnloadPrefabContents(root);
}
}
private GameObject ResolveExistingWorldPrefab()
{
if (existingWorldPrefab != null)
return existingWorldPrefab;
return existingDefinition != null && existingDefinition.WorldPrefab != null
? existingDefinition.WorldPrefab.gameObject
: null;
}
private ItemCreationDefinitionKind EffectiveDefinitionKind =>
existingDefinition != null ? GetDefinitionKind(existingDefinition) : definitionKind;
private static ItemCreationDefinitionKind GetDefinitionKind(ItemDefinition definition) =>
definition switch
{
MaterialItemDefinition => ItemCreationDefinitionKind.Material,
PartItemDefinition => ItemCreationDefinitionKind.Part,
RocketItemDefinition => ItemCreationDefinitionKind.Rocket,
UsableItemDefinition => ItemCreationDefinitionKind.Usable,
_ => ItemCreationDefinitionKind.Generic
};
private ItemDefinition CreateDefinition(string path)
{
ItemDefinition definition = definitionKind switch
{
ItemCreationDefinitionKind.Material =>
CreateInstance<MaterialItemDefinition>(),
ItemCreationDefinitionKind.Part =>
CreateInstance<PartItemDefinition>(),
ItemCreationDefinitionKind.Rocket =>
CreateInstance<RocketItemDefinition>(),
ItemCreationDefinitionKind.Usable =>
CreateInstance<UsableItemDefinition>(),
_ => CreateInstance<ItemDefinition>()
};
definition.name = assetName.Trim();
AssetDatabase.CreateAsset(definition, path);
SetString(definition, "itemId", NormalizeId(itemId));
SetString(definition, "displayName", displayName?.Trim());
if (definition is PartItemDefinition)
{
SetString(definition, "partTypeId", NormalizeId(partTypeId));
SetInt(definition, "processingState", (int)partProcessingState);
}
else if (definition is RocketItemDefinition)
{
SetString(definition, "rocketTypeId", NormalizeId(rocketTypeId));
SetFloat(definition, "criticalImpactSpeed", Mathf.Max(0.1f, criticalImpactSpeed));
}
return definition;
}
private GameObject ConfigureOrCreateWorldPrefab(
string safeAssetName,
ItemDefinition definition)
{
string prefabPath;
GameObject prefabAsset = ResolveExistingWorldPrefab();
if (prefabAsset != null)
{
prefabPath = AssetDatabase.GetAssetPath(prefabAsset);
}
else
{
EnsureFolder(prefabFolder);
prefabPath = $"{prefabFolder}/{safeAssetName}.prefab";
if (AssetDatabase.LoadMainAssetAtPath(prefabPath) != null)
throw new InvalidOperationException($"Prefab already exists: {prefabPath}");
CreateInitialPrefab(prefabPath, safeAssetName);
}
GameObject root = PrefabUtility.LoadPrefabContents(prefabPath);
try
{
ConfigurePrefabRoot(root, definition);
PrefabUtility.SaveAsPrefabAsset(root, prefabPath);
}
finally
{
PrefabUtility.UnloadPrefabContents(root);
}
return AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
}
private void CreateInitialPrefab(string prefabPath, string safeAssetName)
{
GameObject root = new(safeAssetName);
try
{
if (visualPrefab != null)
{
GameObject visual = PrefabUtility.InstantiatePrefab(visualPrefab) as GameObject;
if (visual == null)
visual = Instantiate(visualPrefab);
visual.name = "Visual";
visual.transform.SetParent(root.transform, false);
}
PrefabUtility.SaveAsPrefabAsset(root, prefabPath);
}
finally
{
DestroyImmediate(root);
}
}
private void ConfigurePrefabRoot(GameObject root, ItemDefinition definition)
{
EnsureSingleMovementBehaviour(root);
NetworkObject networkObject = GetOrAdd<NetworkObject>(root);
NetworkTransform networkTransform = GetOrAdd<NetworkTransform>(root);
networkTransform.AuthorityMode = NetworkTransform.AuthorityModes.Server;
networkTransform.UseUnreliableDeltas = true;
NetworkItem networkItem = GetOrAdd<NetworkItem>(root);
SetObjectReference(networkItem, "definition", definition);
bool needsPhysics = movementKind != ItemCreationMovementKind.Static ||
EffectiveDefinitionKind == ItemCreationDefinitionKind.Rocket;
Rigidbody body = needsPhysics ? GetOrAdd<Rigidbody>(root) : root.GetComponent<Rigidbody>();
if (body != null)
{
body.interpolation = RigidbodyInterpolation.Interpolate;
body.collisionDetectionMode = CollisionDetectionMode.Continuous;
}
if (needsPhysics)
{
NetworkRigidbody networkRigidbody = GetOrAdd<NetworkRigidbody>(root);
networkRigidbody.UseRigidBodyForMotion = true;
EnsureCollider(root);
}
switch (movementKind)
{
case ItemCreationMovementKind.Carry:
GetOrAdd<NetworkCarryableInteractable>(root);
if (body != null)
body.mass = 1f;
break;
case ItemCreationMovementKind.Drag:
NetworkDraggableInteractable draggable =
GetOrAdd<NetworkDraggableInteractable>(root);
float safeWeight = Mathf.Max(0.1f, dragWeight);
SetFloat(draggable, "weight", safeWeight);
if (body != null)
body.mass = safeWeight;
break;
case ItemCreationMovementKind.Hand:
Type handBehaviourType = ResolveHandItemBehaviourType();
if (root.GetComponent(handBehaviourType) == null)
root.AddComponent(handBehaviourType);
if (body != null)
body.mass = 0.5f;
break;
}
if (EffectiveDefinitionKind == ItemCreationDefinitionKind.Rocket)
{
GetOrAdd<NetworkRocketItem>(root);
GetOrAdd<RocketItemView>(root);
}
if (addFurnaceMetalSource)
{
FurnaceMetalSource source = GetOrAdd<FurnaceMetalSource>(root);
SetFloat(source, "metalAmount", Mathf.Max(0.01f, furnaceMetalAmount));
}
EditorUtility.SetDirty(networkObject);
}
private void EnsureSingleMovementBehaviour(GameObject root)
{
bool hasCarry = root.GetComponent<NetworkCarryableInteractable>() != null;
bool hasDrag = root.GetComponent<NetworkDraggableInteractable>() != null;
bool hasHand = root.GetComponent<NetworkHandItemInteractable>() != null;
int existingCount = (hasCarry ? 1 : 0) + (hasDrag ? 1 : 0) + (hasHand ? 1 : 0);
if (existingCount > 1)
{
throw new InvalidOperationException(
"The prefab already contains more than one movement behaviour.");
}
bool compatible = existingCount == 0 ||
(movementKind == ItemCreationMovementKind.Carry && hasCarry) ||
(movementKind == ItemCreationMovementKind.Drag && hasDrag) ||
(movementKind == ItemCreationMovementKind.Hand && hasHand);
if (!compatible)
{
throw new InvalidOperationException(
"The prefab already has a different movement behaviour. Remove or change it manually first.");
}
if (movementKind == ItemCreationMovementKind.Hand && hasHand)
{
NetworkHandItemInteractable existingHand =
root.GetComponent<NetworkHandItemInteractable>();
if (existingHand.GetType() != ResolveHandItemBehaviourType())
{
throw new InvalidOperationException(
$"The prefab already uses {existingHand.GetType().Name}, but another hand " +
"behaviour was selected.");
}
}
}
private Type ResolveHandItemBehaviourType()
{
if (handItemBehaviour == null)
return typeof(NetworkUsableHandItemInteractable);
Type type = handItemBehaviour.GetClass();
if (type == null || type.IsAbstract ||
!typeof(NetworkHandItemInteractable).IsAssignableFrom(type))
{
throw new InvalidOperationException(
"Use Behaviour must be a non-abstract NetworkHandItemInteractable script.");
}
return type;
}
private void EnsureUniqueItemId(string candidateId)
{
string normalizedId = NormalizeId(candidateId);
foreach (string guid in AssetDatabase.FindAssets("t:ItemDefinition"))
{
string path = AssetDatabase.GUIDToAssetPath(guid);
ItemDefinition definition = AssetDatabase.LoadAssetAtPath<ItemDefinition>(path);
if (definition != null && definition.ItemId == normalizedId)
{
throw new InvalidOperationException(
$"Item Id '{normalizedId}' is already used by {path}.");
}
}
}
private void RegisterInCatalog(ItemDefinition definition)
{
if (catalog == null)
return;
SerializedObject serializedCatalog = new(catalog);
SerializedProperty items = serializedCatalog.FindProperty("items");
for (int index = 0; index < items.arraySize; index++)
{
if (items.GetArrayElementAtIndex(index).objectReferenceValue == definition)
return;
}
int newIndex = items.arraySize;
items.InsertArrayElementAtIndex(newIndex);
items.GetArrayElementAtIndex(newIndex).objectReferenceValue = definition;
serializedCatalog.ApplyModifiedPropertiesWithoutUndo();
EditorUtility.SetDirty(catalog);
}
private void RegisterNetworkPrefab(GameObject prefab)
{
if (networkPrefabsList == null || networkPrefabsList.Contains(prefab))
return;
networkPrefabsList.Add(new NetworkPrefab { Prefab = prefab });
EditorUtility.SetDirty(networkPrefabsList);
}
private static T GetOrAdd<T>(GameObject gameObject) where T : Component =>
gameObject.TryGetComponent(out T component) ? component : gameObject.AddComponent<T>();
private static void EnsureCollider(GameObject root)
{
if (root.GetComponentInChildren<Collider>(true) != null)
return;
BoxCollider collider = root.AddComponent<BoxCollider>();
if (!TryCalculateLocalRendererBounds(root, out Bounds bounds))
return;
collider.center = bounds.center;
collider.size = bounds.size;
}
private static bool TryCalculateLocalRendererBounds(GameObject root, out Bounds bounds)
{
Renderer[] renderers = root.GetComponentsInChildren<Renderer>(true);
if (renderers.Length == 0)
{
bounds = new Bounds(Vector3.zero, Vector3.one);
return false;
}
bool initialized = false;
bounds = default;
foreach (Renderer renderer in renderers)
{
Bounds worldBounds = renderer.bounds;
Vector3 center = root.transform.InverseTransformPoint(worldBounds.center);
Vector3 extents = worldBounds.extents;
for (int x = -1; x <= 1; x += 2)
for (int y = -1; y <= 1; y += 2)
for (int z = -1; z <= 1; z += 2)
{
Vector3 worldCorner = worldBounds.center +
Vector3.Scale(extents, new Vector3(x, y, z));
Vector3 localCorner = root.transform.InverseTransformPoint(worldCorner);
if (!initialized)
{
bounds = new Bounds(center, Vector3.zero);
initialized = true;
}
bounds.Encapsulate(localCorner);
}
}
return initialized;
}
private static void EnsureFolder(string folderPath)
{
string normalized = folderPath.Replace('\\', '/').TrimEnd('/');
if (AssetDatabase.IsValidFolder(normalized))
return;
string parent = Path.GetDirectoryName(normalized)?.Replace('\\', '/');
string folderName = Path.GetFileName(normalized);
if (string.IsNullOrEmpty(parent) || string.IsNullOrEmpty(folderName))
throw new InvalidOperationException($"Invalid folder path: {folderPath}");
EnsureFolder(parent);
AssetDatabase.CreateFolder(parent, folderName);
}
private static string NormalizeId(string value) =>
value.Trim().ToLowerInvariant().Replace(' ', '-');
private static string SanitizeFileName(string value)
{
foreach (char invalidCharacter in Path.GetInvalidFileNameChars())
value = value.Replace(invalidCharacter, '-');
return value;
}
private static void SetString(UnityEngine.Object target, string propertyName, string value) =>
Edit(target, propertyName, property => property.stringValue = value);
private static void SetFloat(UnityEngine.Object target, string propertyName, float value) =>
Edit(target, propertyName, property => property.floatValue = value);
private static void SetInt(UnityEngine.Object target, string propertyName, int value) =>
Edit(target, propertyName, property => property.intValue = value);
private static void SetObjectReference(
UnityEngine.Object target,
string propertyName,
UnityEngine.Object value) =>
Edit(target, propertyName, property => property.objectReferenceValue = value);
private static void Edit(
UnityEngine.Object target,
string propertyName,
Action<SerializedProperty> edit)
{
SerializedObject serializedObject = new(target);
SerializedProperty property = serializedObject.FindProperty(propertyName);
if (property == null)
throw new MissingFieldException(target.GetType().Name, propertyName);
edit(property);
serializedObject.ApplyModifiedPropertiesWithoutUndo();
EditorUtility.SetDirty(target);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d1e6d4d81ab34d2e9ee3a1bfa201f120
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,357 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &2376631388558761094
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1632953841127884940}
- component: {fileID: 7456648717704822871}
- component: {fileID: 7292341886911989191}
- component: {fileID: 8891747045554379344}
- component: {fileID: 3133876013448598132}
- component: {fileID: 193117245170608707}
- component: {fileID: 4269639932524941515}
- component: {fileID: 6493104616022895181}
- component: {fileID: 3635736001397410476}
m_Layer: 0
m_Name: Ready-Rocket-Standart
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &1632953841127884940
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2376631388558761094}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1646663282415760833}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &7456648717704822871
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2376631388558761094}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3}
m_Name:
m_EditorClassIdentifier:
GlobalObjectIdHash: 3332465624
InScenePlacedSourceGlobalObjectIdHash: 0
DeferredDespawnTick: 0
Ownership: 1
AlwaysReplicateAsRoot: 0
SynchronizeTransform: 1
m_InScenePlaced: 0
ActiveSceneSynchronization: 0
SceneMigrationSynchronization: 1
SpawnWithObservers: 1
DontDestroyWithOwner: 0
AutoObjectParentSync: 1
SyncOwnerTransformWhenParented: 1
AllowOwnerToParent: 0
--- !u!114 &7292341886911989191
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2376631388558761094}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e96cb6065543e43c4a752faaa1468eb1, type: 3}
m_Name:
m_EditorClassIdentifier:
ShowTopMostFoldoutHeaderGroup: 1
NetworkTransformExpanded: 0
AutoOwnerAuthorityTickOffset: 1
PositionInterpolationType: 0
RotationInterpolationType: 0
ScaleInterpolationType: 0
PositionLerpSmoothing: 1
PositionMaxInterpolationTime: 0.1
RotationLerpSmoothing: 1
RotationMaxInterpolationTime: 0.1
ScaleLerpSmoothing: 1
ScaleMaxInterpolationTime: 0.1
AuthorityMode: 0
TickSyncChildren: 0
UseUnreliableDeltas: 1
SyncPositionX: 1
SyncPositionY: 1
SyncPositionZ: 1
SyncRotAngleX: 1
SyncRotAngleY: 1
SyncRotAngleZ: 1
SyncScaleX: 1
SyncScaleY: 1
SyncScaleZ: 1
PositionThreshold: 0.001
RotAngleThreshold: 0.01
ScaleThreshold: 0.01
UseQuaternionSynchronization: 0
UseQuaternionCompression: 0
UseHalfFloatPrecision: 0
InLocalSpace: 0
SwitchTransformSpaceWhenParented: 0
Interpolate: 1
SlerpPosition: 0
--- !u!114 &8891747045554379344
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2376631388558761094}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 323282b7a87fc4344bd33250e88822e8, type: 3}
m_Name:
m_EditorClassIdentifier:
ShowTopMostFoldoutHeaderGroup: 1
definition: {fileID: 11400000, guid: 5353380be6a58a041bc52d32971d1d9b, type: 2}
--- !u!54 &3133876013448598132
Rigidbody:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2376631388558761094}
serializedVersion: 4
m_Mass: 40
m_Drag: 0
m_AngularDrag: 0.05
m_CenterOfMass: {x: 0, y: 0, z: 0}
m_InertiaTensor: {x: 1, y: 1, z: 1}
m_InertiaRotation: {x: 0, y: 0, z: 0, w: 1}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_ImplicitCom: 1
m_ImplicitTensor: 1
m_UseGravity: 1
m_IsKinematic: 0
m_Interpolate: 1
m_Constraints: 0
m_CollisionDetection: 1
--- !u!114 &193117245170608707
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2376631388558761094}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f6c0be61502bb534f922ebb746851216, type: 3}
m_Name:
m_EditorClassIdentifier:
ShowTopMostFoldoutHeaderGroup: 1
NetworkRigidbodyBaseExpanded: 0
UseRigidBodyForMotion: 1
AutoUpdateKinematicState: 1
AutoSetKinematicOnDespawn: 1
--- !u!114 &4269639932524941515
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2376631388558761094}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e5e737931b3bc22468ea0fe857062359, type: 3}
m_Name:
m_EditorClassIdentifier:
ShowTopMostFoldoutHeaderGroup: 1
interactionLabel: Interact
maximumInteractionDistance: 3
weight: 40
pullStrengthPerPlayer: 50
maximumDraggers: 2
dragOffset: {x: 0, y: 1, z: 1.8}
maximumDragSpeed: 3
minimumSpeedMultiplier: 0.35
maximumDraggerDistance: 4.5
positionSmoothTime: 0.12
rotationFollowSpeed: 8
rotationOffset: {x: 0, y: 0, z: 0}
movementMode: 0
groundLayers:
serializedVersion: 2
m_Bits: 4294967295
groundContactPoint: {fileID: 0}
groundProbeRadius: 0.2
groundProbeHeight: 1.25
maximumGroundDrop: 2
maximumGroundAngle: 55
groundClearance: 0.02
maximumGroundVerticalSpeed: 6
alignToGround: 1
groundAlignmentSpeed: 10
physicsBody: {fileID: 3133876013448598132}
objectColliders:
- {fileID: 5222869138606293483}
- {fileID: 6894540284203297686}
- {fileID: 1623627698583667245}
- {fileID: 7077397027048864733}
collisionRefreshInterval: 0.25
--- !u!114 &6493104616022895181
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2376631388558761094}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d1e6d4d81ab34d2e9ee3a1bfa201f111, type: 3}
m_Name:
m_EditorClassIdentifier:
ShowTopMostFoldoutHeaderGroup: 1
networkItem: {fileID: 8891747045554379344}
physicsBody: {fileID: 3133876013448598132}
--- !u!114 &3635736001397410476
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2376631388558761094}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d1e6d4d81ab34d2e9ee3a1bfa201f112, type: 3}
m_Name:
m_EditorClassIdentifier:
rocket: {fileID: 6493104616022895181}
onNormalQualityApplied:
m_PersistentCalls:
m_Calls: []
onDefectiveQualityApplied:
m_PersistentCalls:
m_Calls: []
onDamagedStateApplied:
m_PersistentCalls:
m_Calls: []
onCriticalImpact:
m_PersistentCalls:
m_Calls: []
onCriticalImpactPoint:
m_PersistentCalls:
m_Calls: []
onCriticalImpactSpeed:
m_PersistentCalls:
m_Calls: []
--- !u!1001 &5717474834163769460
PrefabInstance:
m_ObjectHideFlags: 0
serializedVersion: 2
m_Modification:
serializedVersion: 3
m_TransformParent: {fileID: 1632953841127884940}
m_Modifications:
- target: {fileID: 2553015719312062053, guid: 0b36d4cab828ea645bb5ab46d25c2204, type: 3}
propertyPath: m_VersionIndex
value: 1173
objectReference: {fileID: 0}
- target: {fileID: 3949508846642722774, guid: 0b36d4cab828ea645bb5ab46d25c2204, type: 3}
propertyPath: m_VersionIndex
value: 3154
objectReference: {fileID: 0}
- target: {fileID: 4560519800175472911, guid: 0b36d4cab828ea645bb5ab46d25c2204, type: 3}
propertyPath: m_VersionIndex
value: 3693
objectReference: {fileID: 0}
- target: {fileID: 6449878790825891253, guid: 0b36d4cab828ea645bb5ab46d25c2204, type: 3}
propertyPath: m_LocalPosition.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6449878790825891253, guid: 0b36d4cab828ea645bb5ab46d25c2204, type: 3}
propertyPath: m_LocalPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6449878790825891253, guid: 0b36d4cab828ea645bb5ab46d25c2204, type: 3}
propertyPath: m_LocalPosition.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6449878790825891253, guid: 0b36d4cab828ea645bb5ab46d25c2204, type: 3}
propertyPath: m_LocalRotation.w
value: 1
objectReference: {fileID: 0}
- target: {fileID: 6449878790825891253, guid: 0b36d4cab828ea645bb5ab46d25c2204, type: 3}
propertyPath: m_LocalRotation.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6449878790825891253, guid: 0b36d4cab828ea645bb5ab46d25c2204, type: 3}
propertyPath: m_LocalRotation.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6449878790825891253, guid: 0b36d4cab828ea645bb5ab46d25c2204, type: 3}
propertyPath: m_LocalRotation.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6449878790825891253, guid: 0b36d4cab828ea645bb5ab46d25c2204, type: 3}
propertyPath: m_LocalEulerAnglesHint.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6449878790825891253, guid: 0b36d4cab828ea645bb5ab46d25c2204, type: 3}
propertyPath: m_LocalEulerAnglesHint.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 6449878790825891253, guid: 0b36d4cab828ea645bb5ab46d25c2204, type: 3}
propertyPath: m_LocalEulerAnglesHint.z
value: 0
objectReference: {fileID: 0}
- target: {fileID: 8990336511305880825, guid: 0b36d4cab828ea645bb5ab46d25c2204, type: 3}
propertyPath: m_Name
value: Visual
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
m_AddedGameObjects: []
m_AddedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: 0b36d4cab828ea645bb5ab46d25c2204, type: 3}
--- !u!64 &1623627698583667245 stripped
MeshCollider:
m_CorrespondingSourceObject: {fileID: 6471890211788923481, guid: 0b36d4cab828ea645bb5ab46d25c2204, type: 3}
m_PrefabInstance: {fileID: 5717474834163769460}
m_PrefabAsset: {fileID: 0}
--- !u!4 &1646663282415760833 stripped
Transform:
m_CorrespondingSourceObject: {fileID: 6449878790825891253, guid: 0b36d4cab828ea645bb5ab46d25c2204, type: 3}
m_PrefabInstance: {fileID: 5717474834163769460}
m_PrefabAsset: {fileID: 0}
--- !u!136 &5222869138606293483 stripped
CapsuleCollider:
m_CorrespondingSourceObject: {fileID: 514491035522146719, guid: 0b36d4cab828ea645bb5ab46d25c2204, type: 3}
m_PrefabInstance: {fileID: 5717474834163769460}
m_PrefabAsset: {fileID: 0}
--- !u!64 &6894540284203297686 stripped
MeshCollider:
m_CorrespondingSourceObject: {fileID: 1222409323731209186, guid: 0b36d4cab828ea645bb5ab46d25c2204, type: 3}
m_PrefabInstance: {fileID: 5717474834163769460}
m_PrefabAsset: {fileID: 0}
--- !u!64 &7077397027048864733 stripped
MeshCollider:
m_CorrespondingSourceObject: {fileID: 3273969631134882729, guid: 0b36d4cab828ea645bb5ab46d25c2204, type: 3}
m_PrefabInstance: {fileID: 5717474834163769460}
m_PrefabAsset: {fileID: 0}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 746f04d9ba6c87d4bae8a09287c5d090
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -16,13 +16,12 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
[Header("Holding Follow")]
[SerializeField, Min(0.1f)] private float maximumFollowSpeed = 12f;
[SerializeField, Min(0.1f)] private float followResponsiveness = 10f;
[SerializeField, Min(0.1f)] private float maximumFollowAcceleration = 80f;
[SerializeField, Min(0.5f)] private float maximumHoldDistance = 4.5f;
[SerializeField, Min(0.1f)] private float rotationFollowSpeed = 14f;
[Header("Local Presentation")]
[Tooltip("Removes the server round-trip delay for the player currently carrying the item.")]
[SerializeField] private bool correctLocalHolderPresentation = true;
[SerializeField, Min(0.1f)] private float maximumLocalCorrectionDistance = 2f;
[SerializeField, Min(0.1f)] private float rotationResponsiveness = 10f;
[SerializeField, Min(0.1f)] private float maximumAngularAcceleration = 80f;
[Header("Throw")]
[SerializeField, Min(0f)] private float throwForce = 6f;
@@ -41,10 +40,10 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
private readonly HashSet<Collider> ignoredPlayerColliders = new();
private readonly HashSet<Collider> desiredIgnoredColliders = new();
private readonly List<Collider> collidersToRestore = new();
private readonly RaycastHit[] localCorrectionHits = new RaycastHit[24];
private float nextCollisionRefreshTime;
private bool initialUseGravity;
private CollisionDetectionMode initialCollisionDetectionMode;
private NetworkTransform networkTransform;
protected abstract Vector3 HoldingOffset { get; }
protected abstract string DropPrompt { get; }
@@ -80,6 +79,8 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
initialUseGravity = physicsBody.useGravity;
initialCollisionDetectionMode = physicsBody.collisionDetectionMode;
}
networkTransform = GetComponent<NetworkTransform>();
}
public override void OnNetworkSpawn()
@@ -87,6 +88,12 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
SpawnedHeldObjects.Add(this);
holderClientId.OnValueChanged += HandleHolderChanged;
isExternallyControlled.OnValueChanged += HandleExternalControlChanged;
if (networkTransform != null)
{
networkTransform.Interpolate = true;
networkTransform.UseUnreliableDeltas = true;
}
ApplyHoldingState();
}
@@ -129,28 +136,6 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
}
private void LateUpdate()
{
if (!correctLocalHolderPresentation || IsServer || !IsSpawned ||
!IsHeldByLocalPlayer || IsExternallyControlled || physicsBody == null ||
!TryGetPlayerObject(holderClientId.Value, out NetworkObject holder))
{
return;
}
Transform anchor = ResolveHoldingAnchor(holder);
Vector3 targetPosition = anchor.TransformPoint(HoldingOffset);
Vector3 targetDelta = targetPosition - physicsBody.position;
if (targetDelta.sqrMagnitude >
maximumLocalCorrectionDistance * maximumLocalCorrectionDistance ||
HasBlockingColliderForLocalCorrection(targetDelta))
{
return;
}
transform.SetPositionAndRotation(targetPosition, anchor.rotation);
}
public void RequestThrow()
{
if (IsSpawned && IsHeldByLocalPlayer)
@@ -292,10 +277,14 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
return false;
}
Vector3 targetVelocity = targetDelta / Mathf.Max(Time.fixedDeltaTime, 0.001f);
physicsBody.linearVelocity = Vector3.ClampMagnitude(
targetVelocity,
float fixedDeltaTime = Mathf.Max(Time.fixedDeltaTime, 0.001f);
Vector3 targetVelocity = Vector3.ClampMagnitude(
targetDelta * followResponsiveness,
maximumFollowSpeed);
physicsBody.linearVelocity = Vector3.MoveTowards(
physicsBody.linearVelocity,
targetVelocity,
maximumFollowAcceleration * fixedDeltaTime);
DriveRotation(anchor.rotation);
return true;
@@ -306,57 +295,25 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
Quaternion rotationDelta = targetRotation * Quaternion.Inverse(physicsBody.rotation);
rotationDelta.ToAngleAxis(out float angle, out Vector3 axis);
if (axis.sqrMagnitude < 0.0001f || float.IsNaN(axis.x))
{
physicsBody.angularVelocity = Vector3.MoveTowards(
physicsBody.angularVelocity,
Vector3.zero,
maximumAngularAcceleration * Mathf.Max(Time.fixedDeltaTime, 0.001f));
return;
}
if (angle > 180f)
angle -= 360f;
Vector3 targetAngularVelocity = axis.normalized *
(angle * Mathf.Deg2Rad / Mathf.Max(Time.fixedDeltaTime, 0.001f));
physicsBody.angularVelocity = Vector3.ClampMagnitude(
targetAngularVelocity,
float fixedDeltaTime = Mathf.Max(Time.fixedDeltaTime, 0.001f);
Vector3 targetAngularVelocity = Vector3.ClampMagnitude(
axis.normalized * angle * Mathf.Deg2Rad * rotationResponsiveness,
rotationFollowSpeed);
}
private bool HasBlockingColliderForLocalCorrection(Vector3 targetDelta)
{
float distance = targetDelta.magnitude;
if (distance <= 0.0001f)
return false;
int hitCount = Physics.RaycastNonAlloc(
physicsBody.position,
targetDelta / distance,
localCorrectionHits,
distance,
~0,
QueryTriggerInteraction.Ignore);
for (int hitIndex = 0; hitIndex < hitCount; hitIndex++)
{
Collider hitCollider = localCorrectionHits[hitIndex].collider;
if (hitCollider == null ||
ignoredPlayerColliders.Contains(hitCollider) ||
IsObjectCollider(hitCollider))
{
continue;
}
return true;
}
return false;
}
private bool IsObjectCollider(Collider candidate)
{
foreach (Collider objectCollider in objectColliders)
{
if (objectCollider == candidate)
return true;
}
return false;
physicsBody.angularVelocity = Vector3.MoveTowards(
physicsBody.angularVelocity,
targetAngularVelocity,
maximumAngularAcceleration * fixedDeltaTime);
}
private void HandleHolderChanged(ulong _, ulong __) => ApplyHoldingState();
@@ -369,6 +326,9 @@ public abstract class NetworkHeldInteractable : NetworkInteractable, IWorkstatio
{
bool isPhysicallyHeld = IsHeld && !IsExternallyControlled;
bool shouldBeKinematic = IsExternallyControlled || !IsServer;
physicsBody.interpolation = IsServer
? RigidbodyInterpolation.Interpolate
: RigidbodyInterpolation.None;
if (shouldBeKinematic)
{
@@ -23,7 +23,7 @@ public sealed class ItemCatalog : ScriptableObject
return false;
}
return itemsById.TryGetValue(itemId.Trim().ToLowerInvariant(), out definition);
return itemsById.TryGetValue(NormalizeId(itemId), out definition);
}
private void RebuildIndex()
@@ -39,4 +39,7 @@ public sealed class ItemCatalog : ScriptableObject
Debug.LogError($"Duplicate item id '{definition.ItemId}' in {name}.", this);
}
}
private static string NormalizeId(string value) =>
value.Trim().ToLowerInvariant().Replace(' ', '-');
}
@@ -1,33 +1,23 @@
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
public class ItemDefinition : ScriptableObject
{
[SerializeField] private string itemId;
[SerializeField] private string displayName;
[SerializeField] private ItemCategory category;
[SerializeField] private bool isAssemblyReady;
[SerializeField] private NetworkObject worldPrefab;
public string ItemId => itemId;
public string DisplayName => string.IsNullOrWhiteSpace(displayName) ? name : displayName;
public ItemCategory Category => category;
public bool IsAssemblyReady => isAssemblyReady;
public NetworkObject WorldPrefab => worldPrefab;
private void OnValidate()
protected virtual void OnValidate()
{
itemId = itemId?.Trim().ToLowerInvariant();
itemId = NormalizeId(itemId);
displayName = displayName?.Trim();
}
protected static string NormalizeId(string value) =>
value?.Trim().ToLowerInvariant().Replace(' ', '-');
}
@@ -10,5 +10,11 @@ public sealed class NetworkItem : NetworkBehaviour
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;
public bool TryGetDefinition<TDefinition>(out TDefinition typedDefinition)
where TDefinition : ItemDefinition
{
typedDefinition = definition as TDefinition;
return typedDefinition != null;
}
}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d1e6d4d81ab34d2e9ee3a1bfa201f100
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
using UnityEngine;
[CreateAssetMenu(
fileName = "MaterialItemDefinition",
menuName = "FORT-BO/Items/Definitions/Material")]
public sealed class MaterialItemDefinition : ItemDefinition
{
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d1e6d4d81ab34d2e9ee3a1bfa201f101
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,29 @@
using UnityEngine;
public enum PartProcessingState : byte
{
Raw,
Ready
}
[CreateAssetMenu(
fileName = "PartItemDefinition",
menuName = "FORT-BO/Items/Definitions/Part")]
public sealed class PartItemDefinition : ItemDefinition
{
[Tooltip("Data-driven part family id, for example rocket-engine or rocket-cone-standard.")]
[SerializeField] private string partTypeId;
[SerializeField] private PartProcessingState processingState;
public string PartTypeId => string.IsNullOrWhiteSpace(partTypeId)
? ItemId
: partTypeId;
public PartProcessingState ProcessingState => processingState;
public bool IsReadyForAssembly => processingState == PartProcessingState.Ready;
protected override void OnValidate()
{
base.OnValidate();
partTypeId = NormalizeId(partTypeId);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d1e6d4d81ab34d2e9ee3a1bfa201f102
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,22 @@
using UnityEngine;
[CreateAssetMenu(
fileName = "RocketItemDefinition",
menuName = "FORT-BO/Items/Definitions/Rocket")]
public sealed class RocketItemDefinition : ItemDefinition
{
[Tooltip("Stable rocket family id used by launchers and orders.")]
[SerializeField] private string rocketTypeId;
[SerializeField, Min(0.1f)] private float criticalImpactSpeed = 7f;
public string RocketTypeId => string.IsNullOrWhiteSpace(rocketTypeId)
? ItemId
: rocketTypeId;
public float CriticalImpactSpeed => criticalImpactSpeed;
protected override void OnValidate()
{
base.OnValidate();
rocketTypeId = NormalizeId(rocketTypeId);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d1e6d4d81ab34d2e9ee3a1bfa201f103
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
using UnityEngine;
[CreateAssetMenu(
fileName = "UsableItemDefinition",
menuName = "FORT-BO/Items/Definitions/Usable")]
public sealed class UsableItemDefinition : ItemDefinition
{
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d1e6d4d81ab34d2e9ee3a1bfa201f104
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d1e6d4d81ab34d2e9ee3a1bfa201f110
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,168 @@
using System;
using Unity.Netcode;
using UnityEngine;
public enum RocketAssemblyQuality : byte
{
Normal,
Defective
}
public struct RocketCriticalImpactState : INetworkSerializable,
IEquatable<RocketCriticalImpactState>
{
public uint Sequence;
public Vector3 Point;
public float Speed;
public RocketCriticalImpactState(uint sequence, Vector3 point, float speed)
{
Sequence = sequence;
Point = point;
Speed = speed;
}
public void NetworkSerialize<T>(BufferSerializer<T> serializer)
where T : IReaderWriter
{
serializer.SerializeValue(ref Sequence);
serializer.SerializeValue(ref Point);
serializer.SerializeValue(ref Speed);
}
public bool Equals(RocketCriticalImpactState other) =>
Sequence == other.Sequence && Point == other.Point && Speed.Equals(other.Speed);
}
[DisallowMultipleComponent]
[RequireComponent(typeof(NetworkItem), typeof(Rigidbody))]
public sealed class NetworkRocketItem : NetworkBehaviour
{
[SerializeField] private NetworkItem networkItem;
[SerializeField] private Rigidbody physicsBody;
private readonly NetworkVariable<RocketAssemblyQuality> assemblyQuality = new(
RocketAssemblyQuality.Normal,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
private readonly NetworkVariable<RocketCriticalImpactState> criticalImpactState = new(
default,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server);
public RocketItemDefinition Definition =>
networkItem != null && networkItem.TryGetDefinition(out RocketItemDefinition definition)
? definition
: null;
public string RocketTypeId => Definition != null ? Definition.RocketTypeId : string.Empty;
public RocketAssemblyQuality AssemblyQuality => assemblyQuality.Value;
public bool IsDefective => AssemblyQuality == RocketAssemblyQuality.Defective;
public bool IsCriticallyDamaged => criticalImpactState.Value.Sequence > 0;
public uint CriticalImpactSequence => criticalImpactState.Value.Sequence;
public Vector3 LastCriticalImpactPoint => criticalImpactState.Value.Point;
public float LastCriticalImpactSpeed => criticalImpactState.Value.Speed;
public event Action Changed;
public event Action CriticalImpact;
private void Reset() => ResolveReferences();
private void Awake() => ResolveReferences();
public override void OnNetworkSpawn()
{
assemblyQuality.OnValueChanged += HandleAssemblyQualityChanged;
criticalImpactState.OnValueChanged += HandleCriticalImpactChanged;
if (Definition == null)
{
Debug.LogError(
$"{name} has NetworkRocketItem but its NetworkItem does not reference " +
"a RocketItemDefinition.",
this);
}
Changed?.Invoke();
}
public override void OnNetworkDespawn()
{
assemblyQuality.OnValueChanged -= HandleAssemblyQualityChanged;
criticalImpactState.OnValueChanged -= HandleCriticalImpactChanged;
}
private void OnCollisionEnter(Collision collision)
{
if (!IsServer || !IsSpawned || IsCriticallyDamaged || Definition == null)
return;
float impactSpeed = collision.relativeVelocity.magnitude;
if (impactSpeed < Definition.CriticalImpactSpeed)
return;
Vector3 impactPoint = collision.contactCount > 0
? collision.GetContact(0).point
: transform.position;
TryTriggerCriticalImpact(impactPoint, impactSpeed);
}
public bool TryTriggerCriticalImpact(Vector3 impactPoint, float impactSpeed)
{
if (!IsServer || !IsSpawned || IsCriticallyDamaged || Definition == null ||
impactSpeed < Definition.CriticalImpactSpeed)
{
return false;
}
criticalImpactState.Value = new RocketCriticalImpactState(
criticalImpactState.Value.Sequence + 1,
impactPoint,
impactSpeed);
return true;
}
public bool TryInitializeAssemblyQuality(bool isDefectiveResult)
{
if (!IsServer || !IsSpawned)
return false;
assemblyQuality.Value = isDefectiveResult
? RocketAssemblyQuality.Defective
: RocketAssemblyQuality.Normal;
return true;
}
[ContextMenu("Debug/Trigger Critical Impact (Server Play Mode)")]
private void TriggerCriticalImpactFromContextMenu()
{
if (Application.isPlaying && IsServer && IsSpawned && Definition != null)
{
TryTriggerCriticalImpact(
transform.position,
Definition.CriticalImpactSpeed);
}
}
private void ResolveReferences()
{
if (networkItem == null)
networkItem = GetComponent<NetworkItem>();
if (physicsBody == null)
physicsBody = GetComponent<Rigidbody>();
}
private void HandleAssemblyQualityChanged(
RocketAssemblyQuality _,
RocketAssemblyQuality __) => Changed?.Invoke();
private void HandleCriticalImpactChanged(
RocketCriticalImpactState previous,
RocketCriticalImpactState current)
{
Changed?.Invoke();
if (current.Sequence != previous.Sequence)
CriticalImpact?.Invoke();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d1e6d4d81ab34d2e9ee3a1bfa201f111
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,102 @@
using UnityEngine;
using UnityEngine.Events;
[DisallowMultipleComponent]
public sealed class RocketItemView : MonoBehaviour
{
[SerializeField] private NetworkRocketItem rocket;
[Header("State")]
[SerializeField] private UnityEvent onNormalQualityApplied;
[SerializeField] private UnityEvent onDefectiveQualityApplied;
[SerializeField] private UnityEvent onDamagedStateApplied;
[Header("Critical Impact")]
[SerializeField] private UnityEvent onCriticalImpact;
[SerializeField] private UnityEvent<Vector3> onCriticalImpactPoint;
[SerializeField] private UnityEvent<float> onCriticalImpactSpeed;
private uint observedImpactSequence;
private RocketAssemblyQuality observedQuality;
private bool hasAppliedQuality;
private bool hasAppliedDamagedState;
private void Reset() => rocket = GetComponentInParent<NetworkRocketItem>();
private void Awake()
{
if (rocket == null)
rocket = GetComponentInParent<NetworkRocketItem>();
}
private void OnEnable()
{
if (rocket == null)
return;
observedImpactSequence = rocket.CriticalImpactSequence;
observedQuality = rocket.AssemblyQuality;
hasAppliedQuality = false;
hasAppliedDamagedState = false;
rocket.Changed += ApplyState;
rocket.CriticalImpact += HandleCriticalImpact;
ApplyState();
}
private void OnDisable()
{
if (rocket == null)
return;
rocket.Changed -= ApplyState;
rocket.CriticalImpact -= HandleCriticalImpact;
}
[ContextMenu("Preview/Critical Impact")]
private void PreviewCriticalImpact()
{
onCriticalImpactPoint?.Invoke(transform.position);
onCriticalImpactSpeed?.Invoke(7f);
onCriticalImpact?.Invoke();
onDamagedStateApplied?.Invoke();
}
[ContextMenu("Preview/Normal Quality")]
private void PreviewNormalQuality() => onNormalQualityApplied?.Invoke();
[ContextMenu("Preview/Defective Quality")]
private void PreviewDefectiveQuality() => onDefectiveQualityApplied?.Invoke();
private void ApplyState()
{
if (rocket == null)
return;
if (!hasAppliedQuality || observedQuality != rocket.AssemblyQuality)
{
observedQuality = rocket.AssemblyQuality;
hasAppliedQuality = true;
if (observedQuality == RocketAssemblyQuality.Defective)
onDefectiveQualityApplied?.Invoke();
else
onNormalQualityApplied?.Invoke();
}
if (rocket.IsCriticallyDamaged && !hasAppliedDamagedState)
{
hasAppliedDamagedState = true;
onDamagedStateApplied?.Invoke();
}
}
private void HandleCriticalImpact()
{
if (rocket == null || rocket.CriticalImpactSequence == observedImpactSequence)
return;
observedImpactSequence = rocket.CriticalImpactSequence;
onCriticalImpactPoint?.Invoke(rocket.LastCriticalImpactPoint);
onCriticalImpactSpeed?.Invoke(rocket.LastCriticalImpactSpeed);
onCriticalImpact?.Invoke();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d1e6d4d81ab34d2e9ee3a1bfa201f112
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d1e6d4d81ab34d2e9ee3a1bfa201f130
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,6 @@
using Unity.Netcode;
public interface IHandItemUseAction
{
bool TryUseOnServer(NetworkObject playerObject);
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d1e6d4d81ab34d2e9ee3a1bfa201f131
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,49 @@
using Unity.Netcode;
using UnityEngine;
[DisallowMultipleComponent]
public sealed class NetworkUsableHandItemInteractable : NetworkHandItemInteractable
{
[Tooltip("Component on this prefab implementing IHandItemUseAction.")]
[SerializeField] private MonoBehaviour useActionComponent;
private IHandItemUseAction UseAction => useActionComponent as IHandItemUseAction;
protected override void Awake()
{
base.Awake();
ResolveUseAction();
}
private void OnValidate() => ResolveUseAction();
protected override void UseOnServer(NetworkObject playerObject)
{
if (UseAction == null)
{
Debug.LogWarning(
$"{name} was used but has no IHandItemUseAction component assigned.",
this);
return;
}
UseAction.TryUseOnServer(playerObject);
}
private void ResolveUseAction()
{
if (useActionComponent is IHandItemUseAction)
return;
foreach (MonoBehaviour component in GetComponents<MonoBehaviour>())
{
if (component is IHandItemUseAction)
{
useActionComponent = component;
return;
}
}
useActionComponent = null;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d1e6d4d81ab34d2e9ee3a1bfa201f132
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -4,6 +4,7 @@ using UnityEngine;
public sealed class FirstPersonCameraController : NetworkBehaviour, IPlayerSystem
{
[SerializeField, Range(1f, 89f)] private float verticalLookLimit = 80f;
[SerializeField, Min(0.1f)] private float remotePitchFollowSpeed = 20f;
private readonly NetworkVariable<float> synchronizedPitch = new(
0f,
@@ -14,6 +15,7 @@ public sealed class FirstPersonCameraController : NetworkBehaviour, IPlayerSyste
private AudioListener audioListener;
private MeshRenderer playerRenderer;
private float pitch;
private float targetPitch;
private bool isInitialized;
public Camera PlayerCamera => playerCamera;
@@ -34,7 +36,10 @@ public sealed class FirstPersonCameraController : NetworkBehaviour, IPlayerSyste
playerCamera.gameObject.SetActive(isLocalPlayer);
if (!isLocalPlayer)
ApplyCameraPitch(synchronizedPitch.Value);
{
targetPitch = synchronizedPitch.Value;
ApplyCameraPitch(targetPitch);
}
if (!isLocalPlayer)
return;
@@ -62,7 +67,10 @@ public sealed class FirstPersonCameraController : NetworkBehaviour, IPlayerSyste
synchronizedPitch.OnValueChanged += HandleSynchronizedPitchChanged;
if (isInitialized && !IsOwner)
ApplyCameraPitch(synchronizedPitch.Value);
{
targetPitch = synchronizedPitch.Value;
ApplyCameraPitch(targetPitch);
}
if (isInitialized && IsOwner)
CaptureCursor();
@@ -70,7 +78,17 @@ public sealed class FirstPersonCameraController : NetworkBehaviour, IPlayerSyste
private void LateUpdate()
{
if (!isInitialized || !IsOwner || !Application.isFocused)
if (!isInitialized)
return;
if (!IsOwner)
{
float interpolation = 1f - Mathf.Exp(-remotePitchFollowSpeed * Time.deltaTime);
ApplyCameraPitch(Mathf.LerpAngle(pitch, targetPitch, interpolation));
return;
}
if (!Application.isFocused)
return;
if (Cursor.lockState != CursorLockMode.Locked || Cursor.visible)
@@ -89,6 +107,7 @@ public sealed class FirstPersonCameraController : NetworkBehaviour, IPlayerSyste
return;
pitch = Mathf.Clamp(pitch - pitchDelta, -verticalLookLimit, verticalLookLimit);
targetPitch = pitch;
ApplyCameraPitch(pitch);
if (IsSpawned)
@@ -110,7 +129,7 @@ public sealed class FirstPersonCameraController : NetworkBehaviour, IPlayerSyste
private void HandleSynchronizedPitchChanged(float _, float currentPitch)
{
if (!IsOwner)
ApplyCameraPitch(currentPitch);
targetPitch = currentPitch;
}
private void ApplyCameraPitch(float currentPitch)
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.Serialization;
public enum RocketAssemblyWorkstationState : byte
{
@@ -28,7 +29,9 @@ public sealed class NetworkRocketAssemblyWorkstation : NetworkWorkstationControl
[Header("Interaction")]
[SerializeField, Min(0.1f)] private float maximumTakeDistance = 3.5f;
[SerializeField, Min(0.05f)] private float reinsertCooldownAfterTake = 0.5f;
[FormerlySerializedAs("reinsertCooldownAfterTake")]
[SerializeField, Min(0f)] private float minimumReinsertBlockDuration = 0.1f;
[SerializeField, Min(0f)] private float reinsertClearance = 0.1f;
private readonly NetworkVariable<RocketAssemblyWorkstationState> state = new(
RocketAssemblyWorkstationState.Loading,
@@ -63,7 +66,8 @@ public sealed class NetworkRocketAssemblyWorkstation : NetworkWorkstationControl
private readonly NetworkItem[] slottedItems = new NetworkItem[MaximumSlotCount];
private readonly IWorkstationItemControl[] slottedItemControls =
new IWorkstationItemControl[MaximumSlotCount];
private readonly Dictionary<ulong, double> reinsertBlockedUntil = new();
private readonly Dictionary<ulong, ReinsertBlock> reinsertBlocks = new();
private readonly List<ulong> clearedReinsertBlockIds = new();
private NetworkItem currentOutput;
@@ -170,6 +174,7 @@ public sealed class NetworkRocketAssemblyWorkstation : NetworkWorkstationControl
if (!IsServer || !IsSpawned)
return;
UpdateReinsertBlocks();
RemoveMissingSlottedItems();
if (State == RocketAssemblyWorkstationState.Assembling &&
@@ -311,8 +316,11 @@ public sealed class NetworkRocketAssemblyWorkstation : NetworkWorkstationControl
NetworkItem item = slottedItems[slotIndex];
if (item != null)
{
reinsertBlockedUntil[item.NetworkObjectId] =
GetNetworkTime() + reinsertCooldownAfterTake;
reinsertBlocks[item.NetworkObjectId] = new ReinsertBlock(
item,
slotIndex,
item.GetComponentsInChildren<Collider>(true),
GetNetworkTime() + minimumReinsertBlockDuration);
}
ClearSlot(slotIndex);
@@ -333,7 +341,8 @@ public sealed class NetworkRocketAssemblyWorkstation : NetworkWorkstationControl
Array.Clear(slottedItems, 0, slottedItems.Length);
Array.Clear(slottedItemControls, 0, slottedItemControls.Length);
reinsertBlockedUntil.Clear();
reinsertBlocks.Clear();
clearedReinsertBlockIds.Clear();
currentOutput = null;
selectedRecipeIndex.Value = FindFirstValidRecipeIndex();
assemblyStartedAt.Value = 0d;
@@ -380,9 +389,7 @@ public sealed class NetworkRocketAssemblyWorkstation : NetworkWorkstationControl
}
bool defective = assemblyWillBeDefective.Value;
ItemDefinition outputDefinition = defective
? recipe.DefectiveOutputItem
: recipe.OutputItem;
RocketItemDefinition outputDefinition = recipe.OutputItem;
if (outputDefinition == null || outputDefinition.WorldPrefab == null)
{
CancelAssemblyOnServer();
@@ -408,12 +415,14 @@ public sealed class NetworkRocketAssemblyWorkstation : NetworkWorkstationControl
outputObject.Spawn();
currentOutput = outputObject.GetComponent<NetworkItem>();
if (currentOutput == null ||
NetworkRocketItem rocketItem = outputObject.GetComponent<NetworkRocketItem>();
if (currentOutput == null || rocketItem == null ||
!WorkstationItemControlUtility.TryGetControl(currentOutput, out _))
{
Debug.LogError(
$"{name} cannot produce '{outputDefinition.DisplayName}': the output prefab " +
"must contain NetworkItem and a supported Carry or Drag interaction component.",
"must contain NetworkItem, NetworkRocketItem and a supported Carry or Drag " +
"interaction component.",
this);
outputObject.Despawn(true);
currentOutput = null;
@@ -421,6 +430,8 @@ public sealed class NetworkRocketAssemblyWorkstation : NetworkWorkstationControl
return;
}
rocketItem.TryInitializeAssemblyQuality(defective);
assemblyStartedAt.Value = 0d;
assemblyEndsAt.Value = 0d;
state.Value = RocketAssemblyWorkstationState.OutputReady;
@@ -513,17 +524,51 @@ public sealed class NetworkRocketAssemblyWorkstation : NetworkWorkstationControl
private bool IsTemporarilyBlockedFromReinsert(NetworkItem item)
{
if (item == null ||
!reinsertBlockedUntil.TryGetValue(item.NetworkObjectId, out double blockedUntil))
return item != null && reinsertBlocks.ContainsKey(item.NetworkObjectId);
}
private void UpdateReinsertBlocks()
{
if (reinsertBlocks.Count == 0)
return;
double now = GetNetworkTime();
clearedReinsertBlockIds.Clear();
foreach (KeyValuePair<ulong, ReinsertBlock> pair in reinsertBlocks)
{
return false;
ReinsertBlock block = pair.Value;
bool canReleaseBlock = block.Item == null || !block.Item.IsSpawned ||
(now >= block.MinimumBlockedUntil &&
(!IsValidSlotIndex(block.SourceSlotIndex) ||
!slotsBottomToTop[block.SourceSlotIndex].OverlapsAny(
block.ItemColliders,
reinsertClearance)));
if (canReleaseBlock)
clearedReinsertBlockIds.Add(pair.Key);
}
if (GetNetworkTime() < blockedUntil)
return true;
foreach (ulong networkObjectId in clearedReinsertBlockIds)
reinsertBlocks.Remove(networkObjectId);
}
reinsertBlockedUntil.Remove(item.NetworkObjectId);
return false;
private readonly struct ReinsertBlock
{
public ReinsertBlock(
NetworkItem item,
int sourceSlotIndex,
Collider[] itemColliders,
double minimumBlockedUntil)
{
Item = item;
SourceSlotIndex = sourceSlotIndex;
ItemColliders = itemColliders;
MinimumBlockedUntil = minimumBlockedUntil;
}
public NetworkItem Item { get; }
public int SourceSlotIndex { get; }
public Collider[] ItemColliders { get; }
public double MinimumBlockedUntil { get; }
}
private NetworkItem ResolveSynchronizedItem(int slotIndex)
@@ -588,8 +633,9 @@ public sealed class NetworkRocketAssemblyWorkstation : NetworkWorkstationControl
slotIndex >= 0 && slotIndex < SlotCount && slotsBottomToTop[slotIndex] != null;
private static bool IsReadyRocketPart(NetworkItem item) =>
item != null && item.IsSpawned && item.Definition != null &&
item.Category == ItemCategory.RocketPart && item.Definition.IsAssemblyReady;
item != null && item.IsSpawned &&
item.TryGetDefinition(out PartItemDefinition definition) &&
definition.IsReadyForAssembly;
private static bool IsRecipeValid(RocketAssemblyRecipe recipe) =>
recipe != null && recipe.IsConfigured;
@@ -8,6 +8,7 @@ public sealed class RocketAssemblyInputSlot : MonoBehaviour, IInteractable
[SerializeField] private Transform snapPoint;
private NetworkRocketAssemblyWorkstation workstation;
private Collider triggerCollider;
private int slotIndex = -1;
public Transform SnapPoint => snapPoint != null ? snapPoint : transform;
@@ -21,10 +22,17 @@ public sealed class RocketAssemblyInputSlot : MonoBehaviour, IInteractable
private void Reset()
{
snapPoint = transform;
GetComponent<Collider>().isTrigger = true;
ResolveTriggerCollider();
triggerCollider.isTrigger = true;
}
private void OnValidate() => GetComponent<Collider>().isTrigger = true;
private void Awake() => ResolveTriggerCollider();
private void OnValidate()
{
ResolveTriggerCollider();
triggerCollider.isTrigger = true;
}
public void Configure(NetworkRocketAssemblyWorkstation owner, int index)
{
@@ -51,4 +59,30 @@ public sealed class RocketAssemblyInputSlot : MonoBehaviour, IInteractable
if (item != null)
workstation.TryInsertItem(slotIndex, item);
}
public bool OverlapsAny(Collider[] itemColliders, float clearance)
{
ResolveTriggerCollider();
if (triggerCollider == null || !triggerCollider.enabled || itemColliders == null)
return false;
Bounds triggerBounds = triggerCollider.bounds;
triggerBounds.Expand(Mathf.Max(0f, clearance) * 2f);
foreach (Collider itemCollider in itemColliders)
{
if (itemCollider != null && itemCollider.enabled &&
triggerBounds.Intersects(itemCollider.bounds))
{
return true;
}
}
return false;
}
private void ResolveTriggerCollider()
{
if (triggerCollider == null)
triggerCollider = GetComponent<Collider>();
}
}
@@ -12,9 +12,8 @@ public sealed class RocketAssemblyRecipe : ScriptableObject
[SerializeField] private string recipeId;
[SerializeField] private string displayName;
[Tooltip("Exact assembly order. Element 0 is the lowest rocket part.")]
[SerializeField] private List<ItemDefinition> requiredPartsBottomToTop = new();
[SerializeField] private ItemDefinition outputItem;
[SerializeField] private ItemDefinition defectiveOutputItem;
[SerializeField] private List<PartItemDefinition> requiredPartsBottomToTop = new();
[SerializeField] private RocketItemDefinition outputItem;
[SerializeField, Min(0.05f)] private float assemblyDuration = 5f;
[Header("Blueprint")]
@@ -23,10 +22,9 @@ public sealed class RocketAssemblyRecipe : ScriptableObject
public string RecipeId => recipeId;
public string DisplayName => string.IsNullOrWhiteSpace(displayName) ? name : displayName;
public IReadOnlyList<ItemDefinition> RequiredPartsBottomToTop => requiredPartsBottomToTop;
public IReadOnlyList<PartItemDefinition> RequiredPartsBottomToTop => requiredPartsBottomToTop;
public int RequiredPartCount => requiredPartsBottomToTop.Count;
public ItemDefinition OutputItem => outputItem;
public ItemDefinition DefectiveOutputItem => defectiveOutputItem;
public RocketItemDefinition OutputItem => outputItem;
public float AssemblyDuration => assemblyDuration;
public string BlueprintTitle => string.IsNullOrWhiteSpace(blueprintTitle)
? DisplayName
@@ -36,8 +34,7 @@ public sealed class RocketAssemblyRecipe : ScriptableObject
public bool IsConfigured =>
requiredPartsBottomToTop.Count is > 0 and <= MaximumPartCount &&
requiredPartsBottomToTop.TrueForAll(IsReadyRocketPart) &&
IsSpawnable(outputItem) &&
IsSpawnable(defectiveOutputItem);
IsSpawnable(outputItem);
public bool Matches(IReadOnlyList<NetworkItem> slottedItems)
{
@@ -75,14 +72,12 @@ public sealed class RocketAssemblyRecipe : ScriptableObject
requiredPartsBottomToTop.Count - MaximumPartCount);
}
private static bool MatchesDefinition(NetworkItem item, ItemDefinition definition) =>
private static bool MatchesDefinition(NetworkItem item, PartItemDefinition definition) =>
item.Definition == definition ||
(!string.IsNullOrEmpty(definition.ItemId) && item.ItemId == definition.ItemId);
private static bool IsReadyRocketPart(ItemDefinition definition) =>
definition != null &&
definition.Category == ItemCategory.RocketPart &&
definition.IsAssemblyReady;
private static bool IsReadyRocketPart(PartItemDefinition definition) =>
definition != null && definition.IsReadyForAssembly;
private static bool IsSpawnable(ItemDefinition definition) =>
definition != null && definition.WorldPrefab != null;