(Feat) Add Remove Building

This commit is contained in:
2026-07-09 21:38:14 +02:00
parent 24b15d23a3
commit f5de33eac2
13 changed files with 520 additions and 242 deletions
+121 -8
View File
@@ -62,6 +62,13 @@ namespace Ashwild.Building
[Tooltip("Layers that block a build (structures, props, players). Must NOT include the ground.")]
[SerializeField] private LayerMask obstructionMask;
[Header("Demolition")]
[Tooltip("Layers the demolition aim can hit to target a committed structure. Narrow this to the structures' layer so walls, roofs, etc. are the only demolishable hits.")]
[SerializeField] private LayerMask buildMask = ~0;
[Tooltip("Overlay material added on top of the aimed build while it is the demolition target (e.g. a translucent red 'about to break' pass). Should be a transparent/additive material since it draws over the model.")]
[SerializeField] private Material demolitionMaterial;
#endregion
#region State
@@ -95,10 +102,16 @@ namespace Ashwild.Building
private float yaw;
/// <summary>
/// The local player's aim transform (camera), resolved when the ghost is attached.
/// The local player's aim transform (camera), resolved lazily and shared by placement and
/// demolition.
/// </summary>
private Transform aim;
/// <summary>
/// The committed build currently under the demolition aim, or null when none is targeted.
/// </summary>
private BuiltStructure demoTarget;
/// <summary>
/// Frame the ghost was attached, so a click landing that frame never confirms instantly.
/// </summary>
@@ -141,6 +154,8 @@ namespace Ashwild.Building
PlayerEvents.AttackPressed += HandleConfirm;
PlayerEvents.SecondaryUsePressed += HandleCancel;
PlayerEvents.HotbarScroll += HandleRotate;
PlayerEvents.BuildRotatePressed += HandleRotateKey;
PlayerEvents.DemolishModeChanged += HandleDemolishModeChanged;
}
/// <summary>
@@ -152,16 +167,27 @@ namespace Ashwild.Building
PlayerEvents.AttackPressed -= HandleConfirm;
PlayerEvents.SecondaryUsePressed -= HandleCancel;
PlayerEvents.HotbarScroll -= HandleRotate;
PlayerEvents.BuildRotatePressed -= HandleRotateKey;
PlayerEvents.DemolishModeChanged -= HandleDemolishModeChanged;
EndPlacement();
ClearDemoTarget();
}
/// <summary>
/// Drives the owned ghost along the aim each frame; frozen while input is locked (e.g. the
/// menu was reopened over an active placement).
/// Each frame, in exactly one mode: in demolition mode it tracks which build the player aims
/// at; otherwise it drives the owned ghost along the aim. Both are frozen while input is locked
/// (e.g. the menu reopened over a placement), and leaving demolition mode clears the target.
/// </summary>
private void Update()
{
if (PlayerEvents.IsDemolishing && !PlayerEvents.InputLocked)
{
UpdateDemolitionTarget();
return;
}
ClearDemoTarget();
if (ghost == null) return;
if (PlayerEvents.InputLocked) return;
@@ -268,9 +294,7 @@ namespace Ashwild.Building
ghost = go;
ghostValidity = go.GetComponentInChildren<BuildGhost>(true);
ghostSnaps = go.GetComponentsInChildren<BuildSnapPoint>(true);
aim = aimCamera != null ? aimCamera.transform : (Camera.main != null ? Camera.main.transform : null);
if (aim == null)
Debug.LogError("[BuildManager] Pas de caméra d'aim (Camera.main introuvable — la caméra du joueur est-elle taguée MainCamera ?).", this);
ResolveAim();
placementBeganFrame = Time.frameCount;
UpdateGhostPose();
@@ -278,11 +302,18 @@ namespace Ashwild.Building
}
/// <summary>
/// Left-click: commits the ghost's pose to the registry (spawned for everyone) and ends the
/// placement. Blocked on an invalid spot and on the very frame the ghost was attached.
/// Left-click: demolishes the aimed build while in demolition mode, otherwise commits the
/// ghost's pose to the registry (spawned for everyone) and ends the placement. Placement is
/// blocked on an invalid spot and on the very frame the ghost was attached.
/// </summary>
private void HandleConfirm()
{
if (PlayerEvents.IsDemolishing)
{
TryDemolish();
return;
}
if (ghost == null) return;
if (PlayerEvents.InputLocked) return;
if (Time.frameCount == placementBeganFrame) return;
@@ -316,6 +347,18 @@ namespace Ashwild.Building
else if (scroll < 0f) yaw -= rotationStep;
}
/// <summary>
/// R key: yaws the ghost by one rotation step in the same direction as a scroll-up, so the
/// build can be turned without the mouse wheel. Ignored when no ghost is being placed.
/// </summary>
private void HandleRotateKey()
{
if (ghost == null) return;
if (PlayerEvents.InputLocked) return;
yaw += rotationStep;
}
/// <summary>
/// Poses the ghost each frame: quantise the aim hit to the grid with the current yaw, then
/// let a nearby matching socket pull it into an exact connection (sockets win over the grid).
@@ -410,6 +453,76 @@ namespace Ashwild.Building
}
}
/// <summary>
/// Resolves and caches the local player's aim transform — the assigned camera, else the
/// runtime Camera.main (the FPS camera). Logs once when neither is found.
/// </summary>
private Transform ResolveAim()
{
if (aim != null) return aim;
aim = aimCamera != null ? aimCamera.transform : (Camera.main != null ? Camera.main.transform : null);
if (aim == null)
Debug.LogError("[BuildManager] Pas de caméra d'aim (Camera.main introuvable — la caméra du joueur est-elle taguée MainCamera ?).", this);
return aim;
}
#endregion
#region Demolition
/// <summary>
/// Leaving demolition mode drops any current target so the crosshair returns to normal; entering
/// it needs nothing here — Update starts tracking on the next frame.
/// </summary>
private void HandleDemolishModeChanged(bool on)
{
if (!on) ClearDemoTarget();
}
/// <summary>
/// Casts the aim ray for a committed build and moves the demolition highlight when the target
/// changes: the previous build removes its overlay and the new one gets the demolition material
/// added on top, so exactly the aimed build reads as "about to break".
/// </summary>
private void UpdateDemolitionTarget()
{
Transform a = ResolveAim();
if (a == null) { ClearDemoTarget(); return; }
BuiltStructure hit = null;
if (Physics.Raycast(a.position, a.forward, out RaycastHit info, placeRange, buildMask, QueryTriggerInteraction.Ignore))
hit = info.collider.GetComponentInParent<BuiltStructure>();
if (hit == demoTarget) return;
if (demoTarget != null) demoTarget.ClearHighlight();
demoTarget = hit;
if (demoTarget != null) demoTarget.Highlight(demolitionMaterial);
}
/// <summary>
/// Left-click while demolishing: asks the registry to remove the aimed build for everyone. The
/// target stays until the next frame re-evaluates, so a miss simply retargets.
/// </summary>
private void TryDemolish()
{
if (demoTarget == null) return;
if (BuildRegistry.Instance != null)
BuildRegistry.Instance.RequestDemolish(demoTarget.gameObject);
}
/// <summary>
/// Drops the current demolition target, restoring its authored materials. Safe to call when
/// nothing is targeted.
/// </summary>
private void ClearDemoTarget()
{
if (demoTarget == null) return;
demoTarget.ClearHighlight();
demoTarget = null;
}
#endregion
}
}