Adding a ton of funcitonality to the builder's movement and build queue
This commit is contained in:
parent
a63cce53e2
commit
f05734e19b
31 changed files with 3104 additions and 339 deletions
|
|
@ -7,22 +7,48 @@ using TD.Core;
|
|||
namespace TD.Gameplay
|
||||
{
|
||||
/// <summary>
|
||||
/// Owner-only client-side controller for builder input. Handles right-click-to-move,
|
||||
/// deferring to placement mode (right-click cancels placement instead).
|
||||
/// Owner-only client-side controller for builder input. Handles selection,
|
||||
/// right-click-to-move (with side-effects: pause active construction and
|
||||
/// refund tail jobs), right-click-on-paused-build-site (resume), and
|
||||
/// Escape-to-deselect.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>Owner-only.</b> This component lives on the same GameObject as
|
||||
/// <see cref="Builder"/> but only its owning client processes input. Non-owner clients
|
||||
/// have this component but its Update is a no-op. The owner sends move requests via
|
||||
/// <see cref="Builder.RequestMoveRpc"/>.</para>
|
||||
/// <see cref="Builder"/> but only its owning client processes input. Non-owner
|
||||
/// clients have this component but its Update is a no-op.</para>
|
||||
///
|
||||
/// <para><b>Right-click priority.</b> If <c>TowerPlacementController.IsPlacing</c> is
|
||||
/// true, right-click cancels placement (handled by <c>TowerPlacementController</c>
|
||||
/// itself). When NOT placing, right-click moves the builder.</para>
|
||||
/// <para><b>Selection model.</b> Left-click raycast against the
|
||||
/// <see cref="selectionLayerMask"/> (the builder's selection trigger collider
|
||||
/// sits on this layer). Hitting the local builder selects it; hitting empty
|
||||
/// space or another collider clears the selection. Selection lives in the
|
||||
/// singleton <see cref="SelectionState"/>.</para>
|
||||
///
|
||||
/// <para><b>Raycast target.</b> The cursor is raycast against the BuildablePlane layer
|
||||
/// (same as placement). The hit point's XZ is sent as the target; Y is recomputed by
|
||||
/// the server via terrain raycast.</para>
|
||||
/// <para><b>Right-click — selection required.</b> Without selection, right-click
|
||||
/// is a no-op. With selection:
|
||||
/// <list type="bullet">
|
||||
/// <item>Right-click on the local builder's PAUSED build site (raycast hits
|
||||
/// <see cref="buildSiteLayerMask"/>): resume that job. Builder walks back
|
||||
/// into range; preserved progress picks up where it left off.</item>
|
||||
/// <item>Right-click anywhere else on the buildable plane: move-and-pause.
|
||||
/// Builder walks to the clicked location. Side effects:
|
||||
/// <list type="bullet">
|
||||
/// <item>If currently Constructing → pause the active build, refund
|
||||
/// tail jobs.</item>
|
||||
/// <item>If head is Queued → refund the entire queue.</item>
|
||||
/// <item>If head is Paused → no queue change (tail was already
|
||||
/// refunded at pause time).</item>
|
||||
/// </list>
|
||||
/// </item>
|
||||
/// </list></para>
|
||||
///
|
||||
/// <para><b>Escape.</b> Clears selection. No queue cancellation in D2 — that's
|
||||
/// deferred to the HUD path.</para>
|
||||
///
|
||||
/// <para><b>Placement mode interaction.</b> If the local TowerPlacementController
|
||||
/// reports IsPlacing == true, all input handled here yields to it (placement-mode
|
||||
/// right-click cancels placement, placement-mode left-click submits). Selection
|
||||
/// raycasts are also suppressed so a placement click doesn't accidentally
|
||||
/// deselect the builder.</para>
|
||||
/// </remarks>
|
||||
public class BuilderInputController : NetworkBehaviour
|
||||
{
|
||||
|
|
@ -32,6 +58,18 @@ namespace TD.Gameplay
|
|||
"against this layer to determine the move target.")]
|
||||
[SerializeField] private LayerMask buildablePlaneLayerMask;
|
||||
|
||||
[Tooltip("Physics layer mask for the builder selection trigger collider. The " +
|
||||
"builder prefab's child selection collider sits on this layer. The mask " +
|
||||
"must NOT overlap with BuildablePlane or TerrainGeometry; selection is " +
|
||||
"a separate concern.")]
|
||||
[SerializeField] private LayerMask selectionLayerMask;
|
||||
|
||||
[Tooltip("Physics layer mask for build-site visual click targets. The " +
|
||||
"BuildSiteVisual prefab carries a small trigger collider on this layer " +
|
||||
"so right-click can identify a paused build site for resume. Must NOT " +
|
||||
"overlap with BuildablePlane, Selection, or TerrainGeometry.")]
|
||||
[SerializeField] private LayerMask buildSiteLayerMask;
|
||||
|
||||
[Tooltip("Maximum raycast distance for cursor → world conversion.")]
|
||||
[SerializeField] private float raycastMaxDistance = 500f;
|
||||
|
||||
|
|
@ -71,21 +109,113 @@ namespace TD.Gameplay
|
|||
if (!IsOwner) return;
|
||||
|
||||
var mouse = Mouse.current;
|
||||
var keyboard = Keyboard.current;
|
||||
if (mouse == null) return;
|
||||
|
||||
bool isPlacing = IsLocalPlayerPlacing();
|
||||
|
||||
// Left-click: selection. Suppressed during placement mode (left-click is
|
||||
// the placement-submit gesture there).
|
||||
if (!isPlacing && mouse.leftButton.wasPressedThisFrame)
|
||||
{
|
||||
HandleLeftClickSelection(mouse.position.ReadValue());
|
||||
}
|
||||
|
||||
// Escape: clear selection. Allowed during placement mode too — Escape never
|
||||
// means anything else here, and clearing selection during placement is fine.
|
||||
if (keyboard != null && keyboard.escapeKey.wasPressedThisFrame)
|
||||
{
|
||||
SelectionState.Instance?.Clear();
|
||||
}
|
||||
|
||||
// Right-click. Suppressed entirely during placement mode (TowerPlacementController
|
||||
// handles right-click as cancel-placement there).
|
||||
if (isPlacing) return;
|
||||
if (!mouse.rightButton.wasPressedThisFrame) return;
|
||||
|
||||
// Defer to placement mode: if the player is placing a tower, right-click cancels
|
||||
// placement rather than moving the builder. TowerPlacementController handles
|
||||
// the cancel itself; we just don't process the click here.
|
||||
if (IsLocalPlayerPlacing()) return;
|
||||
HandleRightClick(mouse.position.ReadValue());
|
||||
}
|
||||
|
||||
// Cursor → world.
|
||||
if (!TryGetBuildablePlaneHit(mouse.position.ReadValue(), out Vector3 worldPoint))
|
||||
// ----- Selection (left-click) -------------------------------------
|
||||
|
||||
private void HandleLeftClickSelection(Vector2 screenPos)
|
||||
{
|
||||
var selection = SelectionState.Instance;
|
||||
if (selection == null) return;
|
||||
|
||||
var cam = Camera.main;
|
||||
if (cam == null) return;
|
||||
|
||||
Ray ray = cam.ScreenPointToRay(new Vector3(screenPos.x, screenPos.y, 0f));
|
||||
if (Physics.Raycast(ray, out RaycastHit hit, raycastMaxDistance, selectionLayerMask))
|
||||
{
|
||||
// Walk up the hierarchy to find a Builder component (the selection
|
||||
// collider may sit on a child of the Builder's root).
|
||||
var hitBuilder = hit.collider.GetComponentInParent<Builder>();
|
||||
|
||||
// Only allow selecting OUR builder. A click on someone else's builder
|
||||
// collider clears our selection rather than selecting theirs.
|
||||
if (hitBuilder != null && hitBuilder == builder)
|
||||
{
|
||||
selection.Select(builder);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Clicked empty space or a non-selectable thing — clear selection.
|
||||
selection.Clear();
|
||||
}
|
||||
|
||||
// ----- Right-click dispatch ---------------------------------------
|
||||
|
||||
private void HandleRightClick(Vector2 screenPos)
|
||||
{
|
||||
// Right-click is gated by selection: nothing happens unless the local
|
||||
// builder is the selected one.
|
||||
var selection = SelectionState.Instance;
|
||||
if (selection == null || !selection.IsSelected(builder)) return;
|
||||
|
||||
var cam = Camera.main;
|
||||
if (cam == null) return;
|
||||
Ray ray = cam.ScreenPointToRay(new Vector3(screenPos.x, screenPos.y, 0f));
|
||||
|
||||
// Test 1: did we hit a build-site visual? If so, and it belongs to OUR
|
||||
// builder, AND it's currently shelved, this is a resume command.
|
||||
// The BuildSite raycast is checked first because the build-site visual
|
||||
// sits ABOVE the buildable plane visually; a click on the cube should
|
||||
// be interpreted as a build-site click, not a "move to under the cube" click.
|
||||
if (Physics.Raycast(ray, out RaycastHit buildSiteHit, raycastMaxDistance,
|
||||
buildSiteLayerMask))
|
||||
{
|
||||
var visual = buildSiteHit.collider.GetComponentInParent<BuildSiteVisual>();
|
||||
if (visual != null
|
||||
&& visual.IsShelved
|
||||
&& visual.OwnerClientId == NetworkManager.Singleton.LocalClientId)
|
||||
{
|
||||
// Resume command. Send the visual's NetworkObjectReference so the
|
||||
// server can identify which shelved tower the player clicked.
|
||||
// The server handles pre-empting any in-progress queue work.
|
||||
var visualNetObj = visual.GetComponent<NetworkObject>();
|
||||
if (visualNetObj != null)
|
||||
{
|
||||
builder.RequestResumeShelvedRpc(new NetworkObjectReference(visualNetObj));
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Hit a build-site visual that wasn't shelved-and-ours. Fall through
|
||||
// to the move case so the click still does something useful (move
|
||||
// the builder to that approximate world position).
|
||||
}
|
||||
|
||||
// Test 2: hit the buildable plane? Standard move-and-pause command.
|
||||
if (Physics.Raycast(ray, out RaycastHit planeHit, raycastMaxDistance,
|
||||
buildablePlaneLayerMask))
|
||||
{
|
||||
builder.RequestMoveAndPauseRpc(planeHit.point);
|
||||
return;
|
||||
}
|
||||
|
||||
// Submit to server.
|
||||
builder.RequestMoveRpc(worldPoint);
|
||||
// Hit nothing relevant — no-op.
|
||||
}
|
||||
|
||||
// ----- Helpers ----------------------------------------------------
|
||||
|
|
@ -101,21 +231,5 @@ namespace TD.Gameplay
|
|||
}
|
||||
return cachedPlacementController.IsPlacing;
|
||||
}
|
||||
|
||||
private bool TryGetBuildablePlaneHit(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, buildablePlaneLayerMask))
|
||||
{
|
||||
hitPoint = hit.point;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue