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