331 lines
15 KiB
C#
331 lines
15 KiB
C#
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 => placed.Count;
|
|
public int Energy { get; private set; } = 2000;
|
|
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;
|
|
private CanvasGroup bookGroup;
|
|
private GameObject prefab, preview, controls;
|
|
private Transform container, area;
|
|
private SpriteRenderer ghost, footprint;
|
|
private TMP_Text controlsText, feedback, resources, buildHint;
|
|
private Image feedbackBox;
|
|
private RectTransform feedbackBoxRect;
|
|
private float messageStarted;
|
|
private Color feedbackBackground;
|
|
private Camera worldCamera;
|
|
private float innerRadius, outerRadius, messageUntil;
|
|
private HandbookTowerEntry draggingEntry;
|
|
private Vector3 target;
|
|
private readonly List<PlacedRecord> placed = new List<PlacedRecord>();
|
|
private readonly List<LineRenderer> guides = new List<LineRenderer>();
|
|
private LineRenderer previewOutline, innerOutline, outerOutline;
|
|
private Material lineMaterial;
|
|
private struct PlacedRecord { public Vector3 position; public float radius; public GameObject instance; }
|
|
|
|
public void Initialize(HandbookController owner, BuildMenuController menu, RectTransform openBook, GameObject testPrefab)
|
|
{
|
|
book = openBook;
|
|
bookHome = book.anchoredPosition;
|
|
bookGroup = book.GetComponent<CanvasGroup>();
|
|
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<SpriteRenderer>();
|
|
footprint = preview.transform.Find("FootprintVisual").GetComponent<SpriteRenderer>();
|
|
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<TMP_Text>(true);
|
|
feedback = transform.Find("FeedbackMessage").GetComponent<TMP_Text>();
|
|
var boxTransform = transform.Find("FeedbackBox") ?? transform.Find("Feedbackbox");
|
|
if (boxTransform)
|
|
{
|
|
feedbackBox = boxTransform.GetComponent<Image>();
|
|
feedbackBoxRect = (RectTransform)boxTransform;
|
|
feedbackBackground = feedbackBox.color;
|
|
feedbackBox.raycastTarget = false;
|
|
feedbackBox.gameObject.SetActive(false);
|
|
}
|
|
resources = menu.transform.Find("RightPage/ResourcesText").GetComponent<TMP_Text>();
|
|
buildHint = menu.transform.Find("BuildHintText").GetComponent<TMP_Text>();
|
|
innerRadius = RadiusOf("BackgroundTower", 3f);
|
|
outerRadius = RadiusOf("BackgroundOuterwall", 4f);
|
|
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<SpriteRenderer>() : 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;
|
|
if (placed.Count >= MaximumTowers) { Message("Turmlimit erreicht"); return false; }
|
|
if (Energy < entry.energyCost) { Message("Nicht genug Energie"); return false; }
|
|
draggingEntry = entry;
|
|
IsDragging = true;
|
|
preview.SetActive(true);
|
|
footprint.transform.localScale = new Vector3(entry.placementRadius * 2f, entry.placementRadius * 2f, 1f);
|
|
DrawCircle(previewOutline, entry.placementRadius);
|
|
controls.SetActive(true);
|
|
bookGroup.blocksRaycasts = false;
|
|
ShowGuides(true);
|
|
foreach (var record in placed)
|
|
{
|
|
var guide = MakeCircle("ExistingTowerRadius", container, 8);
|
|
guide.transform.position = record.position;
|
|
DrawCircle(guide, record.radius);
|
|
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;
|
|
string reason = Check(draggingEntry, target);
|
|
bool outside = reason == "Außerhalb des Schienenbereichs";
|
|
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)
|
|
{
|
|
if (entry == null) return "Kein Turm ausgewählt";
|
|
float distance = Vector2.Distance(position, area.position);
|
|
if (distance - entry.baseRadius < innerRadius || distance + entry.baseRadius > outerRadius)
|
|
return "Außerhalb des Schienenbereichs";
|
|
foreach (var record in placed)
|
|
if (Vector2.Distance(position, record.position) < entry.placementRadius + record.radius)
|
|
return "Zu nah an einem anderen Turm";
|
|
if (placed.Count >= MaximumTowers) return "Turmlimit erreicht";
|
|
if (Energy < entry.energyCost) return "Nicht genug Energie";
|
|
return null;
|
|
}
|
|
|
|
public bool TryPlaceAt(HandbookTowerEntry entry, Vector3 position, out string reason)
|
|
{
|
|
reason = Check(entry, position);
|
|
if (reason != null) return false;
|
|
var tower = Instantiate(prefab, position, Quaternion.identity, container);
|
|
tower.name = entry.displayName + " (Test)";
|
|
placed.Add(new PlacedRecord { position = position, radius = entry.placementRadius, instance = tower });
|
|
Energy -= entry.energyCost;
|
|
RefreshResources();
|
|
return true;
|
|
}
|
|
|
|
public void Cancel()
|
|
{
|
|
if (!IsDragging) return;
|
|
cancelFrame = Time.frameCount;
|
|
Finish();
|
|
}
|
|
|
|
private void Finish()
|
|
{
|
|
IsDragging = false;
|
|
draggingEntry = null;
|
|
preview.SetActive(false);
|
|
controls.SetActive(false);
|
|
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 Move(Mouse.current.position.ReadValue());
|
|
}
|
|
bool lowered = IsDragging || IsWaitingForReturn;
|
|
if (!lowered) bookGroup.blocksRaycasts = true;
|
|
var position = lowered ? new Vector2(bookHome.x, -book.rect.height + 100f) : bookHome;
|
|
book.anchoredPosition = Vector2.Lerp(book.anchoredPosition, position,
|
|
1f - Mathf.Exp(-18f * Time.unscaledDeltaTime));
|
|
if (IsDragging)
|
|
{
|
|
bool outside = Check(draggingEntry, target) == "Außerhalb des Schienenbereichs";
|
|
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)
|
|
{
|
|
feedback.gameObject.SetActive(true);
|
|
feedback.transform.SetAsLastSibling();
|
|
feedback.color = Color.white;
|
|
feedback.text = reason;
|
|
messageStarted = Time.unscaledTime;
|
|
messageUntil = messageStarted + 2.65f;
|
|
if (feedbackBox)
|
|
{
|
|
feedbackBox.gameObject.SetActive(true);
|
|
feedbackBox.transform.SetAsLastSibling();
|
|
feedback.transform.SetAsLastSibling();
|
|
}
|
|
UpdateFeedback();
|
|
}
|
|
|
|
private void UpdateFeedback()
|
|
{
|
|
if (!feedback || messageUntil <= 0) return;
|
|
float elapsed = Time.unscaledTime - messageStarted;
|
|
if (Time.unscaledTime >= messageUntil) { ClearFeedback(); return; }
|
|
float alpha = elapsed > 2.4f ? Mathf.Clamp01((messageUntil - Time.unscaledTime) / 0.25f) : 1f;
|
|
feedback.color = new Color(1, 1, 1, alpha);
|
|
if (feedbackBox)
|
|
{
|
|
float pulse = elapsed < 0.4f ? Mathf.Pow(Mathf.Sin(elapsed / 0.4f * Mathf.PI * 2f), 2f) : 0;
|
|
Color color = Color.Lerp(feedbackBackground, new Color(0.48f, 0.34f, 0.15f, 1), pulse);
|
|
color.a = alpha;
|
|
feedbackBox.color = color;
|
|
float canvasHeight = ((RectTransform)transform).rect.height;
|
|
float y = Mathf.Min(-10f, book.anchoredPosition.y + book.rect.height - canvasHeight + 70f);
|
|
feedbackBoxRect.anchoredPosition = new Vector2(0, y);
|
|
feedback.rectTransform.anchoredPosition = new Vector2(0, y);
|
|
}
|
|
}
|
|
|
|
private void ClearFeedback()
|
|
{
|
|
messageUntil = 0;
|
|
if (feedback) { feedback.text = ""; feedback.color = Color.white; }
|
|
if (feedbackBox) feedbackBox.gameObject.SetActive(false);
|
|
}
|
|
private void RefreshResources()
|
|
{
|
|
resources.text = "Energie: " + Energy + " · Gedärme: 100 (Test)\nTürme: " + placed.Count + "/" + MaximumTowers;
|
|
if (buildHint)
|
|
{
|
|
bool full = placed.Count >= MaximumTowers;
|
|
buildHint.text = full ? "Turmlimit erreicht (" + placed.Count + "/" + MaximumTowers + ") · 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);
|
|
}
|
|
}
|
|
private void ShowGuides(bool show)
|
|
{
|
|
innerOutline.gameObject.SetActive(show);
|
|
outerOutline.gameObject.SetActive(show);
|
|
}
|
|
private LineRenderer MakeCircle(string name, Transform parent, int order)
|
|
{
|
|
var obj = new GameObject(name);
|
|
obj.transform.SetParent(parent, false);
|
|
var line = obj.AddComponent<LineRenderer>();
|
|
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<Keyframe>();
|
|
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 (lineMaterial) Destroy(lineMaterial); }
|
|
}
|
|
}
|