Files
DepthsOfGreed/Assets/Scripts/HandbookUI/TestPlacementController.cs
T

310 lines
16 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 => 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<LineRenderer> guides = new List<LineRenderer>();
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<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>();
ghostPlaceholder = ghost.sprite;
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>();
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);
if (gameplayBackend && gameplayBackend is IHandbookGameplay real) Backend = real;
else
{
var prototype = gameObject.GetComponent<PrototypeHandbookGameplay>();
prototype.Initialize(container, area.position, innerRadius, outerRadius, prefab); Backend = prototype;
}
Backend.StateChanged += RefreshResources;
general = gameObject.GetComponent<GeneralFeedbackController>(); 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<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;
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<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 (Backend != null) Backend.StateChanged -= RefreshResources; if (lineMaterial) Destroy(lineMaterial); }
}
}