using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.UI; namespace DepthsOfGreed.HandbookUI { // Prototype backend only: no dependency on enemy, tower or ammunition scripts. public sealed class TestPlacementController : MonoBehaviour { public bool IsDragging { get; private set; } public bool ConsumedCancelThisFrame => cancelFrame == Time.frameCount; public int TowerCount => Backend == null ? 0 : Backend.WallCount; public int Energy => Backend == null ? 0 : Backend.Energy; public IHandbookGameplay Backend { get; private set; } public Vector3 AreaCenter => area.position; public float AreaRadius => outerRadius; public string LastFailure { get; private set; } private string failedTypeId; public int MaximumTowers = 8; public float SuccessfulPlacementDelay = 0.5f; public bool IsWaitingForReturn => Time.unscaledTime < returnAfter; private float returnAfter; private int cancelFrame = -1; private RectTransform book; private Vector2 bookHome; [Header("Book position while building")] [Tooltip("Automatic lowering accounts for the book scale. Disable to set an explicit anchored position.")] [SerializeField] private bool automaticLoweredBookPosition = true; [Tooltip("Height of the book left visible above the bottom edge, in parent UI units. Tabs can extend above it.")] [SerializeField, Min(0)] private float visibleBookHeightWhileBuilding = 15f; [SerializeField] private Vector2 loweredBookPosition = new Vector2(0f, -675f); private CanvasGroup bookGroup; private GameObject prefab, preview, controls; private Transform container, area; private SpriteRenderer ghost, footprint; private Sprite ghostPlaceholder; private TMP_Text controlsText, feedback, resources, buildHint; private Image feedbackBox; private Camera worldCamera; private float innerRadius, outerRadius; private HandbookTowerEntry draggingEntry; private Vector3 target; private readonly List guides = new List(); private LineRenderer previewOutline, innerOutline, outerOutline; private Material lineMaterial; private float placementAngle; private GeneralFeedbackController general; public void Initialize(HandbookController owner, BuildMenuController menu, RectTransform openBook, GameObject testPrefab, MonoBehaviour gameplayBackend = null) { book = openBook; bookHome = book.anchoredPosition; bookGroup = book.GetComponent(); prefab = testPrefab; worldCamera = Camera.main; var test = GameObject.Find("PlacementTest"); if (!test || !prefab || !worldCamera) { Debug.LogError("Testplatzierung: TestTower-Prefab, PlacementTest oder Main Camera fehlt.", this); enabled = false; return; } area = test.transform.Find("BuildArea"); container = test.transform.Find("PlacedTowers") ?? test.transform.Find("PlaceTowers"); preview = test.transform.Find("PlacementPreview").gameObject; ghost = preview.transform.Find("GhostVisual").GetComponent(); ghostPlaceholder = ghost.sprite; footprint = preview.transform.Find("FootprintVisual").GetComponent(); var controlTransform = transform.Find("PlacementControlBar") ?? transform.Find("PlacementController"); if (!area || !container || !controlTransform) { Debug.LogError("Testplatzierung: Baubereich, Turmcontainer oder Kontrollleiste fehlt.", this); enabled = false; return; } controls = controlTransform.gameObject; controlsText = controls.GetComponentInChildren(true); feedback = transform.Find("FeedbackMessage").GetComponent(); var boxTransform = transform.Find("FeedbackBox") ?? transform.Find("Feedbackbox"); if (boxTransform) { feedbackBox = boxTransform.GetComponent(); feedbackBox.raycastTarget = false; feedbackBox.gameObject.SetActive(false); } resources = menu.transform.Find("RightPage/ResourcesText").GetComponent(); buildHint = menu.transform.Find("BuildHintText").GetComponent(); innerRadius = RadiusOf("BackgroundTower", 3f); outerRadius = RadiusOf("BackgroundOuterwall", 4f); if (gameplayBackend && gameplayBackend is IHandbookGameplay real) Backend = real; else { var prototype = gameObject.GetComponent(); prototype.Initialize(container, area.position, innerRadius, outerRadius, prefab); Backend = prototype; } Backend.StateChanged += RefreshResources; general = gameObject.GetComponent(); general.Initialize(book, controlsText.font); lineMaterial = new Material(Shader.Find("Sprites/Default")); previewOutline = MakeCircle("PlacementOutline", preview.transform, 11); innerOutline = MakeCircle("BuildArea_Inner", area, 8); outerOutline = MakeCircle("BuildArea_Outer", area, 8); DrawCircle(innerOutline, innerRadius); DrawCircle(outerOutline, outerRadius); ShowGuides(false); preview.SetActive(false); controls.SetActive(false); RefreshResources(); } private static float RadiusOf(string name, float fallback) { var obj = GameObject.Find(name); var renderer = obj ? obj.GetComponent() : null; return renderer ? Mathf.Min(renderer.bounds.extents.x, renderer.bounds.extents.y) : fallback; } public bool Begin(HandbookTowerEntry entry, Vector2 screenPosition) { if (!enabled || IsDragging || IsWaitingForReturn || entry == null) return false; var availability = Backend.CheckAvailability(entry); if (!availability.Allowed) { failedTypeId = entry.id; Message(availability.message); return false; } draggingEntry = entry; placementAngle = 0; LastFailure = null; ghost.sprite = entry.previewSprite ? entry.previewSprite : ghostPlaceholder; Vector2 visualSize = entry.baseShape == BaseShape.Rectangle ? entry.baseSize : Vector2.one * entry.baseRadius * 1.4142136f; ghost.transform.localScale = new Vector3(visualSize.x / ghost.sprite.bounds.size.x, visualSize.y / ghost.sprite.bounds.size.y, 1); IsDragging = true; preview.SetActive(true); footprint.transform.localScale = new Vector3(entry.placementRadius * 2f, entry.placementRadius * 2f, 1f); DrawCircle(previewOutline, entry.placementRadius); controls.SetActive(false); general.ShowControls("Loslassen: Bauen\nRechtsklick / Esc: Abbrechen" + (entry.allowPlayerRotation ? "\nR: Drehen" : "")); bookGroup.blocksRaycasts = false; ShowGuides(true); foreach (var record in Backend.BuiltObjects) { var guide = MakeCircle("ExistingTowerRadius", container, 8); guide.transform.position = record.position; DrawCircle(guide, record.type.placementRadius); Tint(guide, new Color(1f, 1f, 1f, 0.5f)); guides.Add(guide); } Move(screenPosition); return true; } public void Move(Vector2 screenPosition) { if (!IsDragging) return; var ray = worldCamera.ScreenPointToRay(screenPosition); var plane = new Plane(Vector3.forward, new Vector3(0, 0, 0)); if (!plane.Raycast(ray, out float distance)) return; target = ray.GetPoint(distance); target.z = 0; preview.transform.position = target; preview.transform.rotation = Quaternion.Euler(0, 0, placementAngle); string reason = Check(draggingEntry, target); bool outside = Backend.CheckBuild(new BuildRequest(draggingEntry, target, placementAngle)).failure == BuildFailure.OutsideRegion; Color color = outside ? Color.gray : reason == null ? Color.green : Color.red; footprint.color = new Color(color.r, color.g, color.b, outside ? 0.08f : 0.18f); ghost.color = new Color(1f, 1f, 1f, outside ? 0.3f : 0.65f); Tint(previewOutline, color); DrawCircle(previewOutline, draggingEntry.placementRadius, outside); controlsText.text = "Loslassen: Platzieren · Rechtsklick / Esc: Abbrechen"; if (reason != null && !outside) controlsText.text += "\n" + reason; } public void End(Vector2 screenPosition) { if (!IsDragging) return; Move(screenPosition); bool success = TryPlaceAt(draggingEntry, target, out string reason); if (!success) Message(reason); Finish(); if (success) { returnAfter = Time.unscaledTime + Mathf.Max(0, SuccessfulPlacementDelay); bookGroup.blocksRaycasts = false; } } public string Check(HandbookTowerEntry entry, Vector3 position) { return Backend.CheckBuild(new BuildRequest(entry, position, placementAngle)).message; } public bool TryPlaceAt(HandbookTowerEntry entry, Vector3 position, out string reason) { var result = Backend.ExecuteBuild(new BuildRequest(entry, position, placementAngle)); reason = result.message; return result.Allowed; } public void Cancel() { if (!IsDragging) return; cancelFrame = Time.frameCount; Finish(); } private void Finish() { IsDragging = false; draggingEntry = null; preview.SetActive(false); controls.SetActive(false); general.HideControls(); bookGroup.blocksRaycasts = true; ShowGuides(false); foreach (var guide in guides) Destroy(guide.gameObject); guides.Clear(); } public void ResetBookPresentation() { returnAfter = 0; if (book) book.anchoredPosition = bookHome; if (bookGroup) bookGroup.blocksRaycasts = true; ClearFeedback(); } private void LateUpdate() { if (!enabled || !book) return; if (IsDragging && Mouse.current != null) { if (Mouse.current.rightButton.wasPressedThisFrame || (Keyboard.current != null && Keyboard.current.escapeKey.wasPressedThisFrame)) Cancel(); else { if (draggingEntry.allowPlayerRotation && Keyboard.current != null && Keyboard.current.rKey.wasPressedThisFrame) placementAngle += 90; Move(Mouse.current.position.ReadValue()); } } bool lowered = IsDragging || IsWaitingForReturn; if (!lowered) bookGroup.blocksRaycasts = true; float scaledHeight = book.rect.height * Mathf.Abs(book.localScale.y); var lowerTarget = automaticLoweredBookPosition ? new Vector2(bookHome.x, -scaledHeight * (1f - book.pivot.y) + visibleBookHeightWhileBuilding) : loweredBookPosition; var position = lowered ? lowerTarget : bookHome; book.anchoredPosition = Vector2.Lerp(book.anchoredPosition, position, 1f - Mathf.Exp(-18f * Time.unscaledDeltaTime)); if (IsDragging) { bool outside = Backend.CheckBuild(new BuildRequest(draggingEntry, target, placementAngle)).failure == BuildFailure.OutsideRegion; float alpha = outside ? 0.35f + 0.2f * Mathf.Sin(Time.unscaledTime * 5f) : 0.4f; Tint(innerOutline, new Color(1f, 0.8f, 0.3f, alpha)); Tint(outerOutline, new Color(1f, 0.8f, 0.3f, alpha)); } UpdateFeedback(); } private void Message(string reason) { LastFailure = reason; if (draggingEntry) failedTypeId = draggingEntry.id; general.ShowError(reason, !IsDragging); if (!IsDragging && buildHint) buildHint.text = reason; } private void UpdateFeedback() { } private void ClearFeedback() { if (feedback) feedback.text = ""; if (feedbackBox) feedbackBox.gameObject.SetActive(false); } private void RefreshResources() { resources.text = "Energie: " + Energy + " · Gedärme: " + Backend.Entrails + "\nMauer: " + TowerCount + "/" + Backend.WallLimit; if (buildHint) { bool full = TowerCount >= Backend.WallLimit; buildHint.text = full ? "Turmlimit erreicht (" + TowerCount + "/" + Backend.WallLimit + ") · Klicken: Infos" : "Ziehen: Bauen · Klicken: Infos"; buildHint.color = full ? new Color(0.7f, 0.15f, 0.1f) : new Color(0.3f, 0.3f, 0.3f); } } public void ShowCardHint(HandbookTowerEntry entry) { var result = Backend.CheckAvailability(entry); if (buildHint) buildHint.text = !result.Allowed ? result.message : entry && entry.id == failedTypeId && LastFailure != null ? LastFailure : "Ziehen: Bauen · Klicken: Infos"; } public void ClearCardHint() { RefreshResources(); } private void ShowGuides(bool show) { innerOutline.gameObject.SetActive(show); outerOutline.gameObject.SetActive(show && draggingEntry && draggingEntry.region == BuildRegion.WallRing); } private LineRenderer MakeCircle(string name, Transform parent, int order) { var obj = new GameObject(name); obj.transform.SetParent(parent, false); var line = obj.AddComponent(); line.sharedMaterial = lineMaterial; line.useWorldSpace = false; line.widthMultiplier = 0.025f; line.sortingOrder = order; return line; } private static void Tint(LineRenderer line, Color color) { line.startColor = line.endColor = color; } private static void DrawCircle(LineRenderer line, float radius, bool dashed = false) { const int segments = 96; line.loop = true; line.positionCount = segments; for (int i = 0; i < segments; i++) { float angle = i * Mathf.PI * 2f / segments; line.SetPosition(i, new Vector3(Mathf.Cos(angle) * radius, Mathf.Sin(angle) * radius, 0)); } // Alternating width provides a procedural broken outline without an extra sprite. if (dashed) { var keys = new List(); for (int i = 0; i <= 48; i++) keys.Add(new Keyframe(i / 48f, i % 2 == 0 ? 1f : 0f)); line.widthCurve = new AnimationCurve(keys.ToArray()); } else line.widthCurve = AnimationCurve.Constant(0, 1, 1); } private void OnDestroy() { if (Backend != null) Backend.StateChanged -= RefreshResources; if (lineMaterial) Destroy(lineMaterial); } } }