using System; using System.Collections; using System.Linq; using UnityEngine; using UnityEngine.Events; public class GhostManagerV2 : MonoBehaviour { private static GhostManagerV2 instance; public static GhostManagerV2 getInstance() => instance; [SerializeField] private UnityEvent onClearEvent; private BlockEditContext _ctx; private bool _isDragged; private bool _isJustPlaced; // managers private RayInteractor rayInteractor; private SnapSystem snappingSystem; private Environment environment; private CommandHandler commandHandler; private SocketIterator socketIterator; private BaseInputProvider inputProvider; private void Awake() { if (instance == null) instance = this; else Destroy(gameObject); } private void Start() { rayInteractor = RayInteractor.getInstance(); snappingSystem = SnapSystem.getInstance(); environment = Environment.getInstance(); commandHandler = CommandHandler.getInstance(); socketIterator = SocketIterator.getInstance(); inputProvider = InputProviderManager.getInstance()?.getCurrentInputProvider(); environment.onBlockAddedEvent.AddListener(placed => AfterCreation(placed.gameObject)); inputProvider.OnPlace.AddListener(OnPlace); inputProvider.OnDraggingEnd.AddListener(OnPlaceWithDragging); inputProvider.OnRotate.AddListener(OnRotate); inputProvider.OnCancel.AddListener(ClearGhostData); } private void Update() { if (_ctx == null || !_ctx.IsActive) return; UpdateSurfaceTracking(); snappingSystem.UpdateSockets(_ctx.SurfacePoint, _ctx.GhostTransform); SocketPoint targetSocket = snappingSystem.GetClosestSocket(); SocketPoint mySocket = socketIterator.GetCurrentActiveSocket(); bool compatible = targetSocket != null && (mySocket == null || targetSocket.CanAccept(mySocket)); _ctx.SetFrameState( _ctx.SurfacePoint, _ctx.SurfaceNormal, mySocket, compatible ? targetSocket : null); // Build movement context and run strategy var movCtx = BuildMovementContext(); _ctx.CurrentStrategy?.UpdateMovement(ref movCtx); _ctx.SetRotationAxis(movCtx.RotationAxis); // Validate and give feedback _ctx.IsValidPlacement = _ctx.Validator?.IsValid(_ctx) ?? true; if (_ctx.IsValidPlacement) _ctx.VisualFeedback?.OnValid(_ctx); else _ctx.VisualFeedback?.OnInvalid(_ctx); } public void setDrag(bool drag) => _isDragged = drag; private void OnPlace() { if (InputModeManager.Is(InputMode.UI)) return; if (_ctx == null || !_ctx.IsActive || !_ctx.IsValidPlacement) return; if (_isJustPlaced) return; snappingSystem.Clear(); _ctx.PlacementHandler?.Commit(_ctx); _isJustPlaced = true; StartCoroutine(ResetPlaced()); ClearGhostDataInternal(false); } private void OnPlaceWithDragging() { if (_isDragged) OnPlace(); } private void OnRotate() { _ctx.RotationHandler?.performRotation(_ctx); } public void SetupBlock(BlockData blockData = null) { if (blockData == null && _ctx?.GhostObject != null) { if (!InputModeManager.Has(InputMode.ObjectDragging)) InputModeManager.Push(InputMode.ObjectDragging); _ctx.GhostObject.SetActive(true); return; } if (_ctx?.GhostObject != null) Destroy(_ctx.GhostObject); if (blockData == null) { _ctx = null; return; } if (!InputModeManager.Has(InputMode.ObjectDragging)) InputModeManager.Push(InputMode.ObjectDragging); GameObject prefab = blockData.blockPrefab; GameObject ghost = Instantiate(prefab, rayInteractor.GetHitPosition(), prefab.transform.rotation); SetLayerRecursively(ghost, LayerMask.NameToLayer("Ignore Raycast")); _ctx = BuildContext(ghost, blockData, false, null, Vector3.zero, Quaternion.identity); _ctx.PlacementHandler = new NewBlockPlacementHandler(commandHandler); _ctx.CurrentStrategy = new FreeMoveStrategy(); } public void SetupExistingBlock(GameObject sourceObject, IGhostMovementStrategy strategy = null) { if (sourceObject == null) return; if (_ctx?.GhostObject != null && !_ctx.IsEditingExisting) Destroy(_ctx.GhostObject); sourceObject.SetActive(false); GameObject ghost = sourceObject; // Disconnect all sockets so it can freely snap to new locations while moving Block block = ghost.GetComponent(); if (block != null) { SocketManagerChecker checker = FindObjectOfType(); if (checker != null) checker.DisconnectAllSockets(block); } ghost.SetActive(true); SetLayerRecursively(ghost, LayerMask.NameToLayer("Ignore Raycast")); _ctx = BuildContext( ghost, sourceObject.GetComponent()?.getBlockData(), true, sourceObject, sourceObject.transform.position, sourceObject.transform.rotation); _ctx.PlacementHandler = new ExistingBlockPlacementHandler(commandHandler); _ctx.CurrentStrategy = strategy ?? new FreeMoveStrategy(); if (!InputModeManager.Has(InputMode.ObjectDragging)) InputModeManager.Push(InputMode.ObjectDragging); } public void SetMovementStrategy(IGhostMovementStrategy strategy) { if (_ctx != null) _ctx.CurrentStrategy = strategy ?? new FreeMoveStrategy(); } public bool HasActiveGhost() => _ctx != null && _ctx.IsActive; public bool isGhost(GameObject obj) => _ctx?.GhostObject == obj; public Transform GhostTransform => _ctx?.GhostTransform; public void ClearGhost() { if (_ctx?.GhostObject != null) _ctx.GhostObject.SetActive(false); } public void ClearGhostData() { if (InputModeManager.Is(InputMode.UI)) return; ClearGhostDataInternal(true); } private void ClearGhostDataInternal(bool isCancel) { if (InputModeManager.Has(InputMode.ObjectDragging)) InputModeManager.Pop(InputMode.ObjectDragging); if (_ctx != null) { if (isCancel && _ctx.IsEditingExisting) _ctx.PlacementHandler?.Cancel(_ctx); else if (!_ctx.IsEditingExisting && _ctx.GhostObject != null) Destroy(_ctx.GhostObject); } _ctx = null; snappingSystem?.Clear(); socketIterator?.ClearActiveContainer(); onClearEvent.Invoke(); } public void onClearAddListener(UnityAction action) => onClearEvent.AddListener(action); public void setupBlock(BlockData blockData = null) => SetupBlock(blockData); public void clearGhost() => ClearGhost(); public void clearGhostData() => ClearGhostData(); /// /// Commits the current ghost unconditionally (no validity check). /// Mirrors GhostManager.ForcePlace() for editor/test tooling. /// public void ForceCommit() { if (_ctx == null || !_ctx.IsActive) return; snappingSystem.Clear(); _ctx.PlacementHandler?.Commit(_ctx); ClearGhostDataInternal(false); } public void TryCommitOrCancel() { if (_ctx == null || !_ctx.IsActive) return; if (_ctx.IsValidPlacement) { snappingSystem.Clear(); _ctx.PlacementHandler?.Commit(_ctx); ClearGhostDataInternal(false); } else { ClearGhostDataInternal(true); } } private BlockEditContext BuildContext( GameObject ghost, BlockData blockData, bool isExisting, GameObject source, Vector3 originalPos, Quaternion originalRot) { var ctx = new BlockEditContext { GhostObject = ghost, BlockData = blockData, IsEditingExisting = isExisting, SourceObject = source, OriginalPosition = originalPos, OriginalRotation = originalRot, Validator = new DefaultBlockValidator(), VisualFeedback = new DefaultVisualFeedback(), SnapBehavior = new DefaultSnapBehavior(), RotationHandler = new DefaultRotationHandler() }; GrabInteractable grab = ghost.GetComponent(); if (grab != null) { ctx.AlignToSurfaceNormal = grab.alignToSurfaceNormal; ctx.SnapRotation = grab.snapRotation; ctx.FollowSpeed = grab.followSpeed; ctx.HeightOffset = grab.heightOffset; ctx.ValidTint = grab.validTint; ctx.InvalidTint = grab.invalidTint; ctx.AttachPoint = grab.GetAttachPoint(); if (!isExisting) Destroy(grab); } else { ctx.AttachPoint = new GameObject("attachPoint").transform; ctx.AttachPoint.SetParent(ghost.transform); } ctx.Block = ghost.GetComponent(); ctx.JointBlock = ghost.GetComponent(); ctx.Container = ghost.GetComponent(); if (ctx.Container != null && ctx.Container.GetSocketCount() > 0) socketIterator.SetActiveContainer(ctx.Container); else socketIterator.ClearActiveContainer(); ctx.GhostRenderers = ghost.GetComponentsInChildren().Where(x => x.gameObject.layer != LayerMask.NameToLayer("Gizmos")).ToArray(); foreach (var r in ctx.GhostRenderers) foreach (var mat in r.materials) { if (mat.HasProperty("_Color")) { mat.color = new Color(mat.color.r, mat.color.g, mat.color.b, 0.5f); } } return ctx; } private GhostMovementContext BuildMovementContext() => new GhostMovementContext( ghostTransform: _ctx.GhostTransform, surfacePoint: _ctx.SurfacePoint, surfaceNormal: _ctx.SurfaceNormal, mySocket: _ctx.MySocket, targetSocket: _ctx.TargetSocket, attachPoint: _ctx.AttachPoint, followSpeed: _ctx.FollowSpeed, heightOffset: _ctx.HeightOffset, alignToSurfaceNormal: _ctx.AlignToSurfaceNormal, snapRotation: _ctx.SnapRotation); private void UpdateSurfaceTracking() { if (rayInteractor.GetHitInfo(out RaycastHit hitInfo)) _ctx.SetFrameState(hitInfo.point, hitInfo.normal.normalized, _ctx.MySocket, _ctx.TargetSocket); else _ctx.SetFrameState(_ctx.SurfacePoint, Vector3.up, _ctx.MySocket, _ctx.TargetSocket); } private IEnumerator ResetPlaced() { yield return new WaitForSeconds(0.1f); _isJustPlaced = false; } private void AfterCreation(GameObject blockObj) { if (blockObj == null) return; if (blockObj.GetComponent() == null && blockObj.layer != LayerMask.NameToLayer("Ignore Interaction")) blockObj.AddComponent(); SetLayerRecursively(blockObj, LayerMask.NameToLayer("Interactable")); } private void SetLayerRecursively(GameObject obj, int layer, Func Method) { if (Method(obj.transform)) obj.layer = layer; foreach (Transform child in obj.transform) SetLayerRecursively(child.gameObject, layer, Method); } private void SetLayerRecursively(GameObject obj, int layer) { SetLayerRecursively(obj, layer, x => { return x.gameObject.layer != LayerMask.NameToLayer("Gizmos"); }); } }