Paint towers with some the colors of the wind
This commit is contained in:
parent
fec4433691
commit
04ead32846
15 changed files with 584 additions and 32 deletions
216
Assets/_Project/Scripts/Gameplay/TowerPaintController.cs
Normal file
216
Assets/_Project/Scripts/Gameplay/TowerPaintController.cs
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
// Assets/_Project/Scripts/Gameplay/TowerPaintController.cs
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using TD.Core;
|
||||
using TD.UI;
|
||||
|
||||
namespace TD.Gameplay
|
||||
{
|
||||
/// <summary>
|
||||
/// Per-client controller for the tower Paint UX. Mirrors
|
||||
/// <see cref="TowerPlacementController"/>: a plain (non-networked) MonoBehaviour that
|
||||
/// owns purely local, cosmetic state — the active brush color and the hardware cursor.
|
||||
/// The authoritative recolor lives on <see cref="TowerInstance"/> via
|
||||
/// <see cref="TowerInstance.RequestPaintServerRpc"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>Paint mode.</b> <see cref="BeginPaint"/> activates a brush (Red/Green/Blue
|
||||
/// or the <see cref="PaintColor.None"/> Reset brush) and swaps the OS cursor to a
|
||||
/// tinted circle. While active, a left-click that hits a <see cref="TowerInstance"/>
|
||||
/// the local player owns sends a paint request; the player stays in paint mode so
|
||||
/// several towers can be painted in a row. Right-click or Escape exits.</para>
|
||||
///
|
||||
/// <para><b>Active vs. color.</b> <see cref="IsPainting"/> tracks whether a brush is
|
||||
/// active independently of <see cref="CurrentColor"/>, because <c>None</c> is a valid
|
||||
/// active brush (Reset) — not the "off" state.</para>
|
||||
///
|
||||
/// <para><b>Interaction with selection.</b> <see cref="BuilderInputController"/> checks
|
||||
/// <see cref="IsPainting"/> and suppresses its own left-/right-click handling while a
|
||||
/// brush is active, so painting never also moves or deselects the builder.</para>
|
||||
/// </remarks>
|
||||
public class TowerPaintController : MonoBehaviour
|
||||
{
|
||||
// ----- Inspector --------------------------------------------------
|
||||
|
||||
[Tooltip("Maximum raycast distance from the camera into the world. Should be at " +
|
||||
"least as large as the camera's far clip plane.")]
|
||||
[SerializeField] private float raycastMaxDistance = 500f;
|
||||
|
||||
[Tooltip("Diameter in pixels of the generated paint cursor. Larger than a dot so " +
|
||||
"it reads clearly as a paint brush.")]
|
||||
[SerializeField] private int cursorSize = 28;
|
||||
|
||||
// ----- State ------------------------------------------------------
|
||||
|
||||
private PaintColor currentColor = PaintColor.None;
|
||||
|
||||
/// <summary>True while a paint brush is active. Independent of the brush color
|
||||
/// (the Reset / <see cref="PaintColor.None"/> brush is still an active brush).</summary>
|
||||
public bool IsPainting { get; private set; }
|
||||
|
||||
/// <summary>The active brush color. Only meaningful while <see cref="IsPainting"/>.</summary>
|
||||
public PaintColor CurrentColor => currentColor;
|
||||
|
||||
// Generated cursor textures, cached per color so we don't rebuild every activation.
|
||||
private readonly Dictionary<PaintColor, Texture2D> cursorTextures =
|
||||
new Dictionary<PaintColor, Texture2D>();
|
||||
|
||||
// ----- Lifecycle --------------------------------------------------
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
// Restore the OS cursor if this is torn down mid-paint.
|
||||
CancelPaint();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!IsPainting) return;
|
||||
|
||||
var mouse = Mouse.current;
|
||||
if (mouse == null) return;
|
||||
var keyboard = Keyboard.current;
|
||||
|
||||
// Right-click or Escape exits paint mode. (Escape is ignored while a HUD text
|
||||
// field has focus so it means "cancel typing" there, matching other systems.)
|
||||
bool escape = keyboard != null
|
||||
&& keyboard.escapeKey.wasPressedThisFrame
|
||||
&& !HUDController.IsTextInputActive;
|
||||
if (mouse.rightButton.wasPressedThisFrame || escape)
|
||||
{
|
||||
CancelPaint();
|
||||
return;
|
||||
}
|
||||
|
||||
// Left-click paints the tower under the cursor (if the local player owns it).
|
||||
if (!mouse.leftButton.wasPressedThisFrame) return;
|
||||
|
||||
Vector2 mousePos = mouse.position.ReadValue();
|
||||
if (HUDController.IsPointerOverInteractiveHud(mousePos)) return; // clicked the HUD
|
||||
|
||||
TryPaintAtCursor(mousePos);
|
||||
}
|
||||
|
||||
// ----- Public API -------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Activates the given paint brush and swaps the cursor. Calling this with the
|
||||
/// already-active brush toggles paint mode off (so a second press of the same
|
||||
/// hotkey cancels). Call from HUD paint swatches.
|
||||
/// </summary>
|
||||
public void BeginPaint(PaintColor color)
|
||||
{
|
||||
// Toggle off if the same brush is re-selected.
|
||||
if (IsPainting && color == currentColor)
|
||||
{
|
||||
CancelPaint();
|
||||
return;
|
||||
}
|
||||
|
||||
currentColor = color;
|
||||
IsPainting = true;
|
||||
|
||||
var tex = GetCursorTexture(color);
|
||||
Vector2 hotspot = new Vector2(tex.width * 0.5f, tex.height * 0.5f);
|
||||
Cursor.SetCursor(tex, hotspot, CursorMode.Auto);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exits paint mode and restores the default OS cursor. Safe to call when idle.
|
||||
/// </summary>
|
||||
public void CancelPaint()
|
||||
{
|
||||
if (!IsPainting) return;
|
||||
IsPainting = false;
|
||||
Cursor.SetCursor(null, Vector2.zero, CursorMode.Auto);
|
||||
}
|
||||
|
||||
// ----- Painting ---------------------------------------------------
|
||||
|
||||
private void TryPaintAtCursor(Vector2 screenPos)
|
||||
{
|
||||
var cam = Camera.main;
|
||||
if (cam == null) return;
|
||||
|
||||
Ray ray = cam.ScreenPointToRay(new Vector3(screenPos.x, screenPos.y, 0f));
|
||||
|
||||
// A tower is identified by its TowerInstance component, not by a physics
|
||||
// layer — its selection/body colliders may live on different layers. Raycast
|
||||
// all layers (including triggers, since selection volumes are triggers) and
|
||||
// keep the nearest hit that resolves to a TowerInstance; non-tower hits
|
||||
// (terrain, buildable plane, the builder) are simply ignored.
|
||||
var hits = Physics.RaycastAll(ray, raycastMaxDistance, ~0,
|
||||
QueryTriggerInteraction.Collide);
|
||||
|
||||
TowerInstance tower = null;
|
||||
float nearest = float.MaxValue;
|
||||
foreach (var h in hits)
|
||||
{
|
||||
var t = h.collider.GetComponentInParent<TowerInstance>();
|
||||
if (t != null && h.distance < nearest)
|
||||
{
|
||||
nearest = h.distance;
|
||||
tower = t;
|
||||
}
|
||||
}
|
||||
|
||||
if (tower == null) return; // clicked empty space / non-tower — no-op
|
||||
|
||||
// Own-towers-only: skip non-owned towers locally (the server re-validates too).
|
||||
if (tower.Owner != GetLocalPlayerSlot()) return;
|
||||
|
||||
tower.RequestPaintServerRpc(currentColor);
|
||||
}
|
||||
|
||||
private static PlayerSlot GetLocalPlayerSlot()
|
||||
=> PlayerMatchState.Local?.Slot ?? PlayerSlot.None;
|
||||
|
||||
// ----- Cursor texture ---------------------------------------------
|
||||
|
||||
// Builds (and caches) a filled-circle cursor texture tinted to the paint color,
|
||||
// with a darker rim so it reads against any background. The None/Reset brush is a
|
||||
// neutral gray circle. Pixels outside the circle are transparent.
|
||||
private Texture2D GetCursorTexture(PaintColor color)
|
||||
{
|
||||
if (cursorTextures.TryGetValue(color, out var cached) && cached != null)
|
||||
return cached;
|
||||
|
||||
int size = Mathf.Max(8, cursorSize);
|
||||
var tex = new Texture2D(size, size, TextureFormat.RGBA32, false);
|
||||
|
||||
Color fill = PaintColors.Get(color);
|
||||
Color rim = fill * 0.45f;
|
||||
rim.a = 1f;
|
||||
|
||||
float center = (size - 1) * 0.5f;
|
||||
float outer = center; // outer edge of the disc
|
||||
float rimStart = outer - 2f; // 2px rim
|
||||
|
||||
var pixels = new Color[size * size];
|
||||
for (int y = 0; y < size; y++)
|
||||
{
|
||||
for (int x = 0; x < size; x++)
|
||||
{
|
||||
float dx = x - center;
|
||||
float dy = y - center;
|
||||
float dist = Mathf.Sqrt(dx * dx + dy * dy);
|
||||
|
||||
Color c;
|
||||
if (dist > outer) c = new Color(0f, 0f, 0f, 0f); // transparent outside
|
||||
else if (dist >= rimStart) c = rim; // dark rim
|
||||
else c = fill; // solid center
|
||||
|
||||
pixels[y * size + x] = c;
|
||||
}
|
||||
}
|
||||
|
||||
tex.SetPixels(pixels);
|
||||
tex.Apply();
|
||||
tex.filterMode = FilterMode.Bilinear;
|
||||
|
||||
cursorTextures[color] = tex;
|
||||
return tex;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue