using Unity.Netcode; using Unity.Netcode.Components; using UnityEditor; using UnityEngine; public static class MapRadarNetworkSetup { private const string LeverPrefabPath = "Assets/CONTENT/Game/Prefabs/Interaction/PhysicalLever.prefab"; private const string EnemyBalancePath = "Assets/CONTENT/FEATURES/MapRadar/Configs/MapEnemyBalance.asset"; [MenuItem("FORT-BO/Map Radar/Configure Network Prototype")] public static void ConfigureNetworkPrototype() { MapController mapController = Object.FindFirstObjectByType(); if (mapController == null) { Debug.LogError("MapController was not found in the open scene."); return; } MapCoordinateSystem coordinateSystem = mapController.CoordinateSystem; if (coordinateSystem == null || !coordinateSystem.HasValidCorners()) { Debug.LogError("MapCoordinateSystem is not configured.", mapController); return; } Undo.RegisterFullObjectHierarchyUndo(mapController.transform.root.gameObject, "Configure network map prototype"); GetOrAdd(mapController.gameObject); NetworkBaseHealth baseHealth = GetOrAdd(mapController.gameObject); MapControlBinder binder = GetOrAdd(mapController.gameObject); MapEnemyBalanceConfig enemyBalance = GetOrCreateEnemyBalance(); Transform controlsRoot = GetOrCreateChild( mapController.transform.root, "Map Controls"); MapBasis basis = CreateBasis(coordinateSystem); NetworkPhysicalWheelInteractable horizontalWheel = CreateWheel( controlsRoot, "Map Horizontal Wheel", basis.ControlCenter - basis.Horizontal * 1.8f, basis.Rotation, "Aim horizontal"); NetworkPhysicalWheelInteractable verticalWheel = CreateWheel( controlsRoot, "Map Vertical Wheel", basis.ControlCenter - basis.Horizontal * 0.6f, basis.Rotation, "Aim vertical"); NetworkPhysicalLeverInteractable fireLever = CreateFireLever( controlsRoot, basis.ControlCenter + basis.Horizontal * 0.8f, basis.Rotation); NetworkButtonInteractable scanButton = CreateScanButton( controlsRoot, basis.ControlCenter + basis.Horizontal * 2f, basis.Rotation); SerializedObject map = new(mapController); SetObject(map, "roundGameLoop", Object.FindFirstObjectByType()); SetObject(map, "baseHealth", baseHealth); SetObject(map, "enemyBalance", enemyBalance); map.ApplyModifiedPropertiesWithoutUndo(); SerializedObject binding = new(binder); SetObject(binding, "mapController", mapController); SetObject(binding, "horizontalWheel", horizontalWheel); SetObject(binding, "verticalWheel", verticalWheel); SetObject(binding, "fireLever", fireLever); SetObject(binding, "scanButton", scanButton); binding.ApplyModifiedPropertiesWithoutUndo(); EditorUtility.SetDirty(mapController); EditorUtility.SetDirty(binder); EditorUtility.SetDirty(baseHealth); UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty( mapController.gameObject.scene); UnityEditor.SceneManagement.EditorSceneManager.SaveOpenScenes(); Selection.activeObject = controlsRoot.gameObject; Debug.Log( "[Map] Network prototype configured: X/Y wheels, fire lever, " + "scan button, enemy balance and base health are connected.", mapController); } private static MapEnemyBalanceConfig GetOrCreateEnemyBalance() { MapEnemyBalanceConfig config = AssetDatabase.LoadAssetAtPath(EnemyBalancePath); if (config != null) return config; config = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(config, EnemyBalancePath); EditorUtility.SetDirty(config); AssetDatabase.SaveAssets(); return config; } private static NetworkPhysicalWheelInteractable CreateWheel( Transform parent, string name, Vector3 position, Quaternion rotation, string interactionLabel) { Transform root = GetOrCreateChild(parent, name); root.SetPositionAndRotation(position, rotation); root.localScale = Vector3.one; GetOrAdd(root.gameObject); NetworkTransform networkTransform = GetOrAdd(root.gameObject); networkTransform.AuthorityMode = NetworkTransform.AuthorityModes.Server; networkTransform.UseUnreliableDeltas = true; Transform legacyVisual = root.Find("Wheel Visual"); Transform visual = root.Find("Wheel Pivot"); if (visual == null) { if (legacyVisual != null) Object.DestroyImmediate(legacyVisual.gameObject); visual = new GameObject("Wheel Pivot").transform; visual.SetParent(root, false); } visual.localPosition = Vector3.zero; visual.localRotation = Quaternion.identity; visual.localScale = Vector3.one; Transform disk = visual.Find("Wheel Disk"); if (disk == null) { GameObject cylinder = GameObject.CreatePrimitive(PrimitiveType.Cylinder); cylinder.name = "Wheel Disk"; disk = cylinder.transform; disk.SetParent(visual, false); } disk.localPosition = Vector3.zero; disk.localRotation = Quaternion.Euler(90f, 0f, 0f); disk.localScale = new Vector3(0.48f, 0.08f, 0.48f); Transform handle = visual.Find("Handle"); if (handle == null) { GameObject handleObject = GameObject.CreatePrimitive(PrimitiveType.Cube); handleObject.name = "Handle"; handle = handleObject.transform; handle.SetParent(visual, false); Object.DestroyImmediate(handleObject.GetComponent()); } handle.localPosition = new Vector3(0f, 0.65f, 0f); handle.localRotation = Quaternion.identity; handle.localScale = new Vector3(0.14f, 0.4f, 0.14f); NetworkPhysicalWheelInteractable wheel = GetOrAdd(root.gameObject); SerializedObject serializedWheel = new(wheel); SetObject(serializedWheel, "wheelVisual", visual); SetVector3(serializedWheel, "neutralLocalEulerAngles", Vector3.zero); SetString(serializedWheel, "interactionLabel", interactionLabel); SetVector3(serializedWheel, "localRotationAxis", Vector3.forward); SetVector3(serializedWheel, "localReferenceDirection", Vector3.up); SetFloat(serializedWheel, "minimumAngle", -180f); SetFloat(serializedWheel, "maximumAngle", 180f); SetFloat(serializedWheel, "initialNormalizedValue", 0.5f); SetFloat(serializedWheel, "maximumInteractionDistance", 3.5f); SetFloat(serializedWheel, "maximumControlDistance", 4f); serializedWheel.ApplyModifiedPropertiesWithoutUndo(); return wheel; } private static NetworkPhysicalLeverInteractable CreateFireLever( Transform parent, Vector3 position, Quaternion rotation) { Transform existing = parent.Find("Map Fire Lever"); GameObject root; if (existing != null) { root = existing.gameObject; } else { GameObject prefab = AssetDatabase.LoadAssetAtPath(LeverPrefabPath); if (prefab == null) throw new MissingReferenceException($"Lever prefab not found: {LeverPrefabPath}"); root = (GameObject)PrefabUtility.InstantiatePrefab(prefab, parent); root.name = "Map Fire Lever"; } root.transform.SetPositionAndRotation(position, rotation); root.transform.localScale = Vector3.one * 0.55f; NetworkPhysicalLeverInteractable lever = root.GetComponent(); SerializedObject serializedLever = new(lever); SetString(serializedLever, "interactionLabel", "Fire"); SetBool(serializedLever, "initialPositionIsMaximum", false); SetBool(serializedLever, "returnToInitialPositionAfterSwitch", true); SetFloat(serializedLever, "automaticReturnDelay", 0.2f); SetFloat(serializedLever, "maximumInteractionDistance", 3.5f); SetFloat(serializedLever, "maximumControlDistance", 4f); serializedLever.ApplyModifiedPropertiesWithoutUndo(); return lever; } private static NetworkButtonInteractable CreateScanButton( Transform parent, Vector3 position, Quaternion rotation) { Transform root = GetOrCreateChild(parent, "Map Scan Button"); root.SetPositionAndRotation(position, rotation); root.localScale = Vector3.one; GetOrAdd(root.gameObject); NetworkTransform networkTransform = GetOrAdd(root.gameObject); networkTransform.AuthorityMode = NetworkTransform.AuthorityModes.Server; Transform visual = root.Find("Button Visual"); if (visual == null) { GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube); cube.name = "Button Visual"; visual = cube.transform; visual.SetParent(root, false); } visual.localPosition = Vector3.zero; visual.localRotation = Quaternion.identity; visual.localScale = new Vector3(0.55f, 0.55f, 0.18f); NetworkButtonInteractable button = GetOrAdd(root.gameObject); SerializedObject serializedButton = new(button); SetString(serializedButton, "interactionLabel", "Scan map"); SetFloat(serializedButton, "maximumInteractionDistance", 3.5f); serializedButton.ApplyModifiedPropertiesWithoutUndo(); return button; } private static MapBasis CreateBasis(MapCoordinateSystem coordinates) { Vector3 bottomLeft = coordinates.BottomLeft.position; Vector3 horizontalEdge = coordinates.BottomRight.position - bottomLeft; Vector3 verticalEdge = coordinates.TopLeft.position - bottomLeft; Vector3 horizontal = horizontalEdge.normalized; Vector3 vertical = verticalEdge.normalized; Vector3 normal = Vector3.Cross(horizontal, vertical).normalized; Vector3 bottomCenter = bottomLeft + horizontalEdge * 0.5f; Vector3 controlCenter = bottomCenter - vertical * 0.85f + normal * 0.55f; return new MapBasis( horizontal, controlCenter, Quaternion.LookRotation(normal, vertical)); } private static Transform GetOrCreateChild(Transform parent, string name) { Transform existing = parent.Find(name); if (existing != null) return existing; GameObject child = new(name); child.transform.SetParent(parent, false); Undo.RegisterCreatedObjectUndo(child, $"Create {name}"); return child.transform; } private static T GetOrAdd(GameObject target) where T : Component { T component = target.GetComponent(); return component != null ? component : Undo.AddComponent(target); } private static void SetObject(SerializedObject target, string property, Object value) => target.FindProperty(property).objectReferenceValue = value; private static void SetString(SerializedObject target, string property, string value) => target.FindProperty(property).stringValue = value; private static void SetFloat(SerializedObject target, string property, float value) => target.FindProperty(property).floatValue = value; private static void SetBool(SerializedObject target, string property, bool value) => target.FindProperty(property).boolValue = value; private static void SetVector3(SerializedObject target, string property, Vector3 value) => target.FindProperty(property).vector3Value = value; private readonly struct MapBasis { public readonly Vector3 Horizontal; public readonly Vector3 ControlCenter; public readonly Quaternion Rotation; public MapBasis(Vector3 horizontal, Vector3 controlCenter, Quaternion rotation) { Horizontal = horizontal; ControlCenter = controlCenter; Rotation = rotation; } } }