diff --git a/Assets/GAME/InputSystem_Actions.inputactions b/Assets/GAME/InputSystem_Actions.inputactions index 606f9041..26af22f6 100644 --- a/Assets/GAME/InputSystem_Actions.inputactions +++ b/Assets/GAME/InputSystem_Actions.inputactions @@ -194,6 +194,15 @@ "processors": "", "interactions": "", "initialStateCheck": false + }, + { + "name": "BuildRotate", + "type": "Button", + "id": "c1d2e3f4-0004-4004-8004-000000000004", + "expectedControlType": "Button", + "processors": "", + "interactions": "", + "initialStateCheck": false } ], "bindings": [ @@ -450,6 +459,17 @@ "isComposite": false, "isPartOfComposite": false }, + { + "name": "", + "id": "c1d2e3f4-0005-4005-8005-000000000005", + "path": "/r", + "interactions": "", + "processors": "", + "groups": ";Keyboard&Mouse", + "action": "BuildRotate", + "isComposite": false, + "isPartOfComposite": false + }, { "name": "", "id": "b1b01001-0001-0001-0001-000000000001", diff --git a/Assets/GAME/Materials/New Material.mat b/Assets/GAME/Materials/New Material.mat deleted file mode 100644 index 81892765..00000000 --- a/Assets/GAME/Materials/New Material.mat +++ /dev/null @@ -1,204 +0,0 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!21 &2100000 -Material: - serializedVersion: 8 - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_Name: New Material - m_Shader: {fileID: 4800000, guid: 69c1f799e772cb6438f56c23efccb782, type: 3} - m_Parent: {fileID: 0} - m_ModifiedSerializedProperties: 0 - m_ValidKeywords: [] - m_InvalidKeywords: [] - m_LightmapFlags: 4 - m_EnableInstancingVariants: 0 - m_DoubleSidedGI: 0 - m_CustomRenderQueue: -1 - stringTagMap: {} - disabledShaderPasses: - - MOTIONVECTORS - m_LockedProperties: - m_SavedProperties: - serializedVersion: 3 - m_TexEnvs: - - _BaseMap: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _BumpMap: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _Control: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _DetailAlbedoMap: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _DetailMask: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _DetailNormalMap: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _EmissionMap: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _MainTex: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _Mask0: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _Mask1: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _Mask2: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _Mask3: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _MetallicGlossMap: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _Normal0: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _Normal1: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _Normal2: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _Normal3: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _OcclusionMap: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _ParallaxMap: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _SpecGlossMap: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _Splat0: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _Splat1: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _Splat2: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _Splat3: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - _TerrainHolesTexture: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - unity_Lightmaps: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - unity_LightmapsInd: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - - unity_ShadowMasks: - m_Texture: {fileID: 0} - m_Scale: {x: 1, y: 1} - m_Offset: {x: 0, y: 0} - m_Ints: [] - m_Floats: - - _AddPrecomputedVelocity: 0 - - _AlphaClip: 0 - - _AlphaToMask: 0 - - _Blend: 0 - - _BlendModePreserveSpecular: 1 - - _BumpScale: 1 - - _ClearCoatMask: 0 - - _ClearCoatSmoothness: 0 - - _Cull: 2 - - _Cutoff: 0.5 - - _DetailAlbedoMapScale: 1 - - _DetailNormalMapScale: 1 - - _DstBlend: 0 - - _DstBlendAlpha: 0 - - _EnableHeightBlend: 0 - - _EnableInstancedPerPixelNormal: 1 - - _EnvironmentReflections: 1 - - _GlossMapScale: 0 - - _Glossiness: 0 - - _GlossyReflections: 0 - - _HeightTransition: 0 - - _Metallic: 0 - - _Metallic0: 0 - - _Metallic1: 0 - - _Metallic2: 0 - - _Metallic3: 0 - - _NumLayersCount: 1 - - _OcclusionStrength: 1 - - _Parallax: 0.005 - - _QueueOffset: 0 - - _ReceiveShadows: 1 - - _Smoothness: 0.5 - - _Smoothness0: 0.5 - - _Smoothness1: 0.5 - - _Smoothness2: 0.5 - - _Smoothness3: 0.5 - - _SmoothnessTextureChannel: 0 - - _SpecularHighlights: 1 - - _SrcBlend: 1 - - _SrcBlendAlpha: 1 - - _Surface: 0 - - _WorkflowMode: 1 - - _XRMotionVectorsPass: 1 - - _ZWrite: 1 - m_Colors: - - _BaseColor: {r: 1, g: 1, b: 1, a: 1} - - _Color: {r: 1, g: 1, b: 1, a: 1} - - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} - - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} - m_BuildTextureStacks: [] - m_AllowLocking: 1 ---- !u!114 &200115796970648388 -MonoBehaviour: - m_ObjectHideFlags: 11 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3} - m_Name: - m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion - version: 10 diff --git a/Assets/GAME/Materials/New Material.mat.meta b/Assets/GAME/Materials/New Material.mat.meta deleted file mode 100644 index a213359b..00000000 --- a/Assets/GAME/Materials/New Material.mat.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: d804f7d018e008f4db0ea774eeaadda6 -NativeFormatImporter: - externalObjects: {} - mainObjectFileID: 2100000 - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/GAME/Scenes/Dev Scene/Dev Scene.unity b/Assets/GAME/Scenes/Dev Scene/Dev Scene.unity index 8139e1eb..cc67d974 100644 --- a/Assets/GAME/Scenes/Dev Scene/Dev Scene.unity +++ b/Assets/GAME/Scenes/Dev Scene/Dev Scene.unity @@ -2517,6 +2517,10 @@ MonoBehaviour: obstructionMask: serializedVersion: 2 m_Bits: 512 + buildMask: + serializedVersion: 2 + m_Bits: 512 + demolitionMaterial: {fileID: 2100000, guid: 0a594aa41ef0d7e428398c2c32772c64, type: 2} --- !u!1001 &298545449 PrefabInstance: m_ObjectHideFlags: 0 @@ -5514,6 +5518,100 @@ Transform: m_CorrespondingSourceObject: {fileID: 8499178216258465, guid: e9894ffe485f7d5448260fd9338ebd0e, type: 3} m_PrefabInstance: {fileID: 663482573} m_PrefabAsset: {fileID: 0} +--- !u!1001 &668733573 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 0} + m_Modifications: + - target: {fileID: 781270504346949712, guid: 8b0c536290aeb0b4caafd67bc736e9d2, type: 3} + propertyPath: m_Name + value: Stations_5 + objectReference: {fileID: 0} + - target: {fileID: 781270504346949712, guid: 8b0c536290aeb0b4caafd67bc736e9d2, type: 3} + propertyPath: m_Layer + value: 6 + objectReference: {fileID: 0} + - target: {fileID: 985001107293900664, guid: 8b0c536290aeb0b4caafd67bc736e9d2, type: 3} + propertyPath: 'm_Materials.Array.data[0]' + value: + objectReference: {fileID: 1537286330038668216, guid: 73f5d288fb608fb4e9959cf5e9655783, type: 3} + - target: {fileID: 8601272150287418368, guid: 8b0c536290aeb0b4caafd67bc736e9d2, type: 3} + propertyPath: m_LocalPosition.x + value: 49.20204 + objectReference: {fileID: 0} + - target: {fileID: 8601272150287418368, guid: 8b0c536290aeb0b4caafd67bc736e9d2, type: 3} + propertyPath: m_LocalPosition.y + value: -0.005176544 + objectReference: {fileID: 0} + - target: {fileID: 8601272150287418368, guid: 8b0c536290aeb0b4caafd67bc736e9d2, type: 3} + propertyPath: m_LocalPosition.z + value: 55.359283 + objectReference: {fileID: 0} + - target: {fileID: 8601272150287418368, guid: 8b0c536290aeb0b4caafd67bc736e9d2, type: 3} + propertyPath: m_LocalRotation.w + value: 0.70710784 + objectReference: {fileID: 0} + - target: {fileID: 8601272150287418368, guid: 8b0c536290aeb0b4caafd67bc736e9d2, type: 3} + propertyPath: m_LocalRotation.x + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 8601272150287418368, guid: 8b0c536290aeb0b4caafd67bc736e9d2, type: 3} + propertyPath: m_LocalRotation.y + value: -0.70710576 + objectReference: {fileID: 0} + - target: {fileID: 8601272150287418368, guid: 8b0c536290aeb0b4caafd67bc736e9d2, type: 3} + propertyPath: m_LocalRotation.z + value: -0 + objectReference: {fileID: 0} + - target: {fileID: 8601272150287418368, guid: 8b0c536290aeb0b4caafd67bc736e9d2, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 8601272150287418368, guid: 8b0c536290aeb0b4caafd67bc736e9d2, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: -90 + objectReference: {fileID: 0} + - target: {fileID: 8601272150287418368, guid: 8b0c536290aeb0b4caafd67bc736e9d2, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: + - targetCorrespondingSourceObject: {fileID: 781270504346949712, guid: 8b0c536290aeb0b4caafd67bc736e9d2, type: 3} + insertIndex: -1 + addedObject: {fileID: 668733575} + m_SourcePrefab: {fileID: 100100000, guid: 8b0c536290aeb0b4caafd67bc736e9d2, type: 3} +--- !u!1 &668733574 stripped +GameObject: + m_CorrespondingSourceObject: {fileID: 781270504346949712, guid: 8b0c536290aeb0b4caafd67bc736e9d2, type: 3} + m_PrefabInstance: {fileID: 668733573} + m_PrefabAsset: {fileID: 0} +--- !u!65 &668733575 +BoxCollider: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 668733574} + m_Material: {fileID: 0} + m_IncludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_ExcludeLayers: + serializedVersion: 2 + m_Bits: 0 + m_LayerOverridePriority: 0 + m_IsTrigger: 0 + m_ProvidesContacts: 0 + m_Enabled: 1 + serializedVersion: 3 + m_Size: {x: 2.7156103, y: 1.8032616, z: 1.15291} + m_Center: {x: 0.3753096, y: 0.90680677, z: 0.050604135} --- !u!1 &713085368 GameObject: m_ObjectHideFlags: 0 @@ -6309,6 +6407,8 @@ MonoBehaviour: _prefab: Assets/GAME/Prefabs/Structure/Cookingpot_Campfire.prefab - _gameObject: {fileID: 245990655} _prefab: Assets/External/VFXPACK_FIRE_WALLCOEUR/Prefab/VFX_Fire.prefab + - _gameObject: {fileID: 668733574} + _prefab: Assets/External/Static Soul Studio/Crafting Statinos/Built-in/Prefabs/Stations_5.prefab --- !u!4 &740572252 Transform: m_ObjectHideFlags: 11 @@ -29616,3 +29716,4 @@ SceneRoots: - {fileID: 1836939580} - {fileID: 1902801754} - {fileID: 1957565059} + - {fileID: 668733573} diff --git a/Assets/GAME/Script/Building/BuildManager.cs b/Assets/GAME/Script/Building/BuildManager.cs index 532ae841..563fd6a8 100644 --- a/Assets/GAME/Script/Building/BuildManager.cs +++ b/Assets/GAME/Script/Building/BuildManager.cs @@ -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; /// - /// 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. /// private Transform aim; + /// + /// The committed build currently under the demolition aim, or null when none is targeted. + /// + private BuiltStructure demoTarget; + /// /// Frame the ghost was attached, so a click landing that frame never confirms instantly. /// @@ -141,6 +154,8 @@ namespace Ashwild.Building PlayerEvents.AttackPressed += HandleConfirm; PlayerEvents.SecondaryUsePressed += HandleCancel; PlayerEvents.HotbarScroll += HandleRotate; + PlayerEvents.BuildRotatePressed += HandleRotateKey; + PlayerEvents.DemolishModeChanged += HandleDemolishModeChanged; } /// @@ -152,16 +167,27 @@ namespace Ashwild.Building PlayerEvents.AttackPressed -= HandleConfirm; PlayerEvents.SecondaryUsePressed -= HandleCancel; PlayerEvents.HotbarScroll -= HandleRotate; + PlayerEvents.BuildRotatePressed -= HandleRotateKey; + PlayerEvents.DemolishModeChanged -= HandleDemolishModeChanged; EndPlacement(); + ClearDemoTarget(); } /// - /// 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. /// 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(true); ghostSnaps = go.GetComponentsInChildren(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 } /// - /// 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. /// 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; } + /// + /// 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. + /// + private void HandleRotateKey() + { + if (ghost == null) return; + if (PlayerEvents.InputLocked) return; + + yaw += rotationStep; + } + /// /// 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 } } + /// + /// 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. + /// + 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 + + /// + /// 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. + /// + private void HandleDemolishModeChanged(bool on) + { + if (!on) ClearDemoTarget(); + } + + /// + /// 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". + /// + 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(); + + if (hit == demoTarget) return; + + if (demoTarget != null) demoTarget.ClearHighlight(); + demoTarget = hit; + if (demoTarget != null) demoTarget.Highlight(demolitionMaterial); + } + + /// + /// 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. + /// + private void TryDemolish() + { + if (demoTarget == null) return; + if (BuildRegistry.Instance != null) + BuildRegistry.Instance.RequestDemolish(demoTarget.gameObject); + } + + /// + /// Drops the current demolition target, restoring its authored materials. Safe to call when + /// nothing is targeted. + /// + private void ClearDemoTarget() + { + if (demoTarget == null) return; + demoTarget.ClearHighlight(); + demoTarget = null; + } + #endregion } } diff --git a/Assets/GAME/Script/Building/BuildRegistry.cs b/Assets/GAME/Script/Building/BuildRegistry.cs index 34b641b7..c0596506 100644 --- a/Assets/GAME/Script/Building/BuildRegistry.cs +++ b/Assets/GAME/Script/Building/BuildRegistry.cs @@ -99,6 +99,43 @@ namespace Ashwild.Building #endregion + #region Demolish Build + + /// + /// Called by BuildManager when the local player demolishes a build they aimed at: resolves the + /// hit instance to its index in the replicated list and asks the server to remove it. The index + /// is a valid shared key because stays aligned with + /// on every machine. No-op when the object is not one of our committed builds. + /// + public void RequestDemolish(GameObject builtInstance) + { + if (builtInstance == null) return; + + int index = spawned.IndexOf(builtInstance); + if (index < 0) + { + Debug.LogWarning($"[BuildRegistry] '{builtInstance.name}' is not a committed build — nothing to demolish.", this); + return; + } + RequestDemolishServerRpc(index); + } + + /// + /// Server-side: removes the record at this index, which replicates the removal (and the local + /// teardown + occupancy refresh) to every client. Range-guarded against a stale index from a + /// build that another player demolished during the round-trip. + /// + [ServerRpc(RequireOwnership = false)] + private void RequestDemolishServerRpc(int index, NetworkConnection conn = null) + { + if (index < 0 || index >= builds.Count) return; + + // TODO: refund the builder's resources (conn → PlayerInventory) once build cost lands. + builds.RemoveAt(index); + } + + #endregion + #region Networked Ghost Preview /// @@ -187,6 +224,7 @@ namespace Ashwild.Building case SyncListOperation.RemoveAt: DestroyAt(index); spawned.RemoveAt(index); + RecomputeOccupancy(); break; case SyncListOperation.Clear: @@ -210,7 +248,10 @@ namespace Ashwild.Building Debug.LogWarning($"[BuildRegistry] Buildable id {record.buildableId} introuvable ou sans BuiltPrefab — rien de spawné.", this); return null; } - return Instantiate(data.BuiltPrefab, record.position, record.rotation); + + GameObject go = Instantiate(data.BuiltPrefab, record.position, record.rotation); + if (go.GetComponent() == null) go.AddComponent(); + return go; } /// @@ -246,6 +287,24 @@ namespace Ashwild.Building MarkOccupancy(go); } + /// + /// Recomputes occupancy from scratch after a removal: clears every socket, then re-marks the + /// connected pairs across what remains. Without this a demolished piece would leave its former + /// neighbours' sockets stuck "occupied", so nothing could ever be snapped back into the gap. + /// + private void RecomputeOccupancy() + { + foreach (GameObject go in spawned) + { + if (go == null) continue; + foreach (BuildSnapPoint sp in go.GetComponentsInChildren(true)) + sp.SetOccupied(false); + } + + foreach (GameObject go in spawned) + MarkOccupancy(go); + } + /// /// Marks the sockets a piece shares a position with — and the matching sockets on the pieces /// it touches — as occupied, so placement snapping skips them. Occupancy is derived purely diff --git a/Assets/GAME/Script/Building/BuiltStructure.cs b/Assets/GAME/Script/Building/BuiltStructure.cs new file mode 100644 index 00000000..2a9f83b6 --- /dev/null +++ b/Assets/GAME/Script/Building/BuiltStructure.cs @@ -0,0 +1,91 @@ +using UnityEngine; + +namespace Ashwild.Building +{ + /// + /// Runtime marker added to every committed structure instance the BuildRegistry spawns, so the + /// demolition aim can recognise a build from a raycast hit (GetComponentInParent) and hand its + /// root back to the registry to remove. It also owns the demolition highlight: while it is the + /// hammer's target the demolition material is added on top of every renderer (an extra overlay + /// pass, like the ghost), and removed again when the aim leaves — so the player sees exactly which + /// build will break. Pure local visual + a pure tag: nothing about it ever crosses the wire. + /// + [DisallowMultipleComponent] + public class BuiltStructure : MonoBehaviour + { + #region State + + /// + /// Every child renderer, cached so the highlight can swap and restore their materials. + /// + private Renderer[] renderers; + + /// + /// Each renderer's original materials, kept so the swap is fully reversible. + /// + private Material[][] baseMaterials; + + /// + /// Whether the demolition material is currently applied, so swaps stay idempotent. + /// + private bool highlighted; + + #endregion + + #region Unity Lifecycle + + /// + /// Caches every child renderer and its authored materials up front so highlighting is a cheap + /// array assignment with no per-frame allocation of the base look. + /// + private void Awake() + { + renderers = GetComponentsInChildren(true); + baseMaterials = new Material[renderers.Length][]; + for (int i = 0; i < renderers.Length; i++) + baseMaterials[i] = renderers[i] != null ? renderers[i].sharedMaterials : new Material[0]; + } + + #endregion + + #region Public API + + /// + /// Adds the demolition material on top of each renderer's authored materials (an extra draw + /// pass), marking this build as the hammer's current target without hiding the model. Mirrors + /// the ghost overlay. No-op when already highlighted or no material was provided. + /// + public void Highlight(Material demolitionMaterial) + { + if (highlighted || demolitionMaterial == null) return; + highlighted = true; + + for (int i = 0; i < renderers.Length; i++) + { + Renderer r = renderers[i]; + if (r == null) continue; + + Material[] baseMats = baseMaterials[i]; + Material[] combined = new Material[baseMats.Length + 1]; + for (int j = 0; j < baseMats.Length; j++) combined[j] = baseMats[j]; + combined[baseMats.Length] = demolitionMaterial; + + r.materials = combined; + } + } + + /// + /// Restores each renderer's authored materials. No-op when not highlighted. + /// + public void ClearHighlight() + { + if (!highlighted) return; + highlighted = false; + + for (int i = 0; i < renderers.Length; i++) + if (renderers[i] != null) renderers[i].materials = baseMaterials[i]; + } + + #endregion + } +} diff --git a/Assets/GAME/Script/Building/BuiltStructure.cs.meta b/Assets/GAME/Script/Building/BuiltStructure.cs.meta new file mode 100644 index 00000000..47b99c95 --- /dev/null +++ b/Assets/GAME/Script/Building/BuiltStructure.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: fe38331f524a7db4cb3d53381b9ea244 \ No newline at end of file diff --git a/Assets/GAME/Script/Player/Events/PlayerEvents.cs b/Assets/GAME/Script/Player/Events/PlayerEvents.cs index 9ea80dc6..fe2504b9 100644 --- a/Assets/GAME/Script/Player/Events/PlayerEvents.cs +++ b/Assets/GAME/Script/Player/Events/PlayerEvents.cs @@ -25,6 +25,8 @@ namespace Ashwild.Player public static bool IsBuildMenuOpen { get; private set; } // True while a build ghost is being positioned in the world (menu closed, no structure placed yet). public static bool IsPlacingBuild { get; private set; } + // True while the build hammer is in demolition mode (right-click held, menu closed, not placing). + public static bool IsDemolishing { get; private set; } public static bool IsPaused { get; private set; } // True when a networked session is live (host or client). @@ -52,11 +54,13 @@ namespace Ashwild.Player public static event Action CrouchPressed; public static event Action AttackPressed; public static event Action SecondaryUsePressed; // right-click / secondary use (build menu, aim, block, ...) + public static event Action SecondaryUseHeld; // right-click pressed (true) / released (false) — for tap-vs-hold gestures public static event Action InteractPressed; public static event Action DropPressed; public static event Action InventoryTogglePressed; public static event Action HotbarSlotPressed; public static event Action HotbarScroll; + public static event Action BuildRotatePressed; // rotate the build ghost one step (R key) while placing public static event Action CancelPressed; // UI "Cancel" (Escape) — back / pause / close // ============================================================ @@ -128,6 +132,7 @@ namespace Ashwild.Player public static event Action BuildMenuToggleRequested; // the build hammer asks to open/close the construction menu public static event Action BuildMenuOpenChanged; // the construction menu opened (true) or closed (false) public static event Action BuildPlacingChanged; // a build ghost started (true) / stopped (false) being positioned + public static event Action DemolishModeChanged; // the hammer entered (true) / left (false) demolition mode (right-click held) // ============================================================ // Cooking events @@ -182,11 +187,13 @@ namespace Ashwild.Player public static void RaiseCrouchPressed() { Log(nameof(CrouchPressed)); CrouchPressed?.Invoke(); } public static void RaiseAttackPressed() { Log(nameof(AttackPressed)); AttackPressed?.Invoke(); } public static void RaiseSecondaryUsePressed() { Log(nameof(SecondaryUsePressed)); SecondaryUsePressed?.Invoke(); } + public static void RaiseSecondaryUseHeld(bool held) { Log(nameof(SecondaryUseHeld)); SecondaryUseHeld?.Invoke(held); } public static void RaiseInteractPressed() { Log(nameof(InteractPressed)); InteractPressed?.Invoke(); } public static void RaiseDropPressed() { Log(nameof(DropPressed)); DropPressed?.Invoke(); } public static void RaiseInventoryTogglePressed() { Log(nameof(InventoryTogglePressed)); InventoryTogglePressed?.Invoke(); } public static void RaiseHotbarSlotPressed(int i) { Log(nameof(HotbarSlotPressed)); HotbarSlotPressed?.Invoke(i); } public static void RaiseHotbarScroll(float f) { HotbarScroll?.Invoke(f); } + public static void RaiseBuildRotatePressed() { Log(nameof(BuildRotatePressed)); BuildRotatePressed?.Invoke(); } public static void RaiseCancelPressed() { Log(nameof(CancelPressed)); CancelPressed?.Invoke(); } public static void RaiseGroundedChanged(bool grounded, float impactSpeed) @@ -294,6 +301,17 @@ namespace Ashwild.Player BuildPlacingChanged?.Invoke(placing); } + /// + /// Toggles demolition mode (right-click held with the hammer out). Owner-side only; the + /// hammer raises it and BuildManager drives the world-facing targeting/destroy from it. + /// + public static void RaiseDemolishModeChanged(bool on) + { + IsDemolishing = on; + Log(nameof(DemolishModeChanged)); + DemolishModeChanged?.Invoke(on); + } + public static void RaiseFoodPlacedToCook(ItemData raw) { Log(nameof(FoodPlacedToCook)); FoodPlacedToCook?.Invoke(raw); } public static void RaiseFoodCookedReady(ItemData cooked) { Log(nameof(FoodCookedReady)); FoodCookedReady?.Invoke(cooked); } public static void RaiseFuelAdded(ItemData fuel) { Log(nameof(FuelAdded)); FuelAdded?.Invoke(fuel); } @@ -376,6 +394,7 @@ namespace Ashwild.Player IsChestOpen = false; IsBuildMenuOpen = false; IsPlacingBuild = false; + IsDemolishing = false; IsPaused = false; HoveredInteractable = null; } diff --git a/Assets/GAME/Script/Player/HeldItems/BuildHammerBehaviour.cs b/Assets/GAME/Script/Player/HeldItems/BuildHammerBehaviour.cs index a16aeb2b..8c3475bb 100644 --- a/Assets/GAME/Script/Player/HeldItems/BuildHammerBehaviour.cs +++ b/Assets/GAME/Script/Player/HeldItems/BuildHammerBehaviour.cs @@ -4,21 +4,28 @@ using Ashwild.Inventory; namespace Ashwild.Player { /// - /// Held-item logic for the build hammer, placed on its hand prefab. While equipped it - /// listens to the secondary-use input (right-click) and asks the construction UI to - /// open/close through the bus — it never reaches for the menu itself, so the hammer - /// stays a pure input source and the UI owns its own state. Left-click placement will - /// be added on top of this once the build menu exists. + /// Held-item logic for the build hammer, placed on its hand prefab. It turns the single + /// right-click into two gestures without any extra keybind: a quick tap opens/closes the + /// construction menu through the bus, while holding right-click past a short threshold puts + /// the hammer into demolition mode for as long as it is held (releasing leaves it). The hammer + /// stays a pure input source — it only raises bus requests; BuildManager owns the menu and the + /// world-facing targeting/destroy, so this never reaches for either. /// [DisallowMultipleComponent] public class BuildHammerBehaviour : MonoBehaviour, IHeldItemBehaviour { #region Serialized Fields - [Header("Cooldown")] + [Header("Gestures")] /// - /// Minimum time, in seconds, between two menu toggles so a held right-click - /// does not flicker the menu open and shut. + /// How long, in seconds, right-click must stay held before it counts as a demolition hold + /// instead of a menu tap. A release before this opens/closes the menu. + /// + [SerializeField] private float holdThreshold = 0.2f; + + /// + /// Minimum time, in seconds, between two menu toggles so a burst of taps does not flicker the + /// menu open and shut. /// [SerializeField] private float toggleCooldown = 0.25f; @@ -31,24 +38,58 @@ namespace Ashwild.Player /// private float nextToggleTime; + /// + /// True between an accepted press and its release. A press ignored at the gate (input locked + /// or already placing) never sets this, so its release is ignored too. + /// + private bool secondaryDown; + + /// + /// Time, in seconds, the current accepted press began — measured against . + /// + private float secondaryDownTime; + + /// + /// True while the hold has crossed into demolition mode, so the release ends the mode rather + /// than toggling the menu. + /// + private bool demoActive; + #endregion #region Unity Lifecycle /// - /// Subscribes to the secondary-use input for as long as the hammer is held. + /// Subscribes to the right-click hold stream for as long as the hammer is held. /// private void OnEnable() { - PlayerEvents.SecondaryUsePressed += HandleSecondaryUse; + PlayerEvents.SecondaryUseHeld += HandleSecondaryUse; } /// - /// Unsubscribes when the hammer is put away (the prefab is destroyed). + /// Unsubscribes and leaves demolition mode cleanly when the hammer is put away (its prefab is + /// destroyed mid-hold), so the mode never sticks on with no hammer to release it. /// private void OnDisable() { - PlayerEvents.SecondaryUsePressed -= HandleSecondaryUse; + PlayerEvents.SecondaryUseHeld -= HandleSecondaryUse; + if (demoActive) ExitDemolition(); + secondaryDown = false; + } + + /// + /// Promotes a sustained press into demolition mode once it outlasts the tap threshold. Held + /// out of Update (not a timer) so the mode drops instantly if input locks or a placement + /// begins mid-hold. + /// + private void Update() + { + if (!secondaryDown || demoActive) return; + if (PlayerEvents.InputLocked || PlayerEvents.IsPlacingBuild) return; + if (Time.time - secondaryDownTime < holdThreshold) return; + + EnterDemolition(); } #endregion @@ -56,8 +97,8 @@ namespace Ashwild.Player #region IHeldItemBehaviour /// - /// Nothing to link yet: toggling the menu needs no player refs. Present to satisfy - /// the held-item contract; placement logic added later will use the context. + /// Nothing to link: the hammer only raises bus requests. Present to satisfy the held-item + /// contract; BuildManager holds the player/world refs the build UX needs. /// public void Setup(HeldItemContext context, ItemData item) { @@ -68,20 +109,60 @@ namespace Ashwild.Player #region Event Handlers /// - /// Requests the construction menu to open/close on right-click, gated by lock and - /// cooldown. While a ghost is being positioned, right-click cancels the placement instead - /// (handled by the placement controller), so the menu is left alone here. + /// The single right-click gesture, split by press and release: a press is tracked (unless + /// input is locked or a ghost is being placed, where right-click means "cancel" and is left to + /// BuildManager); its release either ends demolition mode (if the hold engaged it) or, for a + /// quick tap, toggles the construction menu under the cooldown. /// - private void HandleSecondaryUse() + private void HandleSecondaryUse(bool held) { - if (PlayerEvents.InputLocked) return; - if (PlayerEvents.IsPlacingBuild) return; - if (Time.time < nextToggleTime) return; + if (held) + { + if (PlayerEvents.InputLocked || PlayerEvents.IsPlacingBuild) return; + secondaryDown = true; + secondaryDownTime = Time.time; + return; + } + if (!secondaryDown) return; + secondaryDown = false; + + if (demoActive) ExitDemolition(); + else ToggleMenu(); + } + + #endregion + + #region Internal Helpers + + /// + /// Requests the construction menu to open/close, gated by the toggle cooldown. + /// + private void ToggleMenu() + { + if (Time.time < nextToggleTime) return; nextToggleTime = Time.time + toggleCooldown; PlayerEvents.RaiseBuildMenuToggleRequested(); } + /// + /// Enters demolition mode and announces it on the bus. + /// + private void EnterDemolition() + { + demoActive = true; + PlayerEvents.RaiseDemolishModeChanged(true); + } + + /// + /// Leaves demolition mode and announces it on the bus. + /// + private void ExitDemolition() + { + demoActive = false; + PlayerEvents.RaiseDemolishModeChanged(false); + } + #endregion } } diff --git a/Assets/GAME/Script/Player/Input/InputManager.cs b/Assets/GAME/Script/Player/Input/InputManager.cs index 04c69ce2..51fae27c 100644 --- a/Assets/GAME/Script/Player/Input/InputManager.cs +++ b/Assets/GAME/Script/Player/Input/InputManager.cs @@ -200,10 +200,12 @@ namespace Ashwild.Player WireButton(playerMap, "Crouch", PlayerEvents.RaiseCrouchPressed); WireButton(playerMap, "Attack", PlayerEvents.RaiseAttackPressed); WireButton(playerMap, "SecondaryUse", PlayerEvents.RaiseSecondaryUsePressed); + WireHold(playerMap, "SecondaryUse", PlayerEvents.RaiseSecondaryUseHeld); WireButton(playerMap, "Interact", PlayerEvents.RaiseInteractPressed); WireButton(playerMap, "Drop", PlayerEvents.RaiseDropPressed); WireButton(playerMap, "Inventory", PlayerEvents.RaiseInventoryTogglePressed); WireScrollY(playerMap, "HotbarScroll", PlayerEvents.RaiseHotbarScroll); + WireButton(playerMap, "BuildRotate", PlayerEvents.RaiseBuildRotatePressed); for (int i = 0; i < 10; i++) { diff --git a/Packages/com.distantlands.cozy.core/Content/Art/Materials/Particles/Day Bug.mat b/Packages/com.distantlands.cozy.core/Content/Art/Materials/Particles/Day Bug.mat index 87fa9b90..253261dc 100644 --- a/Packages/com.distantlands.cozy.core/Content/Art/Materials/Particles/Day Bug.mat +++ b/Packages/com.distantlands.cozy.core/Content/Art/Materials/Particles/Day Bug.mat @@ -13,6 +13,7 @@ Material: m_ModifiedSerializedProperties: 0 m_ValidKeywords: - _ALPHATEST_ON + - _EMISSION m_InvalidKeywords: - _FLIPBOOKBLENDING_OFF m_LightmapFlags: 6 diff --git a/Packages/com.distantlands.cozy.core/Content/Art/Materials/Particles/Leaves.mat b/Packages/com.distantlands.cozy.core/Content/Art/Materials/Particles/Leaves.mat index dca07d82..42ab2025 100644 --- a/Packages/com.distantlands.cozy.core/Content/Art/Materials/Particles/Leaves.mat +++ b/Packages/com.distantlands.cozy.core/Content/Art/Materials/Particles/Leaves.mat @@ -13,6 +13,7 @@ Material: m_ModifiedSerializedProperties: 0 m_ValidKeywords: - _ALPHATEST_ON + - _EMISSION m_InvalidKeywords: - _FLIPBOOKBLENDING_OFF m_LightmapFlags: 6