draft options working; spells won't cast though
This commit is contained in:
parent
2429efbf6a
commit
7cf15dcaa6
37 changed files with 1340 additions and 0 deletions
226
Assets/_Project/Scripts/Gameplay/BuilderSpellCastController.cs
Normal file
226
Assets/_Project/Scripts/Gameplay/BuilderSpellCastController.cs
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
// Assets/_Project/Scripts/Gameplay/BuilderSpellCastController.cs
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.Rendering.Universal;
|
||||
using TD.Core;
|
||||
using TD.UI;
|
||||
using TD.Gameplay.BuilderSpells;
|
||||
|
||||
namespace TD.Gameplay
|
||||
{
|
||||
/// <summary>
|
||||
/// Per-client controller for the builder-spell casting UX. Watches
|
||||
/// <see cref="SpellHotkeys"/> to enter aim mode, raycasts against a broad ground layer
|
||||
/// (must cover the enemy path, not just buildable tiles), previews the aim point, and
|
||||
/// dispatches the cast to <see cref="PlayerSpellLoadout"/> via RPC on confirm.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>Plain MonoBehaviour.</b> Aiming visuals are purely cosmetic and local, same
|
||||
/// rationale as <see cref="TowerPlacementController"/>. All server-authoritative
|
||||
/// resolution lives in <see cref="PlayerSpellLoadout.RequestCastSpellRpc"/>.</para>
|
||||
///
|
||||
/// <para><b>Point vs. area preview.</b> <see cref="SpellTargetType.PointTarget"/> shows a
|
||||
/// small fixed-size reticle, even if the spell has an internal splash radius.
|
||||
/// <see cref="SpellTargetType.AreaOfEffect"/> scales the same decal to the spell's
|
||||
/// <see cref="BuilderSpellDefinition.Radius"/>, mirroring <see cref="BuildRangeIndicator"/>'s
|
||||
/// diameter-sizing idiom.</para>
|
||||
///
|
||||
/// <para><b>Single-shot only.</b> Unlike tower placement, there is no chained/shift-held
|
||||
/// casting — one keypress aims one cast.</para>
|
||||
/// </remarks>
|
||||
public class BuilderSpellCastController : MonoBehaviour
|
||||
{
|
||||
// ----- Inspector --------------------------------------------------
|
||||
|
||||
[Tooltip("Physics layer(s) the aim raycast hits. Must cover the whole level's ground " +
|
||||
"(including over the enemy path), not just buildable tiles — set this to " +
|
||||
"'TerrainGeometry', not 'BuildablePlane'.")]
|
||||
[SerializeField] private LayerMask groundLayerMask;
|
||||
|
||||
[Tooltip("Maximum raycast distance from the camera to the ground.")]
|
||||
[SerializeField] private float raycastMaxDistance = 500f;
|
||||
|
||||
[Tooltip("Ground decal used for both the point reticle and the area-of-effect " +
|
||||
"radius preview. Auto-found in children if empty.")]
|
||||
[SerializeField] private DecalProjector aimDecal;
|
||||
|
||||
[Tooltip("Fixed diameter of the reticle shown for PointTarget spells, regardless of " +
|
||||
"any internal splash radius the spell may use server-side.")]
|
||||
[SerializeField] private float pointReticleDiameter = 1.5f;
|
||||
|
||||
[Tooltip("Vertical thickness of the decal projector's projection volume. Should " +
|
||||
"exceed the map's vertical range so it projects onto terrain at any height.")]
|
||||
[SerializeField] private float projectionDepth = 50f;
|
||||
|
||||
// ----- Active-aim state ---------------------------------------------
|
||||
|
||||
// -1 when not aiming.
|
||||
private int activeSlot = -1;
|
||||
private BuilderSpellDefinition activeDefinition;
|
||||
|
||||
private bool lastHitValid;
|
||||
private Vector3 lastHitPoint;
|
||||
|
||||
// ----- Lifecycle ----------------------------------------------------
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (aimDecal == null) aimDecal = GetComponentInChildren<DecalProjector>();
|
||||
if (aimDecal != null)
|
||||
{
|
||||
aimDecal.transform.localRotation = Quaternion.Euler(90f, 0f, 0f);
|
||||
aimDecal.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
ExitAimMode();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!HUDController.IsTextInputActive)
|
||||
ScanHotkeys();
|
||||
|
||||
if (activeSlot < 0) return; // idle — nothing to aim
|
||||
|
||||
var keyboard = Keyboard.current;
|
||||
if (keyboard != null && keyboard.escapeKey.wasPressedThisFrame)
|
||||
{
|
||||
ExitAimMode();
|
||||
return;
|
||||
}
|
||||
|
||||
var mouse = Mouse.current;
|
||||
if (mouse == null) return;
|
||||
|
||||
if (mouse.rightButton.wasPressedThisFrame)
|
||||
{
|
||||
ExitAimMode();
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryGetGroundHit(mouse.position.ReadValue(), out Vector3 hitPoint))
|
||||
{
|
||||
lastHitValid = true;
|
||||
lastHitPoint = hitPoint;
|
||||
ShowAimDecal(hitPoint);
|
||||
}
|
||||
else
|
||||
{
|
||||
lastHitValid = false;
|
||||
HideAimDecal();
|
||||
}
|
||||
|
||||
if (mouse.leftButton.wasPressedThisFrame && lastHitValid)
|
||||
{
|
||||
TrySubmitCast();
|
||||
}
|
||||
}
|
||||
|
||||
// ----- Hotkey scan --------------------------------------------------
|
||||
|
||||
private void ScanHotkeys()
|
||||
{
|
||||
if (activeSlot >= 0) return; // already aiming — hotkeys re-scanned only when idle
|
||||
|
||||
var loadout = PlayerSpellLoadout.Local;
|
||||
if (loadout == null) return;
|
||||
|
||||
var keyboard = Keyboard.current;
|
||||
if (keyboard == null) return;
|
||||
|
||||
var layout = SpellHotkeys.Layout;
|
||||
int slotCount = Mathf.Min(loadout.SlotCount, layout.Length);
|
||||
|
||||
for (int i = 0; i < slotCount; i++)
|
||||
{
|
||||
if (!keyboard[layout[i]].wasPressedThisFrame) continue;
|
||||
if (loadout.IsSlotOnCooldown(i)) continue;
|
||||
|
||||
BeginAiming(i, loadout);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void BeginAiming(int slot, PlayerSpellLoadout loadout)
|
||||
{
|
||||
var kind = loadout.GetKind(slot);
|
||||
if (kind == null) return;
|
||||
|
||||
var definition = BuilderSpellPool.Instance?.Get(kind.Value);
|
||||
if (definition == null)
|
||||
{
|
||||
Debug.LogWarning($"[BuilderSpellCastController] No BuilderSpellDefinition for {kind.Value}.");
|
||||
return;
|
||||
}
|
||||
|
||||
activeSlot = slot;
|
||||
activeDefinition = definition;
|
||||
lastHitValid = false;
|
||||
}
|
||||
|
||||
private void ExitAimMode()
|
||||
{
|
||||
activeSlot = -1;
|
||||
activeDefinition = null;
|
||||
lastHitValid = false;
|
||||
HideAimDecal();
|
||||
}
|
||||
|
||||
// ----- Raycasting ---------------------------------------------------
|
||||
|
||||
private bool TryGetGroundHit(Vector2 screenPos, out Vector3 hitPoint)
|
||||
{
|
||||
hitPoint = Vector3.zero;
|
||||
|
||||
var cam = Camera.main;
|
||||
if (cam == null) return false;
|
||||
|
||||
Ray ray = cam.ScreenPointToRay(new Vector3(screenPos.x, screenPos.y, 0f));
|
||||
if (Physics.Raycast(ray, out RaycastHit hit, raycastMaxDistance, groundLayerMask))
|
||||
{
|
||||
hitPoint = hit.point;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ----- Aim decal -----------------------------------------------------
|
||||
|
||||
private void ShowAimDecal(Vector3 point)
|
||||
{
|
||||
if (aimDecal == null) return;
|
||||
|
||||
float diameter = activeDefinition.TargetType == SpellTargetType.AreaOfEffect
|
||||
? activeDefinition.Radius * 2f
|
||||
: pointReticleDiameter;
|
||||
|
||||
aimDecal.size = new Vector3(diameter, diameter, projectionDepth);
|
||||
aimDecal.pivot = Vector3.zero;
|
||||
aimDecal.transform.position = point;
|
||||
aimDecal.enabled = true;
|
||||
}
|
||||
|
||||
private void HideAimDecal()
|
||||
{
|
||||
if (aimDecal != null) aimDecal.enabled = false;
|
||||
}
|
||||
|
||||
// ----- Cast submission ------------------------------------------------
|
||||
|
||||
private void TrySubmitCast()
|
||||
{
|
||||
var loadout = PlayerSpellLoadout.Local;
|
||||
if (loadout == null)
|
||||
{
|
||||
ExitAimMode();
|
||||
return;
|
||||
}
|
||||
|
||||
loadout.RequestCastSpellRpc(activeSlot, lastHitPoint);
|
||||
ExitAimMode();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue